Creating an asynchronous select control

Name: *
My email: *
Recipient email: *
Message: *
Fields marked as bold are compulsory.
You haven't filled in compulsory values. The email is not correct

Everyone knows the basic select element that is the HTML equivalent to .NET's DropDownList control. Imagine an HTML select containing one hundred or more elements. This would take much time to send and much HTML text to keep your data. Let alone SEO issues that might occur. Instead, data could be filled asynchronously when user activated the control or asked for it using a WCF service leading to far better results. To accomplish that we are going to use JQuery's autocomplete widget.
 
 

Getting started

 
This is a sketch of what we are going to do.
 
Looks better, doesn't it?
 
The select element looks like this 
 
    <select>
        <option value="1">Chocolate chip cookie</option>
        <option value="2">Chocolate ice cream</option>
        <option value="3">Cookies and Cream</option>
        <option value="4">Strawberry ice cream</option>
        <option value="5">Vanilla ice cream</option>
    </select>
 
In case you haven't noticed our data consists of ice cream flavours!
 

Creating the WCF service

 
Before writing the HTML and JavaScript code we are going to create the WCF service that will send the results. I am taking into account that you know the basics of WCF services. In case you don't, you could use an old fashioned web service or anything else that will return some JSON data instead.
 
In order to set up our service we will need some info attached to the web.config file. This will do.
  <system.serviceModel>
    <behaviors>
      <endpointBehaviors>
        <behavior name="DotNetHints.IceCream">
          <enableWebScript />
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
    <services>
      <service name="IceCream">
        <endpoint address="" behaviorConfiguration="DotNetHints.IceCream" binding="webHttpBinding" contract="IceCream" />
      </service>
    </services>
  </system.serviceModel>
  
