With the HTML5 movement taking shape, organizations and their web-enabled architects embracing a newer, ever-evolving standard in web/mobile application development, so too has user experience (UX) demands and minimum functionality norms. One such example is user search, specifically keywords that deliver users to specific targeted content. Today's example demonstrates how to combine such keywords (i.e.: product model number, name, etc.) with the normal "free form" input text field courtesy of jQuery UI's Autocomplete widget, AJAX, JSON and ASP.NET MVC 4.

Our sample solution will make use of ASP.NET MVC 4, jQuery UI and PetaPoco for accessing a stock AdventureWorksLT SQL database. Sample solution source code is provided at the bottom of the post for those interested, now lets get started!

Installing Dependencies

For post brevity, we've created a stock MVC 4 Internet application, removed non application-specific content (i.e.: account login, etc.) ahead of time. With stock application in hand, we're now ready to install our jQuery, jQuery UI and PetaPoco dependencies. Regarding PetaPoco, please see a previous blog post: Micro ORM Data Mapping with PetaPoco and ASP.NET MVC 4 for more detailed information that we won't cover here.

Execute the following in Visual Studio's Package Manager console:

PM> Install-Package jQuery

PM> Install-Package jQuery.UI.Combined

PM> Install-Package PetaPoco.Core

POCOs and Data Access

With dependencies installed, we create a new Keyword POCO (Plain Old CLR Object) class that defines a KeywordTerm string object.

namespace KeywordSearch.Sample.Models
{
    public class Keyword
    {
        public string KeywordTerm { get; set; }
    }
}

