My good friend and colleague, George Tryfonas had developed a server control for exposing dictionary items in the page editor, for a 6.5 installation. It was a brilliant little gem, named "DictionaryLiteral" that got forgotten later on, in subsequent Sitecore projects.

Recently, we have undertaken the task of redeveloping an existing 6-year-old non-Sitecore site into a Sitecore application, and of course we went with Sitecore 8, the (currently) latest version. One of the first things we decided was to re-iterate that (now 2-year-old) implementation to include dictionary domains. 

First things first, if you do not know what dictionary domains are, do read up on them. It's worth it.

So, I had the following information to go on with:

  • The original implementation was a subclass of itecore.Web.UI.WebControls.Text and searched the /System/Dictionary item for a specific subpath, provided as a custom extra attribute. Once it found a match, it would then assign the match as the Sitecore.Web.UI.WebControls.Text.Item, and the "Phrase" property as the field to render. I wanted to keep that functionality as close as possible, at the very least, still subclass Sitecore.Web.UI.WebControls.Text.
  • Each dictionary domain is a sitecore item within the content database, with dictionary folders and dictionary items beneath it.
  • We can define a "default" dictionary domain per sitecore logical site, under /configuration/sitecore/websites/website/@dictionaryDomain attribute. The value of that attribute is the name of the dictionary domain item to use as default for that site. When the attribute is present, it should override the /Sitecore/System/Dictionary, and use that particular item as the "dictionary".
  • Each dictionary domain may have a "Fallback Dictionary" [domain] property, so there is potentially an entire chain of dictionary domains one could go down into to dig up the requested resource.
  • We can either ask for a dictionary item by key only, where
    • we search through the hierarchy of dictionary domains and fallbacks if the site has a default dictionary domain defined or
    • we search through /Sitecore/System/Dictionary if the site doesn't have a default dictionary domain or the resource was not found within the aforementioned hierarchy
    • Or we can ask for a dictionary key in a specific dictionary domain, where we override the site's default dictionary domain [hierarchy] (if it exists) and go with the one provided in the override.
  • The original DictionaryLiteral implementation required that the dictionary item name was identical to the dictionary item "Key" field value. That little quirk had caused a few minor problems, particularly during the first stages of entering the data within the dictionary. Since I was revisiting the idea with hindsight, I thought I should remove that requirement altogether.

So the plan now was:

1. Create a class inheriting from Sitecore.Web.UI.WebControls.Text:

using System;
using System.Text;
namespace MySitecoreHelpers.FieldTypes
{
    public class DictionaryLiteral : Sitecore.Web.UI.WebControls.Text
    { //code goes here
    }
}

2. Define a custom "Key" property, that will be used as our entry point, as per the original implementation. 

using System;
using System.Text;
namespace MySitecoreHelpers.FieldTypes
{
    public class DictionaryLiteral : Sitecore.Web.UI.WebControls.Text
    {
        private string _key = string.Empty;
        private string _domain = string.Empty;
        public string Key
        {
            get
            {
                return _key;
            }
            set
            {
                _key = value;
                //code for domain and search goes here
            }
        }
    }
}

3. I had a choice of either adding a second property for domain, or go with a single property, key that would be "composite", so probably a delimited string. I opted for the latter, to avoid any race conditions where the setter for the "Key" might run before the setter for a potential "Domain" property. Therefore, providing the "Key" should be sufficient for us to derive both domain and dictionary key. The obvious choice was a delimited string. I chose the colon (":") as the separator, mainly influenced by the way ASP.NET handled inline resource file handling, like so:

<asp:Label ID="Label1" Runat="server" Text="<%$ Resources:LocalizedText, Msg1 %>"></asp:Label>

Therefore, I decided for a schema as 

[dictionaryDomainName:]DictionaryItemKeyFieldValue

For the value of the "Key" property, where apparently, the part in brackets is optional. So, in order to start the search, we first identify which dictionary domain we are going to use:

using System;
using System.Text;
namespace MySitecoreHelpers.FieldTypes
{
    public class DictionaryLiteral : Sitecore.Web.UI.WebControls.Text
    {
        private string _key = string.Empty;
        private string _domain = string.Empty;
        public string Key
        {
            get
            {
                return _key;
            }
            set
            {
                _key = value;
                var Searchkey=_key;
                if (_key.Contains(":"))
                {
                    _domain = _key.Split(new []{':'},2)[0];
                    Searchkey=_key.Split(new []{':'},2)[1];
                }
                else if (!string.IsNullOrEmpty(Sitecore.Context.Site.DictionaryDomain))
                {
                    _domain = Sitecore.Context.Site.DictionaryDomain;
                }
                if (!string.IsNullOrEmpty(_domain))
                {
                    //Search within a specific dictionary domain goes here
                }
                if (Item==null)
                {
                    var dictionaryBaseItem = Sitecore.Context.Database.GetItem("/sitecore/system/Dictionary");
                    //either the dictionary key was not found in the dictionary domain hierarchy, or //there is no dictionary domain to begin with. In both cases, we end up in this //part of the code, where the code to search within the standard dictionary should go
                }
                this.Field = "Phrase";
            }
        }
    }
}

