How to create an Experience Editor default rendering button

I recently had a client request to speed up the process of adding a component to multiple language version of an item. Rather than going to each language version and adding a component to a placeholder, I created a default rendering button to set up the component on one language version of the item and copy it to all the other language versions.

The first thing I did was navigate to /sitecore/content/Applications/WebEdit/Default Rendering Buttons in the core database and added an item based off the /sitecore/templates/System/WebEdit/WebEdit Button template called Copy to other language versions.

Copy to other language versions

In order to get the click I had to do two more steps. First, I added a command to call into my custom code.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="webedit:CopyToOtherLanguageVersions" type="Hi.Shared.BrentsCoolCopyRendLangVersModule.CopyToOtherLanguageVersions, Hi.Shared.BrentsCoolCopyRendLangVersModule"/>
    </commands>
  </sitecore>
</configuration>

Next, I changed the /sitecore/shell/Applications/Page Modes/ChromeTypes/RenderingChromeType.js and added a new runcommand so I can pass get the renderings unique id in my code. I first changed the javascript here:

handleMessage: function(message, params, sender) {
    switch (message) {
      case "chrome:rendering:sort":
        this.sort();
        break;
      case "chrome:rendering:properties":
        this.editProperties();
        break;
      case "chrome:rendering:propertiescompleted":
        this.editPropertiesCompleted();
        break;
      case "chrome:rendering:delete": 
        this.deleteControl();
        break;
      case "chrome:rendering:morph":
        this.morph(params);
        break;
      case "chrome:rendering:morphcompleted":
        this.morphCompleted(params.id, params.openProperties);
        break;
      case "chrome:rendering:personalize":
        if (Sitecore.PageModes.Personalization) {
          this.personalize(params.command);
        }
        break;
      case "chrome:rendering:personalizationcompleted":
        if (Sitecore.PageModes.Personalization) {
          this.presonalizationCompleted(params, sender);
        }                
        break;
      case "chrome:personalization:conditionchange":
        if (Sitecore.PageModes.Personalization) {
          this.changeCondition(params.id, sender);
        }
        break;      
      case "chrome:rendering:editvariations":
        if (Sitecore.PageModes.Testing) {
          this.editVariations(params.command, sender);
        }
        break;
      case "chrome:rendering:editvariationscompleted":
        if (Sitecore.PageModes.Testing) {
          this.editVariationsCompleted(params, sender);
        }
        break;
      case "chrome:testing:variationchange":
        if (Sitecore.PageModes.Testing) {
          this.changeVariation(params.id, sender);
        }        
        break;      
	case "chrome:rendering:runcommand":
		this.runcommand(params.command, sender);
		break;
    }
  },

Lines 51-53 are the added code and next I added the runcommand function to RenderingChromeType.js:

runcommand: function(commandName, sender) {
    var ribbon = Sitecore.PageModes.PageEditor.ribbon();
    Sitecore.PageModes.PageEditor.layoutDefinitionControl().value = Sitecore.PageModes.PageEditor.layout().val();
    var controlId = this.controlId();
    if (sender) {
      controlId = sender.controlId(); 
    }

    Sitecore.PageModes.PageEditor.postRequest(commandName + "(uniqueId=" + this.uniqueId() + ",controlId=" + controlId + ")");
  },

Next, I opened up dotPeek to look at how webedit:personalize at Sitecore.Shell.Applications.WebEdit.Commands.Personalize, Sitecore.ExperienceEditor was written so I could follow that pattern. I came up with this class.

