I’ve been doing quite a bit of work in Sitecore recently and last week I encountered a situation that it appears many others have hit. I was working with a field that had been configured originally as a grouped droplink, but now needed to be updated to support additional levels of hierarchy in the folder structure. If you’ve done any work in Sitecore that statement makes sense, but if not it may seem a bit cryptic. Sitecore offers a number of different field types and a subset of these field types focus on providing links either to other items on the content tree or to content that is not stored in Sitecore. In the case of the grouped droplink, the field is configured with a “root” folder and each direct descendant of this folder is considered to be a header for a grouping of other items and displayed in a dropdown. A picture is worth a thousand words, so consider the following piece of a content tree: If I configure a grouped droplink field to use the “Current” folder as its datasource, the control that gets to my content author looks like this: This presents a nicely organized display and limits the user to selecting only the direct grandchildren of the folder root. It also presents the limitation that struck as we were thinking through the content architecture and how it would hold up over time – the authors cannot further organize content under the root folder because of the structure required for the dropdown to work. Over time, not allowing the hierarchy to go any deeper would prevent out authors from being able to organize their content in a way that it would be found when needed, so the grouped droplink data type was not going to fit the bill. I needed to look for an alternative data type that allowed for selection of a single item and limited my choices to descendants of a specific node on the content tree. After looking at the options available for links in Sitecore and considering them against each other, one option stood out as nearly perfect – the droptree. This field type stores its data identically to the droplink and allows for the selection of zero or one items under a specific node in the content tree. By changing my data template to use droptree instead of grouped droplink, the author is now presented with the following when selecting a linked item: Sounds great, but a did say almost perfect – there’s still one flaw. The code intended to display the linked item is expecting the selection to use a specific data template (or more precisely it makes certain assumptions about the fields that will be present), but the droptree does nothing to prevent the author from selecting a folder (since folders are items too) instead of one of the items contained within a folder. I looked to see if anyone had already solved this problem. I found many people discussing the problem, but the closest that I found to a solution was the statement “the best thing would probably be to create a custom validator” with no further discussion in regards to what this validator might look like. I needed to create my own validator to ensure that the user had not selected a folder. Since so many people had the same issue, I decided to make the validator as reusable as possible and share it here. The validator that I created inherits from StandardValidator. In order to make the validator more intuitive to developers that are familiar with the TreeList controls in Sitecore, I chose to implement the following parameters: ExcludeTemplatesForSelection – serves as a “deny list”. If the data template of the selected item is in this list it will not validate IncludeTemplatesForSelection – this can either be empty to indicate that any template not contained in the exclusion list is acceptable or it can contain the list of acceptable templates Now that I’ve explained the parameters and the purpose of the validator, I’ll let the code do the rest of the talking: 1: /// <summary>
2: /// Validates that a link field value meets template requirements
3: /// specified using the following parameters:
4: /// - ExcludeTemplatesForSelection: If present, the item being
5: /// based on an excluded template will cause validation to fail.
6: /// - IncludeTemplatesForSelection: If present, the item not being
7: /// based on an included template will cause validation to fail
8: ///
9: /// ExcludeTemplatesForSelection trumps IncludeTemplatesForSelection
10: /// if the same value appears in both lists. Lists are comma seperated
11: /// </summary>
12: [Serializable]
13: public class LinkItemTemplateValidator : StandardValidator
14: {
15: public LinkItemTemplateValidator()
16: {
17: }
18:
19: /// <summary>
20: /// Serialization constructor is required by the runtime
21: /// </summary>
22: /// <param name="info"></param>
23: /// <param name="context"></param>
24: public LinkItemTemplateValidator(SerializationInfo info, StreamingContext context) : base(info, context) { }
25:
26: /// <summary>
27: /// Returns whether the linked item meets the template
28: /// constraints specified in the parameters
29: /// </summary>
30: /// <returns>
31: /// The result of the evaluation.
32: /// </returns>
33: protected override ValidatorResult Evaluate()
34: {
35: if (string.IsNullOrWhiteSpace(ControlValidationValue))
36: {
37: return ValidatorResult.Valid; // let "required" validation handle
38: }
39:
40: var excludeString = Parameters["ExcludeTemplatesForSelection"];
41: var includeString = Parameters["IncludeTemplatesForSelection"];
42: if (string.IsNullOrWhiteSpace(excludeString) && string.IsNullOrWhiteSpace(includeString))
43: {
44: return ValidatorResult.Valid; // "allow anything" if no params
45: }
46:
47: Guid linkedItemGuid;
48: if (!Guid.TryParse(ControlValidationValue, out linkedItemGuid))
49: {
50: return ValidatorResult.Valid; // probably put validator on wrong field
51: }
52:
53: var item = GetItem();
54: var linkedItem = item.Database.GetItem(new ID(linkedItemGuid));
55:
56: if (linkedItem == null)
57: {
58: return ValidatorResult.Valid; // this validator isn't for broken links
59: }
60:
61: var exclusionList = (excludeString ?? string.Empty).Split(',');
62: var inclusionList = (includeString ?? string.Empty).Split(',');
63:
64: if ((inclusionList.Length == 0 || inclusionList.Contains(linkedItem.TemplateName))
65: && !exclusionList.Contains(linkedItem.TemplateName))
66: {
67: return ValidatorResult.Valid;
68: }
69:
70: Text = GetText("The field \"{0}\" specifies an item which is based on template \"{1}\". This template is not valid for selection", GetFieldDisplayName(), linkedItem.TemplateName);
71:
72: return GetFailedResult(ValidatorResult.FatalError);
73: }
74:
75: protected override ValidatorResult GetMaxValidatorResult()
76: {
77: return ValidatorResult.FatalError;
78: }
79:
80: public override string Name
81: {
82: get { return @"LinkItemTemplateValidator"; }
83: }
84: }
In this blog entry, I have shared some code that I found useful in solving a problem that seemed fairly common. Hopefully the next person that is looking for this answer finds it useful as well.