Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2014/multiple-data-source-locations-in-the-select-associated-content-dialog-box

Multiple data source locations in the 'Select Associated Content' dialog box

Published 21 March 2014
Updated 25 August 2016

Working on some components for a client's site recently, it struck me that there were circumstances where it would be helpful to editors to allow the "Select Associated Content" dialog to have multiple options without just showing the whole content tree. Scenarios like having both a global shared content folder and a sub-site specific shared content folder that editors could choose between, for example. Something that looked like this:

Select associated content dialog

More than one root item displayed in the selection list allows editors to choose an appropriate location without the option to put their data in places you didn't want them to...

When I started looking into whether this was possible or not, I initially assumed it would end up being quite a complicated modification – involving modifying this dialog, and the pipelines and commands which are used for this bit of UI. But after a bit of digging through the code, it turns out that it's actually much simpler than this, since the dialog already supports the idea of multiple roots. What we need to provide is the right user interface to configure the roots and a bit of code to ensure this config is passed through to the dialog. Here's what I came up with, after a bit of hacking:

So what happens when you click the Associated Content button in the UI? Well looking at the commands configuration for the Page Editor, we find that the UI triggers the command Sitecore.Shell.Applications.WebEdit.Commands.SetDatasource. Digging through that class with Reflector, it seems that the Execute() method gets the client page to call the Run() method. This method does all sorts of stuff, but the interesting bit is that it calls a static method called CreatePipelineArgs() to set up the data for the rest of the pipeline. And this is where the interesting stuff happens:

private static GetRenderingDatasourceArgs CreatePipelineArgs(ClientPipelineArgs args, Item renderingItem)
{
    Item clientContentItem = WebEditUtil.GetClientContentItem(Client.ContentDatabase);
    GetRenderingDatasourceArgs getRenderingDatasourceArgs = new GetRenderingDatasourceArgs(renderingItem)
    {
        FallbackDatasourceRoots = new List
        {
            Client.ContentDatabase.GetRootItem()
        },
        ContentLanguage = (clientContentItem != null) ? clientContentItem.Language : null,
        ContextItemPath = (clientContentItem != null) ? clientContentItem.Paths.FullPath : string.Empty,
        ShowDialogIfDatasourceSetOnRenderingItem = true
    };
    LayoutDefinition currentLayoutDefinition = SetDatasource.GetCurrentLayoutDefinition();
    ID clientDeviceId = WebEditUtil.GetClientDeviceId();
    string uniqueId = args.Parameters["uniqueId"];
    if (currentLayoutDefinition != null && !ID.IsNullOrEmpty(clientDeviceId))
    {
        RenderingDefinition renderingByUniqueId = currentLayoutDefinition.GetDevice(clientDeviceId.ToString()).GetRenderingByUniqueId(uniqueId);
        if (renderingByUniqueId != null)
        {
            getRenderingDatasourceArgs.CurrentDatasource = renderingByUniqueId.Datasource;
        }
    }
    return getRenderingDatasourceArgs;
}

					

The critical bit for what we're trying to achieve is where the FallbackDatasourceRoots property is assigned a list of items. If we follow this through the code for the rest of the display of the dialog, this property is used to set the contents of the tree view. So this is the bit of code we need to modify to deal with the Datasource Location field of a sublayout or rendering having multiple items assigned.

Slightly annoyingly, the class defining this method doesn't make life easy for modifying it. In fact due to the use of static methods, we're pretty much stuck with the idea that we need to decompile the whole class and copy it into our own codebase in order to modify it. Once we've done that, we need some code that can process the Datasource Location field and generate a List that contains whatever items it finds.

If we assume that the data is going to be formatted using the standards for a multi-select field, then we could write a method something like this:

private static List fetchDatasourceRoots(Item renderingItem)
{
    List roots = new List();

    string itemIDs = renderingItem.Fields["Datasource Location"].Value;

    if (string.IsNullOrWhiteSpace(itemIDs))
    {
        roots.Add(Client.ContentDatabase.GetRootItem());
        return roots;
    }

    string[] ids = itemIDs.Split('|');

    if (ids.Length > 0)
    {
        foreach (string id in ids)
        {
            roots.Add(Client.ContentDatabase.GetItem(id));
        }
    }
    else
    {
        roots.Add(Client.ContentDatabase.GetRootItem());
    }

    return roots;
}

					

We take the item that represents the Rendering or Sublayout and we extract the value for the Datasource Location field. If it's empty then we return the default data – the same data the original code used. If it's not empty then we split it into individual IDs and add each of the items these represent to the collection we return.

And with that, we can modify the code of the CreatePipelineArgs() to initialise the FallbackDatasourceRoots property using our new method instead of the original code.

Two more things need we need to do for this to work. First, we need Sitecore to use our replacement custom SetDatasource class. That just needs a quick config patch, along the lines of:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="webedit:setdatasource">
        <patch:attribute name="type">
          Testing.CustomSetDataSource, Testing
        </patch:attribute>
      </command>
    </commands>
  </sitecore>
</configuration>

					

The "patch:attribute" element here tells the configuration system to replace the value of the specified attribute of the context element with the new value we provide.

The second thing is that the Datasource Location field of the Rendering and Sublayout templates needs to become a multi-select type of field. The TreeListEx control seems appropriate. That field lives at /sitecore/templates/System/Layout/Sections/Rendering Options/Editor Options/Datasource Location in the content tree, so we can change the type:

Field type for DataSource location

That will change the type of this field for all the renderings and sublayouts. And with that, you can now set multiple items as the Datasource Location:

Picking multiple locations

And now both of those items will show up as roots in the Set Associated Content dialog.

Now, there are a few caveats with this modification as it stands. One is that it's not compatible with the relative data source locations code from a previous post, because it stores GUIDs in the Datasource Location field rather than a string. It also means that any pre-existing data in the Datasource Location field is now of the wrong type – so you will have to reset any values set before this change. And thirdly, because we've had to copy and replace an entire command class any future modifications to this in the Sitecore codebase won't make it into our solution without us doing it manually. So borrow this with a bit of caution...

↑ Back to top