Component Events
Today, I will be talking about a feature that has been part of Kentico for quite some time, however, even many experienced developers are not aware of it. This is partly caused by missing documentation. This mysterious feature is called Component Events. We have mentioned it in many other articles, however, we have never made this feature as prominent as to have its own article.
The reason behind the introduction of this feature is the lack of similar support of the .NET framework. Traditionally in the .NET world, there are events, but they are always coupled to the place where they are used and are sometimes not very easy to manage. Web forms, in particular, are really event driven. Events used to be part of many developers’ nightmares when implemented incorrectly, or when the complexity of the events grew. I remember trying to get my head around custom grids, which started as quite a simple component, but over time became a monster for anyone that wanted to implement any new feature, or even worse, when fixing something. This was caused by numerous events added over the time. As a result, many developers these days try to stay away from events as much as they can.
We in Kentico are more than aware of this issue so, for this reason, we decided to make the whole process as simple as possible. We decided to get rid of all unnecessary dependencies between each component, which makes code easier to manage. Our component events are managed globally, or per request, rather through dependencies in the code. You can register to any number of events. On the other hand, multiple controls can be registered for the same event. But be careful and try not to over architect your solution, because bigger complexity can be harder to maintain, such as in the traditional c# events model.
Please do not get confused with Kentico Global Events. These events have a different purpose and behavior. You normally use global events when you need to extend inbuilt functionality. Whereas you use component events when you would like to communicate between multiple components in a system. The most common scenario is the communication between two web parts on the page. A great example of using component events is our shopping cart. Please check out our E-commerce Best Practices article.
As I have already mentioned, there are two types of component events – Global and per request. Which one you use really depends on your scenario. I believe that most of the time you will be looking at Request events. In most cases, you will want to communicate with different components, such as web parts, within the single request, but you can take advantage of global events for a more complex situation involving multiple parts of the system. All events can be accessed via the CMS.Helpers.ComponentEvents helper. There are two getters for the available events types (Global and Request).
How to Set Up Events
There are two steps you need to follow to be able to use these events.
- You need to register for the event. There are multiple overrides you can use. These methods are the same for Request events as for Global events.
RegisterForComponentEvent(string componentName, string eventName, EventHandler<EventArgs> handler);
RegisterForComponentEvent(string componentName, string eventName, string actionName, EventHandler<EventArgs> handler);
RegisterForComponentEvent<ArgsType>(string componentName, string eventName, string actionName, EventHandler<ArgsType> handler) where ArgsType : EventArgs;
RegisterForEvent(string eventName, EventHandler<EventArgs> handler);
RegisterForEvent(string eventName, string actionName, EventHandler<EventArgs>
RegisterForEvent<ArgsType>(string eventName, string actionName, EventHandler<ArgsType> handler) where ArgsType : EventArgs;
Parameters explanation:
componentName: name of the component, another level of granularity (e.g. web part id)
eventName: name of the event (e.g. Update)
actionName: action name, adds granularity (e.g. Before, After)
handler: handler method for the event
Tip:
When using generic<ArgsType> methods, you can easily implement custom EventArgs with additional properties.
Example:
public class SelectedCountryArg : EventArgs
{
public int? SelectedCountryID { get; set; }
}
- Once an event is registered, you need to inject your code where you are planning to initiate this event. Typically from another web part.
RaiseComponentEvent(object sender, EventArgs e, string componentName, string eventName);
RaiseComponentEvent(object sender, EventArgs e, string componentName, string eventName, string actionName);
RaiseEvent(object sender, EventArgs e, string eventName);
RaiseEvent(object sender, EventArgs e, string eventName, string actionName);
The parameters are the same as in the registration step.
So how does this work?
- Events and handlers are stored in a hashtable - case insensitive.
- During event registration, a new entry is created in the hashtable.
- When an event is raised, the system calls all event handler methods registered (saved) for a given key combination of the component name, event and action.
The hashtable looks like this:
componentName:eventName|sub-hashtable or eventName|sub-hashtable
sub-hashtable:
- key: actionName or ##global##
- value: handler
A debug example of how events are stored in the hashtable when calling two registration methods
ComponentEvents.RequestEvents.RegisterForComponentEvent("testComponent", "testEvent", "testAction", SomeMethod)
ComponentEvents.RequestEvents.RegisterForComponentEvent("testComponent", "testEvent", "testAction2", SomeMethod)
These methods create a hashtable within the events hashtable with the event key “testComponent:testEvent”. The sub-hashtable contains two values, with keys “testAction” and “testAction2”:
An example of how to use Component Events
We will create two independent web parts. Each contains just a single drop-down list. The first web part contains a drop-down list with all available countries and the second one a list of related states. Our scenario will support countries that have states as well as countries without states. For simplicity, our demo will use a traditional postback when the country selector is changed. Modern approaches might use AJAX calls to achieve the same behavior, but then we would have to choose a different approach altogether. So for our demo, let’s stick to the postback approach.
The first component with the country selector is responsible for informing the second anytime a user changes its value. As mentioned above, this is handled via autopostback on the drop-down list.
The second component needs to accept these calls. As a result, this component needs to be registered for the current request to be able to receive data from the other component. In our scenario state, the drop-down list needs to receive selected country.
When we start developing, it doesn’t matter which component we create first. However, to keep our flow from the previous paragraph, we therefore create the country selector web part first. The code is very simple. We just need to bind the country selectors with the list of values and then on ItemIndexChanged, notify all registered events.
<asp:DropDownList ID="ucCountries" runat="server" AutoPostBack="true" OnSelectedIndexChanged="ucCountries_SelectedIndexChanged" DataValueField="CountryID" DataTextField="CountryName">
</asp:DropDownList>
protected override void OnInit(EventArgs e)
{
ucCountries.DataSource = CountryInfoProvider.GetAllCountries();
ucCountries.DataBind();
ucCountries.Items.Insert(0, new ListItem("---Please Select---", "0"));
}
protected void ucCountries_SelectedIndexChanged(object sender, EventArgs e)
{
var arg = new SelectedCountryArg();
if (string.IsNullOrWhiteSpace(ucCountries.SelectedValue))
{
arg.SelectedCountryID = null;
}
else
{
arg.SelectedCountryID = int.Parse(ucCountries.SelectedValue);
}
ComponentEvents.RequestEvents.RaiseComponentEvent(sender, arg, "CountryDropdown", "CountryItemUpdated");
}
Please note that at this stage if you place only the country web part on the page, it will work, but no other component is listening for this, so nothing happens when a user selects a country.
The state selector web part is not very complicated. With OnInit, we simply register for our component event. Then we need to create a handler that binds our state selector based on the countryID. Just on the page load, we hide this component as we want to show it only for countries that have states.
<asp:DropDownList ID="ucStates" runat="server" DataValueField="StateID" DataTextField="StateName">
</asp:DropDownList>
protected override void OnInit(EventArgs e)
{
ComponentEvents.RequestEvents.RegisterForComponentEvent<SelectedCountryArg>("CountryDropdown", "CountryItemUpdated", null, CountryUpdatedHandler);
}
protected void Page_Load(object sender, EventArgs e)
{
this.Visible = false;
}
public void CountryUpdatedHandler(object sender, SelectedCountryArg e)
{
var states = StateInfoProvider.GetCountryStates(e.SelectedCountryID.GetValueOrDefault(0));
this.Visible = DataHelper.GetItemsCount(states) > 0;
ucStates.DataSource = states;
ucStates.DataBind();
ucStates.Items.Insert(0, new ListItem("---Please Select---", "0"));
}
This is a simple example and in a real world scenario, you would also need to handle all other postbacks that can occur on the page. You can test this as your own exercise.
It is worth mentioning that, apart from component events, you can pass values between each component via RequestStockHelper. This is just a static helper which acts as a wrapper around Request.Items. This approach makes passing values between components in a single request very easy. However, this approach can make some developers’ heads start spinning as there is no real connection from where the data is coming from.
That is all for today. There are numerous scenarios when you can use component events. Any two web parts on any page can now be a standalone component. This approach allows you to create more modularized solutions, such as we did with our shopping cart. Try to break big pages into smaller parts and you will see how nicely everything can work together.