Ajax action calls in Sitecore MVC

In our current project we are using Sitecore 7.2, with MVC.

First thing to note here, is the Url structure that is needed to be able to access controller actions from within a sitecore solution.

A sample Url would be:

http://local.mysite.com/api/sitecore/samplecontroller/sampleaction?sampleparam=abc

As you see above, the action can be accessed via sitecore’s api url, and this Url can be programmatically formed using the MVC helper method – Url.Action –

    Url.Action("sampleaction","samplecontroller")

Our site is very javascript / ajax heavy and at the beginning of this implementation, while making ajax calls to MVC actions, we came across the following issue:

net::ERR_CONNECTION_REFUSED n.ajaxTransport.k.cors.a.crossdomain.send

2015-02-27_014903

Even though we were making the ajax call from an http page – to another http url, we still faced this issue. The solution here was to ensure that all controller actions which would be invoked through ajax, have in the response header the ‘Access-Control-Allow-Origin *’ token.

Since we were using MVC – we decided to implement this using a custom MVC action filter and override the OnActionExecuting method.

using System.Web.Mvc;

namespace MySite._Classes.Shared.Customizations.MVC
{
    public class AllowCrossSiteJsonAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            filterContext.RequestContext.HttpContext.Response.AddHeader("Access-Control-Allow-Origin", "*");
            base.OnActionExecuting(filterContext);
        }
    }
}

Now all you’ve got to do after this, is add this new attribute above any method you intend to invoke via ajax! So following the example i mentioned above, here’s the action:

        [AllowCrossSiteJson]
        public JsonResult SignInJson(string email, string password, string redirectUrl = null,
            string pipeDelimitedMailingListIds = null, bool isRedirect = true)
        {
            ...
            ...
            return Json(<Serializable object>, JsonRequestBehavior.AllowGet);
        }

Also note the use of JsonRequestBehavior.AllowGet here – which would be absolutely mandatory in case you are trying to invoke this method via ajax using a GET request. In absence of this, you will get the following error:
This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet
Also to note, an excerpt from the ASP.NET MVC Wrox publication book:

By default, the ASP.NET MVC framework does not allow you to respond to an HTTP GET request with a JSON payload. If you need to send JSON in response to a GET, you’ll need to explicitly allow the behavior by using JsonRequestBehavior.AllowGet as the second parameter to the Json method. However, there is a chance a malicious user can gain access to the JSON payload through a process known as JSON Hijacking. You do not want to return sensitive information using JSON in a GET request.

Now additionally – if you wanted to avoid adding the JsonRequestBehavior.AllowGet property on EVERY json returning action you write, you could choose the alternative route of overriding the OnResultExecuting() method in your custom action filter:

 public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        var jsonResult = filterContext.Result as JsonResult;

        if (jsonResult == null)
            throw new ArgumentException("Action does not return a JsonResult, attribute AllowJsonGet is not allowed");

        jsonResult.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
        base.OnResultExecuting(filterContext);
    }

Nifty aint it?

Advertisements

Unique item name constraint using index search in Sitecore

While working on a project we came across a requirement of adding a constraint for having unique item names. While you could just instruct the content authors to ensure distinct item names, it would be great to be able to enforce this through code with a pretty message to the content authors if they did create items with duplicate names, wouldn’t it?

2015-02-27_012756

Now this might me a business requirement, but it is also something that would need to be enforced if you use custom routes (another post about custom routes to follow shortly!), resolving the item by item name in the route.

We are basically going to add sitecore events to execute on item creation and item save – where we will do an index search to determine whether an item of the same name already exists.

So whatever be the reason, if you find the need to add a unique name constraint on items – be the scope a section in sitecore or the entire sitecore content tree, here’s how we go about it –

Code

using Sitecore.Data;
using Sitecore.Data.Events;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;
using Sitecore.SecurityModel;
using System;
using System.Collections.Generic;
using System.Linq;
using Event = Sitecore.Events.Event;