using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Layouts;
using Sitecore.Shell.Applications.WebEdit.Commands;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web;
using Sitecore.Web.UI.Sheer;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Hi.Shared.BrentsCoolCopyRendLangVersModule
{
    [Serializable]
    public class CopyToOtherLanguageVersions : WebEditCommand
    {
        public CopyToOtherLanguageVersions()
        {
        }

        protected virtual string ConvertToXml(string layout)
        {
            Assert.ArgumentNotNull(layout, "layout");
            return WebEditUtil.ConvertJSONLayoutToXML(layout);
        }

        public override void Execute(CommandContext context)
        {
            Assert.ArgumentNotNull(context, "context");
            ItemUri itemUri = ItemUri.ParseQueryString();
            if (itemUri != null)
            {
                Item item = Database.GetItem(itemUri);
                if (item != null && !WebEditUtil.CanDesignItem(item))
                {
                    SheerResponse.Alert("The action cannot be executed because of security restrictions.", new string[0]);
                    return;
                }
            }

            string formValue = WebUtil.GetFormValue("scLayout");
            Assert.IsNotNullOrEmpty(formValue, "Layout Definition");
            string str = ShortID.Decode(WebUtil.GetFormValue("scDeviceID"));
            Assert.IsNotNullOrEmpty(str, "device ID");
            string str1 = ShortID.Decode(context.Parameters["uniqueId"]);
            Assert.IsNotNullOrEmpty(str1, "Unique ID");
            string xml = this.ConvertToXml(formValue);
            Assert.IsNotNullOrEmpty(xml, "convertedLayoutDefition");

            NameValueCollection nameValueCollection = new NameValueCollection();
            nameValueCollection["deviceId"] = str;
            nameValueCollection["uniqueId"] = str1;
            nameValueCollection["contextItemUri"] = itemUri != null ? itemUri.ToString() : string.Empty;
            
            Context.ClientPage.Start(this, "Run", nameValueCollection);
        }

        [UsedImplicitly]
        protected void Run(ClientPipelineArgs args)
        {
            string deviceId;
            string uniqueId;
            string contextItemUri;

            Assert.ArgumentNotNull(args, "args");
            try
            {
                deviceId = args.Parameters["deviceId"];
                Assert.IsNotNull(deviceId, "deviceId");
                uniqueId = args.Parameters["uniqueId"];
                Assert.IsNotNull(uniqueId, "uniqueId");
                contextItemUri = args.Parameters["contextItemUri"];
                Assert.IsNotNull(contextItemUri, "contextItemUri");
            }
            catch
            {
                throw;
            }

            if (args.IsPostBack)
            {
                if (args.Result == "yes")
                {
                    if (!RunCopy(deviceId, uniqueId, contextItemUri))
                    {
                        SheerResponse.Alert("Cannont copy this rendering. Please save first.");
                    }
                }
            }
            else
            {
                try
                {
                    SheerResponse.Confirm("Are you sure you want to copy renderings to the most recent language versions?");
                    args.WaitForPostBack();
                }
                catch
                {
                    throw;
                }
            }
        }
        
        public bool RunCopy(string deviceId, string uniqueId, string contextItemUri)
        {
            Item i = Database.GetItem(new ItemUri(contextItemUri));
            RenderingDefinition rd = i.GetRenderingDefinitionByUniqueId(uniqueId, deviceId);
            if (i != null && rd != null)
            {
                List<Language> langList = i.Languages.Where(e => !e.CultureInfo.Name.Equals(i.Language.CultureInfo.Name)).ToList();
                foreach (Language l in langList)
                {
                    Item li = i.Database.GetItem(i.ID, l);
                    if (li != null && li.Versions.Count > 0)
                    {
                        li.CopyRenderingReference(rd, deviceId);
                    }
                }
                return true;
            }
            return false;
        }
    }
}

The difference between my code and Sitecore.Shell.Applications.WebEdit.Commands.Personalize is that I have removed the session code to keep track of the dialog. The RunCopy method is what does the bulk of the work to copy rendering references to the other language versions. This is done with a help of a few extension methods located here.

When it’s all done, you will have a nice rendering button to help content authors.

copy rendering button

Advertisements

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:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getRenderingDatasource>
        <processor type="Hi.Sc.Pipelines.GetRenderingDatasource.GetCodeDatasourceLocations, Hi.Sc" patch:before="*[1]" />
      </getRenderingDatasource>
    </pipelines>
  </sitecore>
</configuration>

Now lets take a look at our new class:

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
    }
}

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:

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 };
        }
    }
}

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 1

To 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:

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();
        }
    }
}

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 2

To 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:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <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>

Next let’s take a look at the implementation of our class:

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
    }
}

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!

View All Items in the Sitecore Archive for Non Admin Users

I recently had a requirement that specific non-admin content authors should be able to view all items in the Sitecore Archive. I opened Sitecore.Shell.Applications.Archives.RecycleBin.RecycleBinPage from the Sitecore.Client.dll in dotPeek and noticed that the method GetArchiveEntries() calls into the ArchiveManager.GetArchive and returns an IPageable object. So, we can override the default archive provider and return our own archive. To override the archive provider we can use the following patch config:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <archives defaultProvider="sql" enabled="true">
      <providers>
        <add name="sql" type="Sitecore.Data.Archiving.SqlArchiveProvider, Sitecore.Kernel" database="*">
          <patch:attribute name="type">HI.Global.Archiving.SqlArchiveProvider, HI.Global</patch:attribute>
        </add>
      </providers>
    </archives>
  </sitecore>
</configuration>

Now we can add our custom provider:

using Sitecore.Data;
using Sitecore.Data.Archiving;
using Sitecore.Xml;
using System.Xml;

namespace HI.Global.Archiving
{
    public class SqlArchiveProvider: Sitecore.Data.Archiving.SqlArchiveProvider
    {
        protected override Archive GetArchive(XmlNode configNode, Database database)
        {
            string attribute = XmlUtil.GetAttribute("name", configNode);
            if (string.IsNullOrEmpty(attribute))
            { 
                return null;
            } 
            return new HI.Global.Archiving.SqlArchive(attribute, database);
        }
    }
}

The above class returns our custom archive so now all we need to do is override two methods of the SqlArchive class and all entries will be shared:

using Sitecore.Data;
using Sitecore.Data.Archiving;
using Sitecore.Security.Accounts;
using System.Collections.Generic;

namespace HI.Global.Archiving
{
    public class SqlArchive : Sitecore.Data.Archiving.SqlArchive
    {
        public SqlArchive(string name, Database database) : base(name, database)
        {
        }

        protected override IEnumerable<ArchiveEntry> GetEntries(User user, int pageIndex, int pageSize, ID archivalId)
        {
            return base.GetEntries(null, pageIndex, pageSize, archivalId);
        }

        protected override int GetEntryCount(User user)
        {
            return base.GetEntryCount(null);
        }
    }
}

The above code works because when null is passed into both base.GentEntries and base.GetEntryCount, it removed a SQL where statement to search for archived entries by user name. We can also extend this further by checking for a specific role:

using Sitecore.Data;
using Sitecore.Data.Archiving;
using Sitecore.Security.Accounts;
using System.Collections.Generic;

namespace HI.Global.Archiving
{
    public class SqlArchive : Sitecore.Data.Archiving.SqlArchive
    {
        public SqlArchive(string name, Database database) : base(name, database)
        {
        }

        protected override IEnumerable<ArchiveEntry> GetEntries(User user, int pageIndex, int pageSize, ID archivalId)
        {
            if (IsAllowedToViewAllItems(user))
                return base.GetEntries(null, pageIndex, pageSize, archivalId);
            return base.GetEntries(user, pageIndex, pageSize, archivalId);
        }

        protected override int GetEntryCount(User user)
        {
            if (IsAllowedToViewAllItems(user))
                return base.GetEntryCount(null);
            return base.GetEntryCount(user);
        }

        private bool IsAllowedToViewAllItems(User user)
        {
            if (user != null)
            {
                return user.IsInRole("sitecore\\my specific role i created for viewing all archive items");
            }
            return false;
        }
    }
}

If you forget to override GetEntryCount(), Your archive won’t page correctly. The reason we override GetEntries(), is because all GetEntries() methods eventually call into GetEntries(User user, int pageIndex, int pageSize, ID archivalId). Also, please note that this can also be applied to the Recycle Bin by following the same steps.

Programmatically Get Multi Variate Test Datasource Items of a Sitecore Item

