Adding code: query to a Rendering's Datasource Location Field


I have a multi site environment with an item bucket in different locations in each site. I have a component rendering that I want to be shared across each site. Rather than copying the rendering for each site, I decided to implement the code: prefix in the Renderings Datasource Location field. If you need more information on the code: prefix in source fields, please see this blog post.First, you will need to add reference to Sitecore.Buckets.dll in our project so we can reuse Sitecore.Buckets.FieldTypes.IDataSource. Next we will need to add a reference to Sitecore.ContentSearch.Linq.dll so we can reuse Sitecore.ContentSearch.Utilities.ReflectionUtility.Next, we can create a patch config to add our custom class for finding data source items based on code to the getRenderingDatasource pipeline:[code language="xml"]<configuration xmlns:patch=""> <sitecore> <pipelines> <getRenderingDatasource> <processor type="Hi.Sc.Pipelines.GetRenderingDatasource.GetCodeDatasourceLocations, Hi.Sc" patch:before="*[1]" /> </getRenderingDatasource> </pipelines> </sitecore></configuration>[/code]Now lets take a look at our new class:[code language="csharp"]using Sitecore.Buckets.FieldTypes;using Sitecore.ContentSearch.Utilities;using Sitecore.Data;using Sitecore.Data.Items;using Sitecore.Diagnostics;using Sitecore.Pipelines.GetRenderingDatasource;using Sitecore.Text;using System;namespace Hi.Sc.Pipelines.GetRenderingDatasource{ public class GetCodeDatasourceLocations { #region Fields private const string _codePrefix = "code:"; private const string _datasourceLcationFieldName = "Datasource Location"; private Item _contextItem; #endregion #region Properties private Database ContentDatabase { get; set; } private Item ContextItem { get { if (_contextItem == null) { _contextItem = ContentDatabase.GetItem(ContextItemPath); } return _contextItem; } } private string ContextItemPath { get; set; } #endregion #region Methods public void Process(GetRenderingDatasourceArgs args) { Assert.IsNotNull(args, "args"); ContentDatabase = args.ContentDatabase; ContextItemPath = args.ContextItemPath; foreach (string str in new ListString(args.RenderingItem[_datasourceLcationFieldName])) { if (str.StartsWith(_codePrefix, StringComparison.InvariantCulture)) { IDataSource dataSource = ReflectionUtility.CreateInstance(Type.GetType(str.Substring(_codePrefix.Length))) as IDataSource; if (dataSource != null) { args.DatasourceRoots.AddRange(dataSource.ListQuery(ContextItem)); } } } } #endregion }}[/code]Line 57 calls ListString(args.RenderingItem[_datasourceLcationFieldName]) which takes whatever is in your Rendering's Datasource Location field and splits it into separate string delimited by the pipe (|) character. Line 59 checks to see if our string starts with the "code:" prefix. If it does, we will use the ReflectionUtility on line 61 to call our class which implements IDataSource. If dataSource is not null, we can add the call to dataSource.ListQuery to args.DatasourceRoots as a location to put our data source item when we are adding a component with the Experience Editor. Let's look at an example. On a new Sitecore instance. I've updated the /sitecore/layout/Renderings/Sample/Sample Rendering Datasource Location field to code:Hi.Mvc.Configuration.Renderings.DatasourceLocations.HomeItemDatasourceLocation, Hi.Mvc

datasource location field

Next, let's look at our HomeItemDatasourceLocation implementation:[code language="csharp"]using Sitecore.Buckets.FieldTypes;using Sitecore.Data.Items;namespace Hi.Mvc.Configuration.Renderings.DatasourceLocations{ public class HomeItemDatasourceLocation : IDataSource { public Item[] ListQuery(Item item) { Item homeItem = item; while (homeItem.Parent != null && homeItem.ID != Sitecore.ItemIDs.ContentRoot) { homeItem = homeItem.Parent; } return new Item[] { homeItem }; } }}[/code]This class takes the current item and traverses to the item with a parent of /sitecore/content. When I go to the Experience Editor and add a sample rendering to my page, it correctly selects the /sitecore/content/Home item as my data source location. For my problem described in the first paragraph, I wrote some code to find the root item, then used the link database to find all the item buckets and filtered out the ones that didn't have a path starting with my root items path.Problems:You'll notice that we have two problems. The first is that the Broken Links item validation shows a warning for datasource location. The second is that saving the rendering causes the warning message:The item "Sample Rendering" contains broken links in these fields: - Datasource Location: "code:Hi.Mvc.Configuration.Renderings.DatasourceLocations.HomeItemDatasourceLocation, Hi.Mvc" Do you want to save anyway?I wanted to solve this by overriding Sitecore.Links.ItemLinks but there is no provider or way to override it. So we need to do something different.Problem 1To fix the first problem, we need to use our favorite decompiler (dotPeek or Just Decompile) and navigate to Sitecore.Data.Validators.ItemValidators.BrokenLinkValidator in the Sitecore.Kernel.dll. We will basically copy this class and override the Evaluate method so we can change one line:[code language="csharp"]using Sitecore.Data;using Sitecore.Data.Fields;using Sitecore.Data.Items;using Sitecore.Data.Validators;using Sitecore.Globalization;using Sitecore.Links;using System.Collections.Generic;using System.Linq;using System.Text;namespace Hi.Sc.Data.Validators.ItemValidators{ public class BrokenLinkValidator : Sitecore.Data.Validators.ItemValidators.BrokenLinkValidator { protected override Sitecore.Data.Validators.ValidatorResult Evaluate() { Item obj = this.GetItem(); if (obj == null) { return ValidatorResult.Valid; } ItemLink[] brokenLinks = GetBrokenLinks(obj); if (brokenLinks.Length <= 0) { return ValidatorResult.Valid; } StringBuilder stringBuilder = new StringBuilder(); foreach (ItemLink itemLink in brokenLinks) { stringBuilder.Append("\n"); ID sourceFieldId = itemLink.SourceFieldID; if (ID.IsNullOrEmpty(sourceFieldId)) { stringBuilder.Append(Translate.Text("Template or branch")); } else { Field field = obj.Fields[sourceFieldId]; if (field == null) { stringBuilder.Append(Translate.Text("[Unknown field]")); } else { stringBuilder.Append(field.DisplayName); } } } this.Text = this.GetText("This item contains broken links in:{0}", stringBuilder.ToString()); return this.GetFailedResult(ValidatorResult.Warning); } public static ItemLink[] GetBrokenLinks(Item obj, bool allVersions = false) { List<ItemLink> brokenItemLinkList = new List<ItemLink>(); Field datasourceLocationField = obj.Fields["Datasource Location"]; foreach (ItemLink link in obj.Links.GetBrokenLinks(allVersions)) { if (datasourceLocationField != null && link.SourceFieldID == datasourceLocationField.ID) { if (!datasourceLocationField.Value.Contains("code:")) { brokenItemLinkList.Add(link); } } else { brokenItemLinkList.Add(link); } } return brokenItemLinkList.ToArray(); } }}[/code]We are overriding the Evaluate method so we can change line 23 to call into our custom method to return broken links. We are making this a static method so we can reuse it to solve the second problem. Our static GetBrokenLinks method on line 55 does the normal call to obj.Links.GetBrokenLinks but filters out any ItemLink object where the datasource location field contains our "code:" prefix. Please note that this works a datasource location field with only a code block in it, but if you pipe delimit a broken ID or path (such as code:myclass, mydll|{bad guid}|/sitecore/content/missing-item-path) this broken links code will not catch that. I'll leave it up to you to fix it. To finish off problem 1, go into the Sitecore Content Editor and navigate to /sitecore/system/Settings/Validation Rules/Item Rules/Links/Broken Links. Now all we need to do is replace the "Type" field with our custom implementation of Hi.Sc.Data.Validators.ItemValidators.BrokenLinkValidator,Hi.Sc. This will stop the item validation warnings from appearing.Problem 2To solve problem two, we will do something very similar. If you open up /sitecore/admin/showconfig.aspx in your browser, you can go to sitecore/processors/saveUI and see the processor with a type of Sitecore.Pipelines.Save.CheckLinks, Sitecore.Kernel is what is causing our warning message to appear. We'll need to decompile this class so we can change one line again. First, let's look at the patch config:[code language="csharp"]<configuration xmlns:patch=""> <sitecore> <processors> <saveUI> <processor mode="on" type="Sitecore.Pipelines.Save.CheckLinks, Sitecore.Kernel"> <patch:attribute name="type">Hi.Sc.Pipelines.Save.CheckLinks, Hi.Sc</patch:attribute> </processor> </saveUI> </processors> </sitecore></configuration>[/code]Next let's take a look at the implementation of our class:[code language="csharp"]using Hi.Sc.Data.Validators.ItemValidators;using Sitecore;using Sitecore.Data;using Sitecore.Data.Fields;using Sitecore.Data.Items;using Sitecore.Globalization;using Sitecore.Links;using Sitecore.Pipelines.Save;using System.Text;namespace Hi.Sc.Pipelines.Save{ public class CheckLinks { #region Methods public void Process(SaveArgs args) { 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 = BrokenLinkValidator.GetBrokenLinks(obj, allVersions); if (brokenLinks.Length > 0) { CheckLinks.ShowDialog(obj, brokenLinks); args.WaitForPostBack(); break; } 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", (object)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}]", (object)itemLink.SourceFieldID.ToString())); } if (!string.IsNullOrEmpty(itemLink.TargetPath) && !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(stringBuilder.ToString()); } #endregion }}[/code]Unfortunately the whole class needed to be copied in order to change line 49 which calls into our static method we created in our BrokenLinksValidator. Now that this class is added with our patch config we don't see a warning message when we click save on our rendering.Hope you enjoyed this. Leave questions or comments if you feel like it!