March 26, 2013

Ajax lazy loading with Sitecore

For one of our project we need to call some sublayout in ajax. This allow you to do some pagination or lazy loading and let sitecore resolve the context.

To do that, we will create our own LayoutResolver to respond to our query with only the corresponding rendering.

Our call ajax wll look like:
var url = "http://myurl/myItem?UseAjax=true&PresentationId=BACBFEE1-C0BB-4D8E-BA1A-D9F50CCA5B7F";

$.ajax({
 type: 'GET',
 url: url
}).done(function (data) {
 //Your code
});

We will also need a layout for the ajax queries (in the following code the MY_AJAX_LAYOUT_ID is the id of this layout) and this layout will only contain one placeholder (MY_AJAX_PLACEHOLDER in the code)

Now here is the custom layout resolver (look at the comments into the code):
using System;
using System.Linq;
using System.Web;
using Sitecore.Data.Items;
using Sitecore.Pipelines.HttpRequest;

namespace MyNamespace
{
    public class AjaxLayoutResolver : LayoutResolver
    {
        public override void Process(HttpRequestArgs args)
        {
            bool useAjax;
     //First of all let's check if we are in ajax mode or not if not don't continue
            var result = bool.TryParse(HttpContext.Current.Request.Params["UseAjax], out useAjax);
            if (!result || !useAjax)
            {
                return;
            }

     //The second parameter we need to pass is the PresentationId if not present the query if not valid -> don't continue
            var presentationId = HttpContext.Current.Request.Params["PresentationId"];
            if (string.IsNullOrEmpty(presentationId))
            {
                return;
            }

     //If the current item is null return
            if (Sitecore.Context.Item == null)
            {
                return;
            }

     //Let's resolve the sublayout
            try
            {
  //Get the list of rendering for the current item
                var renderings = Sitecore.Context.Item.Visualization.GetRenderings(Sitecore.Context.Device, false);
  //If found
                if (renderings != null && renderings.Any())
                {
      //Get the first rendering corresponding to the requested one
                    var rendering = renderings.First(sublayout => sublayout.RenderingID.ToString().Equals(presentationId));
                    if (rendering != null)
                    {             
                        //Put this rendering into ajax layout
                        rendering.Placeholder = MY_AJAX_PLACEHOLDER;
                        var layoutItem = Sitecore.Context.Database.GetItem(MY_AJAX_LAYOUT_ID);
                        if (layoutItem != null)
                        {
                            var layout = new LayoutItem(layoutItem);
                            Sitecore.Context.Page.FilePath = layout.FilePath;
                            Sitecore.Context.Page.ClearRenderings();
                            Sitecore.Context.Page.AddRendering(rendering);
                        }
                    }
                }
            }
            catch (Exception exception)
            {
                Log.Warn("Cannot render this!", this, exception);
            }
        }
    }
}
And of could you need to include this resolver after the existing one by adding this into a .config file in \App_Config\Include\
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>          
          <processor patch:after="*[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']" type="MyNamespace.AjaxLayoutResolver, MyNamespace"/>          
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

8 comments:

  1. This works brilliantly, it works like a filter.

    Changed a couple of things, like alwyas clearing the page .. even when the rendering could not be found. As I don't want to end up with the entire page duplicated on the website.

    Now the only thing left is ... how do I automatically generate the Ajax call for specific renderings, sublayouts and placeholders :)

    ReplyDelete
    Replies
    1. I'd use a system whereby you create container elements in your markup, a predictable naming scheme for URLs to request over AJAX, and HTML5 data-* attributes to give details of what to request. That way your JS is dirven entirely by markup and it very generic. e.g.

      HTML:

      <div data-lazyload="BACBFEE1-C0BB-4D8E-BA1A-D9F50CCA5B7F"></div>

      jQuery:

      $("[data-lazyload]").each(function() {
      var $this = $(this),
      id = $this.data("lazyload"),
      url = "http://myurl/myItem?UseAjax=true&PresentationId=" + id;

      $.ajax({
      type: 'GET',
      url: url
      }).done(function (data) {
      $this.append(data);
      });
      });

      Delete
    2. Thanks for the tip, this is now the script that I am using:

      $j(function() {

      function fetch(i, ph) {
      var $ph = $j(ph), opt = ($ph.data('options') || {});
      function doFetch(useFx) {
      $j.get('?_lazy=' + $ph.data('lazy'), function (c) {
      var $c = $j(c), fx = opt.fx, p = $ph;
      if (useFx && fx && fx.length > 0) {
      $ph.hide().html(c);
      for (var f = 0; f < fx.length - 1; f += 2) {
      p = p[fx[f]].call(p, fx[f + 1]);
      }
      } else {
      $ph.html($c);
      }
      // handle 'possible' (bad practice) nested lazy content
      $ph.find('[data-lazy]').each(fetch); // TODO shouldnt we get the children first and then show the content?
      });
      }

      setTimeout(function() {
      doFetch(true);
      if (opt.interval && opt.interval > 0) setInterval(doFetch, opt.interval);
      }, opt.wait || 0);
      }

      $j('[data-lazy]').each(fetch);
      });

      Delete
  2. Just one question (And this is what I eventually had to do).

    Should this layout resolver actually be split in two?
    1. This layout resolver, but only responsible for setting the layout.
    2. A new InsertRenderingsProcessor (I called it KeepAjaxRenderings) and have that added to the pipeline after the AddRenderings processor.

    The thing this second processor has to do is to filter out the renderings and only leave the specified rendering/sublayout and change the placeholder.

    ...
    var renderings = args.Renderings.
    args.Renderings.Clear();

    var rendering = renderings.FirstOrDefault(r => r.RenderingID == renderingId);

    if (rendering != null)
    {
    rendering.Placeholder = MY_AJAX_PLACEHOLDER;
    args.Renderings.Add(rendering);
    }
    ...

    The problem is that you don't know what renderings are being added at the end of the line. We for example have implemented rendering inheritance. Which would not nicely with your solution.

    ReplyDelete
    Replies
    1. Yes it is maybe more logic to split it in two part one to switch the layouts and one to filter the renderings it could be a good improvement of this code thank you for the suggestion.

      Delete
    2. It's not really filtering the layouts. All renderings should actually stay, only the requested rendering should have it's Placeholder set. This way you can support sublayouts with placeholders that have renderings added to it.

      Also I found that the RenderingReference's UniqueID should be used and not the ID of the associated RenderingItem.

      Delete
  3. The technic with the jquery is also a great suggestion thank you

    ReplyDelete
  4. Many thanks for this idea. I extended it for usage with Sitecore MVC layouts: http://sitecorevn.blogspot.com/2014/09/ajax-lazy-loading-with-sitecore-mvc.html

    ReplyDelete