Friday, August 19, 2011

Sitecore Output Caching: Kick it up a Notch


image Ever wondered how Sitecore caches works? It is pretty cool, actually. Sitecore can cache the output of any presentation component that it is aware of (Sublayout, WebControl, XSL Rendering). Basically, this output may vary on data context (Context Item or Datasource), device, whether user is logged in or not, parameters passed to the rendering, query string and Context user:
This gives a ton of options and in most cases this is sufficient, but what if the output of your rendering depends on inner logic embedded within the control? Sometimes you could solve this with VaryByParm and by passing different parameters to the rendering, but what if that’s not the case?
This is exactly the use case I have been presented during another onsite visit with a customer.
Here is a very simple way of extending the cache variation logic for a Sublayout.
If you are Sitecore blackbelt, consider moving on to the “Solution” part below.
Consider the following example.
We have a new list control which we want to personalize by “audience”. When user is authenticated, we imagine a process that “attaches” the current user session with a particular “audience type” and saves this in a cookie. In order to simulate this, I’ve created a control called “AudienceSwitcher” which sets the current “Audience Type” explicitly for this sake of this example:
SNAGHTML155c400
Whenever “Audience Type” changes, the following method is called:

AudienceManager.SetCurrentAudience(selectedAudience);

AudienceManager is a custom class that incorporates all audience related actions, very simple:
public class AudienceManager {
 
  private const string Key = "audience";
 
  public static string CurrentAudienceName {
      get
      {
         var key = CurrentAudienceKey;
         if (ID.IsID(key)){
            var audienceItem = Sitecore.Context.Database.GetItem(key);
            if (audienceItem != null) return audienceItem.DisplayName;
         }
            return string.Empty;
         }
      }
 
      public static string CurrentAudienceKey {
         get { return WebUtil.GetCookieValue(Key) ?? AudienceTypes.Consumer; }
      }
 
      public static void SetCurrentAudience(string audience){ 
        WebUtil.SetCookieValue(Key, audience);
      }
}
This has an effect on the NewsList control, which renders only the news items that are targeted to a specific audience type:
<asp:Repeater ID="NewsRepeater" runat="server">
   <ItemTemplate>
      <li><% DataBinder.Eval(Container.DataItem, "Name") %></li>
   </ItemTemplate>
</asp:Repeater>
protected void Page_Load(object sender, EventArgs e)
{
   if(!IsPostBack)
   {
      var news = Sitecore.Context.Database.SelectItems("/sitecore/content/home/news//*[contains(@audiencetype, '{0}')]".FormatWith(AudienceManager.CurrentAudienceKey));
 
      NewsRepeater.DataSource = news;
      NewsRepeater.DataBind();
   }
}
All news items are stored under /home/news and tagged to an audience via a multilist:
image
The “Audience Types” referenced from the multilist above are stored in a separate area in the content tree:
image
After verifying that our NewsList control is working correctly and displaying different news for different audiences:
for consumer:
image
for travel agent
SNAGHTML1647516
Let’s try and cache it this rendering by checking Cacheable and VaryByData.
image
If you don’t run live mode, you would need to publish after this.
The expected result of this configuration will not be satisfactory. The same output of the NewsList control will be cached once and served up from memory every time it is rendered, no matter what the current “Audience Type” is.
Here is the solution.
1. Subclass the default Sublayout implementation:
   1: public class AudienceSublayout : Sitecore.Web.UI.WebControls.Sublayout
   2: {
   3:    public override string GetCacheKey()
   4:    {
   5:       SiteContext site = Sitecore.Context.Site;
   6:       if ((Cacheable && ((site == null) || site.CacheHtml)) && !SkipCaching())
   7:       {
   8:          if (VaryByParm)
   9:          {
  10:             return base.GetCacheKey() + "_#audience:" + AudienceManager.CurrentAudienceKey;
  11:          }
  12:  
  13:          return base.GetCacheKey();
  14:       }
  15:  
  16:       return string.Empty;
  17:    }
  18: }
Very basic what we do here. We override the default implementation of the GetCacheKey method which is evaluated by Sitecore’s output caching mechanism every time a control needs to get cached. The key returned by this method needs to uniquely identify the control’s output in the cache collection.
Line 10 is the most important, this is where we call our friend AudienceManager and reading the current audience key (basically a value from cookie).
2. It’s important to note that this is done only if “VaryByParm” is checked on the control definition item in Sitecore, so we need to make sure it is checked:
image
The last thing you need to do is to wire in the custom AudienceSublayout by overriding the “SublayoutRenderingType” component:
3. Compile the following class:
   1: public class SublayoutRenderingType : Sitecore.Web.UI.SublayoutRenderingType
   2: {
   3:    public override System.Web.UI.Control GetControl(NameValueCollection parameters, bool assert)
   4:    {
   5:       var sublayout = new AudienceSublayout();
   6:       foreach (string key in parameters.Keys)
   7:       {
   8:          ReflectionUtil.SetProperty(sublayout, key, parameters[key]);
   9:       }
  10:       return sublayout;
  11:    }
  12: }
The only think we change here is line 5. Instead of the default Sublayout we instantiate our custom AudienceSublayout.
4. Define custom SublayoutRenderingType in web.config, line 3:
   1: <renderingControls>
   2:  <control template="method rendering" type="Sitecore.Web.UI.WebControls.Method, Sitecore.Kernel" propertyMap="AssemblyName=assembly, ClassName=class, MethodName=method"/>
   3:  <control template="sublayout" type="SearchDemo.SublayoutRenderingType, SearchDemo" propertyMap="Path=path" />
   4:  <control template="url rendering" type="Sitecore.Web.UI.WebControls.WebPage, Sitecore.Kernel" propertyMap="Url=url"/>
   5:  <control template="xsl rendering" type="Sitecore.Web.UI.XslControlRenderingType, Sitecore.Kernel" propertyMap="Path=path"/>
   6:  <control template="webcontrol" type="Sitecore.Web.UI.WebControlRenderingType, Sitecore.Kernel" propertyMap="assembly=assembly, namespace=namespace, class=tag, properties=parameters"/>
   7:  <control template="xmlcontrol" type="Sitecore.Web.UI.XmlControlRenderingType, Sitecore.Kernel" propertyMap="controlName=control name, properties=parameters"/>
   8: </renderingControls>
That’s all you need to do. Now the output of the NewsList control will vary depending on the current “Audience Type”.
Here is where you can download all the sources for this post:
http://resources.alexshyba.com/blog/CustomVaryByCaching.zip
Obviously, this is only one way of doing it. If you have other ideas, please share in the comments below.
Have a great weekend!
Further reading:

0 comments: