Sitecore Orphan Children after Parent Component Removal

I was tasked with building a navigation carousel component, designed so the carousel itself is just a placeholder that will allow components to be added to it. This design allowed for easily adding components, removing them, and moving them around within the carousel. The problem arose when we tried to remove the carousel that had components on the placeholder, I’ll call them navigation items. When reviewing the layout details after removing the carousel, the navigation items would remain in the layout. If a new carousel was then added to the page, the old navigation items would appear in the component instead of an empty placeholder. I decided to use dynamic placeholders as part of the solution; this is the dynamic placeholder solution that appends the unique identifier of the parent component as the placeholder identifier. Dynamic placeholders got rid of the problem of the old navigation items showing up when adding a new carousel, but they were still present in the layout details. Dynamic placeholders would allow, not to mention more flexibility of the components added to the placeholder, finding any control that had a dynamic placeholder and making sure there was a parent control with that unique id on the page.

The first hurdle was to determine where to put this logic; I was pointed to this article based on a similar situation. From the article, we need to hook into the saveUI pipeline:

<sitecore>
  <processors>
    <saveUI>
      <processor type="Project.SaveUI.ConvertDynamicLayout, Project" 
       patch:instead="processor[@type='Sitecore.Pipelines.Save.ConvertLayoutField, Sitecore.Kernel']" />
    </saveUI>
  </processors>
</sitecore>

We are replacing the ConvertLayoutField in order to keep most of the logic but make our changes once we have the layouts in XmlDocuments. Once in XmlDocument form, we need to copy the InnerXml for comparison after attempting to remove the orphans. We want to save the new layout if it has changed after looking for orphans:

XmlDocument xmlDocument = XmlUtil.LoadXml(value);
XmlDocument xmlDocument1 = XmlUtil.LoadXml(str);

if (xmlDocument1 == null || xmlDocument == null) continue;

var before = xmlDocument1.InnerXml;
RemoveDynamicOrphans(xmlDocument1);

if (!string.IsNullOrWhiteSpace(before) &amp;&amp; before != xmlDocument1.InnerXml)
{
    saveField.Value = xmlDocument1.InnerXml;
}
else if (CompareNodes(xmlDocument1, xmlDocument))
{
    saveField.Value = xmlDocument1.InnerXml;
}

The first step in the RemoveDynamicOrphans method is to drill down to the components on the page, using the GetChildElements from the ConvertLayoutField class:

var root = GetChildElements(newNode);
if (!root.Any()) return;

var pageElement = GetChildElements(root.FirstOrDefault());
if (!pageElement.Any()) return;

var components = GetChildElements(pageElement.FirstOrDefault());
if (!components.Any()) return;

Then, separate the components into ones with dynamic placeholders and ones without:

var dynamicComponents = new Dictionary<XmlNode, string>();
var otherComponents = new Dictionary<XmlNode, string>();

ParseComponents(components, dynamicComponents, otherComponents);

if (dynamicComponents.Count < 1) return;

The ParseComponents method consists of looping through the components, checking for a Regex match on the placeholder and adding to the appropriate dictionary.

const string ph = "s:ph", uid = "uid";
foreach (var component in components)
{
    if (component.Attributes == null) continue;
    var attributes = component.Attributes;

    if (attributes[ph] == null) continue;

    var regex = new Regex(DYNAMIC_KEY_REGEX);
    var match = regex.Match(attributes[ph].Value);

    if (match.Success &amp;&amp; match.Groups.Count > 0)
    {
        dynamicComponents.Add(component, match.Groups[0].ToString().ToLower());
    }
    else
    {
        otherComponents.Add(component, attributes[uid].Value.ToLower().Trim('{', '}'));
    }
}

Once the dictionaries are set, we can look at the dynamic components and compare to the other components to determine if the dynamic placeholder uid matches the uid of another component. If not, it is an orphan that needs to be removed:

var orphans = (from node in dynamicComponents
    let foundOne = otherComponents.Any(other => other.Value == node.Value)
    where !foundOne
    select node.Key).ToList();

if (orphans.Count < 1) return;

Now that we have our orphans, all that’s left is to remove them and rebuild the InnerXml of the parent node:

foreach (var orphan in orphans)
{
    components.Remove(orphan);
}

var rebuiltInner = components.Aggregate(string.Empty, (current, component) => component.OuterXml + current);
if (!string.IsNullOrWhiteSpace(rebuiltInner))
{
    pageElement.First().InnerXml = rebuiltInner;
}
Advertisements

5 Responses to “Sitecore Orphan Children after Parent Component Removal”

  1. Daniel Wood Says:

    Hi,
    I am trying to implement this in my project.
    I am wondering if you can post your class Project.SaveUI.ConvertDynamicLayout as I cant seem to work out how that will look!
    Cheers,
    Daniel

    • Mike Tschida Says:

      Hi Daniel,

      This is basically it for the class. If you look at Sitecore.Pipelines.Save.ConvertLayouField with a decompiler you can see I pick up in Process at the two LoadXml lines.

      • Daniel Wood Says:

        Yea great I have got that far I think.
        I am wondering how your structure your Regex?

      • Mike Tschida Says:

        If you followed the linked article for dynamic placeholders, you’d want to use one for guid’s. I’m using “[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}”

  2. Daniel Wood Says:

    Hi can you please post your class Project.SaveUI.ConvertDynamicLayout I cannot seem to work out how this will look!
    Cheers


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: