Sunday, 28 June 2009

EPiServer Composer - How to figure out which content function is first (or last) within a content area

In my previous post (EPiServer-based site in 4 weeks?) I have presented number of quite high level thoughts regarding our last EPiServer project. In this and a few forthcoming post I would like to focus more on technical details and present several more interesting EPiServer Composer (version 3.2.5) use cases.

How to figure out which content function is first (or last) within a content area.

Imagine following piece of user interface:


It's an area where editors can define four arbitrary components. Components can be a text, image + text or just an image. Here is a typical html for this use case:

   1:  <div class="options">
   2:     <div class="option first"> ... </div>
   3:     <div class="option"> ... </div>
   4:     <div class="option"> ... </div>
   5:     <div class="option last"> .... </div>
   6:  </div>

Div with class options is a wrapper for all components. Each component has a class option. Quite typically in situation like this, first and last elements are a bit different. Depending on site design it's required to add some visual effects and that is why additional classes are added to the first and last element. This is very flexible approach as changes in html are minimal and final visual effect depends fully on CSS.

The problem for developer is that EPiServer Composer allows to simply drag and drop any number of content functions and each function is fairly autonomous. Content function is responsible for displaying html, it has no knowledge about context in which it was used. It doesn't know anything about content area to which it belongs, also it doesn't know anything about other content functions within the same content area. That is why to get information about context you need to operate one level above the functions.

But firstly, each content function need to implement following interface:

   1:  public interface IListItemPosition
   2:  {
   3:      bool IsLastElement { get; set; }
   4:      bool IsFirstElement { get; set; }
   5:  }

And now tricky part - changes on the higher level. A few things need to be added to the page/class which inherits from ExtensionBaseTemplate.

You need to override OnInit method:

   1:  protected override void OnInit(EventArgs e)
   2:  {
   3:      CurrentExtensionPageHandler.LoadContentArea += CurrentExtensionPageHandler_LoadContentArea;
   4:      CurrentExtensionPageHandler.LoadContentFunction += CurrentExtensionPageHandler_LoadContentFunction;
   5:  
   6:      base.OnInit(e);
   7:  }
Above code adds our handlers to the following events:
  • LoadContentArea - this way we will get information about all available content areas and what is more important - count of content functions for each area!
  • LoadContentFunction - Composer fires this event for each content function. Having a total number of content functions for each area we can easily detect first and last elements.

Here are implementations for the handlers:

   1:  #region ContentAreaFunctionCounter
   2:  
   3:  protected ContentAreaFunctionCounter ContentAreaFunctionCounter = new ContentAreaFunctionCounter();
   4:  
   5:  protected void CurrentExtensionPageHandler_LoadContentFunction(object sender, ExtensionPageEventArgs e)
   6:  {
   7:      var element = e.ContentFunction as IListItemPosition;
   8:      if (element == null) return;
   9:      
  10:      element.IsFirstElement = IsFirstElement(e);
  11:      element.IsLastElement = IsLastElement(e);
  12:  }
  13:  
  14:  private static bool IsFirstElement(ExtensionPageEventArgs e)
  15:  {
  16:      return e.ContentArea.Controls.Count == 0;
  17:  }
  18:  
  19:  private bool IsLastElement(ExtensionPageEventArgs e)
  20:  {
  21:      var numberOfElements = ContentAreaFunctionCounter.GetContentAreaFunctionCount(e.ContentArea.ID);
  22:      return e.ContentArea.Controls.Count == (numberOfElements - 1);
  23:  }
  24:  
  25:  protected void CurrentExtensionPageHandler_LoadContentArea(object sender, ExtensionPageEventArgs e)
  26:  {
  27:      var functionsCount = e.ContentAreaData == null ? 0 : e.ContentAreaData.ContentFunctions.Count;
  28:      ContentAreaFunctionCounter.SetContentAreaFunctionCount(e.ContentArea.ID, functionsCount);
  29:  }
  30:  
  31:  #endregion

Missing part is class ContentAreaFunctionCounter which is responsible only for keeping information about available content areas and functions count:

   1:  public class ContentAreaFunctionCounter
   2:  {
   3:      private readonly IDictionary<string, int> contentAreaFunctionCount = new Dictionary<string, int>(); 
   4:      
   5:      public void SetContentAreaFunctionCount(string contentArea, int functionCount)
   6:      {
   7:          contentAreaFunctionCount[contentArea] = functionCount;
   8:      }
   9:  
  10:      public int GetContentAreaFunctionCount(string contentArea)
  11:      {
  12:          if (contentAreaFunctionCount.ContainsKey(contentArea))
  13:          {
  14:              return contentAreaFunctionsCount[contentArea];
  15:          }
  16:  
  17:          return 0;
  18:      }
  19:  }

I would recommend creating a base class encapsulating this functionality. Each content function which needs to be aware of it's position just need to implement the IListItemPosition interface. And that's it! :)

If it's necessary you can easily extend IListItemPosition interface to contain int Position {get; set;} property and this way give all functions idea about it's position (not just first and last one).

1 comment:

Orvar said...

Nice post Marek!