4. At first, when implementing the search within a specific dictionary domain, I thought I should go with a sitecore content search to get the sitecore item for the dictionary domain. However, since this part included searching down the hierarchy, I went to see the definition of the data template for the dicitonary domain (/sitecore/templates/System/Dictionary/Dictionary Domain) to see how the dropdown with the Fallback Domain choice was populated, and saw that the datasource was this:

query:fast://*[@@templateid='{0A2847E6-9885-450B-B61E-F9E6528480EF}' and @@key!='__standard values']

Which essentially means: 

traverse the entire tree and find all items with template being "/sitecore/templates/System/Dictionary/Dictionary Domain" (that's the template whose ID is  {0A2847E6-9885-450B-B61E-F9E6528480EF}).

Thus I chose to get the current dictionary domain (and all Fallbacks) in the good old-fashioned way of the Sitecore Fast Query:

using System;
using System.Text;

namespace MySitecoreHelpers.FieldTypes
{
    public class DictionaryLiteral : Sitecore.Web.UI.WebControls.Text
    {
        private string _key = string.Empty;
        private string _domain = string.Empty;
        public string Key
        {
            get
            {
                return _key;
            }
            set
            {
                _key = value;
                var Searchkey=_key;
                if (_key.Contains(":"))
                {
                    _domain = _key.Split(new []{':'},2)[0];
                    Searchkey=_key.Split(new []{':'},2)[1];
                }
                else if (!string.IsNullOrEmpty(Sitecore.Context.Site.DictionaryDomain))
                {
                    _domain = Sitecore.Context.Site.DictionaryDomain;
                }
                if (!string.IsNullOrEmpty(_domain))
                {
                    var dictionaryBaseItem = Sitecore.Context.Database.SelectSingleItem(
                        string.Format(
                            "fast://*[@@templateid='{{0A2847E6-9885-450B-B61E-F9E6528480EF}}' and @@key='{0}']",
                            _domain)
                    );
                    //code for search and dive into fallbacks goes here
                }
                if (Item==null)
                {
                    var dictionaryBaseItem = Sitecore.Context.Database.GetItem("/sitecore/system/Dictionary");
                    //search under the system dictionary
                }
                if (Item == null)
                {
                    //last resort, try to display something that says "this resource was not found"
                    this.Item = Sitecore.Context.Database.GetItem("/sitecore/system/Dictionary/DictionaryNotFound");
                    if (Item==null)
                    {
                        return;
                    }
                }
                this.Field = "Phrase";
            }
        }
    }
}

5. Now comes the hard part. We have an Item (either a dictionary domain or /Sitecore/System/Dictionary) and we need to search within this item's subtree for a child item that has a "Key" field, whose value should match our "SearchKey" variable's value. This sounds like a job for Sitecore Content Search, and indeed it is!

using System.Collections.Generic;
using System.Linq;
using Sitecore.ContentSearch;
using System;
using System.Text;

namespace MySitecoreHelpers.FieldTypes
{
    public class DictionaryLiteral : Sitecore.Web.UI.WebControls.Text
    {
        private string _key = string.Empty;
        private string _domain = string.Empty;
        public string Key
        {
            get
            {
                return _key;
            }
            set
            {
                _key = value;
                var Searchkey=_key;
                if (_key.Contains(":"))
                {
                    _domain = _key.Split(new []{':'},2)[0];
                    Searchkey=_key.Split(new []{':'},2)[1];
                }
                else if (!string.IsNullOrEmpty(Sitecore.Context.Site.DictionaryDomain))
                {
                    _domain = Sitecore.Context.Site.DictionaryDomain;
                }
                if (!string.IsNullOrEmpty(this.Field) || this.Item!=null)
                {
                    this.Field = "Phrase";
                    return;
                }
                if (!string.IsNullOrEmpty(_domain))
                {
                    var dictionaryBaseItem = Sitecore.Context.Database.SelectSingleItem(string.Format("fast://*[@@templateid='{{0A2847E6-9885-450B-B61E-F9E6528480EF}}' and @@key='{0}']", _domain));
                    while (dictionaryBaseItem!=null && Item==null)
                    {
                        using (var context = ContentSearchManager.CreateSearchContext((SitecoreIndexableItem)dictionaryBaseItem ))
                        {
                            var query = context.GetQueryable<Sitecore.ContentSearch.SearchTypes.SearchResultItem>().Where(p=> p["Key"].Equals(Searchkey) && p.Paths.Contains(dictionaryBaseItem.Id.ToString()));
                            if (query.Count()>0)
                            {
                                this.Item = Sitecore.Context.Database.GetItem(query.First().ItemId);
                            }
                        }
                        if (Item==null)
                        {
                            dictionaryBaseItem =string.IsNullOrEmpty(dictionaryBaseItem["Fallback Domain"])?null:Sitecore.Context.Database.GetItem(Sitecore.Data.ID.Parse(dictionaryBaseItem["Fallback Domain"]));
                        }
                    }
                }
                if (Item==null)
                {
                    var dictionaryBaseItem = Sitecore.Context.Database.GetItem("/sitecore/system/Dictionary");
                    using (var context = ContentSearchManager.CreateSearchContext((IIndexable)dictionaryBaseItem))
                    {
                        var query = context.GetQueryable<Sitecore.ContentSearch.SearchTypes.SearchResultItem>().Where(p => p["Key"].Equals(Searchkey) && p.Paths.Contains(dictionaryBaseItem.ID.ToString()));
                        if (query.Count() > 0)
                        {
                            this.Item = Sitecore.Context.Database.GetItem(query.First().ItemId);
                        }
                    }
                }
                if (Item == null)
                {
                    this.Item = Sitecore.Context.Database.GetItem("/sitecore/system/Dictionary/DictionaryNotFound");
                    if (Item==null)
                    {
                        return;
                    }
                }
                this.Field = "Phrase";
            }
        }
    }
}

So in the above code, we get the dictionary domain item (if it exists) and search within it (that's what the extra predicate with the Path.Contains is for) for a matching item. If not found, we get the fallback domain and do it all over. If we end up having no fallbacks, we try the /Sitecore/System/Dictionary subtree as a last resort. If we started off without a domain to begin with, then we just search within /Sitecore/System/Dictionary.

 

There are two possible enhancements to the above code.

1. The fallback chain could lead to a circle. I'm not sure how Sitecore checks for circular references between droplinks (if at all) but we should add code for breaking the while loop if there is a circular reference

2. The actual item should be cached somehow, so that multiple calls to DictionaryLiteral do not trigger muliple searches.

 

So, here's the complete code:

using System.Collections.Generic;
using System.Linq;
using Sitecore.ContentSearch;
using System;
using System.Text;

namespace MySitecoreHelpers.FieldTypes
{
    public class DictionaryLiteral : Sitecore.Web.UI.WebControls.Text
    {
        private string _key = string.Empty;
        private string _domain = string.Empty;
        public string Key
        {
            get
            {
                return _key;
            }
            set
            {
                _key = value;
                var Searchkey=_key;
                if (_key.Contains(":"))
                {
                    _domain = _key.Split(new []{':'},2)[0];
                    Searchkey=_key.Split(new []{':'},2)[1];
                }
                else if (!string.IsNullOrEmpty(Sitecore.Context.Site.DictionaryDomain))
                {
                    _domain = Sitecore.Context.Site.DictionaryDomain;
                }
                if (!string.IsNullOrEmpty(this.Field) || this.Item!=null)
                {
                    this.Field = "Phrase";
                    return;
                }
                if (!string.IsNullOrEmpty(_domain))
                {
                    var dictionaryBaseItem = Sitecore.Context.Database.SelectSingleItem(string.Format("fast://*[@@templateid='{{0A2847E6-9885-450B-B61E-F9E6528480EF}}' and @@key='{0}']", _domain));
                    while (dictionaryBaseItem!=null && Item==null)
                    {
                        using (var context = ContentSearchManager.CreateSearchContext((SitecoreIndexableItem)dictionaryBaseItem ))
                        {
                            var query = context.GetQueryable<Sitecore.ContentSearch.SearchTypes.SearchResultItem>().Where(p=> p["Key"].Equals(Searchkey) && p.Paths.Contains(dictionaryBaseItem.Id.ToString()));
                            if (query.Count()>0)
                            {
                                this.Item = Sitecore.Context.Database.GetItem(query.First().ItemId);
                            }
                        }
                        if (Item==null)
                        {
                            dictionaryBaseItem =string.IsNullOrEmpty(dictionaryBaseItem["Fallback Domain"])?null:Sitecore.Context.Database.GetItem(Sitecore.Data.ID.Parse(dictionaryBaseItem["Fallback Domain"]));
                        }
                    }
                }
                if (Item==null)
                {
                    var dictionaryBaseItem = Sitecore.Context.Database.GetItem("/sitecore/system/Dictionary");
                    using (var context = ContentSearchManager.CreateSearchContext((IIndexable)dictionaryBaseItem))
                    {
                        var query = context.GetQueryable<Sitecore.ContentSearch.SearchTypes.SearchResultItem>().Where(p => p["Key"].Equals(Searchkey) && p.Paths.Contains(dictionaryBaseItem.ID.ToString()));
                        if (query.Count() > 0)
                        {
                            this.Item = Sitecore.Context.Database.GetItem(query.First().ItemId);
                        }
                    }
                }
                if (Item == null)
                {
                    this.Item = Sitecore.Context.Database.GetItem("/sitecore/system/Dictionary/DictionaryNotFound");
                    if (Item==null)
                    {
                        return;
                    }
                }
                this.Field = "Phrase";
            }
        }
    }
}

Final result:

1. Create the dictionary domain and a dictionary item

Create a dictionary

2. Register the control in your layouts/sublayouts

Register the control

3. add a dictionary literal somewhere:

Add dictionaryLiteral element in code

4. Done! The phrase is editable from within the page editor:

Dictionary item is now editable

Happy coding!