February 23, 2015 | Charlie Turano
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.
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.
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.
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;
}
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>