5

I am trying to add multiple root folders to the source field of a treelist template field. For example:

Datasource=/sitecore/media library/ | /sitecore/content/

I have done this with sitecore query, but treelist does not use sitecore query. This is for Sitecore 7.1

10

This will require customization. Luckily for you, Kam has an example available at http://kamsar.net/index.php/2015/05/A-Multiple-Root-Treelist-Field/

This should do the trick.

5

Not to self-promote, but I actually wrote a blog post on how to create a Multi-Root Treelist a little while back, based on Kam's post on multi-root Treelists, enhanced with support for using data source parameters with Sitecore Query and also displaying full item paths next to the item names in the tree.

Implementation:

In addition to the code, don't forget that you will need to add a new item for the field type under /sitecore/system/Field types/List Types in the core database.

Below are all the classes and config files that you will need (assuming that you are using Solr; otherwise you may need to update the Custom.FieldTypes.Solr.config to be Lucene-friendly):

MultiRootTreeList.cs:

/// <summary>
/// This field type is like a tree list, but you can specify more than one root item to select from
/// </summary>
/// <remarks>
/// Credit to Kam Figy for providing the foundation for this solution: http://kamsar.net/index.php/2015/05/A-Multiple-Root-Treelist-Field/
/// 
/// The data source roots are specified using pipe delimiting just like regular Sitecore Query language. This is great when a field needs to allow, for example,
/// the selection of both videos and photos. 
/// 
/// Note that this solution also requires a config setting to be added for the field type. For example, the following would be added to the the 
/// sitecore/fieldTypes path: 
///
/// <fieldType name="Multi-Root Treelist" type="Sitecore.Data.Fields.MultilistField,Sitecore.Kernel" /> 
///
/// Additionally, the field type should be added to the ContentSearch configuration, if using ContentSearch
/// </remarks>
public class MultiRootTreeList : TreeList
{
    /// <summary>
    /// Prefix that all Sitecore queries should start with
    /// </summary>
    private const string QueryPrefix = "query:";
    /// <summary>
    /// Parameter key for the DataSource parameter of the Source field
    /// </summary>
    private const string DataSourceParameterKey = "datasource";

    private string[] _sources;
    /// <summary>
    /// The split sources within the <seealso cref="TreeList.Source"/> property
    /// </summary>
    public string[] Sources => _sources ?? (_sources = GetSources());

    private string[] _dataSources;
    /// <summary>
    /// The datasources retrieved from the <seealso cref="Sources"/>
    /// </summary>
    public string[] DataSources
    {
        get
        {
            // if we already found the sources, don't do all this cumbersome logic again
            if (_dataSources != null)
            {
                return _dataSources;
            }

            if (string.IsNullOrEmpty(Source) || global::Sitecore.Context.ContentDatabase == null || ItemID == null)
            {
                return (_dataSources = new string[] { });
            }

            var contextDb = global::Sitecore.Context.ContentDatabase;

            // the current item, from which relatative queries are executed
            var currentItem = contextDb.GetItem(ItemID);

            return (_dataSources = Sources
                .Select(source => GetDatasource(source, currentItem))
                // decided not to fall back here; if the datasource wasn't found then remove it, rather than show a default node
                .Where(datasource => datasource != null)
                .ToArray());
        }
    }

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

        // if this is an event, just run the base logic and return
        if (global::Sitecore.Context.ClientPage.IsEvent)
        {
            return;
        }

        base.OnLoad(args);

        // 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 = ParseDataContexts(dataContext);

        // join the IDs of all of the DataContext objects together, to be added as controls
        impostor.DataContext = 
            string.Join(
                "|", 
                dataContexts
                    .Select(dc => dc.ID));

        // add the new DataContext controls into the selection pane
        foreach (var context in dataContexts)
        {
            dataContextParent.Controls.Add(context);
        }

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

    /// <summary>
    /// Gets all of the datasources from the Source field
    /// </summary>
    protected virtual string[] GetSources()
    {
        if (string.IsNullOrEmpty(Source))
        {
            return new string[] {};
        }

        var datasource = global::Sitecore.StringUtil.ExtractParameter(DataSourceParameterKey, Source).Trim();
        datasource = string.IsNullOrEmpty(datasource)
            ? Source
            : datasource;

        return datasource.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
    }

