1

I'm trying to use the Multi-root treelist that Kam Figy made and that Zachary Kniebel extended. I used this guide to add the Field-type to the Core database.

All is well, until I put query: anywhere in the fields source. In the use-case I have I need to have at least one of the two roots be a query like so:

query:./ancestor-or-self::*[@@templatename='Website']/Sitedata/Facets

The other root can be an absolute path if need be, or a query, for this example let's say it's the content-root, I've tried these values in the Source-field of the template-field with Multi-root Treelist:

query:./ancestor-or-self::*[@@templatename='Website']/Sitedata/Facets|/sitecore/content
// yields only 1st root, the query, content-root isn't there.
datasource=query:./ancestor-or-self::*[@@templatename='Website']/Sitedata/Facets|/sitecore/content
// yields 2nd root correctly, 1st root defaults to /sitecore root
datasource=query:./ancestor-or-self::*[@@templatename='Website']/Sitedata/Facets|datasource=/sitecore/content
// both roots are now defaulting to /sitecore root
datasource=query:./ancestor-or-self::*[@@templatename='Website']/Sitedata/Facets|datasource=query:/sitecore/content
// again both roots now /sitecore root
query:./ancestor-or-self::*[@@templatename='Website']/Sitedata/Facets|query:/sitecore/content
// yields nothing, no roots shown, completely empty field in content-editor

Whether I use Kam's code or Zachary's code, the results are the same. What's going on?

EDIT:

I've done some digging and I think I've narrowed the problem down somewhat. If the value starts with query: then Sitecore does the parsing and disregards anything after the | before passing it on to the custom code. So I'm using the 2nd query, with format datasource=query:./ancestor-etc|/sitecore/content. And I'm using Kam's code-base extended only with the parts of Zachary's code that parse the query: as Kam's code didn't do that. So this is my version of Kam's CreateDataContext:

protected virtual DataContext CreateDataContext(DataContext baseDataContext, string dataSource)
{
    Item datasourceItem = null;
    if (dataSource.StartsWith("query:"))
    {
        try
        {
            var currentItem = Sitecore.Context.ContentDatabase.GetItem(this.ItemID);
            var results = LookupSources.GetItems(currentItem, dataSource);
            datasourceItem = results.FirstOrDefault(item => item != null);
            dataSource = datasourceItem.Paths.FullPath;
        }
        catch (Exception ex)
        {
            Log.Error($"Treelist field failed to execute query: '{dataSource}'", ex, this);
        }
    }

    DataContext dataContext = new DataContext
    {
        ID = GetUniqueID("D"),
        Filter = baseDataContext.Filter,
        DataViewName = "Master",
        Root = dataSource,
        Language = Language.Parse(this.ItemLanguage)
    };

    if (!string.IsNullOrEmpty(this.DatabaseName))
    {
        dataContext.Parameters = "databasename=" + this.DatabaseName;
    }

    return dataContext;
}

Stepping into the code, it looks like LookupSources.GetItems() doesn't like my query as it returns an empty Item-array. To be clear, this query works perfectly fine when used as a datasource for a controller rendering, so it's not the query itself.

I tried the following in the debugger's Immediate Window while pauzed right after the LookupSources.GetItems() line above:

LookupSources.GetItems(currentItem, "query:.");
'LookupSources.GetItems(currentItem, "query:.")' threw an exception of type 'Sitecore.Exceptions.LookupSourceException'
    Data: {System.Collections.ListDictionaryInternal}
    HResult: -2146233088
    HelpLink: null
    InnerException: {"Index was outside the bounds of the array."}
    InnerMessage: "Index was outside the bounds of the array."
    Message: "Invalid lookup source \"query:.\": Index was outside the bounds of the array.."
// ^ so the "query:" part should be trimmed, aha

