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 🙂