namespace MySite.Customizations.Customized_Sitecore
{
    public class UniqueItemNameValidator
    {
        /// <summary>
        /// Called when on item creation. Prevents creating items with duplicate names.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="args">The <see cref="EventArgs"/> instance containing the event data.</param>
        public void OnItemCreating(object sender, EventArgs args)
        {
            using (new SecurityDisabler())
            {
                var arg = Event.ExtractParameter(args, 0) as ItemCreatingEventArgs;
                if (arg == null || (Sitecore.Context.Site != null && Sitecore.Context.Site.Name != "shell")) return;

                try
                {
                    if (!IsUniqueItemNameInContext(arg.ItemName, arg.TemplateId))
                    {
                        ((SitecoreEventArgs)args).Result.Cancel = true;
                        Sitecore.Context.ClientPage.ClientResponse.Alert(string.Format("Name '{0}' is already in use. Please use another name for the item.", arg.ItemName));
                    }
                }
                catch (Exception ex)
                {
                    Log.Error("Error on creating item:" + arg.ItemId + Environment.NewLine + "StackTrace:" + ex.StackTrace, ex);
                }
            }
        }

        /// <summary>
        /// Called when item is saved. Prevents renaming items to have a duplicate name.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="args">The <see cref="EventArgs"/> instance containing the event data.</param>
        public void OnItemSaving(object sender, EventArgs args)
        {
            using (new SecurityDisabler())
            {
                var item = Event.ExtractParameter(args, 0) as Item;
                if (item == null || (Sitecore.Context.Site != null && Sitecore.Context.Site.Name != "shell")) return;
                if (item.Parent == null) return;

                try
                {
                    if (!IsUniqueItemNameInContext(item.Name, item.TemplateID, item.ID, true))
                    {
                        ((SitecoreEventArgs)args).Result.Cancel = true;
                        Sitecore.Context.ClientPage.ClientResponse.Alert(string.Format("Name '{0}' is already in use. Please use another name for the item.", item.Name));
                    }
                }
                catch (Exception ex)
                {
                    Log.Error("Error on saving item:" + item.ID.Guid + Environment.NewLine + "StackTrace:" + ex.StackTrace, ex);
                }
            }
        }

        private bool IsUniqueItemNameInContext(string itemName, ID templateId, ID itemId = null, bool excludeCurrentItem = false)
        {
            // Articles
            if (templateId == IArticleConstants.TemplateId)
            {
                return !CheckItemNameExists(itemName, itemId, templateId, "{<Guid of the Articles folder item>}",
                    Constants.Search.Index.SitecoreMaster, excludeCurrentItem);
            }

            // Projects
            if (templateId == IProjectConstants.TemplateId)
            {
                return !CheckItemNameExists(itemName, itemId, templateId, "{<Guid of the Projects folder>}",
                    Constants.Search.Index.SitecoreMaster, excludeCurrentItem);
            }

            #endregion

            return true;
        }

        private bool CheckItemNameExists
            (string itemName, ID itemId, ID templateId, ID locationId, 
            string indexName, bool excludeCurrentItem)
        {
            IEnumerable<Item> searchResults = SearchHelper.GetItemsOnCreateSave
                (indexName, Sitecore.Context.Language.ToString(), templateId, locationId, itemName);

            if (excludeCurrentItem && itemId != (ID)null && searchResults != null)
            {
                searchResults = searchResults.Where(x => x.ID != itemId);
            }

            return searchResults != null && searchResults.Any();
        }
    }
}

