Getting JQuery UI Autocomplete to work in ASP.NET MVC

I’ve spent the last few days messing around with the JQuery UI autocomplete. It’s a great little feature, but there’s just one problem:

There isn’t a complete tutorial of how to do it in ASP.NET MVC on the internet.  I know this because I searched like hell when it didn’t work for me.  Every example I looked at was missing one piece or another. 

In this blog post, I’m going to jot down what I did to get it working.

The first step is to write the Controller that will handle the calls.

public class QuestionsController : Controller
{
    IStackAPIRepository stackAPIRepository;
    public QuestionsController()
        : this(new StackAPIRepository()) { }
        
    public QuestionsController(IStackAPIRepository repository)
    {
        stackAPIRepository = repository;
    }

    ///<summary>
    /// Search Page used to find sites
    ///</summary> 
    public ActionResult Search()
    {
        return View();
    }
        
    ///<summary>
    ///Ajax call used to retrieve StackAuth Sites
    ///</summary>
    [HttpPost]
    public ActionResult Find(string term)
    {
        string[] sites = stackAPIRepository.FindSites(term);
        return Json(sites); 
    }
}

There are a few interesting parts to this controller, the first is the Dependency Injected repository. This allows me to switch out to a mock repository when testing. This is poorman’s dependency Injection, but for simple projects it is enough. When dealing with a larger project, it’s advisable to use a DI/IoC framework, like Spring.NET. The second part to pay attention to is the Action called Find.  This Action is going to be used to handle the AJAX calls.  The [HttpPost] is normally necessary to prevent Json Hijacking.  In this case it doesn’t really matter because the data isn’t sensitive (it’s just a JSON array of Stack Exchange sites), but I’m including it as an example (since I haven’t found any complete examples with HttpPost out there.

The Repositories aren’t really important to the example, as they just return a string Array that contains the site. For completeness, it is included:

The IStackAPIRepository Interface:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Stacky;

namespace EditIt.Models
{
    public interface IStackAPIRepository
    {
        IEnumerable<Question> GetQuestions(Stacky.Site site);
        IEnumerable<Question> GetQuestions(Stacky.Site site, string[] tags);
        string[] FindSites(string term);
    }
}

The Stacky reference to to the .NET API for Stack Exchange. You can download it on the Stacky codeplex site.

If I had written tests at this point, you’d probably notice that I forgot to abstract out the Stacky.Site so I could test it.  I haven’t actually implemented that yet.

The next code actually implements the interface.  There are two things worth noting in the code. First, I make all the actual calls to the Stack Exchange API here. That’s because it is the source of the data for my controller, hence it is the ‘Model’. More than that though, it makes a clean separation between layers. I don’t have controllers having to call an API for information, and coupling them in that fashion (if I did, it wouldn’t be very MVCish, would it?) The second part is that I also implement caching in this layer so that the API isn’t called every time someone wants to type in a site to look at.  The Stack Exchange sites don’t change that often, so the chances of a new site not being on the list are slim. At most they’ll be on the list a day after they go live, which is acceptable.

using System;
using EditIt.Controllers;
using EditIt.Utility;
using EditIt.Models;
using System.Collections.Generic;
using Stacky;
using System.Linq;
using System.Web;

public class StackAPIRepository : IStackAPIRepository
{
    private StackAuthClient authClient = new StackAuthClient(new UrlClient(), new JsonProtocol());

    public string[] FindSites(string term)
    {
        var sites = HttpRuntime
              .Cache
              .GetOrStore<string[]>(“sites”,
                 () => GetAllSites());

        if (string.IsNullOrEmpty(term))
        {
            return sites;
        }
        else
        {
            var items = ( from s in sites
                       where s.ToLower().Contains(term.ToLower())
                       select s).ToArray();
            return items;
        }
    }

    public string[] GetAllSites()
    {
        IEnumerable<Site> sites = authClient.GetSites();
        return (from s in sites
                select s.Name).ToArray();
    }
}

I’ve pulled out the other methods so that we can focus on the two methods that make things happen here.  The first is the GetAllSites method.  This is the method actually responsible for calling the Stack Exchange API.  Looking at it, it’s not very testable in its current state, but that’s an improvement I can make later.  I’m using LINQ to easily pull out the information I need.  I use the Set syntax because it’s easier for someone who isn’t familiar with LINQ to follow.  Every time I can use that syntax, I do.  The key is to keep the code readable, even by someone who may not know LINQ at first blush. 

The FindSites method first checks the Cache for whether or not someone has already polled the Stack Exchange sites. If they have, then we already have the entire array of sites to work with.  If they haven’t, it polls the API and then stores that into the Cache.  Here’s the GetOrStore extension method responsible for doing that, found from How Do I Cache Objects in ASP.NET MVC? from Stack Overflow.

I modified it a little according to the comments in the answer:

public static class CacheExtensions
{
    public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator)
    {
        var result = cache.Get(key);
        if (result == null)
        {
            result = generator();
            cache.Insert(key, result, null, System.DateTime.UtcNow.AddDays(1), 
            System.Web.Caching.Cache.NoSlidingExpiration);
        }
        return (T)result;
    }
}

