Sitecore Custom Field: Method Field

The purpose of this blog post is primarily to present an idea that I like to refer to as the method field or jokingly “content author IOC”.  However, it also just happens to be a good excuse to build a light tutorial on how to build a custom Sitecore field.

what is it?

The method field is my solution to the case where you want to expose a set of business logic routines to the content author in a clear and concise way.  Giving them the power to control how a page reacts to end user interactions.

I came to this solution when I was considering options for allowing the users select the appropriate start date for a service.  They needed options like “the qualifying event date”, “first of the month after submission”, and the doozey “first of the next month if the current date is before the 15th or the first of the month one month after the next month otherwise”.  I really didn’t want to put a boatload of if/else blocks in the code.

How does it work?

In a nutshell we have a custom field that reads a fully qualified class name from the source of the field.  The method field will then read each static method in the class and expose them as an item on a dropdown list.  In the code behind you can use the value of the field (which is the method name) combined with the fully qualified class name to run the static method.

First let’s build the custom field

It starts with a class that implements Sitecore.Web.UI.HtmlControls.Control.

    class MethodField : Sitecore.Web.UI.HtmlControls.Control
    {
        public string Source
        {
            get;
            set;
        }
        protected override void OnLoad(EventArgs e)
        {
            if (!Sitecore.Context.ClientPage.IsEvent){
                var dl = new System.Web.UI.WebControls.Literal();
                PopulateDropDownList(dl);
                Controls.Add(dl);
            }else
                this.Value = Sitecore.Context.Page.Page.Request.Form[this.ID + "ctr"];

        }
        /// <summary>
        /// takes the source parameter and builds a select out of the static methods found in the source
        /// </summary>
        /// <param name="dl"></param>
        private void PopulateDropDownList(System.Web.UI.WebControls.Literal dl)
        {
            if (string.IsNullOrWhiteSpace(Source))
                return;
            Type t = Type.GetType(Source);
            if (t == null)
                return;
            StringBuilder sb = new StringBuilder();
            sb.Append(string.Format("<select class='scContentControl' id='{0}' name='{0}'><option value=''></option>", this.ID + "ctr"));
            foreach (var method in t.GetMethods())
            {
                if (method.IsStatic)
                {
                    sb.Append(string.Format("<option value='{0}' {2}>{1}</option>", method.Name, Regex.Replace(method.Name, "([a-z](?=[A-Z1-9])|[A-Z1-9](?=[A-Z][a-z]))", "$1 "), method.Name == this.Value ? "selected" : ""));
                }
            }
            sb.Append("</select>");
            dl.Text = sb.ToString();
        }
    }

 

We’re using reflection here to grab the fully qualified class name out of the source field of our custom field.  Then using a regex on the method name to break the camel case into a sentence for the content authors to read.  It’s worth noting here that your static method names should probably ReadLikeASentance.

We then need to wire this into Sitecore and we do this in the core database.  From the desktop interface switch over to the core database and navigate to:
/sitecore/system/Field types

Here you need to either create a new folder for custom fields, or add the Method field to your existing collection of custom fields.

In this folder you create a new item using the template:
/sitecore/templates/System/Templates/Template field type

Once you have this item created, you should enter an identifier for the control as shown below.  Note that “identifier” can be whatever you like, I would suggest something that identifies your company or client.

MethodFieldCore

You then must register the identifier you chose above in the <controlSources> section of the web.config.  As always, it’s a good idea to use a config patch file.

<controlSources>
    <source mode="on" namespace="NAMESPACE.FieldTypes" assembly="ASSEMBLYNAME" prefix="identifier" >
</controlSources>
Now lets hook up some business logic

The business logic that drives the method field needs to exist in a public class with static methods in it.  The return type and parameters of each static method in the class must be the same. They can, however, be anything as long as they’re consistent.  Here’s a simple example of a method that returns the first of the following month.  Note that we’re using the WebEditUtil to get the date, this allows people previewing and editing in page editor to toggle the current date.

public static DateTime FirstOfMonthFollowingReportDate()
        {
            var now = WebEditUtil.GetCurrentDate();
            return new DateTime(now.AddMonths(1).Year, now.AddMonths(1).Month, 1);
        }

We’re now ready to set up a template using the method field. Make sure that the source has the class that contains the static methods like below:
MethodFieldField

Once our method field is all set up, it should look something like this to our content authors:

MethodFieldResult

Now lets consume the authors chosen method

I’ve created an extension method to make calling a method field easy and caching the reflection pieces.  I would advise adding this to your list of Sitecore Item extensions.

        private static Dictionary<string, MethodInfo> MethodCache = new Dictionary<string, MethodInfo>();
        private static object Locker = new object();
        /// <summary>
        /// runs a method field's method without parameters
        /// </summary>
        /// <param name="item">Item that contains the field</param>
        /// <param name="Field">Method Field's name</param>
        /// <returns></returns>
        public static object RunMethodField(this Item item, string Field)
        {
            return RunMethodField(item, Field, null);
        }
        /// <summary>
        /// runs a method field's method with parameters
        /// </summary>
        /// <param name="item">Item that contains the field</param>
        /// <param name="Field">Method Field's name</param>
        /// <param name="Parameters">array of parameters for the methods</param>
        /// <returns></returns>
        public static object RunMethodField(this Item item, string Field, object[] Parameters)
        {
            Assert.IsNotNull(item, "Item must not be null");
            Assert.IsNotNullOrEmpty(Field, "Field must not be null or empty");
            Field f = item.Fields[Field];
            if (f == null)
                return null;
            string key = f.Source + f.Value;
            if (!MethodCache.ContainsKey(key))
            {
                lock (Locker)
                {
                    if (!MethodCache.ContainsKey(key))
                    {
                        Type t = Type.GetType(f.Source);
                        if (t == null)
                            return null;
                        MethodInfo mi = t.GetMethod(f.Value);
                        if (mi == null)
                            return null;
                        MethodCache.Add(key, mi);
                    }
                }
            }
            return MethodCache[key].Invoke(null, Parameters);
        }

Then utilizing this extension method, our code will look like this:

object tmp = SelectedItem.RunMethodField("Coverage Starts");

The object tmp can then be cast to a datetime. Note that the exact return type must be kept track of by the developer setting up this field so you can know what’s coming back as well as what arguments are required.

Making it better

There you have it.  Inversion of control for the content authors.  While I have found the method field very useful in my experience, I’d love to hear from anyone out there that have concerns or thoughts regarding this idea.  Together we can make it better.

Keep innovating, my friends!