The item save event also needs to be written to handle scenarios when an item is renamed – to a duplicate name.
Something to note here is the “SearchHelper.GetItemsOnCreateSave” method! Here’s the code:

        public static List<Item> GetItemsOnCreateSave
            (string indexName, string language, ID templateId, ID locationId, string itemName)
        {
            using (var context = ContentSearchManager.GetIndex(indexName).CreateSearchContext())
            {
                var query = context.GetQueryable<SearchResultItem>().Where(x => x.Language == language);
                query = query.Where(x => x.Paths.Contains(locationId));
                query = query.Where(x => x.TemplateId == templateId);
                query = query.Where(x => x.Name == itemName);
                var temp = query.Select(x => x.GetItem()).ToList();
                return temp;
            }
        }

This method will do a search on the index passed in based on the template we are trying to check this for.
In my case we did have different templates being a part of different indexes in a couple of cases – if you have a single index, you might not need to pass the name around.

Also, additionally, in this case – I could have a project and an article of the same name – which would be a valid scenario, which actually meets the business requirements I had in hand. In case you want to have no duplicate item names in the entire content tree, you could skip the template check / location check and do a blind index search without a location / template filter.

Index search being as efficient as it is – the performance hit should be minimal.

Configuration
Of course, you would also need to add these events to your sitecore configuration:

  <sitecore>
    <events>
      <event name="item:creating">
        <handler type="MySite.Customizations.Customized_Sitecore.UniqueItemNameValidator, MySite" method="OnItemCreating" />
      </event>
      <event name="item:saving">
        <handler type="MySite.Customizations.Customized_Sitecore.UniqueItemNameValidator, MySite" method="OnItemSaving" />
      </event>
    </events>
  </sitecore>

Note: From experience, it would be a good idea to turn this configuration off while installing packages to your site – to avoid additional processing when you don’t need it – if that be the case.

Single Select with Search field in Sitecore

With Sitecore 7.2 we have the new sitecore fields with search available.
The multilist with search is a real life saver when it comes to usage of Sitecore buckets.
By default the Sitecore buckets create a hierarchy of folders with the
Year -> Month -> Day -> Hour -> Minute structure.

2015-02-25_002948

Makes it awfully hard to find items without a good search system in place.

Which is why the multilist with search brings us a sigh of relief. However, in one of our projects, we found the need to be able to restrict the selected number of items to 1 only – like in the case of selecting a featured product etc.

Initially we started off by just taking the first item selected in the multilist with search field, while we processed data in the back end. However, this opened a can of worms when it came to training the content authors. Which is why, we sought an alternate solution, and created a custom field which allows only a single selection with search as is.

This is what it now looks like:

2015-02-25_004829

We also added a validation message to be displayed if a user tried to add an additional selected item.

2015-02-25_004948


Here’s how we did it –

There are 4 parts to this solution:

  • C# code file for the new control – most of the code in this file actually comes from the reflected code for multilist with search. We however did make a few updates for changing the look and feel of the control – Mainly removing the sort icons, move left / right icons and to arrange the fields one below the other, with the ‘selected’ box – as tall as a single item name text.
  • JS file for additional validations & CSS file for a small style update – again, this is the multilist with search js / css brought over and modified for the additional validation of allowing only 1 item to be selected.
  • Item added to the core database – to make the new field available in sitecore.
  • Config update – to add the extension of the new control created.

C# code file
(Refer Sitecore.Buckets.FieldTypes.BucketList)

2015-02-25_013640

using Sitecore;
using Sitecore.Buckets.FieldTypes;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using System.Collections;
using System.Collections.Specialized;
using System.Text;
using System.Web.UI;

namespace MySite.SitecoreFields
{
    public class SingleSelectWithSearchField : BucketList
    {
        protected override void DoRender(HtmlTextWriter output)
        {
            ArrayList selected;
            OrderedDictionary unselected;
            GetSelectedItems(GetItems(Sitecore.Context.ContentDatabase.GetItem(ItemID)), out selected, out unselected);
            StringBuilder stringBuilder = new StringBuilder();
            foreach (DictionaryEntry dictionaryEntry in unselected)
            {
                Item obj = dictionaryEntry.Value as Item;
                if (obj != null)
                {
                    stringBuilder.Append(obj.DisplayName + ",");
                    stringBuilder.Append(GetItemValue(obj) + ",");
                }
            }
            RenderStartLocationInput(output);
            output.Write("<input type='hidden' width='100%' id='multilistValues" + (object)ClientID + "' value='" + stringBuilder + "' style='width: 200px;margin-left:3px;'>");
            ServerProperties["ID"] = ID;
            string str1 = string.Empty;
            if (ReadOnly)
                str1 = " disabled='disabled'";
            output.Write("<input id='" + ID + "_Value' type='hidden' value='" + StringUtil.EscapeQuote(Value) + "' />");
            output.Write("<table" + GetControlAttributes() + ">");
            output.Write("<tr>");
            output.Write("<td class='scContentControlMultilistCaption' width='50%' colspan='4'>" + Translate.Text("All") + "</td>");
            output.Write("</tr>");
            output.Write("<tr>");
            output.Write("<td valign='top' height='100%' colspan='4'>");
            output.Write("<div style='width:200%;overflow:hidden;height:30px'><input type='text' width='100%' class='scIgnoreModified bucketSearch inactive' value='" + TypeHereToSearch + "' id='filterBox" + ClientID + "' " + (Sitecore.Context.ContentDatabase.GetItem(ItemID).Access.CanWrite() ? string.Empty : "disabled") + ">");
            output.Write("<span id='prev" + ClientID + "' class='hovertext' style='cursor:pointer;' onMouseOver=\"this.style.color='#666'\" onMouseOut=\"this.style.color='#000'\"> <img width='10' height='10' src='/sitecore/shell/Applications/Buckets/images/right.png' style='margin-top: 1px;'> " + Translate.Text("prev") + " |</span>");
            output.Write("<span id='next" + ClientID + "' class='hovertext' style='cursor:pointer;' onMouseOver=\"this.style.color='#666'\" onMouseOut=\"this.style.color='#000'\"> " + Translate.Text("next") + " <img width='10' height='10' src='/sitecore/shell/Applications/Buckets/images/left.png' style='margin-top: 1px;'>  </span>");
            output.Write("<span id='refresh" + ClientID + "' class='hovertext' style='cursor:pointer;' onMouseOver=\"this.style.color='#666'\" onMouseOut=\"this.style.color='#000'\"> " + Translate.Text("refresh") + " <img width='10' height='10' src='/sitecore/shell/Applications/Buckets/images/refresh.png' style='margin-top: 1px;'>  </span>");
            output.Write("<span id='goto" + ClientID + "' class='hovertext' style='cursor:pointer;' onMouseOver=\"this.style.color='#666'\" onMouseOut=\"this.style.color='#000'\"> " + Translate.Text("go to item") + " <img width='10' height='10' src='/sitecore/shell/Applications/Buckets/images/text.png' style='margin-top: 1px;'>  </span>");
            output.Write("<span style='padding-left:34px;'><strong>" + Translate.Text("Page Number") + ": </strong></span><span id='pageNumber" + ClientID + "'></span></div>");
            string str2 = !UIUtil.IsIE() || UIUtil.GetBrowserMajorVersion() != 9 ? "10" : "11";
            output.Write("<select id=\"" + ID + "_unselected\" class=\"scContentControlMultilistBox\" size=\"" + str2 + "\"" + str1 + " >");
            foreach (DictionaryEntry dictionaryEntry in unselected)
            {
                Item obj = dictionaryEntry.Value as Item;
                if (obj != null)
                {
                    string str3 = OutputString(obj);
                    output.Write("<option value='" + GetItemValue(obj) + "'>" + str3 + "</option>");
                }
            }
            output.Write("</select>");
            output.Write("</td>");
            output.Write("</tr>");
            output.Write("<tr>");
            output.Write("<td class='scContentControlMultilistCaption' width='100%'>Selected</td>");
            output.Write("</tr>");
            output.Write("<tr>");
            output.Write("<td valign='top' height='100%' colspan='4'>");
            output.Write("<select id='" + ID + "_selected' class='scContentControlMultilistBox scSingleSelectWithSearchSelectedBox' size='10'" + str1 + ">");
            for (int index = 0; index < selected.Count; ++index)
            {
                Item obj1 = selected[index] as Item;
                if (obj1 != null)
                {
                    string str3 = OutputString(obj1);
                    output.Write("<option value='" + GetItemValue(obj1) + "'>" + str3 + "</option>");
                }
                else
                {
                    string path = selected[index] as string;
                    if (path != null)
                    {
                        Item obj2 = Sitecore.Context.ContentDatabase.GetItem(path);
                        string str3 = obj2 == null ? path + ' ' + Translate.Text("[Item not found]") : OutputString(obj2);
                        output.Write("<option value='" + path + "'>" + str3 + "</option>");
                    }
                }
            }
            output.Write("</select>");
            output.Write("</td>");
            output.Write("</tr>");
            output.Write("<div style='border:1px solid #999999;font:8pt tahoma;display:none;padding:2px;margin:4px 0px 4px 0px;height:14px' id='" + ID + "_all_help'></div>");
            output.Write("<div style='border:1px solid #999999;font:8pt tahoma;display:none;padding:2px;margin:4px 0px 4px 0px;height:14px' id='" + ID + "_selected_help'></div>");
            output.Write("</table>");
            RenderScript(output);
        }

