February 23, 2015 by Charlie Turano

Context Based Datasource

The Problem

When building a Sitecore solution, the taxonomy of the solution plays a large role in how difficult or easy the content editor’s job is. Maintaining a poorly organized content tree is far more difficult than a well-organized one. Cluttered and disorganized content trees rarely, if ever, get better as time goes on, making the organization problem worse.

Pages in a modern Sitecore site are built using a large number of Sitecore items. The page under the Home item is the starting point for building a page, but it is rarely the end. Components on the page often use Datasources to refer to additional page content. This article is about organizing additional page content.

Keeping associated page data well organized ensures content editors can quickly create, find and update content as needed. Bringing new content editors onto a project is much easier if the page content follows well defined rules. Defining and enforcing these rules can be a challenge to even the best teams.

Organizing Datasource Content

Page data items can be kept close to the page by creating a data structure directly under the page item. While appealing from a data organization point of view, this doesn’t seem like a good practice because all items under the Home item have a browsable Url. This also adds to “clutter” in the content tree, making it hard to differentiate pages from data. There are many ways of working around this, but it has a bit of an unclean smell to it.

Here at Hedgehog, we like to create a “Data” folder as a sibling of the Home item for page data. This keeps items close, but doesn’t clutter the browsable content tree. The issue we run into with this arrangement is keeping the data folder structure clean.

Ideally, there is a location for page specific data and another for global data. The global data and the associated UI components are used on many pages. In the case where the data for components is only associated with a single page, it would be useful if the content editors created a structure under the Data folder that matched the structure of the items under Home. This would make finding the data items associated with a page very easy. Maintaining this sort of structure requires discipline on the content editor’s part.

Example Folder Structure

This is a very simple example of the folder structure described above.

In most recent versions of Sitecore, the Datasource Location field in the sublayout can be set to multiple locations. The user is presented with the ability to browse to the location for the data item from multiple roots. One of the locations should be the “global” location. Another is the tree for page specific items. The problem content editors face with the page specific locations is the content editor is forced to browse to the correct location every time they need to update the Datasource. It would be very helpful if the content editor was presented with the correct folder whenever they browse for the content associated with the page component.

Extending Sitecore

Since Sitecore is an incredibly robust and extensible platform, there must be an easy way to add our desired functionality. Fortunately, the getRenderingDatasource pipeline provides the perfect place to help the content editor locate the correct place to find or create content. By default, Sitecore uses the pipeline step Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation to parse the value in the Datasource Location field specified in the sublayout definition and populate the collection GetRenderingDatasourceArgs.DatasourceRoots. By replacing this pipeline step with more robust functionality, we can easily improve the content editor experience.

New Pipeline Step

The implementation of the new pipeline step is relatively simple. The Datasource Location field is parsed and if it contains a special macro token, it is replaced with the path from the root of the current site.

Everything in this post was implemented with Sitecore 7.2 (rev. 140526), but it should work well for other versions of Sitecore.

First, we need to create a class and setup a couple of useful items:

public class ContextBasedDatasourceFolder
{
    const string MACRO_NAME = "{$HomePath}";

    static char[] BAR_ARRAY = new char[] { '|' };
    static char[] SLASH_ARRAY = new char[] { '/' };
    static Guid FOLDER_ID = new Guid("{A87A00B1-E6DB-45AB-8B54-636FEC3B5523}");
}
  These will help us in a bit to parse the Datasource Location and create our folders. The Process method is pretty simple:
public void Process(GetRenderingDatasourceArgs args)
{
    string dataSourceFolderExpressions = args.RenderingItem["Datasource Location"];

    ProcessDataSources(args, dataSourceFolderExpressions);
}

 

The Datasource Locations are processed by looking for the macro token in each pipe (‘|’) delimited path. If the macro token is found, the proper path is calculated. If no token is found, the path is processed much like it was in the Sitecore pipeline step. The pipeline step will not only calculate the correct path, it will automatically create the folders if needed.

