Thursday, February 23, 2012

Sitecore Fetch Squad

Automated crawler fetching websites and blogs from Sitecore content

Sorting Sitecore Items by a Field Value

Crawled: On December - 29 - 2008 Source

Sitecore provides a variety of techniques to allow users and developers to control item sorting.

By default, Sitecore sorts the children of every item in alphabetical order by name. Users can sort items in the content tree. For each item, you can override this default by clicking the dialog launcher in the Sort group on the Home tab, and then selecting a sort rule.

Sitecore presents items in XML document order – from the top item in the CMS repository down. This order reflects the child sort rule for each item. In an XSL rendering, you can alter the default item order using the <xsl:sort> XSL element. For example, to sort the children of the context item by the date that they were last updated:

<ul>
  <xsl:for-each select="$sc_currentitem/item">
    <xsl:sort select="sc:fld('__updated',.)" />
    <li>
      <sc:link>
        <xsl:value-of select="@name" />
      </sc:link>
    </li>
  </xsl:for-each>
</ul>

In .NET, you can sort an array of Sitecore.Data.Items.Item objects. One important difference between these two approaches is that in XML, you sort a structure in the XML repository. In .NET, you sort a flat array containing any number of items. To demonstrate this difference, consider a TreelistEx field (or a Checklist field, or a Multilist field, or a Treelist field – any field that stores its value as a pipe-separated list of GUIDs).

You could use a TreelistEx field to let the user select some number of related content items, and use a presentation component to link to each of those items. In most cases you want to process the selected items in the order that the user specified them in the field, but in some cases you want to sort them using logic. In .NET you could sort this list of items by almost any criteria, for example the value of a field in each item, by implementing a System.Collections.Generic.IComparer<Sitecore.Data.Items.Item>.