        protected override void RenderScript(HtmlTextWriter output)
        {
            string str = "<script type='text/javascript'>\r\n                                    (function() {\r\n                                        if (!document.getElementById('SingleSelectWithSearchJs')) {\r\n                                            var head = document.getElementsByTagName('head')[0];\r\n                                            head.appendChild(new Element('script', { type: 'text/javascript', src: '/sitecore/shell/Controls/SingleSelectWithSearch/SingleSelectWithSearch.js', id: 'SingleSelectWithSearchJs' }));\r\n                                            head.appendChild(new Element('link', { rel: 'stylesheet', href: '/sitecore/shell/Controls/SingleSelectWithSearch/SingleSelectWithSearch.css' }));\r\n                                        }\r\n                                        var stopAt = Date.now() + 5000;\r\n                                        var timeoutId = setTimeout(function() {\r\n                                            if (Sitecore.InitSingleSelectWithSearch) {\r\n                                                Sitecore.InitSingleSelectWithSearch(" + ScriptParameters + ");\r\n                                                clearTimeout(timeoutId);\r\n                                            } else if (Date.now() > stopAt) {\r\n                                                clearTimeout(timeoutId);\r\n                                            }\r\n                                        }, 100);\r\n                                    }());\r\n                              </script>";
            output.Write(str);
        }
    }
}