private void ProcessDataSources(GetRenderingDatasourceArgs args, string dataSourceFolderExpressions)
{
    string homePath = FindSiteHome(args.ContextItemPath);

    string[] siteFolders = null;

    if (!string.IsNullOrEmpty(homePath))
    {
        siteFolders = GetFoldersFromHome(homePath, args.ContextItemPath);
    }

    //Loop through the folder definitions
    foreach (string dataSourceFolderExpression in new ListString(dataSourceFolderExpressions))
    {
        //See if there is a replacement macro in the name
        if (siteFolders != null && dataSourceFolderExpression.Contains(MACRO_NAME))
        {
            //Break up the folder name
            int macroNamePos = dataSourceFolderExpression.IndexOf(MACRO_NAME);
            string rootDataFolderPath = dataSourceFolderExpressions.Substring(0, macroNamePos - 1);
            string dataFolderSuffix = "";

            if (macroNamePos + MACRO_NAME.Length + 1 < dataSourceFolderExpression.Length)
            {
                dataFolderSuffix = dataSourceFolderExpression.Substring(macroNamePos + MACRO_NAME.Length + 1);
            }

            //Create the full list of folders
            List<string> allSubFolders = new List<string>(siteFolders);
            allSubFolders.AddRange(dataFolderSuffix.Split(SLASH_ARRAY, StringSplitOptions.RemoveEmptyEntries));

            Item rootDataFolder = args.ContentDatabase.GetItem(rootDataFolderPath);
            if (rootDataFolder != null)
            {
                Item dataSourceFolder = CreateSubFoldersIfNeeded(rootDataFolder, allSubFolders);

                args.DatasourceRoots.Add(dataSourceFolder);
            }
        }
        else
        {
            //Get the current path
            string path = dataSourceFolderExpression;

            //See if it is a relative path (yuck)
            if (path.StartsWith("./", System.StringComparison.InvariantCulture) && !string.IsNullOrEmpty(args.ContextItemPath))
            {
                path = args.ContextItemPath + path.Remove(0, 1);
            }

            //Find the item
            Item item = args.ContentDatabase.GetItem(path);
            if (item != null)
            {
                args.DatasourceRoots.Add(item);
            }
        }
    }
}

 

The support methods are pretty straight forward. The most interesting one is FindSiteHome. This method loops through the defined sites and tries to determine if the current item is under one of the Home items for the site.

/// <summary>
/// Takes a root folder and a list of sub folders and returns the final sub folder. If the folders do not exist, they are created.
/// </summary>
/// <param name="rootDataFolder"></param>
/// <param name="allSubFolders"></param>
/// <returns></returns>
private Item CreateSubFoldersIfNeeded(Item rootDataFolder, List<string> allSubFolders)
{
    Item currentFolder = rootDataFolder;

    foreach (string subFolder in allSubFolders)
    {
        Item childItem = currentFolder.Axes.GetChild(subFolder);

        if (childItem == null)
        {
            childItem = ItemManager.CreateItem(subFolder, currentFolder, new ID(FOLDER_ID));
            childItem.Versions.AddVersion();
        }

        currentFolder = childItem;
    }

    return currentFolder;
}

/// <summary>
/// Retuns an array of strings that represents the names of folders from the home node
/// </summary>
/// <param name="homePath"></param>
/// <param name="currentPath"></param>
/// <returns></returns>
private string[] GetFoldersFromHome(string homePath, string currentPath)
{
    string folders = currentPath.Substring(homePath.Length);

    return folders.Split(SLASH_ARRAY, StringSplitOptions.RemoveEmptyEntries);
}

/// <summary>
/// Returns the path to the site home node
/// </summary>
/// <param name="currentPath"></param>
/// <returns></returns>
private string FindSiteHome(string currentPath)
{
    currentPath = currentPath.ToLower();

    //Loop through the sites looking for one that the current path starts with
    foreach (Site site in SiteManager.GetSites())
    {
        if (site.Properties["database"] != "core")
        {
            string siteHome = site.Properties["rootPath"] + site.Properties["startItem"];

            if (!string.IsNullOrEmpty(siteHome))
            {
                if (currentPath.StartsWith(siteHome.ToLower()))
                {
                    return siteHome;
                }
            }
        }
    }

    return null;
}

 

 

Patch File

To enable the new functionality for datasources, we need to patch the Sitecore configuration and change the getRenderingDatasource pipeline to use our new step:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getRenderingDatasource>
        <!-- Replace the existing processor for getting the data source location-->
        <processor type="Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel">
          <patch:attribute name="type">Hedgehog.Pipelines.ContextBasedDatasourceFolder, Hedgehog</patch:attribute>
        </processor>
      </getRenderingDatasource>
    </pipelines>
  </sitecore>
</configuration>

 

Using the dynamic Datasource

The dynamic Datasource location works much like the default functionality. The main difference is there is the new macro token called {$HomePath} that gets replaced with the current path to the item from the sites' home folder. The following are example folder paths that will work with this component:
  • /sitecore/content/Data/PageDataSources/{$HomePath}
  • /sitecore/content/Data/PageDataSources/{$HomePath}/Modules
  • /sitecore/content/Data/PageDataSources/{$HomePath}|/sitecore/content/Data/Global

Conclusion

Being able to neatly organize the data used by your datasources makes the site much more supportable. Having Sitecore automatically create and/or choose a location that aids this process makes the content editors job much easier. Using this makes the lives of the content editors we support so much easier. Leave a comment letting me know what you think!

Keep in Touch and Stay Informed

Get updates, industry reports, white papers and more Hedgehog love.