In the previous two blog posts, Programmatically Get Datasource Items of a Sitecore Item and Programmatically Get Personalization Datasource Items of a Sitecore Item we learned how to get the datasource items and personalization datasource items from the rendering references. This does not include the items that have been added through Multi Variate Testing. By adding the following methods to our previous post, we can get those items.

        public static List<Item> GetMultiVariateTestDataSourceItems(this Item i)
        {
            List<Item> list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                list.AddRange(reference.GetMultiVariateTestDataSourceItems());
            }
            return list;
        }

        private static List<Item> GetMultiVariateTestDataSourceItems(this RenderingReference reference)
        {
            List<Item> list = new List<Item>();
            if (reference != null && !string.IsNullOrEmpty(reference.Settings.MultiVariateTest))
            {
                using (new SecurityDisabler())
                {
                    var mvVariateTestForLang = Sitecore.Analytics.Testing.TestingUtils.TestingUtil.MultiVariateTesting.GetVariableItem(reference);
                    //var mvVariateTestForLang = reference.Settings.GetMultiVariateTestForLanguage(Sitecore.Context.Language); // < Sitecore 7.5
                    Sitecore.Data.Items.Item variableItem = null;

                    if (mvVariateTestForLang != null)
                    {
                        variableItem = mvVariateTestForLang.InnerItem;
                        //variableItem = reference.Database.GetItem(mvVariateTestForLang); // < Sitecore 7.5
                    }

                    if (variableItem != null)
                    {
                        foreach (Item mvChild in variableItem.Children)
                        {
                            var mvDataSourceItem = mvChild.GetInternalLinkFieldItem("Datasource");
                            if (mvDataSourceItem != null)
                            {
                                list.Add(mvDataSourceItem);
                            }
                        }
                    }
                }
            }
            return list;
        }

        public static Item GetInternalLinkFieldItem(this Item i, string internalLinkFieldName)
        {
            if (i != null)
            {
                InternalLinkField ilf = i.Fields[internalLinkFieldName];
                if (ilf != null && ilf.TargetItem != null)
                {
                    return ilf.TargetItem;
                }
            }
            return null;
        }

This code shown is for Sitecore 7.5 and above while the commented out lines are for versions of Sitecore below 7.5. Now that we have these extension methods, we are able to write some nice code like this:

        foreach (Item multiVariateTestDataSourceItem in Sitecore.Context.Item.GetMultiVariateTestDataSourceItems())
        {
             // do something
        }

Programmatically Get Personalization Datasource Items of a Sitecore Item

In the previous blog post, Programmatically Get Datasource Items of a Sitecore Item we learned how to get the datasource items from the rendering references. This does not include the items that have been added through personalization and rules. By adding the following methods to our previous post, we can get those items.

        public static List<Item> GetPersonalizationDataSourceItems(this Item i)
        {
            List<Item> list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                list.AddRange(reference.GetPersonalizationDataSourceItem());
            }
            return list;
        }

        private static List<Item> GetPersonalizationDataSourceItem(this RenderingReference reference)
        {
            List<Item> list = new List<Item>();
            if (reference != null && reference.Settings.Rules != null && reference.Settings.Rules.Count > 0)
            {
                foreach (var r in reference.Settings.Rules.Rules)
                {
                    foreach (var a in r.Actions)
                    {
                        var setDataSourceAction = a as Sitecore.Rules.ConditionalRenderings.SetDataSourceAction<Sitecore.Rules.ConditionalRenderings.ConditionalRenderingsRuleContext>;
                        if (setDataSourceAction != null)
                        {
                            Item dataSourceItem = GetDataSourceItem(setDataSourceAction.DataSource, reference.Database);
                            if (dataSourceItem != null)
                            {
                                list.Add(dataSourceItem);
                            }
                        }
                    }
                }
            }
            return list;
        }

This code loops through the rules of your personalized component and if the action is Sitecore.Rules.ConditionalRenderings.SetDataSourceAction, then we know that this will be one of the personalization items. Next we can write some nice code like this:

        foreach (Item personalizationItem in Sitecore.Context.Item.GetPersonalizationDataSourceItems())
        {
             // do something
        }

Please see the next blog post in this series: Programmatically Get Multi Variate Test Datasource Items of a Sitecore Item

Programmatically Get Datasource Items of a Sitecore Item

If you need to get the datasource item’s of a particular Sitecore item, we can write a few extension methods to help us out!

public static class ItemExtensions
{
        public static RenderingReference[] GetRenderingReferences(this Item i)
        {
            if (i == null)
            {
                return new RenderingReference[0];
            }
            return i.Visualization.GetRenderings(Sitecore.Context.Device, false);
        }

        public static List<Item> GetDataSourceItems(this Item i)
        {
            List<Item> list = new List<Item>();
            foreach (RenderingReference reference in i.GetRenderingReferences())
            {
                Item dataSourceItem = reference.GetDataSourceItem();
                if (dataSourceItem != null)
                {
                    list.Add(dataSourceItem);
                }
            }
            return list;
        }

