Complex Page Structure Simplified With Branches

Monday, March 02, 2015 @ 09:00

Typical Content Structure

Below is a typical example of how we setup Sitecore content with properly setup presentation, using an item representing the page with sub-items as the principle pieces of content, tied to their presentation components as data sources. These pages can be very easily modified in the Page Editor, but creation of new Blog or Article pages is somewhat cumbersome. We can alleviate this with some clever use of Branches.

Typical Content Setup

Setting Up The Branches

The first step is to setup the branches. Branches, much like standard values, allow you to define an items insert options, up to and including presentation details.

Branch Setup

While we can define presentation details, the datasource definitions however explicate an item ID as their source. If we setup the datasource on our branch presentation details, when we insert our presentation, it will still point at the branch sub-items. We have to fix this issue to make this option viable. The end goal is for our branch-definitions to define their presentation's datasources in relative terms, i.e. ./PageContent/Article, ./PageContent/Sidebar. However, if we do that now, Sitecore will quite justly complain that we have broken links. We have three hurdles to overcome to get Sitecore to accept our new paradigm.

Resolving Relative Datasources

The first hurdle is to give Sitecore meaning surrounding the relative datasource. We can do this by adding an OnItemSaving handler that can resolve the relative path to an item ID. Like all things in Sitecore, the presentation details or LayoutField at it's most fundamental level is just a string. The layout field is an XML Serialization of how we present, but don't be intimidated. The only thing we need to worry about are the datasources of the presentation components. These show up in the XML as “DS='{00000000-0000-0000-0000-000000000000}'”. So all we need to do is find all instances of “DS='./Some/Path'”, resolve that path and replace it with the Guid of that item. Once in place, add the handler to your item:saving event pipeline.

   
public class RelativeDataSourceHandler
{
    public static string rxRelativeDataSource = "(?<=ds\\=[\"'])\\.(/[A-Z0-9]*)*(?=[\"'])";
    Regex expDatasource = new Regex(rxRelativeDataSource, RegexOptions.IgnoreCase);
    private const string RENDERFIELD = "{F1A1FE9E-A60C-4DDB-A3A0-BB5B29FE732E}";

