Before heading into the post I would like to thank Sitecore for giving me the great honor of being awarded as one of the 167 MVPs. I truly enjoy sharing my experiences with people so this means a lot to me. Check out all the skilled MVPs at Sitecore MVPs 2015
I would like to show you guys how to do queryable datasources for your renderings. This is nothing new and there are a lot of posts out there describing it very well, so why reinvent the wheel? I found some really nice posts and used most of their code.
Queries in Datasource location on Sitecore Layouts by Thomas Stern aka Dr Stern 🙂
Multiple Datasource Locations using Queries are back in Sitecore 7.2 Update-2 by Sean Holmesby
As Sean pointed out in his post – Since Sitecore 7.2 you can have multiple datasources, that is really cool.
Sitecore 7.2 Update-2 fixes this, and Sitecore’s default code will allow you to define multiple Datasource locations by having them pipe separated in the ‘Datasource Location’ field on a rendering/sublayout
I checked Sean’s and Thomas code and created my pipeline for supporting queryable datasources to the renderings. I started out doing some queries and it worked great but then I needed to to go “above” my site node to find its datasource folder.
Sitecore
–Content
—Site1
—Site2
—Site3
—Site4
—Datasource content
—–Global
—–Site1
——-Some folder
—–Site2
—–Site3
—–Site4
In other words I need to get the name of an element and then add it to the datasource path, something like this:
query:./ancestor-or-self::*[@@templatename='Site']/../Datasource content/*[@@name=name of site element])/Some folder
I read all kinds of stuff on what you can do with the query using XPath functions. Here are some good posts you should read:
SITECORE QUERY CHEAT SHEET
Querying Items from Sitecore
Good old Sitecore SDN stuff:
Sitecore Query Syntax
Data Definition Reference – chapter 4:3
Using Sitecore Fast Query
At the end I came to the conclusion that I needed following XPath function:
name(node) – XPath, XQuery, and XSLT Functions, unfortunately this one is not supported in Sitecore’s XPath
So what to do? Why not make a custom token since I already have my own pipeline for the queryable datasource. The custom token should be something like this:
<< node?Field attribute >> <<ancestor-or-self::*[@@templatename='Site']?@name>>
I found this nice old post about accessing special properties of a Sitecore item the same way you get a typical field’s value:
Getting a special field value in Sitecore by Sean Kearney
Here is how the datasource finally will look like:
/sitecore/content/Datasource content/Global/Social sharing|query:./ancestor-or-self::*[@@templatename='Site']/../Datasource content/*[@@name=<<ancestor-or-self::*[@@templatename='Site']?@name>>])/Social sharing
Here is the code for the pipeline:
public class MultipleDataSourceLocationsWithQueries
{
private IDatasourceQueryService _datasourceQueryService;
public void Process(GetRenderingDatasourceArgs args)
{
Assert.IsNotNull(args, "args");
this._datasourceQueryService = new DatasourceQueryService(args.ContextItemPath, args.ContentDatabase, args.RenderingItem["Datasource Location"], args.DatasourceRoots);
if (!this._datasourceQueryService.IsQueryInDataSourceLocation())
return;
this._datasourceQueryService.ProcessQuerys();
}
}
Then I created a service for the datasource stuff.
public class DatasourceQueryService : IDatasourceQueryService
{
private readonly string _contextItemPath;
private readonly Database _contentDatabase;
private readonly string _datasourceLocation;
private readonly List<Item> _datasourceRoots;
private readonly Regex _regEx;
public DatasourceQueryService(string contextItemPath, Database contentDatabase, string datasourceLocation, List<Item> datasourceRoots)
{
this._contextItemPath = contextItemPath;
this._contentDatabase = contentDatabase;
this._datasourceLocation = datasourceLocation;
this._datasourceRoots = datasourceRoots;
this._regEx = new Regex(GetRegexPattern());
}
public bool IsQueryInDataSourceLocation()
{
return this._datasourceLocation.Contains(QueryIdentifier);
}
public void ProcessQuerys()
{
ListString possibleQueries = new ListString(this._datasourceLocation);
foreach (string possibleQuery in possibleQueries)
{
if (possibleQuery.StartsWith(QueryIdentifier))
{
ProcessQuery(possibleQuery);
}
}
}
private string GetRegexPattern()
{
return String.Format(".*{0}(.*){1}.*", SpecialStartItemDelimiter, SpecialEndItemDelimiter);
}
private void ProcessQuery(string query)
{
//Remove the "query:."
query = query.Replace(QueryIdentifier, "");
Item[] datasourceLocations = ResolveDatasourceRootFromQuery(query);
if (datasourceLocations == null || !datasourceLocations.Any())
return;
foreach (Item dataSourceLocation in datasourceLocations.Where(dataSourceLocation => !this._datasourceRoots.Exists(item => item.ID.Equals(dataSourceLocation.ID))))
{
this._datasourceRoots.Add(dataSourceLocation);
}
}
private Item[] ResolveDatasourceRootFromQuery(string queryPath)
{
Match matchedData = this._regEx.Match(queryPath);
int numberOfMatchedTokens = 1;
while (matchedData.Success)
{
//Just in case - I hate while loops
if (numberOfMatchedTokens > MaxNumberOfIterations)
break;
queryPath = GenerateQueryPathForSpecialToken(matchedData, queryPath);
matchedData = this._regEx.Match(queryPath);
numberOfMatchedTokens++;
}
return GetItemsFromQuery(queryPath);
}
private string GenerateQueryPathForSpecialToken(Match matchedData, String queryPath)
{
string specialQueryPath = matchedData.Groups[1].ToString();
string translatedValue = ResolveSpecialItemInQuery(specialQueryPath);
return queryPath.Replace(
string.Format("{0}{2}{1}", SpecialStartItemDelimiter, SpecialEndItemDelimiter, specialQueryPath),
translatedValue);
}
private Item[] GetItemsFromQuery(string queryPath)
{
try
{
return this._contentDatabase.SelectItems(string.Format("{0}/{1}", this._contextItemPath, queryPath));
}
catch (Exception ex)
{
Log.Error(String.Format(@"Datasource query was not valid:{0}", queryPath), ex, this);
return null;
}
}
private string ResolveSpecialItemInQuery(string specialQueryPath)
{
if (!specialQueryPath.Contains(FieldDelimiter))
return string.Empty;
string specialItemPath = specialQueryPath.Split(char.Parse(FieldDelimiter))[0];
if (string.IsNullOrWhiteSpace(specialItemPath))
return string.Empty;
Item[] specialItems = GetItemsFromQuery(specialItemPath);
if (!specialItems.Any())
return string.Empty;
string fieldProperty = specialQueryPath.Split(char.Parse(FieldDelimiter))[1];
if (string.IsNullOrWhiteSpace(fieldProperty))
return string.Empty;
return specialItems.Select(item => item[fieldProperty]).FirstOrDefault();
}
private const string QueryIdentifier = "query:.";
private const string SpecialStartItemDelimiter = "<<";
private const string SpecialEndItemDelimiter = ">>";
private const string FieldDelimiter = "?";
private const Int32 MaxNumberOfIterations = 10;
}
The interface for the service
public interface IDatasourceQueryService
{
bool IsQueryInDataSourceLocation();
void ProcessQuerys();
}
In order to make the pipeline work we need to do a patch config file.
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<getRenderingDatasource>
<processor type="Sandbox.QueryableDatasource.Pipelines.GetRenderingDatasource.MultipleDataSourceLocationsWithQueries, Sandbox.QueryableDatasource"
patch:after="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']" />
</processor>
</getRenderingDatasource>
</pipelines>
</sitecore>
</configuration>
That’s all for now folks 🙂