LookupSources.GetItems(currentItem, ".");
{Sitecore.Data.Items.Item[1]}
    "Werken bij banner": Werken bij banner (en#1@master), id: {7D9F219F-19EA-4CEB-AC8B-BCE4894D71B1}
// This is the only child-item of the current item. I kinda expected the current Item, not its children.
// But at least LookupSources isn't completely kaput

LookupSources.GetItems(currentItem, "..");
{Sitecore.Data.Items.Item[2]}
// This is the current Item and its only sibling, so far so good

LookupSources.GetItems(currentItem, "./ancestor-or-self::*[@@templatename='website']/sitedata/facets");
{Sitecore.Data.Items.Item[0]}
// Nothing

LookupSources.GetItems(currentItem, "./ancestor-or-self::*");
{Sitecore.Data.Items.Item[0]}
// Nothing still?

LookupSources.GetItems(currentItem, "./ancestor::*");
{Sitecore.Data.Items.Item[57]}
// So maybe ancestor-or-self is kaput and I should use just ancestor?

LookupSources.GetItems(currentItem, "./ancestor::*[@@templatename='website']");
{Sitecore.Data.Items.Item[0]}
// Nope, still kaput

LookupSources.GetItems(currentItem, "./ancestor::*[@@templateid='{299B2290-E128-4633-8F9A-DCCC1CCBA9DE}']");
{Sitecore.Data.Items.Item[0]}
// Google searches use @@templateid in stead of @@templatename, but nope.

LookupSources.GetItems(currentItem, "./ancestor-or-self::*[@@templateid='{299B2290-E128-4633-8F9A-DCCC1CCBA9DE}']");
{Sitecore.Data.Items.Item[0]}
// ancestor-or-self combined with @@templateid just to be sure, alas

// maybe the string isn't parsed correctly before sending it to GetItems() to begin with so lets make it @""
LookupSources.GetItems(currentItem, @"./ancestor::*[@@templateid='{299B2290-E128-4633-8F9A-DCCC1CCBA9DE}']");
{Sitecore.Data.Items.Item[0]}
LookupSources.GetItems(currentItem, @"./ancestor::*[@@templatename='website']");
{Sitecore.Data.Items.Item[0]}
// still nothing...

TL;DR: LookupSources.GetItems() doesn't seem to do ancestor-or-self and doesn't do *[@@templatename='Website'] or *[@@templateid='{299B2290-E128-4633-8F9A-DCCC1CCBA9DE}']. What to do about that or try differently?

2
  • Have you stepped into the code to see what is happening there?
    – Richard Seal
    May 16, 2019 at 21:22
  • Yes, I put breakpoints on the first lines of all methods and properties of Zachary's code and it seems his private field _dataSources has a value before any of the code is ever executed. I don't understand how this can happen. The value of _dataSources is the result of the query at that point (so a string[1] with an absolute Sitecore path in the only array value).
    – asontu
    May 16, 2019 at 21:51

2 Answers 2

1

I fixed the issue myself with some interesting findings. The customer this concerns is running Sitecore 8.1, different versions might yield different results.

As mentioned in the EDIT's debug copy-pasta, LookupSources.GetItems() does not return the items found with the query, but rather those items' children. As well it doesn't support ancestor-or-self::, only ancestor::. This renders LookupSources.GetItems() useless for me. (Frankly I would consider different implementations of query:-resolving within the same CMS harmful)

In stead I took code from Sitecore's Habitat project to find the correct Item by giving the query-string either to the contextItem's .Axes.SelectItems() or .Database.SelectItems():

query.StartsWith("./", StringComparison.OrdinalIgnoreCase)
    ? currentItem.Axes.SelectItems(query)
    : currentItem.Database.SelectItems(query);

But, this doesn't entirely fix the issue, because Sitecore preprocesses the value given to the above Custom Field code by putting it all in lower case. This is a (big, huge, glaring) problem, because @@templatename might contain upper case letters like in my case 'Website' and @@templateid definitely contains a Guid with upper case letter A-F and matching to a lower case guid does not work.

So you need to use @@templatekey, which is the templatename in lower case.

Looking at the Sitecore documentation, this lower case issue renders a bunch of @@var's useless, like @@id, @@name (use @@key in stead) or any @fieldname of fields where the value might contain letters.

So I switched to using @@templatekey and used Kam's code as a basis, only adding the parts from Zachary that I liked, needed and that worked:

namespace Aeres.Foundation.SitecoreExtensions.CustomFields
{
    using System;
    using System.Linq;
    using Sitecore.Data.Items;
    using Sitecore.Diagnostics;
    using Sitecore.Globalization;
    using Sitecore.Shell.Applications.ContentEditor;
    using Sitecore.Text;
    using Sitecore.Web;
    using Sitecore.Web.UI.HtmlControls;
    using Sitecore.Web.UI.WebControls;

    /// <summary>
    /// This field type is like a tree list, but you can specify more than one root item to select from (for example, videos or photos)
    /// The data source roots are specified using pipe delimiting just like regular Sitecore Query language
    /// </summary>
    public class MultiRootTreeList : TreeList
    {
        private const string QueryPrefix = "query:";

        protected override void OnLoad(EventArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            base.OnLoad(args);

            if (!Sitecore.Context.ClientPage.IsEvent)
            {
                // find the existing TreeviewEx that the base OnLoad added, get a ref to its parent, and remove it from controls
                var existingTreeView = (TreeviewEx)WebUtil.FindControlOfType(this, typeof(TreeviewEx));
                var treeviewParent = existingTreeView.Parent;

                existingTreeView.Parent.Controls.Clear(); // remove stock treeviewex, we replace with multiroot

                // find the existing DataContext that the base OnLoad added, get a ref to its parent, and remove it from controls
                var dataContext = (DataContext)WebUtil.FindControlOfType(this, typeof(DataContext));
                var dataContextParent = dataContext.Parent;

                dataContextParent.Controls.Remove(dataContext); // remove stock datacontext, we parse our own

                // create our MultiRootTreeview to replace the TreeviewEx
                var impostor = new EnhancedMultiRootTreeview
                {
                    ID = existingTreeView.ID,
                    DblClick = existingTreeView.DblClick,
                    Enabled = existingTreeView.Enabled,
                    DisplayFieldName = existingTreeView.DisplayFieldName
                };

                // parse the data source and create appropriate data contexts out of it
                var dataContexts = this.ParseDataContexts(dataContext);

                impostor.DataContext = string.Join("|", dataContexts.Select(x => x.ID));
                foreach (var context in dataContexts)
                {
                    dataContextParent.Controls.Add(context);
                }

                // inject our replaced control where the TreeviewEx originally was
                treeviewParent.Controls.Add(impostor);
            }
        }

        protected virtual DataContext[] ParseDataContexts(DataContext originalDataContext)
        {
            return new ListString(this.DataSource).Select(x => this.CreateDataContext(originalDataContext, x)).ToArray();
        }

        protected virtual DataContext CreateDataContext(DataContext baseDataContext, string dataSource)
        {
            Item dataSourceItem = null;
            if (dataSource.StartsWith(QueryPrefix))
            {
                try
                {
                    var query = dataSource.Substring(QueryPrefix.Length);
                    var currentItem = Sitecore.Context.ContentDatabase.GetItem(this.ItemID);
                    var results = query.StartsWith("./", StringComparison.OrdinalIgnoreCase)
                        ? currentItem.Axes.SelectItems(query)
                        : currentItem.Database.SelectItems(query);
                    dataSourceItem = results.FirstOrDefault(item => item != null);
                    dataSource = dataSourceItem.Paths.FullPath;
                }
                catch (Exception ex)
                {
                    Log.Error($"Treelist field failed to execute query: '{dataSource}'", ex, this);
                }
            }

            DataContext dataContext = new DataContext
            {
                ID = GetUniqueID("D"),
                Filter = baseDataContext.Filter,
                DataViewName = "Master",
                Root = dataSource,
                Language = Language.Parse(this.ItemLanguage)
            };

            if (!string.IsNullOrEmpty(this.DatabaseName))
            {
                dataContext.Parameters = "databasename=" + this.DatabaseName;
            }

            return dataContext;
        }
    }
}

and:

namespace Aeres.Foundation.SitecoreExtensions.CustomFields
{
    using Sitecore.Data.Items;
    using Sitecore.Diagnostics;
    using Sitecore.Web.UI.WebControls;

    public class EnhancedMultiRootTreeview : MultiRootTreeview
    {
        protected override string GetHeaderValue(Item item)
        {
            Assert.ArgumentNotNull(item, "item");

            var nodeTitle = string.IsNullOrEmpty(this.DisplayFieldName) ? item.DisplayName : item[this.DisplayFieldName];
            nodeTitle = string.IsNullOrEmpty(nodeTitle) ? item.DisplayName : nodeTitle;

            // you can get fancy here and make the format of the nodeTitle manageable in config
            nodeTitle = $"{nodeTitle}  -  <span>({item.Paths.ContentPath})</span>";

            return nodeTitle;
        }
    }
}
0

Sorry to give a link-only answer, but this module is worth considering:

https://github.com/mikaelnet/sitecore-multi-treelist

Since it's wise to consider using TreeListEx field over TreeList due to the query performance cost to the Content Editor - if your source query happens to be expensive, it can have a very siginificant effect on performance. Therefore, when your field has a source with multiple queries which this can, then using a version of the field which enables editing via a separate window is a very good idea.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.