    public void OnItemSaving(object sender, EventArgs e)
    {
        Item _saveItem = Event.ExtractParameter(e, 0) as Item;

        if (IsContentItem(_saveItem) || _saveItem.Fields[Sitecore.FieldIDs.LayoutField] ==
             null || string.IsNullOrEmpty(_saveItem.Fields[Sitecore.FieldIDs.LayoutField].Value))
            return;

        string layoutField = _saveItem.Fields[Sitecore.FieldIDs.LayoutField].Value;
        foreach (Match _match in expDatasource.Matches(layoutField))
        {
            string targetPath = _match.Value.Replace(".", _saveItem.Paths.Path);
            Item targetItem = _saveItem.Database.GetItem(targetPath);
            if (targetItem == null)
            {
                Sitecore.Diagnostics.Log.Error(string.Format("Error saving item '{0}'. 
                Relative target datasource ('{1}') does not exist.", _saveItem.Paths.Path, 
_match.Value), new Exception(), this); continue; } layoutField = layoutField.Replace(_match.Value, targetItem.ID.ToString()); } _saveItem.Fields[Sitecore.FieldIDs.LayoutField].Value = layoutField; } /// <summary> /// Assesses whether the item is a template standard value or a branch /// </summary> /// <param name="_item">The item to assess</param> /// <returns></returns> private bool IsContentItem(Item _item) { return _item.Paths.Path.ToLower().Contains("/sitecore/templates") || _item.Paths.Path.ToLower().Contains("/sitecore/layout") || _item.Paths.Path.ToLower().Contains("/sitecore/media library") || _item.Paths.Path.ToLower().Contains("/sitecore/system"); } }

 

Broken Links Gutters

The next two hurdles we need to overcome relate to Sitecore's link checking. When enabled, the Content Editor's left gutter will warn us any time that a link is broken. So we need a broken link check that is aware that we can define links in a relative fashion. In that, we still need to advise the user when we have an unresolved link. Our best bet is to derive from our existing BrokenLink gutter Sitecore.Shell.Applications.ContentEditor.Gutters.BrokenLinks. As our custom save handler is checking the layout fields only, we simply check that field for a relative link, return our custom gutter if so, and if not, we pass the rest of the operation off to our base class:

public class BrokenLinks : Sitecore.Shell.Applications.ContentEditor.Gutters.BrokenLinks
{
    protected override GutterIconDescriptor GetIconDescriptor(Item item)
    {
        ItemLink[] brokenLinks = item.Links.GetBrokenLinks();
        if (brokenLinks == null || brokenLinks.Length == 0)
            return (GutterIconDescriptor)null;

        Regex expRelativeDataSource = new Regex(RelativeDataSourceHandler
        .rxRelativeDataSource, RegexOptions.IgnoreCase);

        if (brokenLinks.Count(i => i.SourceFieldID != Sitecore.FieldIDs.LayoutField) == 
            0 && expRelativeDataSource.IsMatch(item[Sitecore.FieldIDs.LayoutField]))
        {
            GutterIconDescriptor gutterIconDescriptor = new GutterIconDescriptor();
            gutterIconDescriptor.Icon = "Multimedia/16x16/add_to_list.png";
            gutterIconDescriptor.Tooltip = "This item has a relative datasource defined 
in presentation. Datasource will be resolved when the item is saved."; return gutterIconDescriptor; } else return base.GetIconDescriptor(item); } }

 

Once we have the code in place, we need to tell Sitecore to consume it. Gutters are defined in the core database under content\Applications\Content Editor\Gutters\. Edit the Broken Link gutter, change the class to the one we've created and voila! Our gutter is now relative datasource aware.

Save Pipeline CheckLinks

The last component we need to satisfy for our strategy to work is a processor in the saveUI pipeline, CheckLinks. Right now if you attempt to define a relative datasource and save an item, you'll get an error message complaining about the broken link. To get past that, we need a new CheckLinks processor that vets relative datasources. Below is the code for our CheckLinks processor. It looks quite complicated, but not so much as it seems. The broken links gutter was very easy to extend to be relative datasource aware because we had a method we could override. Unfortunately, our CheckLinks processor is not similarly extendable. The code below is the Sitecore Kernel CheckLinks method decompiled, with the relative datasource awareness added to it.

public class CheckForRelativeLinks
{
    public void Process(SaveArgs args)
    {
        Regex expRelativeDataSource = new Regex(RelativeDataSourceHandler
.rxRelativeDataSource, RegexOptions.IgnoreCase); if (!args.HasSheerUI) return; if (args.Result == "no" || args.Result == "undefined") { args.AbortPipeline(); } else { int num = 0; if (args.Parameters["LinkIndex"] == null) args.Parameters["LinkIndex"] = "0"; else num = MainUtil.GetInt(args.Parameters["LinkIndex"], 0); for (int index = 0; index < args.Items.Length; ++index) { if (index >= num) { ++num; SaveArgs.SaveItem saveItem = args.Items[index]; Item obj = Context.ContentDatabase.Items[saveItem.ID,
saveItem.Language, saveItem.Version];
if (obj != null) { obj.Editing.BeginEdit(); foreach (SaveArgs.SaveField saveField in saveItem.Fields) { Field field = obj.Fields[saveField.ID]; if (field != null) field.Value = string.IsNullOrEmpty(saveField.Value) ?
(string)null : saveField.Value; } bool allVersions = false; ItemLink[] brokenLinks = obj.Links.GetBrokenLinks(allVersions) .Where(i => i.SourceFieldID != Sitecore.FieldIDs.LayoutField && expRelativeDataSource.IsMatch(obj[Sitecore.FieldIDs.LayoutField])) .ToArray(); if (brokenLinks.Length > 0) { ShowDialog(obj, brokenLinks); args.WaitForPostBack(); break; } else obj.Editing.CancelEdit(); } } } args.Parameters["LinkIndex"] = num.ToString(); } } private static void ShowDialog(Item item, ItemLink[] links) { StringBuilder stringBuilder = new StringBuilder(Translate.Text("The item \"{0}\" contains broken links in these fields:\n\n", new object[1] { item.DisplayName })); bool flag = false; foreach (ItemLink itemLink in links) { if (!itemLink.SourceFieldID.IsNull) { Field field = item.Fields[itemLink.SourceFieldID]; if (field != null) { stringBuilder.Append(" - "); stringBuilder.Append(field.DisplayName); } else { stringBuilder.Append(" - "); stringBuilder.Append(Translate.Text("[Unknown field: {0}]", new[] { itemLink.SourceFieldID.ToString() })); } if (!string.IsNullOrEmpty(itemLink.TargetPath) &&
!Sitecore.Data.ID.IsID(itemLink.TargetPath)) { stringBuilder.Append(": \""); stringBuilder.Append(itemLink.TargetPath); { stringBuilder.Append("\""); } stringBuilder.Append("\n"); } else flag = true; } if (flag) { stringBuilder.Append("\n"); stringBuilder.Append(Translate.
Text("The template or branch for this item is missing.")); } stringBuilder.Append("\n"); stringBuilder.Append(Translate.Text("Do you want to save anyway?")); Context.ClientPage.ClientResponse.Confirm(((object)stringBuilder).ToString()); } } }

 

In Practice

Once the aforementioned components are in place, you are setup. We have everything we need. Modify the presentation details of our branch items with our relative datasources. You'll note that the relative datasource gutter we created will show for our branches. Once defined, upon initially inserting from these branches, you'll see the new gutter as well. It's not until the items are changed and saved that we'll see the datasources resolve to explicit item ids.

Conclusion

Now that we have relative datasources we can use advanced information architecture without burdening our content authors with it's complexity.