Sitecore for Developers
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:
[code language="xml"]<sitecore> <processors> <saveUI> <processor type="Project.SaveUI.ConvertDynamicLayout, Project" patch:instead="processor[@type='Sitecore.Pipelines.Save.ConvertLayoutField, Sitecore.Kernel']" /> </saveUI> </processors></sitecore>[/code]
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:
[code language="csharp"]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) && before != xmlDocument1.InnerXml){ saveField.Value = xmlDocument1.InnerXml;}else if (CompareNodes(xmlDocument1, xmlDocument)){ saveField.Value = xmlDocument1.InnerXml;}[/code]
The first step in the RemoveDynamicOrphans method is to drill down to the components on the page, using the GetChildElements from the ConvertLayoutField class:
[code language="csharp"]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;[/code]
Then, separate the components into ones with dynamic placeholders and ones without:
[code language="csharp"]var dynamicComponents = new Dictionary<XmlNode, string>();var otherComponents = new Dictionary<XmlNode, string>();
ParseComponents(components, dynamicComponents, otherComponents);
if (dynamicComponents.Count < 1) return;[/code]
The ParseComponents method consists of looping through the components, checking for a Regex match on the placeholder and adding to the appropriate dictionary.
[code language="csharp"]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 && match.Groups.Count > 0) { dynamicComponents.Add(component, match.Groups[0].ToString().ToLower()); } else { otherComponents.Add(component, attributes[uid].Value.ToLower().Trim('{', '}')); }}[/code]
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:
[code language="csharp"]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;[/code]
Now that we have our orphans, all that’s left is to remove them and rebuild the InnerXml of the parent node:
[code language="csharp"]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;}[/code]