There are certain scenarios when working with Sitecore when you need to hide fields on specific Sitecore items in the content editor. Its worth noting the solution shown below actually hides things rather than correcting the template hierarchy. For certain scenarios this may be a quicker solution which doesn’t require editing any of the existing content of the Sitecore tree.
The two key aspects are:
- Configuring the rules for hiding and showing fields
- Applying the rules to the ui via the getContentEditorFields pipeline
As per normal, Sitecore makes this easy once you know which pipelines to tap into. During the lifecycle of an item in the content editor several pipelines run – the difference here is some are from Sitecore.Client rather than the kernel. In the example shown below the bulk of the code is around querying xml (config) for the rules. This could come from any datastore.
The rules I adopted were pretty simple and based around the following idea. Either you always want to show or always want to hide the field. You would then want to decide the opposite case eg when to show the field based on a set of rules – here it uses the context item as the starting point for each rule.
An example rule then becomes: Always hide the logo field unless you are below the navigation node.
Why do this – surely this points at a problem with my template hierachy? Yes it does. However the amount of regression testing involved with changing base templates of a large tree could be high. Certain assertions in code may apply rules based on templateId so changing base templates isnt a feasible option. It may be down the line you do want the logo field on certain items elsewhere in the tree as well – to bring that into play would mean simply config changes.
First you need the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
using System; using System.Collections.Generic; using System.Linq; using System.Xml; using Sitecore.Configuration; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Data.Templates; using Sitecore.Diagnostics; using Sitecore.SecurityModel; using Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields; namespace ###.Domain.Cms.Specialization.Pipelines.GetContentEditorFields { /// <summary> /// Hide fields in specific areas of the tree based on filter rules /// </summary> public class FieldFilter : GetFields { protected override bool CanShowField(Field field, TemplateField templateField) { Assert.ArgumentNotNull(field, "field"); Assert.ArgumentNotNull(templateField, "templateField"); using (new SecurityDisabler()) { FilterableField[] fields = LoadFieldRules().ToArray(); if (field.Item != null) { //find matching filter FilterableField filter = fields.FirstOrDefault(a => a.Id == field.ID.Guid); if (filter != null) { if (filter.Rules.Any(a => a.Evaluate(field.Item))) { return filter.AlwaysHide; } else { return !filter.AlwaysHide; } } } } return base.CanShowField(field, templateField); } private IEnumerable<FilterableField> LoadFieldRules() { XmlNode node = Factory.GetConfigNode("fieldFilters"); if (node != null) { foreach (FilterableField field in LoadFields(node.SelectSingleNode("alwaysHide"), true)) { yield return field; } foreach (FilterableField field in LoadFields(node.SelectSingleNode("alwaysShow"), false)) { yield return field; } } } private IEnumerable<FilterableField> LoadFields(XmlNode rootNode, bool alwaysHide) { if (rootNode != null) { foreach (XmlNode fields in rootNode.ChildNodes) { FilterableField field = new FilterableField(); bool valid = true; try { field.Id = CastGuid(fields.Attributes["id"].Value, Guid.Empty); field.Rules = LoadRules(fields).ToArray(); field.AlwaysHide = alwaysHide; } catch { valid = false; } if (valid) { yield return field; } } } } private IEnumerable<IFilterRule> LoadRules(XmlNode alwaysHideField) { foreach (XmlNode rule in alwaysHideField.ChildNodes) { if (String.Equals(rule.Name, "unlessIsDescendentOf", StringComparison.OrdinalIgnoreCase)) { yield return new IsDescendentOfFilterRule(rule.Attributes["value"].Value); } //parse any new rules you require.... } } internal static Guid CastGuid(object value, Guid defaultValue) { try { Guid guid = new Guid(value.ToString()); return guid; } catch (Exception e) { return defaultValue; } } class FilterableField { public Guid Id { get; set; } public IFilterRule[] Rules { get; set; } /// <summary> /// Determines whether to always show or always hide the field /// </summary> public bool AlwaysHide { get; set; } } interface IFilterRule { bool Evaluate(Item currentItem); } class IsDescendentOfFilterRule : IFilterRule { object _value; public IsDescendentOfFilterRule(object value) { _value = value; } public bool Evaluate(Item currentItem) { Guid childValue = CastGuid(_value, Guid.Empty); if (childValue != Guid.Empty && currentItem != null) { return currentItem.Axes.GetAncestors().Any(a => a.ID.Guid == childValue); } return false; } } } } |
This is then patched into the config via:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <fieldFilters> <alwaysHide> <field id="{61FB9634-CB92-4845-BE90-7E2D648403C2}" notes="link icon on link template - only show if used in the header"> <unlessIsDescendentOf value="{A6C6D94C-ACC3-497D-AE1B-CFA4E6A48B6B}" notes="Primary Navigation" /> </field> </alwaysHide> <alwaysShow> <!--<field .... />--> </alwaysShow> </fieldFilters> <pipelines> <getContentEditorFields> <processor type="Sitecore.Shell.Applications.ContentEditor.Pipelines.GetContentEditorFields.GetFields, Sitecore.Client" > <patch:attribute name="type">###.Domain.Cms.Specialization.Pipelines.GetContentEditorFields.FieldFilter, ###.Domain.Cms</patch:attribute> </processor> </getContentEditorFields> </pipelines> </sitecore> </configuration> |
What I like about this approach:
- Additional field rules can be added without any code changes – its all config (as long as the rules you need eg unlessIsDescendentOf exist)
- The patch:attribute on the include file allows swapping out the existing type attribute
- Its very easy to bolt in new IFilterRules
What I don’t like about this approach:
- I’ve subclassed the dtos and interfaces for eg IFilterRule – the rational here was these are only ever going to needed by the FieldFilter class (and it kept the amount of code blocks in the blog post down :S)
- More complex combinations of IFilterRules eg combining and/or logic doesn’t work yet
- It loads the config for every field – some simple caching would get around this
- Big or naive filter rules could really slow down the content editor