Now we can create WCF services based on the IceCream class. So we create a file called Icecream.svc and place the following code inside
[ServiceContract(Namespace = "")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class IceCream
{
 
    [WebInvoke(Method = "GET", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    [OperationContract]
    public string GetIceCreamFlavours()
    {
       return "";
     }
}
 
If everything works fine, then by requesting MyPath/IceCream.svc/GetIceCreamFlavours on my web browser I get
{"d":""}
 
This is a web service that returns nothing. Now, let's create a service that will actually do something useful.
 
We are going to use a helper class called IceCreamFlavour that contains two string values, ID and Name.
 
public class IceCreamFlavour
{
    public string ID { get; set; }
    public string Name { getset;}
}
 
Then we are creating the FillIceCreamFlavors that will get a list of IceCreamFlavour objects and insert the select elements we have created in the beginning. 
 
public void FillIceCreamFlavors(List<IceCreamFlavour> searchResults)
{
searchResults.Add(new IceCreamFlavour() { ID = "1", Name = "Chocolate chip cookie" });
searchResults.Add(new IceCreamFlavour() { ID = "2", Name = "Chocolate ice cream" });
searchResults.Add(new IceCreamFlavour() { ID = "3", Name = "Cookies and Cream" });
searchResults.Add(new IceCreamFlavour() { ID = "4", Name = "Strawberry ice cream" });
searchResults.Add(new IceCreamFlavour() { ID = "5", Name = "Vanilla ice cream" });
}
 
Finally let's do our GetIceCreamFlavours service
public string GetIceCreamFlavours()
    {
        List<IceCreamFlavour> searchResults = new List<IceCreamFlavour>();
        FillIceCreamFlavours(searchResults);
 
        //Create a jsonSerializer object
        var jsonSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
        jsonSerializer.MaxJsonLength = 2147483647;
        //Return serialized json data
        return jsonSerializer.Serialize(searchResults);
    }
 
Now if I request the same URL as I did earlier I get
{"d":"[{\"ID\":\"1\",\"Name\":\"Chocolate chip cookie\"},{\"ID\":\"2\",\"Name\":\"Chocolate ice cream\"},{\"ID\":\"3\",\"Name\":\"Cookies and Cream\"},{\"ID\":\"4\",\"Name\":\"Strawberry ice cream\"},{\"ID\":\"5\",\"Name\":\"Vanilla ice cream\"}]"}
 
This is our data in JSON form. Now we can take one more step and create a method that will filter out all IceCreamFlavours that do not contain the search keyword within their text.
 
public string GetIceCreamFlavours(string searchKeyword)
    {
        List<IceCreamFlavour> searchResults = new List<IceCreamFlavour>();
        FillIceCreamFlavours(searchResults);
 
        if (!string.IsNullOrEmpty(searchKeyword))
        {
            //Use ToLower so letter case will make no difference
            searchKeyword = searchKeyword.ToLower();
            //Use lambda expression to filter out data
            searchResults = searchResults.Where(sr => sr.Name.ToLower().Contains(searchKeyword)).ToList();
 
            //Return no results message
            if (!searchResults.Any())
                searchResults.Add(new IceCreamFlavour() { ID = "-1", Name = "No results :(" });
        }
 
        //Create a jsonSerializer object
        var jsonSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
        jsonSerializer.MaxJsonLength = 2147483647;
        //Return serialized json data
        return jsonSerializer.Serialize(searchResults);
    }  
 
Now let's try out this URL MyPath/IceCream.svc/GetIceCreamFlavours?searchKeyword=ice
The resulting data is
{"d":"[{\"ID\":\"2\",\"Name\":\"Chocolate ice cream\"},{\"ID\":\"4\",\"Name\":\"Strawberry ice cream\"},{\"ID\":\"5\",\"Name\":\"Vanilla ice cream\"}]"}
 
So far we have successfully created a WCF service that will return IceCreamFlavour data depending on the requested input. Let's move on to the HTML and JavaScript part.
 
 

Using autocomplete

 
To use that JQuery autocomplete widget we need a simple input element like that
<input id="AsyncSearch" type="text" placeholder="Ice Cream Flavour" autocomplete="off" />
 
We also have to use two JQuery library files    
<script src="//code.jquery.com/jquery-1.9.1.js"></script>
<script src="//code.jquery.com/ui/1.10.3/jquery-ui.js"></script>
 
and optionally a css file so that the results will look nice.
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.2/themes/smoothness/jquery-ui.css"> 
 
In addition to the default css I 'd like to add a line of my own to make results look smoother
<style> b, a { font-size:12px;}</style>
 
We are now ready to use the autocomplete function like that
$(function () {
            $("#AsyncSearch").autocomplete({
                source: function (request, response) {
                    $(this.element).data("jqAjax", $.ajax({
                        url: "MyPath/IceCream.svc/GetIceCreamFlavours?searchKeyword=" + encodeURI(request.term),
                        dataType: "json",
                        type: "GET",
                        contentType: "application/json; charset=utf-8",
                        success: function (data) {
 
                            if (data.hasOwnProperty("d"))
                                data = eval(data.d);
 
                            response($.map(data, function (item) {
                                return {
                                    label: item.Name,
                                    value: item.ID,
                                };
                            }));
                        }
                    }));
                },
                minLength: 3,
                select: function (event, ui) {
                    window.location.replace('AsyncSelect.html?id=' + ui.item.value)
                }
            });
        });
 
Even though it looks complicated, it is not hard to handle once you get to know it. Event hough autocomplete has many attributes, we'll use only the basic ones.
 
Source describes the data source. Its attributes describe themselves properly: url, dataType, type and contentType. We can easily tell that our service MyPath/IceCream.svc/GetIceCreamFlavours will be the target of the request and it will contain the searchKeyword parameter having value of  encodeURI(request.term).
Let's take a look at success attribute. This is the code that will run when data is retrieved. The following code is important to be included when using WCF services
if (data.hasOwnProperty("d"))
data = eval(data.d);
 
You may remember that a previous call to our service returned
{"d":"[{\"ID\":\"2\",\"Name\":\"Chocolate ice cream\"},{\"ID\":\"4\",\"Name\":\"Strawberry ice cream\"},{\"ID\":\"5\",\"Name\":\"Vanilla ice cream\"}]"}
This is a fine created JSON object, however this "d" property that is by default placed in the beginning when using WCF services will make things hard for autocomplete to handle. This is the reason we need to have it removed.
 
Moving on, minLength is a useful attribute specifying how many characters need to be filled in order for the AJAX call to be created.
 
The select attribute describes what will happen when an item is selected. In our case, we are going to be redirected to a new page (actually it's the same page) containing the item's ID as an id parameter.
 
Everything is ready. Let's take a look at what happens now.
 
 

Using the catcomplete function.

 
We have created our asynchronous select control. Before completing this article there's one more thing to do. That will be, using the catcomplete function, in order to group our results into categories. 
 
First thing to do is edit the services we have created so that they can support a categories element. We will create the public class 
IceCreamFlavourCategory which is similar to the IceCreamFlavour class, however it includes an extra string Category attribute.
 
public class IceCreamFlavourCategory
{
     public string ID { get; set; }
     public stringName { getset; }
     public string Category { getset; }
 
We will create a new service called GetIceCreamCategoryFlavours which is about the same as the one we had already created.
    
[WebInvoke(Method = "GET", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    [OperationContract]
    public string GetIceCreamCategoryFlavours(string searchKeyword)
    {
        List<IceCreamFlavourCategory> searchResults = new List<IceCreamFlavourCategory>();
        FillIceCreamCategoryFlavours(searchResults);
 
       //Use ToLower so letter case will make no difference
        searchKeyword = searchKeyword.ToLower();
       //Use lambda expression to filter out data
        searchResults = searchResults.Where(sr => sr.Name.ToLower().Contains(searchKeyword)).ToList();
 
        //Return no results message
        if (!searchResults.Any())
            searchResults.Add(new IceCreamFlavourCategory() { ID = "-1", Name = "No results :(", Category = "", });
 
        var jsonSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
        jsonSerializer.MaxJsonLength = 2147483647;
        //Return serialized json data
        return jsonSerializer.Serialize(searchResults);
    }
 
We will also use an extra initialization method we are going to name FillIceCreamFlavours, similar to the FillIceCreamFlavours we created earlier.
 
    public void FillIceCreamCategoryFlavours(List<IceCreamFlavourCategory> searchResults)
    {
        searchResults.Add(new IceCreamFlavourCategory() { ID = "1", Name = "Chocolate chip cookie", Category = "Chocolate" });
        searchResults.Add(new IceCreamFlavourCategory() { ID = "2", Name = "Chocolate ice cream", Category = "Chocolate" });
        searchResults.Add(new  IceCreamFlavourCategory() { ID = "3", Name = "Cookies and Cream", Category = "Cream" });
        searchResults.Add(new IceCreamFlavourCategory() { ID = "4", Name = "Strawberry ice cream", Category = "Strawberry" });
        searchResults.Add(new IceCreamFlavourCategory() { ID = "5", Name = "Vanilla ice cream", Category = "Vanilla" });
 
    }
 
Now, the tuime has come to test our new service. When requesting the MyPath/IceCream.svc/GetIceCreamCategoryFlavours?searchKeyword=ice URl we get
 
{"d":"[{\"ID\":\"2\",\"Name\":\"Chocolate ice cream\",\"Category\":\"Chocolate\"},{\"ID\":\"4\",\"Name\":\"Strawberry ice cream\",\"Category\":\"Strawberry\"},{\"ID\":\"5\",\"Name\":\"Vanilla ice cream\",\"Category\":\"Vanilla\"}]"}
 
Notice the results are the same as the ones we got earlier, however now we have the Category info as well.
 
Finally here's the new script we need to use the code we just created.
 
$(function () {
            $("#AsyncSearch").catcomplete({
                source: function (request, response) {
                    $(this.element).data("jqAjax", $.ajax({
                        url: "/MyPath/IceCream.svc/GetIceCreamCategoryFlavours?searchKeyword=" + encodeURI(request.term),
                        dataType: "json",
                        type: "GET",
                        contentType: "application/json; charset=utf-8",
                        success: function (data) {
 
                            if (data.hasOwnProperty("d"))
                                data = eval(data.d);
 
                            response($.map(data, function (item) {
                                return {
                                    label: item.Name,
                                    value: item.ID,
                                    category: item.Category
                                };
                            }));
                        }
                    }));
                },
                minLength: 3,
                select: function (event, ui) {
                    window.location.replace('AsyncSelect.html?id=' + ui.item.value)
                }
            });
        });
 
This piece of code is similar to the previous one as well. You may notice the category attribute that is inserted in the response part.
 
We will also need the following piece of code to handle the grouping
 
        $.widget("custom.catcomplete", $.ui.autocomplete, {
            _renderMenu: function (ul, items) {
                var that = this,
                 currentCategory = "";
                $.each(items, function (index, item) {
                    if (item.category != currentCategory) {
                        ul.append("<li ><b>" + item.category + "</b></li>");
                        currentCategory = item.category;
                    }
                    that._renderItemData(ul, item);
                });
            }
        });
 
Every time an item containing a new category shows up we place the items category info before presenting the actual info. 
 
The result should look like
 
 
JQuery functions can get many attributes that have not been used on this example. If you want to know more on how to master them, you can visit the official page.
 
 

Summary

 
Using a WCF service that returns JSON results based on input in combination with the JQuery autocomplete and catcomplete functions we have created an asynchronous select element. Autocomplete (or catcomplete if we wish to use grouping) calls the service automatically and then handles the results as soon as it gets them.
 

Back to BlogPreviousNext

Comments



    Leave a comment
    Name: