This week, I’ve been off from work, and have been spending most of my downtime experimenting with Sitecore-ry development things — no, not you Mike — primarily poking around the core database to find things I can override or extend — for a thought provoking article about whether to override or extend things in Sitecore, check out John West’s blog post on this topic — and decided get my hands dirty by creating a custom Sitecore field.
This is something I feel isn’t being done enough by developers out in the wild — myself included — so I decided to take a stab at it, and share how you could go about accomplishing this.
The first field I created was a custom Single-Line Text field with a clear button. Since the jQuery library is available in the Sitecore client, I utilized a jQuery plugin to help me with this. It’s not a very robust plugin — I did have to make a couple of changes due to issues I encountered which I will omit from this article — albeit the purpose of me creating this field was to see how difficult it truly is.
As you can see below, creating a custom Single-Line Text isn’t difficult at all. All one has to do is subclass Sitecore.Shell.Applications.ContentEditor.Text in Sitecore.Kernel.dll, and ultimately override the DoRender() method:
using System; using System.Collections.Generic; using System.Linq; using System.Web.UI; using System.Web; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor { public class ClearableSingleLineText : Sitecore.Shell.Applications.ContentEditor.Text { protected override void DoRender(HtmlTextWriter output) { SetWidthAndHeightStyle(); RenderMyHtml(output); RenderChildren(output); RenderAutoSizeJavascript(output); } private void RenderMyHtml(HtmlTextWriter output) { const string htmlFormat = "<input{0}/>"; output.Write(string.Format(htmlFormat, ControlAttributes)); } private void RenderAutoSizeJavascript(HtmlTextWriter output) { /* I'm calling the jQuery() function directly since $() is defined for prototype.js in the Sitecore client */ const string jsFormat = "<script type=\"text/javascript\">jQuery('#{0}').clearable();</script>"; output.Write(string.Format(jsFormat, ID)); } } }
I then added a script reference to the jQuery plugin above, and embedded some css in /sitecore/shell/Applications/Content Manager/Default.aspx:
<script type="text/javaScript" language="javascript" src="/sitecore/shell/Controls/Lib/jQuery/jquery.clearable.js"></script> <style type="text/css"> /* Most of this css was provided by the jquery.clearable plugin author */ a.clearlink { background: url("/img/close-button.png") no-repeat scroll 0 0 transparent; background-position: center center; cursor: pointer; display: -moz-inline-stack; display: inline-block; zoom:1; *display:inline; height: 12px; width: 12px; z-index: 2000; border: 0px solid; position: relative; top: -18px; left: -5px; float: right; } a.clearlink:hover { background: url("/img/close-button.png") no-repeat scroll -12px 0 transparent; background-position: center center; } </style>
What I did above doesn’t sit well with me. If a future version of Sitecore makes any changes to this .aspx, these changes might be lost (well, hopefully you would keep a copy of this file in a source control system somewhere).
However, I could not think of a better way of adding this javascript and css. If you know of a better way, please leave a comment.
I then defined my library of controls for my custom fields — not much of a library, since I only defined one field so far 🙂 — in a new patch config file /App_Config/Include/CustomFields.config:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <controlSources> <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="contentcustom"/> </controlSources> </sitecore> </configuration>
In the core database, I created a new field type for my custom Single-Line Text:
I then added a new field to my template using this new custom field definition in my master database:
Let’s see this new custom field in action.
I typed in some text on my item, followed by clicking the clear button:
As expected, the text that I had typed was removed from the field:
After having done the above, I wanted to see if I could create a different type of custom field — creating a custom Text field was way too easy, and I wanted something more challenging. I figured creating a custom Multilist field might offer a challenge, so I decided to give it a go.
What I came up with was a custom Multilist field containing Sitecore users, instead of Items as options. I don’t know if there is any practicality in creating such a field, but I decided to go with it just for the purpose of creating a custom Multilist field.
First, I created a class to delegate responsibility for getting selected/unselected users in my field — I creating this class just in case I ever wanted to reuse this in a future custom field that should also contain Sitecore users.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Sitecore.Security.Accounts; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Base { public interface IUsersField { IEnumerable<User> GetSelectedUsers(); IEnumerable<User> GetUnselectedUsers(); string GetProviderUserKey(User user); } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Security; using Sitecore.Configuration; using Sitecore.Diagnostics; using Sitecore.Security.Accounts; using Sitecore.Text; using Sitecore.Sandbox.Shell.Applications.ContentEditor.Base; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor { public class UsersField : IUsersField { private static readonly string DomainParameterName = Settings.GetSetting("UsersField.DomainParameterName"); private ListString _SelectedUsers; private ListString SelectedUsers { get { if (_SelectedUsers == null) { _SelectedUsers = new ListString(Value); } return _SelectedUsers; } } private IEnumerable<User> _UsersInDomain; private IEnumerable<User> UsersInDomain { get { if (_UsersInDomain == null) { _UsersInDomain = GetUsersInDomain(); } return _UsersInDomain; } } private IEnumerable<User> _Users; private IEnumerable<User> Users { get { if(_Users == null) { _Users = GetUsers(); } return _Users; } } private string _Domain; private string Domain { get { if (string.IsNullOrEmpty(_Domain)) { _Domain = FieldSettings[DomainParameterName]; } return _Domain; } } private UrlString _FieldSettings; private UrlString FieldSettings { get { if (_FieldSettings == null) { _FieldSettings = GetFieldSettings(); } return _FieldSettings; } } private string Source { get; set; } private string Value { get; set; } private UsersField(string source, string value) { SetSource(source); SetValue(value); } private void SetSource(string source) { Source = source; } private void SetValue(string value) { Value = value; } private IEnumerable<User> GetUsersInDomain() { if (!string.IsNullOrEmpty(Domain)) { return Users.Where(user => IsUserInDomain(user, Domain)); } return Users; } private static IEnumerable<User> GetUsers() { IEnumerable<User> users = UserManager.GetUsers(); if (users != null) { return users; } return new List<User>(); } private static bool IsUserInDomain(User user, string domain) { Assert.ArgumentNotNull(user, "user"); Assert.ArgumentNotNullOrEmpty(domain, "domain"); string userNameLowerCase = user.Profile.UserName.ToLower(); string domainLowerCase = domain.ToLower(); return userNameLowerCase.StartsWith(domainLowerCase); } private UrlString GetFieldSettings() { try { if (!string.IsNullOrEmpty(Source)) { return new UrlString(Source); } } catch (Exception ex) { Log.Error(this.ToString(), ex, this); } return new UrlString(); } public IEnumerable<User> GetSelectedUsers() { IList<User> selectedUsers = new List<User>(); foreach (string providerUserKey in SelectedUsers) { User selectedUser = UsersInDomain.Where(user => GetProviderUserKey(user) == providerUserKey).FirstOrDefault(); if (selectedUser != null) { selectedUsers.Add(selectedUser); } } return selectedUsers; } public IEnumerable<User> GetUnselectedUsers() { IList<User> unselectedUsers = new List<User>(); foreach (User user in UsersInDomain) { if (!IsUserSelected(user)) { unselectedUsers.Add(user); } } return unselectedUsers; } private bool IsUserSelected(User user) { string providerUserKey = GetProviderUserKey(user); return IsUserSelected(providerUserKey); } private bool IsUserSelected(string providerUserKey) { return SelectedUsers.IndexOf(providerUserKey) > -1; } public string GetProviderUserKey(User user) { Assert.ArgumentNotNull(user, "user"); MembershipUser membershipUser = Membership.GetUser(user.Profile.UserName); return membershipUser.ProviderUserKey.ToString(); } public static IUsersField CreateNewUsersField(string source, string value) { return new UsersField(source, value); } } }
I then modified my patch config file defined above (/App_Config/Include/CustomFields.config) with a new setting — a setting that specifies the parameter name for filtering on users’ Sitecore domain:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <controlSources> <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="contentcustom"/> </controlSources> <settings> <!-- Parameter name for users' Sitecore domain --> <setting name="UsersField.DomainParameterName" value="Domain" /> </settings> </sitecore> </configuration>
I then created a new subclass of Sitecore.Shell.Applications.ContentEditor.MultilistEx — I ascertained this to be the class used by the Multilist field in Sitecore by looking at /sitecore/system/Field types/List Types/Multilist field type in the core database:
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Security; using System.Web.UI; using Sitecore.Configuration; using Sitecore.Diagnostics; using Sitecore.Globalization; using Sitecore.Resources; using Sitecore.Security.Accounts; using Sitecore.Shell.Applications.ContentEditor; using Sitecore.Text; using Sitecore.Sandbox.Shell.Applications.ContentEditor.Base; namespace Sitecore.Sandbox.Shell.Applications.ContentEditor { public class UsersMultilist : MultilistEx { private IUsersField _UsersField; private IUsersField UsersField { get { if (_UsersField == null) { _UsersField = CreateNewUsersField(); } return _UsersField; } } public UsersMultilist() { } protected override void DoRender(HtmlTextWriter output) { Assert.ArgumentNotNull(output, "output"); SetIDProperty(); string disabledAttribute = string.Empty; if (ReadOnly) { disabledAttribute = " disabled=\"disabled\""; } output.Write(string.Format("<input id=\"{0}_Value\" type=\"hidden\" value=\"{1}\" />", ID, StringUtil.EscapeQuote(Value))); output.Write(string.Format("<table{0}>", GetControlAttributes())); output.Write("<tr>"); output.Write(string.Format("<td class=\"scContentControlMultilistCaption\" width=\"50%\">{0}</td>", GetAllLabel())); output.Write(string.Format("<td width=\"20\">{0}</td>", Images.GetSpacer(20, 1))); output.Write(string.Format("<td class=\"scContentControlMultilistCaption\" width=\"50%\">{0}</td>", GetSelectedLabel())); output.Write(string.Format("<td width=\"20\">{0}</td>", Images.GetSpacer(20, 1), "</td>")); output.Write("</tr>"); output.Write("<tr>"); output.Write("<td valign=\"top\" height=\"100%\">"); output.Write(string.Format("<select id=\"{0}_unselected\" class=\"scContentControlMultilistBox\" multiple=\"multiple\"{1} size=\"10\" ondblclick=\"javascript:scContent.multilistMoveRight('{2}')\" onchange=\"javascript:document.getElementById('{3}_all_help').innerHTML=this.selectedIndex>=0?this.options[this.selectedIndex].innerHTML:''\">", ID, disabledAttribute, ID, ID)); IEnumerable<User> unselectedUsers = GetUnselectedUsers(); foreach (User unselectedUser in unselectedUsers) { output.Write(string.Format("<option value=\"{0}\">{1}</option>", GetProviderUserKey(unselectedUser), unselectedUser.Profile.UserName)); } output.Write("</select>"); output.Write("</td>"); output.Write("<td valign=\"top\">"); RenderButton(output, "Core/16x16/arrow_blue_right.png", string.Format("javascript:scContent.multilistMoveRight('{0}')", ID)); output.Write("<br />"); RenderButton(output, "Core/16x16/arrow_blue_left.png", string.Format("javascript:scContent.multilistMoveLeft('{0}')", ID)); output.Write("</td>"); output.Write("<td valign=\"top\" height=\"100%\">"); output.Write(string.Format("<select id=\"{0}_selected\" class=\"scContentControlMultilistBox\" multiple=\"multiple\"{1} size=\"10\" ondblclick=\"javascript:scContent.multilistMoveLeft('{2}')\" onchange=\"javascript:document.getElementById('{3}_selected_help').innerHTML=this.selectedIndex>=0?this.options[this.selectedIndex].innerHTML:''\">", ID, disabledAttribute, ID, ID)); IEnumerable<User> selectedUsers = GetSelectedUsers(); foreach (User selectedUser in selectedUsers) { output.Write(string.Format("<option value=\"{0}\">{1}</option>", GetProviderUserKey(selectedUser), selectedUser.Profile.UserName)); } output.Write("</select>"); output.Write("</td>"); output.Write("<td valign=\"top\">"); RenderButton(output, "Core/16x16/arrow_blue_up.png", string.Format("javascript:scContent.multilistMoveUp('{0}')", ID)); output.Write("<br />"); RenderButton(output, "Core/16x16/arrow_blue_down.png", string.Format("javascript:scContent.multilistMoveDown('{0}')", ID)); output.Write("</td>"); output.Write("</tr>"); output.Write("<tr>"); output.Write("<td valign=\"top\">"); output.Write(string.Format("<div style=\"border:1px solid #999999;font:8pt tahoma;padding:2px;margin:4px 0px 4px 0px;height:14px\" id=\"{0}_all_help\"></div>", ID)); output.Write("</td>"); output.Write("<td></td>"); output.Write("<td valign=\"top\">"); output.Write(string.Format("<div style=\"border:1px solid #999999;font:8pt tahoma;padding:2px;margin:4px 0px 4px 0px;height:14px\" id=\"{0}_selected_help\"></div>", ID)); output.Write("</td>"); output.Write("<td></td>"); output.Write("</tr>"); output.Write("</table>"); } protected void SetIDProperty() { ServerProperties["ID"] = ID; } protected static string GetAllLabel() { return GetLabel("All"); } protected static string GetSelectedLabel() { return GetLabel("Selected"); } protected static string GetLabel(string key) { return Translate.Text(key); } protected IEnumerable<User> GetSelectedUsers() { return UsersField.GetSelectedUsers(); } protected IEnumerable<User> GetUnselectedUsers() { return UsersField.GetUnselectedUsers(); } protected string GetProviderUserKey(User user) { return UsersField.GetProviderUserKey(user); } // Method "borrowed" from MultilistEx control protected void RenderButton(HtmlTextWriter output, string icon, string click) { Assert.ArgumentNotNull(output, "output"); Assert.ArgumentNotNull(icon, "icon"); Assert.ArgumentNotNull(click, "click"); ImageBuilder builder = new ImageBuilder { Src = icon, Width = 0x10, Height = 0x10, Margin = "2px" }; if (!ReadOnly) { builder.OnClick = click; } output.Write(builder.ToString()); } private IUsersField CreateNewUsersField() { return ContentEditor.UsersField.CreateNewUsersField(Source, Value); } } }
Overriding the DoRender() method paved the way for me to insert my custom Multilist options — options containg Sitecore users. Selected users will be saved as a pipe delimitered list of ASP.NET Membership UserIDs.
As I had done for my custom Single-Line Text above, I defined my custom Multilist field in the core database:
In the master database, I added a new field to my template using this new field type:
Now, let’s take this custom field for a test drive.
I went back to my item and saw that all Sitecore users were available for selection in my new field:
Facetiously imagine that a project manager has just run up to you anxiously lamenting — while breathing rapidly and sweating copiously — we cannot show users in the sitecore domain in our field, only those within the extranet domain — to do so will taint our credibility with our clients as Sitecore experts :). You then put on your superhero cape and proudly proclaim “rest assured, we’ve already baked this domain filtering functionality into our custom Multilist field!”:
I saved my template and navigated back to my item:
I selected a couple of users and saved:
I see their Membership UserIDs are being saved as designed:
My intent on creating the above custom fields was to showcase that the option to create your own fields exists in Sitecore. Why not have yourself a field day by creating your own? 🙂
[…] Multilist of Sitecore Users – A blog post by Mike Reynolds showing an example of how to create a custom multilist field. His example shows how to create a multilist field of users that can be filtered by domain (As well as a simple example of customizing the single-line text field). […]