Decoupled Sublayouts in Sitecore

Sitecore provides a way for sublayouts to talk to each other through the Sitecore Event Pool. This can be achieved by defining a publisher and a subscriber sublayouts; and in most cases, a custom class to hold the data transferred from the publisher to the subscriber. This way of communication has multiple benefits such as the ability to build your application in a modular way – where you break the functionality between multiple sublayouts so they can easily be personalized and reused.

To better understand the use of Sitecore Event Pool, consider the following example:

There are two sublayouts in a search page, one is responsible for searching the products in Sitecore and displaying the results. The other is responsible for displaying the search facets. Instead of making two calls to the search provider in each one of them; there will be only one call to the search provider which will get both the search results and the facets data. The facets data then will be passed to the other sublayout as an event parameter.  They work in the following sequence:

  • SearchFacets sublayout subscribes to an event called custom:searchedproducts at the beginning of its life cycle.
  • SearchResults sublayout searches the products in Sitecore, extracts the facets data from the search results and then raises the event custom:searchedproducts with the facets data passed as an event parameter (argument).
  • SearchFacets sublayout handles the event, receives the facets data and then renders it to the page.

sitecore event pool example

The search method in SearchResults sublayout will be like this:

  private void DoSearch(string searchQuery)
 {
   var indexName = string.Format("sitecore_{0}_index", Sitecore.Context.Database.Name);
   var index = ContentSearchManager.GetIndex(indexName);

   using (var context=index.CreateSearchContext())
   {
    var dataQuery = context.GetQueryable<ProductSearchItem>()
    .Where(p => p.TemplateName.Equals("Product"))
    .Where(p => p.Path.StartsWith("/sitecore/content/Home/Product Repository"))
    .Where(p => p.Title.Contains(searchQuery) || p.Color.Contains(searchQuery) || p.Description.Contains(searchQuery));

    var results = dataQuery.FacetOn(p => p.Color).GetResults();

    List<FacetCategory> facets = results.Facets.Categories;
    var args = new SearchResultsEventArgs();
    args.Facets = facets;

    rptResultList.DataSource = results.Hits.Where(h => h != null).Select(h => h.Document).ToList();
    rptResultList.DataBind();

    Sitecore.Events.Event.RaiseEvent("custom:searchedproducts", args);
   }
 }

The code in SearchFacets sublayout will look like the following:

private void Page_Load(object sender, EventArgs e)
 {
  _handler = new EventHandler(FacetsHandler);
  Sitecore.Events.Event.Subscribe(“custom:searchedproducts", _handler);
 }

 protected void FacetsHandler(object sender, EventArgs args)
 {
  var customArgs = Sitecore.Events.Event.ExtractParameter(args, 0) as SearchResultsEventArgs;
  if(customArgs!=null)
  {
   rptSearchFilters.DataSource = customArgs.Facets.FirstOrDefault(fc=>fc.Name=="productcolor").Values.Where(fc=>fc.AggregateCount>0).ToList();
   rptSearchFilters.DataBind();
  }
 }

 protected void Page_Unload(object sender, EventArgs e)
 {
  if (_handler != null)
  {
    Sitecore.Events.Event.Unsubscribe("custom:searchedproducts", _handler);
  }
 }

SearchFacets sublayout in this case can be reused many times on the page with different facets data each time.

More information about the Event Pool can be found in the following articles:

http://www.matthewdresser.com/sitecore/pass-data-between-sublayouts

https://adeneys.wordpress.com/2012/10/21/decoupling-through-the-sitecore-event-pool/

So what is this article about?

This article highlights some issues I came across when I used Sitecore Event Pool for sublayouts communication.

1- Events fired don’t always get handled by subscriber subalyouts 

This can be related to the rendering order of placeholders and sublayouts within the page. Make sure the subscription code runs before the publishing. Consider above example, SearchResults renders before SearchFacets. If SearchResults control raises an event at the page load,  SearchFacets should have the subscription occurs before that. One way to achieve this is, by having subscriptions in the constructor of a user control:

 public SearchFacets()
 {
  _handler = new EventHandler(FacetsHandler);
  Sitecore.Events.Event.Subscribe(“custom:searchedproducts", _handler);
 }

2- Sitecore Event Pool is not session safe

Consider above example, if concurrent users are searching for products at the same time, they might get wrong facets data.

data overlapping

To generate the problem, I requested the page from two browser windows with a different query string parameter. Both requests were almost sent at he same time. Look at the page on the left, although the search results are correct, the facets data is wrong.

What happens under the hood:

All events (including the manually configured ones) and their handlers are held in a global object in Sitecore which is available through the application lifetime. When a sublayout subscribes to a custom event, Sitecore adds the event and it’s handler to the global object. Subsequently, when a sublayout unsubscribe from a custom event, Sitecore removes the event from the global object. The event name is unique in this case so, if two concurrent requests are raising the same event with the same name, Sitecore replaces the data of the later by the data of the former.

How to fix the problem: 

Sitecore event pool is used internally by the CMS to raise events like item:deleted and item:created; and is not designed to be session safe.  If you would like to use it in your application to raise custom events, the names of these events have to be unique per session. This can be achieved by appending something like the Session Id to the event name.

If we apply that to SearchFacets, the code will be like the following :

public SearchFilter()
 {
  _handler = new EventHandler(FacetsHandler);
  Sitecore.Events.Event.Subscribe("custom:searchedproducts"+HttpContext.Current.Session.SessionID, _handler);
 }

 protected void FacetsHandler(object sender, EventArgs args)
 {
  var customArgs = Sitecore.Events.Event.ExtractParameter(args, 0) as SearchResultsEventArgs;
  if(customArgs!=null)
  {
   rptSearchFilters.DataSource = customArgs.Facets.FirstOrDefault(fc=>fc.Name=="productcolor").Values.Where(fc=>fc.AggregateCount>0).ToList();
   rptSearchFilters.DataBind();
  }
 }

 protected void Page_Unload(object sender, EventArgs e)
 {
  if (_handler != null)
  {
   Sitecore.Events.Event.Unsubscribe("custom:searchedproducts" + HttpContext.Current.Session.SessionID, _handler);
  }
 }

And the code in SearchResults will need to be replaced by:

Sitecore.Events.Event.RaiseEvent("custom:searchedproducts"+HttpContext.Current.Session.SessionID, args);

Search facets will be correct now for concurrent requests:

data is not overlapping

This solution worked fine as a work around for this problem, and no performance issues were detected. But, further investigation can be done to see how best to resolve this. Ideas are welcome.