I'm attempting to expand upon a custom Sitecore command to determine if the current item has a parent item matching a certain template id.
I know the query should ideally be as simple as ./ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}']
if the item is the context, or /sitecore/content/home/full/path/to/the-item/ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}']
Unfortunately the item path includes dashes which need to be escaped like /sitecore/content/home/full/path/to/#the-item#/ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}']
.
However, ideally I'd like to just use the full path of the item, since it's available as item.Paths.FullPath
.
Given an item, what's the best way to write a query containing it's full path, and escaping any dashes that might be contained within?
Assumptions
Assuming you have a reference to a Sitecore.Data.Items.Item
(called item
) and you would like to find an ancestor with a given Sitecore.Data.ID
(called id
), there are a number of ways you can access the ancestor.
Using Linq
In a typical Sitecore setup without any custom libs, I would typically use a bit of Linq so as to avoid encoding issues in XPath.
ancestor =
item
.Axes
.GetAncestors()
.FirstOrDefault(ancestor => ancestor.TemplateID == id);
Using Closest
I use a bespoke framework for Sitecore development that involves a wide variety of extension methods and customized item generation. To avoid the overhead of accessing all the ancestors before filtering, I would use the Closest
extension method:
public static Item Closest(this Item source, Func<Item, bool> test)
{
Item cur;
for (cur = source; cur != null; cur = cur.Parent)
if (test(cur))
break;
return cur;
}
Accessing the ancestor would then be:
ancestor =
item
.Closest(ancestor => ancestor.TemplateID == id);
(Actually, I typically use code that looks like)
ancestor = (ITemplateNameItem) item.Closest(Is.Type<ITemplateNameItem>);
Using XPath
I usually avoid XPath and only use it as a tool of last resort because it often makes code harder to read, introduces encoding issues such as the one you're faced with in this question, and has a hard limit on the number of items that can be returned.
That said, Sitecore has many tools available for searching with XPath, and in some circumstances it does simplify things.
The trick to fixing item paths that contain spaces is: Don't use item paths.
Instead, you can safely use the item's ID, with no more context necessary because it's an absolute reference to the item. It's also guaranteed to follow a specific format.
var query =
string.Format(
"//{0}/ancestor::*[@@templateid='{1}']",
item.ID.ToString(),
id.ToString());
/* alternatively
var query =
string.Format(
"{0}/ancestor::*[@@templateid='{1}']",
item.Paths.LongID,
id.ToString());
*/
ancestor =
item
.Database
.SelectSingleItem(query);
Using Sitecore.Data.Query.Query
As I mentioned previously, Sitecore has many tools available for searching with XPath. One of these tools is Sitecore.Data.Query.Query
. The SelectItems
and SelectSingleItem
methods have additional optional parameters, one of which is a Sitecore.Data.Items.Item
as a contextNode
.
Passing an item in as the second parameter uses the item as the context for the XPath query.
var query =
string.Format(
"./ancestor::*[@@templateid='{0}']",
id.ToString());
ancestor =
Sitecore
.Data
.Query
.Query
.SelectSingleItem(query, item);
I've never seen any Sitecore util class or any other out of the box code which does what you need. It's funny that Sitecore does have a util method which returns false
(yes, it does return false
) but doesn't have a method which escapes item path so it can be used in query.
You may find code which does what you want (written by Anders Laub) here: https://blog.istern.dk/2014/10/29/escaping-dashes-in-sitecore-queries-datasource-query-update/
And if you're too lazy to click the link, I've copied to code here:
private string EscapeItemNamesWithDashes(string queryPath)
{
if (!queryPath.Contains("-"))
return queryPath;
var strArray = queryPath.Split(new char[] { '/' });
for (int i = 0; i < strArray.Length; i++)
{
if (strArray[i].IndexOf('-') > 0)
strArray[i] = "#" + strArray[i] + "#";
}
return string.Join("/", strArray);
}
If you want to escape also space character, you can try this regex:
string escapedPath = Regex.Replace(myItem.Paths.FullPath, "[^/]*[- ][^/]*", "#$0#");
Also you should remember that there are some reserved words in Sitecore which should be escaped as well. Those are (copied from http://www.newguid.net/sitecore/2012/escape-characterswords-in-a-sitecore-query/ ):
- ancestor
- and
- child
- descendant
- div
- false
- following
- mod
- or
- parent
- preceding
- self
- true
- xor
There's no public helper method in Sitecore that would escape Sitecore Query paths. You'll need to implement escaping logic by hand.
Code-based approach:
I found some code in Sitecore.Kernel.dll, in the pipeline processor Sitecore.Pipelines.GetLookupSourceItems.ProcessDefaultSource
. I had to rework it so that complex selectors containing :
and [...]
are not escaped:
static string EscapeQueryPath(string queryPath)
{
string[] strArray = queryPath.Split('/');
for (int i = 0; i < strArray.Length; ++i)
{
string part = strArray[i];
if ((part.IndexOf(' ') >= 0 || part.IndexOf('-') >= 0)
&& part.IndexOf(':') == -1
&& part.IndexOf('[') == -1
&& !part.StartsWith("#", StringComparison.InvariantCulture)
&& !part.EndsWith("#", StringComparison.InvariantCulture))
{
strArray[i] = '#' + part + '#';
}
}
return string.Join("/", strArray);
}
Note that this algorithm:
- Is idempotent - it won't escape the parts of the path which have already been escaped, so
/#item-name#/
will not turn into /##item-name##/
.
- Will escape item names that contain either hyphens or spaces.
- Takes into account complex selectors (like
ancestor::*[...]
in your example).
Regex approach:
Here's another approach to escaping. It will have exactly the same results as the code above.
string path = "./my-item/ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}']";
string result = Regex.Replace(str, "/([^/#\\[\\:]*[- ][^/#\\[\\:]*(?=($|/)))", "/#$1#");
// result: ./#my-item#/ancestor::*[@@templateid='{26710865-F082-4714-876B-D5E1F386792F}']
This is shorter, but most likely a bit slower.
I agree with previous answers but there are few more things which must be escaped for the queries - names with words 'or' & 'and' and words starting with numbers. I used code similar to this:
string _path = "/path/to-item/12name and qwe/test"; // your path
string[] pathParts = _path.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries);
string escapedPath = string.Join("/", pathParts.Select(p =>
{
if (p.Contains("and") || p.Contains("or") || p.Contains("-") || char.IsDigit(p[0]))
{
return "#" + p + "#";
}
return p;
}));