        public static Item GetDataSourceItem(this RenderingReference reference)
        {
            if (reference != null)
            {
                return GetDataSourceItem(reference.Settings.DataSource, reference.Database);
            }
            return null;
        }

        private static Item GetDataSourceItem(string id, Database db)
        {
            Guid itemId;
            return Guid.TryParse(id, out itemId)
                                    ? db.GetItem(new ID(itemId))
                                    : db.GetItem(id);
        }
}

From here, we can write some nice code like this:

        foreach (Item dataSourceItem in Sitecore.Context.Item.GetDataSourceItems())
        {
            // do something
        }

Please see the next blog in this series: Programmatically Get Personalization Datasource Items of a Sitecore Item

Passing Configuration Data into a Sitecore Pipeline Processor

I’ve created an example pipeline config and an example pipeline processor to explain how to pass configuration data to your custom processor. Let’s take a look at the patch config and example processor code:

using Sitecore.Collections;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Xml;
using System.Collections;
using System.Collections.Generic;
using System.Xml;

namespace Hi.Sc.Pipelines
{
    public class ExampleProcessor
    {
        #region Fields

        private string _myArgument1 = string.Empty;
        private string _myArgument2 = string.Empty;
        private List<string> MyRawList = new List<string>();
        private List<string> MyStringList = new List<string>();

        #endregion

        #region Properties

        public MyObject MyObject { get; set; }
        public string MyProperty { get; set; }

        #endregion

        #region Constructor

        public ExampleProcessor()
        {

        }

        public ExampleProcessor(string myArgument1, string myArgument2)
        {
            _myArgument1 = myArgument1;
            _myArgument2 = myArgument2;
        }

        #endregion

        #region Methods

        public void Process(PipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Log.Warn("ExampleProcessor.Process", this);
            LogData();
        }

        public void AlternateProcessMethod(PipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Log.Warn("ExampleProcessor.AlternateProcessMethod", this);
            LogData();
        }

        public void AddString(string myStringArgument)
        {
            if (!string.IsNullOrEmpty(myStringArgument))
            { 
                this.MyStringList.Add(myStringArgument);
            }
        }

        public void AddMyRawString(XmlNode configNode)
        {
            Assert.ArgumentNotNull(configNode, "configNode");
            string attributeValue = XmlUtil.GetAttribute("value", configNode);
            MyRawList.Add(attributeValue);
        }

        private void LogData()
        {
            LogString("My Argument 1", _myArgument1);
            LogString("My Argument 2", _myArgument2);
            LogString("My Property", MyProperty);
            LogList("My String List", MyStringList);
            LogList("My Raw List", MyRawList);

            if (MyObject != null)
            {
                Log.Warn("MyObject.Name = " + MyObject.Name, this);
            }
        }

        private void LogList(string listName, IList list)
        {
            if (list != null)
            {
                foreach (string s in list)
                {
                    LogString(listName, s);
                }
            }
            else
            {
                LogString(listName);
            }
        }

        private void LogString(string name, string value = "")
        {
            if (!string.IsNullOrEmpty(value))
            {
                Log.Warn(string.Format("{0} = {1}", name, value), this);
            }
            else
            {
                Log.Warn(string.Format("{0} is empty."), this);
            }
        }

        #endregion
    }

    public class MyObject
    {
        public string Name { get; set; }
        public MyObject()
        {
            Name = "MyObject.Name";
        }
    }
}

Our patch config:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor type="Hi.Sc.Pipelines.ExampleProcessor, Hi.Sc" method="AlternateProcessMethod" runIfAborted="true">

          <param desc="My Argument 1">myArgument1_Value</param>
          <param>myArgument2_Value</param>

          <MyProperty desc="My Property">MyProperty_Value</MyProperty>

          <MyStringList hint="list:AddString">
            <SomeTag1>MyStringList_String1</SomeTag1>
            <SomeTag2>MyStringList_String2</SomeTag2>
            <SomeTag1>MyStringList_String3</SomeTag1>
          </MyStringList>

          <MyRawList hint="raw:AddMyRawString">
            <MyRawString value="MyRawList_MyRawString_Value1"/>
            <MyRawString value="MyRawList_MyRawString_Value2"/>
          </MyRawList>

          <MyObject ref="MyObjectNode/MyObject"/>

        </processor>
      </httpRequestBegin>
    </pipelines>

    <MyObjectNode>
      <MyObject type="Hi.Sc.Pipelines.MyObject, Hi.Sc" />
    </MyObjectNode>

  </sitecore>
