Sitecore MVC Data Source query support

Sitecore query is a very powerful tool for selecting items and is used in many places in the Sitecore CMS. However, there are a few locations where this is not supported by default, and there are good reasons for that too. One is performance. Sitecore query is powerful but can be far more expensive to execute than referring to an item using a regular Guid. Sitecore often provides you with a search/query tool to find a data source, but it’s only the target Guid that’s stored typically.

The Data sources attribute on controls in the Layouts (__Renderings) field is one location where query isn’t supported by default. And that makes sense, since it could potentially have enormous performance impacts if used incorrectly. But sometimes the pros overcomes the cons, so let’s add support for this.

This has been done many times before, so take a look at this great post from our friends at Cognifide for an overview of how this works in WebForms: Reduce multisite chaos with Sitecore queries

In Sitecore MVC we have a new set of pipelines to extend in order to add support for queries. The easiest way is to just add a processor in the <mvc.getXmlBasedLayoutDefinition> pipeline. Here’s a simple snippet (complete code further down) on how to do it:

public override void Process(GetXmlBasedLayoutDefinitionArgs args)
{
  var item = args.ContextItem ?? args.PageContext.Item;

  const string queryPrefix = "query:";
  foreach (var renderingElement in args.Result.Elements("d").Elements("r"))
  {
    var dataSource = renderingElement.GetAttributeValueOrNull("ds");
    if (dataSource != null && dataSource.StartsWith(queryPrefix))
    {
      string query = dataSource.Substring(queryPrefix.Length);
      Item queryItem = item.Axes.SelectSingleItem(query);
      if (queryItem != null)
      {
        renderingElement.SetAttributeValue("ds", queryItem.ID.Guid);
      }
    }
  }
}

Essentially, this code just looks through the Rendering XML and finds data sources and replaces those with a resolved Guid. Not that we don’t get a set of RenderingReferences in this pipeline. Instead we work directly with the XDocument.

Now we can use powerful Sitecore queries in our layout Data sorces on both pages and and template standard values, and reduce content maintenance headaches. One very powerful query to consider when running a multi site instance is query:./ancestor-or-self::*[@@templatekey='website container']/pathOrQueryTo/LocalItem and similar ones.

There is one more location where these kind of queries becomes powerful as well. That is the default data source we can enter on rendering items, such as View renderings, Controller renderings etc. If we can leave the data source field blank on the page Layout, it’ll make life a lot easier for content authors. We don’t want them to write Sitecore queries, do we?

I’ve yet to find an elegant way to solve this, but here’s at least a solution that works. Essentially, we can hook into the same pipeline as above, and if the data source (ds) field is empty, we can load the rendering and see if the data source field on the rendering item contains a query. If so, we can resolve this and add the target Guid as a data source at run time, like this:

public class ResolveMvcQueryableDatasources : GetXmlBasedLayoutDefinitionProcessor
{
  public override void Process(GetXmlBasedLayoutDefinitionArgs args)
  {
    Assert.ArgumentNotNull(args, "args");
    if (args.Result == null || Context.Site == null)
      return;

    var item = args.ContextItem ?? args.PageContext.Item;
    if (item == null)
      return;

    const string queryPrefix = "query:";
    foreach (var renderingElement in args.Result.Elements("d").Elements("r"))
    {
      var dataSource = renderingElement.GetAttributeValueOrNull("ds");
      if (dataSource != null && dataSource.StartsWith(queryPrefix))
      {
        string query = dataSource.Substring(queryPrefix.Length);
        Item queryItem = item.Axes.SelectSingleItem(query);
        if (queryItem != null)
        {
          renderingElement.SetAttributeValue("ds", queryItem.ID.Guid);
        }
      }
      else if (string.IsNullOrWhiteSpace(dataSource))
      {
        var renderingId = (Guid)renderingElement.Attribute("id");
        var renderingItem = RenderingItem.GetItem(new ID(renderingId), args.PageContext.Database, true);
        var renderingDataSource = renderingItem.DataSource;
        if (renderingDataSource != null && renderingDataSource.StartsWith(queryPrefix))
        {
          string query = renderingDataSource.Substring(queryPrefix.Length);
          Item queryItem = item.Axes.SelectSingleItem(query);
          if (queryItem != null)
          {
            renderingElement.SetAttributeValue("ds", queryItem.ID.Guid);
          }
        }
      }
    }
  }
}

To add this into your Sitecore instance, just add a config file such as this one:

<?xml version="1.0" encoding="utf-8" ?> 
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:x="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.getXmlBasedLayoutDefinition>
        <processor patch:after="*[@type='Sitecore.Mvc.Pipelines.Response.GetXmlBasedLayoutDefinition.GetFromLayoutField, Sitecore.Mvc']"
                   type="namespace.ResolveMvcQueryableDatasources, assemblyname" />
      </mvc.getXmlBasedLayoutDefinition>
    </pipelines>
  </sitecore>
</configuration>