    /// <summary>
    /// Gets the item path to the datasource item
    /// </summary>
    /// <param name="source">The source used to find the datasource item</param>
    /// <param name="currentItem">The current item</param>
    protected virtual string GetDatasource(string source, Item currentItem)
    {
        Item datasourceItem = null;

        // if the source is a query then run the query from the current item
        if (source.StartsWith(QueryPrefix))
        {
            try
            {
                var results = LookupSources.GetItems(currentItem, source);
                datasourceItem = results
                    .FirstOrDefault(item => item != null);
            }
            catch (Exception ex)
            {
                Log.Error($"Treelist field failed to execute query: '{source}'", ex, this);
            }
        }
        // otherwise just get the item specified
        else
        {
            datasourceItem = currentItem.Database.GetItem(source);
        }

        return datasourceItem?.Paths.FullPath;
    }

    /// <summary>
    /// Parses multiple source roots into discrete data context controls (e.g. 'dataSource=/sitecore/content|/sitecore/media library')
    /// </summary>
    /// <param name="originalDataContext">The original data context the base control generated. We reuse some of its property values.</param>
    protected virtual DataContext[] ParseDataContexts(DataContext originalDataContext)
    {
        // if there are multiple roots; otherwise, just use the original datasource (shouldn't happen, but just in case...)
        return (DataSources.Any() ? DataSources : new ListString(DataSource).AsEnumerable())
            .Select(datasource => CreateDataContext(originalDataContext, datasource))
            .ToArray();
    }

    /// <summary>
    /// Creates a DataContext control for a given Sitecore path data source
    /// </summary>
    protected virtual DataContext CreateDataContext(DataContext baseDataContext, string dataSource)
    {
        var dataContext = new DataContext
        {
            ID = GetUniqueID("D"),
            Filter = baseDataContext.Filter,
            DataViewName = "Master"
        };

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

        dataContext.Root = dataSource;
        dataContext.Language = Language.Parse(ItemLanguage);

        return dataContext;
    }
}

EnhancedMultiRootTreeView.cs:

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

        var nodeTitle = string.IsNullOrEmpty(DisplayFieldName) ? item.DisplayName : item[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.FullPath})</span>";

        return nodeTitle;
    }
}

Custom.FieldTypes.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <fieldTypes>
      <fieldType name="Multi-Root Treelist" type="Sitecore.Data.Fields.MultilistField,Sitecore.Kernel" patch:after="fieldType[@name='Treelist']" />
    </fieldTypes>
  </sitecore>
</configuration>

Custom.FieldTypes.Solr.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <contentSearch>
      <indexConfigurations>
        <defaultSolrIndexConfiguration type="Sitecore.ContentSearch.SolrProvider.SolrIndexConfiguration, Sitecore.ContentSearch.SolrProvider">
          <fieldMap type="Sitecore.ContentSearch.SolrProvider.SolrFieldMap, Sitecore.ContentSearch.SolrProvider">
            <fieldTypes hint="raw:AddFieldByFieldTypeName">
              <fieldType fieldTypeName="multi-root treelist"   returnType="stringCollection" />
            </fieldTypes>
          </fieldMap>
          <fieldReaders type="Sitecore.ContentSearch.FieldReaders.FieldReaderMap, Sitecore.ContentSearch">
            <mapFieldByTypeName hint="raw:AddFieldReaderByFieldTypeName">
              <fieldReader fieldTypeName="multi-root treelist"    fieldReaderType="Sitecore.ContentSearch.FieldReaders.MultiListFieldReader, Sitecore.ContentSearch" />
            </mapFieldByTypeName>
          </fieldReaders>
        </defaultSolrIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
</configuration>
  • A problem I noticed with this is that the base.OnLoad() call will throw an exception when multiple datasources are present. Is there a known fix for this? – Marcel Apr 8 '19 at 19:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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