This allows me to easily add items to the cache as my application grows, without having to implement more than just this. It does add some coupling that will make testing harder, and in a future blog post I’ll go into getting around that.

The other potential problem with this approach is that it sets a hard deadline for cached items. No matter what, every item will be cached for a full day.  As my application grows, I may want to change that. For now it’s ok, though. YAGNI, and all that.

There’s an old saying by Carl Sagan: “If you want to make an apple pie from scratch, you must first create the universe.”  Now that we’ve created our universe, we can implement autocomplete. 

The toughest part of autcomplete is getting it to work.  Scattered among the dozens of examples on the internet autocomplete libraries that are no longer maintained.  I picked the JQuery UI Autocomplete plugin.  I’ve also chosen to pull the Jquery.js files from Google, instead of loading them locally.

The view is simple enough:

<%@ Page Title=”” Language=”C#” MasterPageFile=”~/Views/Shared/Site.Master” Inherits=”System.Web.Mvc.ViewPage” %>
<asp:Content ID=”Headcontent” ContentPlaceHolderID=”HeadContent” runat=”server”>
  <link href=”http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css” rel=”stylesheet” type=”text/css”/&gt;
  <script src=”http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js” type=”text/javascript”></script&gt;
  <script src=”http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js” type=”text/javascript”></script&gt;
   
    <script type=”text/javascript”>
        $.ajaxSetup({ type: “post” });
        $(document).ready(function() {
            $(“input#autocomplete”).autocomplete({
                source: ‘<%=Url.Action(“Find”, “Questions”) %>‘, minChars:3, delay: 500
            });
        });
  </script>
</asp:Content>

<asp:Content ID=”Content1″ ContentPlaceHolderID=”TitleContent” runat=”server”>
    Search
</asp:Content>

<asp:Content ID=”Content2″ ContentPlaceHolderID=”MainContent” runat=”server”>

    <h2>Search</h2>
    <p>
      <%= Html.TextBox(“autocomplete”)%>
    </p> 
</asp:Content>

The meat and potatoes of the autocomplete happens in the JQuery Script.  The  $.ajaxSetup({ type: “post” }); allows me to override the default behavior for AJAX and force it to use POST for requests.  As I stated earlier, this isnt’ really necessary for this particular script, but it would be necessary if I were using AJAX and were passing around sensitive information.  It doesn’t hurt anything to use it here, and it cuts down on the potential attack vectors.

The rest of the autocomplete is pretty straight forward, and there are dozens of examples of parameters to add on the example page.

There are a few gotchas to be aware of:

  • If the path to JQuery isn’t correct, or the script isn’t loading for some reason, the autocomplete will not work.  If you have Firebug, you can load it up in the ‘Scripts’ section, and it will report all the errors it gets in parsing the JavaScript. The other handy item is Fiddler Web Debugger. It allows you to inspect anything sent across HTTP, which was very useful in debugging problems with autocomplete.
  • Autocomplete sends the information to the url using the ‘term’ variable. : /Questions/Find?term=mytext. That means if your routing isn’t set up correctly, or if your actions aren’t looking for a parameter named ‘term’, it won’t work correctly.  This is a change from the old autocomplete, which used ‘q’.
  • Make sure the route is being sent across correctly. For my solution, I didn’t need to implement a route for this URL to work. With more complicated URLs, you may need to.

That’s really all there is to it, and the end result is a autocomplete that works correctly, the first time.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s