Yes — You, Too, Can Build a Custom Field Control!
Ever since I started building websites in Sitecore over five years ago, I had an itch to build a custom Sitecore Field Control. Sadly, I never put my nose to the grindstone in doing so until a few weeks ago. Armed with .NET Reflector and the Sitecore.Kernel.dll, I sat down and finally built one. This post highlights the fruits of my endeavor.
First, let me give you a quick description on what a Sitecore Field Control is. A Sitecore Field Control is an ASP.NET Web Control that presents data set in a Sitecore field according to logic defined by the control. Sitecore offers a few Field Controls out of the box — these include Image, Link, Text, Date — and all are defined within the namespace Sitecore.Web.UI.WebControls. How to use these Field Controls is illustrated during Sitecore Developer Training and certification.
In this example, I created a custom Field Control that takes a value set within a Single-Line Text, Multi-Line Text, Rich Text, or text field — a deprecated field type used before Sitecore v5.3.1. — and wraps the value around HTML specified by client code. This HTML tag is defined by the HtmlTag attribute of the HtmlTagFieldControl below.
First, I created a new Field Control:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.UI; using Sitecore.Collections; using Sitecore.Web.UI.WebControls; namespace Sitecore.Sandbox.FieldControls { public class HtmlTagFieldControl : FieldControl { public string HtmlTag { get; set; } protected override void PopulateParameters(SafeDictionary<string> parameters) { base.PopulateParameters(parameters); if (!string.IsNullOrEmpty(HtmlTag)) { parameters.Add("htmlTagName", HtmlTag); } foreach (string attributeName in base.Attributes.Keys) { string attributeValue = base.Attributes[attributeName]; parameters.Add(attributeName, attributeValue); } } } }
Next, I built a renderer — including a Data transfer object for passing arguments to the renderer — for the field control defined above:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Xml.Xsl; namespace Sitecore.Sandbox.Renderers.Base { public interface IHtmlTagRenderer { RenderFieldResult Render(); } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using Sitecore; using Sitecore.Collections; using Sitecore.Data.Fields; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Xml.Xsl; using Sitecore.Sandbox.Renderers.Base; using Sitecore.Sandbox.Renderers.DTO; namespace Sitecore.Sandbox.Renderers { public class HtmlTagRenderer : FieldRendererBase, IHtmlTagRenderer { private const string OpenHtmlTagFormat = "<{0}>"; private const string CloseHtmlTagFormat = "</{0}>"; private HtmlTagRendererArgs Arguments { get; set; } private HtmlTagRenderer(HtmlTagRendererArgs arguments) : base() { SetArguments(arguments); } private void SetArguments(HtmlTagRendererArgs arguments) { Assert.ArgumentNotNull(arguments, "arguments"); Arguments = arguments; } public virtual RenderFieldResult Render() { string fieldHtml = CreateFieldHtml(); return new RenderFieldResult(fieldHtml); } private string CreateFieldHtml() { string openHtmlTag = GetOpenHtmlTag(); string closeHtmlTag = GetCloseHtmlTag(); return string.Concat(openHtmlTag, Arguments.FieldValue, closeHtmlTag); } private string GetOpenHtmlTag() { return GetTagHtml(OpenHtmlTagFormat, Arguments.HtmlTagName); } private string GetCloseHtmlTag() { return GetTagHtml(CloseHtmlTagFormat, Arguments.HtmlTagName); } private static string GetTagHtml(string tagFormat, string tagName) { if (!string.IsNullOrEmpty(tagName)) { return string.Format(tagFormat, tagName); } return string.Empty; } public static IHtmlTagRenderer Create(HtmlTagRendererArgs arguments) { return new HtmlTagRenderer(arguments); } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Sitecore.Sandbox.Renderers.DTO { public class HtmlTagRendererArgs { public string HtmlTagName { get; set; } public string FieldName { get; set; } public string FieldValue { get; set; } } }
Last, but not least, I defined a Render Field pipeline to glue all the pieces together — including the pivotal piece of wrapping the field’s value around the wrapper HTML tag:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Data.Items; using Sitecore.Diagnostics; using Sitecore.Pipelines.RenderField; using Sitecore.Xml.Xsl; using Sitecore.Sandbox.Renderers; using Sitecore.Sandbox.Renderers.Base; using Sitecore.Sandbox.Renderers.DTO; namespace Sitecore.Sandbox.Pipelines.RenderField { public class GetHtmlTagFieldControlHtml { private static IEnumerable<string> supportedFieldTypes = new string[] { "text", "rich text", "single-line text", "multi-line text" }; protected virtual IHtmlTagRenderer CreateRenderer(HtmlTagRendererArgs htmlTagRendererArgs) { return HtmlTagRenderer.Create(htmlTagRendererArgs); } public void Process(RenderFieldArgs renderFieldArgs) { if (CanFieldBeProcessed(renderFieldArgs)) { SetRenderFieldArgsResults(renderFieldArgs); } } private bool CanFieldBeProcessed(RenderFieldArgs renderFieldArgs) { string fieldTypeKey = renderFieldArgs.FieldTypeKey.ToLower(); return supportedFieldTypes.Contains(fieldTypeKey); } private void SetRenderFieldArgsResults(RenderFieldArgs renderFieldArgs) { RenderFieldResult renderFieldResult = GetRenderFieldResult(renderFieldArgs); SetRenderFieldArgsResultHtmlProperties(renderFieldResult, renderFieldArgs); } private RenderFieldResult GetRenderFieldResult(RenderFieldArgs renderFieldArgs) { HtmlTagRendererArgs htmlTagRendererArgs = CreateHtmlTagRendererArgs(renderFieldArgs); IHtmlTagRenderer htmlTagRenderer = CreateRenderer(htmlTagRendererArgs); return htmlTagRenderer.Render(); } private HtmlTagRendererArgs CreateHtmlTagRendererArgs(RenderFieldArgs renderFieldArgs) { string htmlTagName = string.Empty; if (renderFieldArgs.Parameters.ContainsKey("htmlTagName")) { htmlTagName = renderFieldArgs.Parameters["htmlTagName"]; } return new HtmlTagRendererArgs { HtmlTagName = htmlTagName, FieldName = renderFieldArgs.FieldName, FieldValue = renderFieldArgs.FieldValue }; } private void SetRenderFieldArgsResultHtmlProperties(RenderFieldResult renderFieldResult, RenderFieldArgs renderFieldArgs) { renderFieldArgs.Result.FirstPart = renderFieldResult.FirstPart; renderFieldArgs.Result.LastPart = renderFieldResult.LastPart; } } }
I added four fields to a data template — each being of a type supported by my custom Field Control:
I added some content:
I wrote some code for the test sublayout used in this example — notice how I’m having a little fun by using the marquee tag :):
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="My Little Sublayout.ascx.cs" Inherits="Sitecore650rev120706.layouts.sublayouts.MyLittleSublayout" %> <div> <strong>Field Name:</strong> title<br /> <strong>Field Type:</strong> text<br /> <strong>Html Tag:</strong> h1<br /> <strong>Field Control Html:</strong><br /> <sj:HtmlTagFieldControl ID="h1Title" HtmlTag="h1" Field="Title" runat="server" /> <hr /> </div> <div> <strong>Field Name:</strong> Subtitle<br /> <strong>Field Type:</strong> Single-Line Text<br /> <strong>Html Tag:</strong> h2<br /> <strong>Field Control Html:</strong><br /> <sj:HtmlTagFieldControl ID="h2Subtitle" HtmlTag="h2" Field="Subtitle" runat="server" /> <hr /> </div> <div> <strong>Field Name:</strong> Blurb<br /> <strong>Field Type:</strong> Multi-Line Text<br /> <strong>Html Tag:</strong> blockquote<br /> <strong>Field Control Html:</strong> <sj:HtmlTagFieldControl ID="HtmlTagFieldControl1" HtmlTag="blockquote" Field="Blurb" runat="server" /> <hr /> </div> <div> <strong>Field Name:</strong> Text<br /> <strong>Field Type:</strong> Rich Text<br /> <strong>Html Tag:</strong> marquee<br /> <strong>Field Control Html:</strong> <sj:HtmlTagFieldControl ID="marqueeText" HtmlTag="marquee" Field="Text" runat="server" /> </div>
Rendered HTML:
Sitecore offering the ability to create custom Field Controls is just another example of its extensibility. I don’t know of many developers that have taken advantage of creating their own custom Field Controls. However, I hope this post kindles the want for doing so, or adds some armament to your arsenal on how to present Sitecore data.
Addendum
Recently, the following query was asked on twitter:
Custom field controls – would you write your own from scratch, or inherit e.g. sc:text and add what you need (and why)?
The Text FieldControl doesn’t offer any logic over it’s FieldControl base — it inherits directly from the FieldControl class without any overrides. Inheriting from it will not add any extra benefit.
Further, the only method that might be of interest in overriding in a FieldControl base class — a class other than Text — would be the PopulateParameters method — albeit an argument could be made for overriding the DoRender method, but I will not discuss this method here. The PopulateParameters method sets parameters that are used by the RendererField pipeline for passing arguments to the Renderer object — the latter object creates the HTML for the control.
Plus, as a general best practice, I would caution against inheriting from classes unless absolutely necessary — more specific FieldControl classes within Sitecore.Web.UI.WebControls are no exceptions. You don’t want to introduce a steep class hierarchy, or cause class explosion — a situation where you create a new class just to add new functionality over a base class (Vlissides, Helm, Johnson, Gamma, 1995; Shalloway and Trott, 2004). Utilizing composition is recommended over inheritance — a model that faciliates in code maintenance, refactoring efforts, and adding new functionality (Vlissides, Helm, Johnson, & Gamma, 1995; Shalloway & Trott, 2004).
The real magic though occurs within the Renderer and RenderField pipeline classes, not the FieldControl classes, per se.
References
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1995). Design patterns: Elements of reusable object-oriented software. Reading: Addison-Wesley.
Shalloway, A., Trott, J. A. (2004) Design Patterns Explained. A new Perspective on Object-Oriented Design (2nd edition). Reading: Addison-Wesley.