5 steps to have Sitecore Support Datasource Parameters and Dynamic Datasources on TreeList Fields

Recently, I have been working on a multi-tenant Sitecore implementation project with a specific requirement. I needed to have a Sitecore TreeList that could support dynamic query datasources while also working side-by-side with datasource parameters. In this context it’s also important to mention that this field was used within a custom media library template which really didn’t have the context for the current site so that needed to be determined dinamically.

As already explained in Kamruz’s post, query and parameters don’t work side-by-side without creating a custom field.

What I was trying to get is something like this declared on the field Source property:

DataSource=query:../../&ExcludeTemplatesForSelection=NotDesirableTemplate.

BUT there is a problem with the query definition above. Since this needed to be dynamically based on the current site, I needed to determine the current tenant and replace it’s path on the datasource query.

You still with me? Remember,  we are trying to have a TreeList field that supports dynamic datasources and still uses parameters like ExcludeTemplatesForSelection.

Below I’ll describe 5 steps I had to take to implement this and a few caveats one might consider.

 

1. Create a Token on the Query Definition

DataSource=query:#mytoken#&ExcludeTemplatesForSelection=NotDesirableTemplate.

Next we need to do two things: Create a custom TreeList field and handle #mytoken# to replace it with the tenant path.

 

2. Create the Custom Field

On Sitecore, switch to the Core database and create a new Custom List Type. To do that navigate to the path: /sitecore/system/Field Types/List Types and create a new List type. In my case I called it ‘CustomTreeList.’

Next, fill in the field definitions for Assembly and Class. Here is a screenshot of my field definitions:

 

TreeList Screenshot

3. Implement the Custom TreeList Field

Now we need to implement the custom field and handle the token set on step 1. I have used Kamruz’s code as a base and adapted it for this requirement. The code looks like this:

 


namespace MyNamespace.CustomFields
{
    public class CustomTreeList : Sitecore.Shell.Applications.ContentEditor.TreeList
    {
        private string _ds = string.Empty;


        /// <summary>
        /// Override the TreeList DataSource property to support the mytoken on the Source
        /// </summary>
        public override string DataSource
        {
            get
            {
                if (_ds.StartsWith("query:"))
                {
                    if (Sitecore.Context.ContentDatabase == null || base.ItemID == null)
                        return null;
                    var current = Sitecore.Context.ContentDatabase.GetItem(base.ItemID);

                    if (_ds.Contains("#mytoken#"))
                    {                       
                        //uses the current media item to resolve what the tenant which it belongs to
                        var myTenant = Tenant.GetTenant(new ID(ItemID));
                        if (myTenant != null)
                        {
                            var sourceFolder = myTenant.GetChildren().FirstOrDefault(i=>; i.Template.ID.ToString() == Constants.Home.ID);
                            if (sourceFolder != null)
                            {
                                //found the Home item under the tenant item so replace the datasource with the Home Path
                                _ds = _ds.Replace("#mytoken#", sourceFolder.Paths.FullPath);
                            }
                        }
                        else
                        {
                            Log.Warn("Could not find tenant for item "+ItemID+ " : the path /sitecore/content is returned by default. This could be because the item is _standard values" , ItemID);
                            _ds = "/sitecore/content";
                        }

                    }

                    Item item = null;
                    try
                    {
                        item = Enumerable.FirstOrDefault<Item>((IEnumerable<Item>)LookupSources.GetItems(current, _ds));
                    }
                    catch (Exception ex)
                    {
                        Log.Error("Treelist field failed executing the query", ex, (object)this);
                    }

                    return item == null ? null : item.Paths.FullPath;
                }
                return _ds;
            }
            set { _ds = value; }
        }
    }

}

 

In a nutshell, when the code finds the #mytoken# set on the Source it will override the DataSource property and resolve the Tenant Home item and replace it’s path on the query.
So, if you have a content tree that has a media item that should be resolved to a Tenant 1 for example, after executing the code above, the outcome of the Source would look like this:
DataSource=query:/Sitecore/Content/Tenant1/Home&ExcludeTemplatesForSelection=NotDesirableTemplate.

This works well, but we are not quite done yet.

 

 
This is a TreeList type which is used by the Links Database. In order to maintain Sitecore’s broken links functionalities (i.e. removing broken links, relinking it to other items, etc.) you still need to inform Sitecore what should happen when it rebuilds the links for this field type.
 
You also need to make sure that this field works correctly with Content Search.
 

4. Rebuild Link Changes

In order for this field to be supported by the Links database, you need to implement a new field which inherits from CustomField class and register it. This is well explained here.

Registering the field:

 



<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <fieldTypes>
            <!--Used to update the link database-->
            <fieldType name="CustomTreeList" type="MyNamespace.CustomFields.CustomTreeListType, MyNamespace" />
        </fieldTypes>
    </sitecore>