Next we'll create a SearchController class responsible for returning AdventureWorksLT.SalesLT.Product SQL data (as configured in the web.config's DbConnection connectionString) as a List<Keyword> object via the GetKeywords method call. It also makes use of some clever T-SQL syntax (much like a "Contains" filter for each whole word in the user's input). We're now able to data map SQL data to our POCO created earlier.

using System.Collections.Generic;

namespace KeywordSearch.Sample.Models
{
    public class SearchManager
    {
        private PetaPoco.Database db = new PetaPoco.Database("DbConnection");

        public List<Keyword> GetKeywords(string term)
        {
            var keywords = db.Fetch<Keyword>("SELECT (ProductNumber + ' ' + Name) AS KeywordTerm FROM SalesLT.Product WHERE ((ProductNumber LIKE '%" + term.Replace(" ", "%' AND ProductNumber LIKE '%") + "%') OR (Name LIKE '%" + term.Replace(" ", "%' AND Name LIKE '%") + "%')) AND (ProductNumber + ' ' + Name) IS NOT NULL ORDER BY ProductNumber");
            return keywords;
        }
    }
}

Integrating the Autocomplete Widget

Within our ~/Views/Shared/_Layout.cshtml View, we'll add the new jQuery UI Autocomplete widget within an Html.BeginForm helper adding appropriate id/name values for future jQuery selector use later in our example.

@using (Html.BeginForm("Index", "Search")){ <input id="term" name="term" class="lookup" /> }

The jQuery UI installed base theme is wonderful and all, but we'd like to change a few things to better match our stock ASP.NET MVC 4 app's color theme. Let's add some styles to our ~/Content/Css/base.css file for manipulating jQuery UI's Autocomplete base theme where appropriate. We've elected to simply replace pre-determined widget styles via stylesheet priority rather than monkeying with any default theme CSS files (who wants to spend half a blog post covering single digit CSS changes?!).

/* autocomplete input text field */
.lookup {
    width: 200px;
    height: 16px;
    padding: 4px 8px;
    margin: 0;
    border: 3px solid #e2e2e2;
    background: #e2e2e2;
    font-family: "Segoe UI", Verdana, Helvetica, Sans-Serif;
    font-size: 0.85em;
    color: #333
    -webkit-border-radius: 4px;
    -moz-border-radius: 4px;
    border-radius: 4px;
}

/* autocomplete drop down override */
.ui-autocomplete {
    max-height: 300px;
    overflow-y: auto;
    overflow-x: hidden;
    font-family: "Segoe UI", Verdana, Helvetica, Sans-Serif;
    font-size: 0.85em;
}

/* autocomplete focus and hover state color override */
.ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { color: #000; background: #e8e8e8; }

At this point, our UI looks a little like this:

Layout before jQuery functionality

Controllers, MapRoutes and JSON

Knowing that we want to auto-forward keyword selections to product display pages and "free form" search queries to a traditional search page, we need to create both Search and Product Controllers reponsible for View aggregation, returning JSON data and some example solution ViewBag values.

using System.Web.Mvc;
using KeywordSearch.Sample.Models;

namespace KeywordSearch.Sample.Controllers
{
    public class SearchController : Controller
    {
        public ActionResult Index(string term)
        {
            ViewBag.SearchedFor = term.ToUpper();
            return View();
        }
    }
}
using System.Web.Mvc;

namespace KeywordSearch.Sample.Controllers
{
    public class ProductController : Controller
    {
        public ActionResult ViewProduct(string item)
        {
            ViewBag.ProductNumber = item.Split('_')[0].ToUpper();
            // quick and dirty formatting
            ViewBag.ProductName = item.Replace(item.Split('_')[0], "").Replace("_","").Replace("-", " ").ToUpper();
            return View();
        }
    }
}

In order to make use of the Autocomplete's rich client-side data functionality, we add a JsonResult Method to our SearchContoller class that will return a JsonResult based on the user input.

public JsonResult GetKeywords(string term)
{
    var manager = new SearchManager();
    var keywords = manager.GetKeywords(term);
    return Json(keywords, JsonRequestBehavior.AllowGet);
}

For this example, we'll expose JSON data via a /keywords URL. Simple, relavent and will be easily wired up with our Autocomplete via jQuery AJAX callbacks in our next section. Before that, however, we need to add some required MapRoute entries for resolving URLs.

...
routes.MapRoute(
    "GetKeywords",
    "Keywords",
    new { controller = "Search", action = "GetKeywords" }
);

routes.MapRoute(
    "Search",
    "search/{term}",
    new { controller = "Search", action = "Index", term = "" }
);

routes.MapRoute(
    "ViewProduct",
    "product/{item}",
    new { controller = "Product", action = "ViewProduct", item = "" }
);
...

Now that we can resolve our /keywords URL, verifying JSON data is relatively easy to do using Fiddler.

Verifying JSON with Fiddler

Getting Our jQuery On

Phew! Goodbye boring stuff and say hello to the fun part, jQueryifying our new Autocomplete control. We add a ~/Scripts/custom.js JavaScript file to our sample application for Autocomplete jQuery logic. Using our previously established input id (#term) for jQuery selector reference, our starting script looks like this.

$(document).ready(function () {
    $("#term").autocomplete({
        minLength: 2
    });
});

Our next step will be to add AJAX callback functionality to our recently JSON exposed /keywords URL via jQuery AJAX call. We'll additionally add the following logic to the Autocomplete's source attribute to retrieve JSON data from our specified URL and map both the dropdown label and value fields to our returned KeywordTerm collection.

...
source: function (req, resp) {
    $.ajax({
        url: "/Keywords",
        type: "POST",
        dataType: "json",
        data: { term: req.term },
        success: function (data) {
            resp($.map(data, function (item) {
                return { label: item.KeywordTerm, value: item.KeywordTerm };
            }));
        }
    });
}
...

As KeywordTerm values may be longer than our current input width, let's add some nifty expand/collapse jQuery animation to the Autocomplete. Upon the user typing, our input will quickly expand to make room for any lengthy returned values shown in the dropdown.

...
$("#term").keydown(function () {
    $(this).animate({
        width: '300'
    }, {
        duration: 80,
        specialEasing: {
            width: 'linear',
            height: 'easeOutBounce'
        }
    });
});

$("#term").blur(function () {
    $(this).animate({
        width: '200'
    }, {
        duration: 80,
        specialEasing: {
            width: 'linear',
            height: 'easeInBounce'
        }
    });
});
...

Congrats! We're now returning live KeywordTerm data from our AdventureWorksLT database client-side via AJAX and displaying them via the Autocomplete's dropdown.

Working Dropdown Data

This works great, but what about forwarding the user to the sample product page upon selecting an item from within the dropdown? Simple. We'll add the following to the Autocomplete's select attribute to handle this.

select: function (event, ui) {
    var selected = ui.item;
    var mdlNum, mdlName;
    if (selected.value !== null) {
        var array = selected.value.split(' ');
        mdlNum = array[0].toLowerCase();
        mdlName = selected.value.replace(array[0], '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
        window.location.replace('http://' + location.host + '/product/' + mdlNum + '_' + mdlName);
    }
}

We retrieve the selected item, split it into mdlNum and mdlName values, replace any non alphanumeric values with - characters to make things URL friendly and finally tell the browser to load the product display page. Note: Using window.location.replace does NOT maintain browser back button history. A suitable replacement would need to be used if history is desireable.

Sample Product Display Page from Dropdown Selection

Regular "free form" search requests are processed via the @Html.BeginForm helper forwarding to the default SearchController Index View.

Sample Search Results Page

Finally, we add a couple jQuery items to auto focus the input on page load and select all input text on click to make things easier for the user.

...
$("#term").focus();

$("#term").click(function () {
    $(this).select();
});
...

That's pretty much it. Client-side querying for whatever KeywordTerm data you want, customizeable via T-SQL query syntax delivered right into a dynamically populated dropdown below your input field. Make your users lives easier. Cheers!