Custom global events with ComponentEvents
18th of November, 2016 0 comments

Custom global events with ComponentEvents

The publish-subscribe pattern is used to broadcast messages that are then received by subscribers. The concept is that there is no specific "recipient" of a message - it is sent to whoever is listening.

Subscribers can then subscribe to a specific message or message type, and they will be notified of any new messages.

This is a great benefit, as the publishers and subscribers are decoupled, and do not need to know about eachother. This is also a downside - changes to the way you publish messages will affect the subscribers, and you need to be aware of all the subscribers if modifications are needed.

There are numerous libraries for the publish-subscribe pattern, however our goal is to utilise what is already present in Kentico.

Introducing ComponentEvents

A good example of where the publish-subscribe pattern is utilised within Kentico is the shopping cart. The recently revamped shopping cart consists of multiple webparts, controlled by a "document wizard manager" webpart. Using ComponentEvents, the cart webparts can broadcast and receive ecommerce-related messages.

For example, the Shipping Selection webpart raises the "shopping cart changed" event, when the shipping option is changed.

ComponentEvents.RequestEvents.RaiseEvent(sender, e, SHOPPING_CART_CHANGED);

The Shopping Cart Content webpart subscribes to the "shopping cart changed" event. Each time the event is fired, the webpart updates with new values.

ComponentEvents.RequestEvents.RegisterForEvent(SHOPPING_CART_CHANGED, Update);

ComponentEvents have been already documented and explained in a few articles on the web, so we won't get too much into details. However, inter-webpart communication is just one of its purposes.

Custom global events

The GlobalEvents property of the ComponentEvents class contains events that persist during the lifetime of the application. They are similar to global system events. Using the RaiseEvent method, you can publish a custom event. As the parameters, the method accepts the sender object, arguments, event name, and an optional action name.

ComponentEvents.GlobalEvents.RaiseEvent(this, new EventArgs(), "MyCustomEvent", "MyCustomAction");

Subscribing to the event can be done using the RegisterForEvent method. The parameters are the event name, event handler method, and optionally the action name.

ComponentEvents.GlobalEvents.RegisterForEvent("MyCustomEvent", "MyCustomAction", MyHandler);
    
private void MyHandler(object sender, EventArgs e)
{
   // Handle event
}

The EventArgs class cannot contain any data, so the RaiseEvent and RegisterForEvent methods accept any class that is derived from the EventArgs class.

// Custom EventArgs class
public class MyCustomArgs : EventArgs
{
   public string MyProperty { get; set; }
}
    
// Subscribe to the event
ComponentEvents.GlobalEvents.RegisterForEvent<MyCustomArgs>("MyCustomEvent", "MyCustomAction", MyHandler);
    
// Publish the event
ComponentEvents.GlobalEvents.RaiseEvent(this, new MyCustomArgs
{
    MyProperty = "Some value"
}, "MyCustomEvent", "MyCustomAction");

Fixing the 'order is paid' ecommerce event

When performing ecommerce customisation and custom development, a typical scenario is to use the OrderPaid ecommerce event, to trigger custom code once an order has been paid.

EcommerceEvents.OrderPaid.Execute += OrderPaidOnExecute;
    
private void OrderPaidOnExecute(object sender, OrderPaidEventArgs args)
{
    // Custom code...
}

The issue with this approach is that there is no way to subscribe to the event of the order payment being reversed. If the 'order is paid' checkbox is unchecked for a paid order, we are unable to revert the actions of our custom code, if necessary.

As mentioned in this article, the recommended approach is to override the ProcessOrderIsPaidChangeInternal method in the OrderInfoProvider class. This can be problematic if our custom code is in a different project/assembly, or if there are potential circular reference issues.

Combining the ProcessOrderIsPaidChangeInternal override with a custom global event gives us an optimal solution.

public class CustomOrderInfoProvider : OrderInfoProvider
{
   protected override void ProcessOrderIsPaidChangeInternal(OrderInfo oi)
   {
      // Determine the status of the order
      var eventType = oi.OrderIsPaid ? "OrderPaid" : "OrderNotPaid";
          
      // Raise our custom event.
      // We will use the built-in OrderPaidEventArgs class to hold the event arguments.
      // Otherwise, we can create a custom class if necessary
      ComponentEvents.GlobalEvents.RaiseEvent(this, new OrderPaidEventArgs
      {
         Order = oi
      }, "Custom.OrderIsPaidChange", eventType);
          
      base.ProcessOrderIsPaidChangeInternal(oi);
   }
}

Our event name is Custom.OrderIsPaidChange, and our event action is either OrderPaid or OrderNotPaid, depending on the status of the order. We also attach the order object to the event, using the existing OrderPaidEventArgs class. This saves us from writing a custom EventArgs class.

Now that we have set up the publishing of the event, we can subscribe to it.

The signature is very similar to the EcommerceEvents.OrderPaid.Execute event we had initially.

// Subscribe to the event of the order being paid
ComponentEvents.GlobalEvents.RegisterForEvent<OrderPaidEventArgs>("Custom.OrderIsPaidChange", "OrderPaid", OrderPaidHandler);
    
public static void OrderPaidHandler(object sender, OrderPaidEventArgs e)
{
   // Order is paid
}
    
// Subscribe to the event of the order no longer being paid
ComponentEvents.GlobalEvents.RegisterForEvent<OrderPaidEventArgs>("Custom.OrderIsPaidChange", "OrderNotPaid", OrderReversedHandler);

public static void OrderReversedHandler(object sender, OrderPaidEventArgs e)
{
   // Order not paid (reversed)
}

The subscribing should occur during your custom module initialisation, or inside the App_Code folder.

This is a clean approach to handling payment reversals using events, without coupling the code with the CustomOrderInfoProvider class.

Written by Kristian Bortnik


Tags

Comments