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>