</configuration>

The code passes values into our processor to give you an idea on how to pass custom data into a processor. This could help give ideas on processor reuse. I’ve added this to the httpRequestBegin pipeline so I can reload my page and check my log files to see the output. I’m also calling Log.Warn instead of Log.Info since I have my log level set to WARN. Here is the output from my log file:

8072 23:03:49 WARN  ExampleProcessor.AlternateProcessMethod
8072 23:03:49 WARN  My Argument 1 = myArgument1_Value
8072 23:03:49 WARN  My Argument 2 = myArgument2_Value
8072 23:03:49 WARN  My Property = MyProperty_Value
8072 23:03:49 WARN  My String List = MyStringList_String1
8072 23:03:49 WARN  My String List = MyStringList_String2
8072 23:03:49 WARN  My String List = MyStringList_String3
8072 23:03:49 WARN  My Raw List = MyRawList_MyRawString_Value1
8072 23:03:49 WARN  My Raw List = MyRawList_MyRawString_Value2
8072 23:03:49 WARN  MyObject.Name

Setting the ExampleProcessor:
Line 5 of our patch config above initializes our processor. The type attribute tells Sitecore the namespace.class, assembly to use. In this case it is Hi.Sc.Pipelines.ExampleProcessor in the Hi.Sc.dll. The method attribute allows us to override the default Process method. Since I have set the method attribute in my processor, my example processor will call the AlternateProcessMethod instead of the default Process method. You can see the result on line 1 of the log file output. If we remove this method attribute, it will call Process instead. The runIfAported attribute set to true will run our processor even if a previous processor in our pipeline calls args.AbortPipeline() to stop processing the subsequent processors in the pipeline.

Passing Arguments to Processor Constructor:
Lines 7 and 8 of our patch config allows us to pass two arguments to the constructor of ExampleProcessor on line 36. If we remove these lines, The constructor on line 31 of ExampleProcessor will be called instead. If we only remove line 7, we will get an error when instantiating our ExampleProcessor as we do not have a constructor defined for only one argument. Please note that the desc attribute of the param tag is optional. These parameters are passed in the order that they appear and if we switch the order of lines 7 and 8, then the values will be swapped around in our log file output.

Passing Properties:
Line 10 of our patch config allows us to pass a value to Property of our processor class. When the ExampleProcessor is initalized, it will match the tag on line 10 of our patch config to the property defined on line 25 of our ExampleProcessor. We just need to make sure the tag name matches the property name.

Passing Values to a List:
Line 12 of our patch config allows us to pass values to a list in our processor. The key here is the hint attribute. By setting the hint attribute to list:[name of method we define], we can fill a list we create in our class. By calling hint=”list:AddString”, this will call the method on line 60 of our ExampleProcessor. The tag names on lines 13 through 15 do not matter, but the value inside them is what will be added to our list on line 64.

Passing Values to a List using Raw:
Line 18 of our patch config allows us to reuse the hint attribute, but instead of using list:, we will use raw:[name of method we define]. When calling hint=”raw:AddMyRawString”, this will call the method on line 68 of our ExampleProcessor. Notice that when we use raw, a System.Xml.XmlNode will be passed in as opposed to the AddString method which has a string argument. When we have an XmlNode, we are able to use Sitecore.Xml.XmlUtil.GetAttribute to extract the value attribute as seen on line 71. We then add that to the MyRawList in this example.

Passing an Object using Ref:
Line 23 is an interesting one in our patch config. We can see that is setting a Property on line 24 of our ExampleProcessor class, since the tag has the same name as the property in our class. Here we can see that type is not a string, MyObject is defined on line 119 of our code. By using ref attribute, this points to another location in out patch config, here it is pointing to line 30 in our patch config where we have set the type attribute to know what type of object to return to our Property. Alternatively, we could just add a type attribute instead of a ref attribute and set the class and assembly to pass that to MyObject.