</configuration>


Class implementation:

namespace MyNamespace.CustomFields
{
    public class CustomTreeListType : CustomField
    {
        public CustomTreeListType(Field innerField) : base(innerField)
        {
        }

        public CustomTreeListType(Field innerField, string runtimeValue) : base(innerField, runtimeValue)
        {
        }
       
        private void AddLink(ID id)
        {
            if (!Value.EndsWith("|") && Value.Length > 0)
            {
                Value += "|";
            }
            Value += id.ToString();
        }

        
        private void ClearLink(ID targetItemID)
        {
            Value = Value.Replace(targetItemID.ToString(), "");
            Value = Value.Replace("||", "|");
            Value = Value.TrimEnd('|');
            Value = Value.TrimStart('|');
        }

        private void ClearAllInvalidLinks()
        {
            foreach (var itemId in Value.Split('|'))
            {
                var targetItem = InnerField.Database.GetItem(new ID(itemId));
                if (targetItem != null)
                    ClearLink(targetItem.ID);
            }
        }
       
        public override void Relink(ItemLink itemLink, Item newLink)
        {
            Assert.ArgumentNotNull(itemLink, "itemLink");
            Assert.ArgumentNotNull(newLink, "newLink");
            Database database = Factory.GetDatabase(itemLink.TargetDatabaseName);
            if (database != null)
            {
                Item targetItem = database.GetItem(itemLink.TargetItemID);
                if (targetItem == null)
                {
                    ClearLink(itemLink.TargetItemID);
                }
                else
                {
                    if (Value.Contains(itemLink.TargetItemID.ToString()))
                    {
                        Value = Value.Replace(itemLink.TargetItemID.ToString(), newLink.ID.ToString());
                    }
                    else
                    {
                        AddLink(newLink.ID);
                    }
                }
            }
        }

        public override void RemoveLink(ItemLink itemLink)
        {
            Assert.ArgumentNotNull(itemLink, "itemLink");
            ClearLink(itemLink.TargetItemID);
        }

        public override void UpdateLink(ItemLink itemLink)
        {
            Assert.ArgumentNotNull(itemLink, "itemLink");
            Database database = Factory.GetDatabase(itemLink.TargetDatabaseName);
            if (database == null)
            {
                ClearLink(itemLink.TargetItemID);
            }
            else
            {
                ClearAllInvalidLinks();
                Item targetItem = database.GetItem(itemLink.TargetItemID);
                if (targetItem != null)
                    AddLink(targetItem.ID);
            }
        }

        public override void ValidateLinks(LinksValidationResult result)
        {
            Assert.ArgumentNotNull(result, "result");

            string itemIds = this.Value;
            if (string.IsNullOrEmpty(itemIds))
            {
                Log.Info("Validate links ID is empty", itemIds);
                return;
            }
                
            Log.Info("IDs = "+Value, result);
            foreach (var itemId in this.Value.Split('|'))
            {
                var targetItem = this.InnerField.Database.GetItem(new ID(itemId));
                if (targetItem != null)
                    result.AddValidLink(targetItem, targetItem.Paths.FullPath);
                else
                    result.AddBrokenLink(itemId);
            }
        }
    }
}


5. Content Search Changes

We’re almost there! The last piece you need to account for is storing the new field so that it will work with Content Search. Otherwise, you will encounter the problem described here. As explained in Richard Seal’s answer, in order for your results to return properly, you need to  create a new config patch field and set the StorageType=Yes. In my case, FieldTypeName=”CustomTreeList” and StorageType=”Yes.”

 


<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <contentSearch>
            <indexConfigurations>
                <defaultLuceneIndexConfiguration>
                    <fieldMap>
                        <fieldTypes hint="raw:AddFieldByFieldTypeName">
                            <!--needed to set the storagetype=Yes so the field shows within the index-->
                            <fieldType fieldTypeName="customtreelist" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" patch:after="*[1]" />
                        </fieldTypes>
                    </fieldMap>
                </defaultLuceneIndexConfiguration>
            </indexConfigurations>
        </contentSearch>
    </sitecore>
</configuration>

 

Working in a multi-tenant environments can present any number of challenges. Hopefully, this 5 step process will be prove helpful to you if you need to have dynamic datasources and still support datasource parameters on your fields.

Until Next Time
Diego

Diego Moretto

I am a software developer with over 13 years of experience. I enjoy building new things, solving problems, learning new technologies and becoming a better developer every day. Ten years ago I started working with .Net and became impassioned with the platform. Since early 2011 my work has been fully dedicated to Sitecore and anything related to its ecosystem. I am a Sitecore certified developer and most recently I was awarded the Sitecore Technology 2017 MVP. In my personal time I like playing video games and tennis, and spending quality time with my family. Learn more about Diego Moretto.

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back