JS File
(Refer ~\sitecore\shell\Controls\BucketList\BucketList.js
To be saved at: ~\sitecore\shell\Controls\SingleSelectWithSearch\SingleSelectWithSearch.js

var Sitecore = Sitecore || {};

Sitecore.InitSingleSelectWithSearch = function (id, clientId, pageNumber, searchHandlerUrl, filter, databaseUrlParameter, typeToSearchString, of, enableSetStartLocation) {
    var self = {};

    self.id = id;
    self.clientId = clientId;
    self.pageNumber = pageNumber;
    self.searchHandlerUrl = searchHandlerUrl;
    self.filter = filter;
    self.databaseUrlParameter = databaseUrlParameter;
    self.typeToSearchString = typeToSearchString;
    self.of = of;
    self.enableSetStartLocation = (enableSetStartLocation.toLowerCase() === 'true');

    self.currentPage = 1;
    self.selectedId = '';

    self.doneTypingInterval = 2000; //time in ms, 2 second for example

    var typingTimer;

    self.format = function (template) {
        var args = arguments;
        return template.replace(/\{(\d+)\}/g, function (m, n) { return args[parseInt(n) + 1]; });
    };

    // Sends 'GET' request to url specified by parameter
    // and apply success handler to multilist element
    self.sendRequest = function (url, multilist) {
        new Ajax.Request(url,
            {
                method: 'GET',
                onSuccess: new self.SuccessHandler(multilist)
            });
    };

    // Cunstructor for request success handler
    self.SuccessHandler = function (multilist) {
        return function (request) {
            var response = eval(request.responseText);
            multilist.options.length = 0;
            multilist.removeClassName('loadingItems');

            for (var i = 0; i < response.items.length; i++) {
                multilist.options[multilist.options.length] = new Option(response.items[i].Name + ' (' + response.items[i].TemplateName + ' - ' + response.items[i].Bucket + ')', response.items[i].ItemId);
            }

            self.pageNumber = response.PageNumbers;
            $('pageNumber' + self.clientId).innerHTML = self.format(self.of, self.currentPage, self.pageNumber);
        };
    };

    // Return id of selected item
    self.getSelectedItemId = function () {
        var all = scForm.browser.getControl(self.id + '_unselected');

        for (var n = 0; n < all.options.length; n++) {
            var option = all.options[n];

            if (option.selected) {
                return option.value;
            }
        }

        return null;
    };

    self.onFilterFocus = function (filterBox) {
        if (filterBox.value == self.typeToSearchString) {
            filterBox.value = '';
        }

        filterBox.addClassName('active').removeClassName('inactive');
    };

    self.onFilterBlur = function (filterBox) {
        if (!filterBox.value) {
            filterBox.value = self.typeToSearchString;
        }

        filterBox.removeClassName('active').addClassName('inactive');
    };

    self.multilistValuesMoveRight = function (allOptions) {
        var all = scForm.browser.getControl(self.id + '_unselected');
        var multilistValues = document.getElementById('multilistValues' + self.id);
        for (var n = 0; n < all.options.length; n++) {
            var option = all.options[n];
            if (option.selected || allOptions) {
                var opt = option.innerHTML + ',' + option.value + ',';
                multilistValues.value = multilistValues.value.replace(opt, '');
            }
        }
    };

    self.multilistValuesMoveLeft = function (allOptions) {
        var selected = scForm.browser.getControl(self.id + '_selected');
        var multilistValues = document.getElementById('multilistValues' + self.id);
        for (var n = 0; n < selected.options.length; n++) {
            var option = selected.options[n];
            if (option.selected || allOptions) {
                var opt = option.innerHTML + ',' + option.value + ',';
                multilistValues.value += opt;
            }
        }
    };

    self.moveToCurrentPage = function () {
        var filterBox = document.getElementById('filterBox' + self.clientId);
        var filterValue = (filterBox.value && filterBox.value != self.typeToSearchString) ? filterBox.value : '*';

        var multilist = $(self.clientId + '_unselected').addClassName('loadingItems');
        var savedStr = encodeURI(filterValue);
        var filterString = self.enableSetStartLocation ? self.getOverrideString('&location=') : self.filter;

        self.sendRequest(self.searchHandlerUrl + '?fromBucketListField=' + savedStr + filterString + '&pageNumber=' + self.currentPage + self.databaseUrlParameter, multilist);
    };

    // Replaces overrideKey value in filter by value from ovverrideInput
    self.getOverrideString = function (overrideKey) {
        var overrideInput = document.getElementById('locationOverride' + self.clientId);

        if (!overrideInput || !overrideInput.value.length > 0) {
            return self.filter;
        }

        var replaceStartIndex = self.filter.indexOf(overrideKey);

        if (!~replaceStartIndex) {
            return self.filter;
        }

        var replaceEndIndex = self.filter.indexOf('&', replaceStartIndex + 1);

        if (!~replaceEndIndex) {
            replaceEndIndex = self.filter.length;
        }

        var stringToReplace = self.filter.substring(replaceStartIndex, replaceEndIndex);

        return self.filter.replace(stringToReplace, overrideKey + overrideInput.value);
    };

    self.initEventHandlers = function () {
        $('filterBox' + self.clientId).observe('focus', function () {
            self.onFilterFocus($('filterBox' + self.clientId));
        });

        $('filterBox' + self.clientId).observe('blur', function () {
            self.onFilterBlur($('filterBox' + self.clientId));
        });

        $('filterBox' + self.clientId).observe('keyup', function () {
            typingTimer = setTimeout(function () { self.currentPage = 1; self.moveToCurrentPage(); }, self.doneTypingInterval);
        });

        $('filterBox' + self.clientId).observe('keydown', function () {
            clearTimeout(typingTimer);
        });

        $('next' + self.clientId).observe('click', function () {
            if (self.currentPage + 1 <= self.pageNumber) {
                self.currentPage++;
                self.moveToCurrentPage();
            }
        });

        $('prev' + self.clientId).observe('click', function () {
            if (self.currentPage > 1) {
                self.currentPage--;
                self.moveToCurrentPage();
            }
        });

        $(self.id + '_unselected').observe('dblclick', function () {
			if(jQuery('#'+self.id+'_selected').find('option').length==0)
			{
				self.multilistValuesMoveRight();
				javascript: scContent.multilistMoveRight(self.id);
			}
			else if(jQuery('#'+self.id+'_selected').find('option').length==1)
			{
				alert('Only one item is allowed here. Please remove the selected item to add a new item.');
			}
        });

        $(self.id + '_selected').observe('dblclick', function () {
            self.multilistValuesMoveLeft();
            javascript: scContent.multilistMoveLeft(self.id);
        });

        $(self.id + '_unselected').observe('click', function () {
            self.selectedId = self.getSelectedItemId();
        });

        $(self.id + '_selected').observe('click', function () {
            self.selectedId = self.getSelectedItemId();
        });

        $('refresh' + self.clientId).observe('click', function () {
            self.currentPage = 1;
            self.moveToCurrentPage();
        });

        $('goto' + self.clientId).observe('click', function () {
            scForm.postRequest('', '', '', 'contenteditor:launchtab(url=' + self.selectedId + ')');
            return false;
        });
    };

    $('pageNumber' + self.clientId).innerHTML = self.format(self.of, self.currentPage, self.pageNumber);
    self.initEventHandlers();
};

CSS Update
To be saved at: ~\sitecore\shell\Controls\SingleSelectWithSearch\SingleSelectWithSearch.css

.loadingItems
{
    background-image: url('/sitecore/shell/Applications/Buckets/images/load.gif');
    background-position: 50%;
    background-repeat: no-repeat;
}

.bucketSearch.active
{
    color: black;
}

.bucketSearch.inactive
{
    color: gray;
}

.scSingleSelectWithSearchSelectedBox
{
    margin-top: 3px;
    height: 19px;
}

Core DB Update

Add an item in the content tree of the core database at:

2015-02-25_013159

The extension will be registered in the sitecore configuration in the next step. The extension is followed by the class name we used in the step above.

Sitecore configuration update

  <sitecore>
    <!-- New control added - Single Select with Search -->
    <controlSources>
      <source mode="on" namespace="MySite.SitecoreFields" assembly="MySite" prefix="contentExtension" />
    </controlSources>
  </sitecore>

That’s it! You should be all set, and the new control should now appear in the list of available list of controls in sitecore:

2015-02-25_015056

Also, the datasource format will remain the same as with Multilist with Search, example:

StartSearchLocation={20265CCE-B2FF-472C-AA85-9375A313F239}&TemplateFilter={18538774-A45B-43A4-94BD-DC73A0C8FBF9}|{D98FC013-5AF4-4F09-A405-F2A52B6AB8D5}&Filter=_language:en