Because the value of a TreelistEx field is a string of GUIDs rather than an XML document, XSL does not provide native facilities to sort this data. This is why Sitecore provides sc:Split() XSL extension method for processing fields that contain a list of pipe-separated GUIDs. The sc:Split() method accepts an item and the name of the field, and returns the IDs as temporary XML structure for easier parsing from XSL. While it might be possible to sort the items referenced by the XML structure returned from sc:Split() using pure XSL, it probably wouldn’t be very efficient to write or execute that code, and especially might not be easy for someone else to read. This is probably an appropriate place to use a .NET XSL extension. For more information about XSL extension methods, see the Presentation Component XSL Reference on the Sitecore Developer Network (http://sdn.sitecore.net).

To sort the items specified by a TreelistEx field by a field value in each of those items, we need an XSL extension method that accepts three parameters: the item containing the TreelistEx field, a string containing the name of the TreelistEx field, and a string containing the name of the field by which to sort the items. We might want a fourth parameter to reverse the sort order.

Because we will be passing the XML representation of an item from the XSL transformation engine to the XSL extension method, our class will inherit from Sitecore.Xml.Xsl.XslHelper so that we can use its GetItem() method, which retrieves the Sitecore.Data.Items.Item for use by .NET code that corresponds to the System.Xml.XPath.XPathNodeIterator() representation of the item that XSL uses.

The rest of the .NET code should be pretty straightforward, though maybe it isn’t well tested. The little mess at the end of the SortListField() method implementation could be avoided if the GetChildIterator() method of Sitecore.Xml.Xsl.XslHelper was not private. Because Sitecore stores dates in the ISO format corresponding to the .NET format string yyyyMMddTHHmmss, it’s probably not necessary to convert these values to dates, but it shouldn’t cost much.

using System;
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;
using Sitecore.Data.Items;

namespace Namespace.Xml.Xsl
{
  public class XslHelper : Sitecore.Xml.Xsl.XslHelper
  {
    public virtual XPathNodeIterator SortListField(string listField,
      string sortField, XPathNodeIterator ni)
    {
      return SortListField(listField, sortField, ni, false);
    }

    public virtual XPathNodeIterator SortListField(string listField,
      string sortField, XPathNodeIterator ni, bool reverse)
    {
      Sitecore.Xml.Packet packet = new Sitecore.Xml.Packet("values");
      XPathNodeIterator iterator = ni.Clone();

      if(iterator.MoveNext())
      {
        Sitecore.Data.Items.Item item = GetItem(iterator);

        if (item != null)
        {
          Sitecore.Data.Fields.MultilistField fList = item.Fields[listField];

          if (fList!= null)
          {
            List<Item> itemList = new List<Item>();

            foreach(Sitecore.Data.Items.Item pointer in fList.GetItems())
            {
              if( pointer != null )
              {
                itemList.Add(pointer);
              }
            }

            Item[] items = itemList.ToArray();
            Array.Sort(items, new ItemFieldComparer(sortField));

            if ( reverse )
            {
              Array.Reverse(items);
            }

            foreach (Sitecore.Data.Items.Item reference in items)
            {
              packet.AddElement("value", reference.ID.ToString());
            }
          }
        }
      }

      XPathNavigator navigator = packet.XmlDocument.CreateNavigator()
        ?? new XmlDocument().CreateNavigator();
      navigator.MoveToRoot();
      navigator.MoveToFirstChild();
      return navigator.SelectChildren(XPathNodeType.Element);
    }
  }

  public class ItemFieldComparer : IComparer<Item>
  {
    private const int Y_FIRST = -1;
    private const int X_EQUALTO_Y = 0;
    private const int X_FIRST = 1;

    private readonly string _fieldName = null;

    public ItemFieldComparer(string fieldName)
    {
      _fieldName = fieldName;
    }

    public int Compare(Item x, Item y)
    {
      if (x == null && y == null)
        return X_EQUALTO_Y;

      if (y == null)
        return X_FIRST;

      if (x == null)
        return Y_FIRST;

      Sitecore.Data.Fields.Field xField = x.Fields[_fieldName];
      Sitecore.Data.Fields.Field yField = y.Fields[_fieldName];

      if (yField == null && xField == null)
        return X_EQUALTO_Y;

      if (xField != null
        && (yField == null || String.IsNullOrEmpty(yField.Value))
        && !String.IsNullOrEmpty(xField.Value))
        return X_FIRST;

      if (yField != null &&
        (xField == null || String.IsNullOrEmpty(xField.Value))
        && !String.IsNullOrEmpty(yField.Value))
        return Y_FIRST;

      if (yField.Value == xField.Value)
        return X_EQUALTO_Y;

      string fieldType = xField.Type.ToLower();

      if (xField.ID == Sitecore.FieldIDs.Sortorder)
        fieldType = "integer";

      switch (fieldType)
      {
        case "date":
        case "datetime":
          return
            ((Sitecore.Data.Fields.DateField)xField).DateTime.CompareTo(
              ((Sitecore.Data.Fields.DateField)yField).DateTime);
        case "integer":
          return Int32.Parse(xField.Value).CompareTo(Int32.Parse(yField.Value));
        default:
          return xField.Value.CompareTo(yField.Value);
      }
    }
  }
}

This code returns an XML structure something like:

<values>
  <value>GUID</value>
  ...
  <value>GUID</value>
</values>

You can update the default sc namespace to include this method by changing the <extension> element in web.config with namespace http://www.sitecore.net/sc:

<extension mode="on" type="Namespace.Xml.Xsl.XslHelper, Assembly" namespace="http://www.sitecore.net/sc" singleInstance="true" />

Then you can then invoke the XSL extension method from XSL renderings as follows, for example to sort the items selected in the field named TreelistExField in the context item by the the value of the field named SortField in each item:

<ul>
  <xsl:for-each select="sc:SortListField('TreelistExField','SortField',$sc_currentitem)">
    <xsl:for-each select="sc:item(text(),$sc_currentitem)">
      <li>
        <sc:link>
          <xsl:value-of select="@name" />
        </sc:link>
      </li>
    </xsl:for-each>
  </xsl:for-each>
</ul>

You can reverse the sort order by passing True as the fourth parameter:

<xsl:for-each select="sc:SortListField('TreelistExField','SortField',$sc_currentitem,true())">

The outer for-each iterates over each of the <value> elements in the XML structure. The inner for-each iterates over the item that corresponds to the GUID within that <value> element. It might seem unusual to for-each over a single element, but actually makes the code more clear by setting the context item within the nested for-each to the referenced item rather than using a variable.

John West Blogs about Sitecore

Comments are closed.

Sitecore Lucene index does not remove old data

Posted by admin
Oct-30-2011 I Comments Off

Teach User Manager how to search by email

Posted by admin
Oct-30-2011 I Comments Off

Language filtered Multilist field

Posted by admin
Oct-30-2011 I Comments Off