Building a Twitter live search app with Knockout, jQuery and ASP.NET MVC 3

Anders Fjeldstad

It’s not trivial to build a well designed web application front-end with the level of responsiveness, performance and features that users expect today. It’s easy to get lost in the boiling mix of jQuery event handlers, HTML markup and AJAX calls, and even a relatively simple GUI can quickly become a nightmare to maintain.

One way of introducing some structure and order on the client-side is to use a framework like Knockout. Knockout is a free and open source Javascript library that helps implementing the Model-View-View Model (MVVM) pattern on the client. It’s well documented and the official web site is a great starting point with a bunch of live examples that demonstrate both how to use the built-in features and how to extend it with your own.

In this post, we will see how Knockout can be used together with the mapping plugin, some jQuery and an ASP.NET MVC 3 backend (or model, if you like) to build a simple but effective Twitter “live search” application.

The goal

Our objective is simple: the user should be able to input some search critera (like a hashtag or Twitter username), and the application should then present the most recent matching tweets. Also, the result set should be updated automatically by some background procedure on the client so that the GUI always shows that latest matching tweets. (This could be useful when following some live event, for example.) In the end, we would like to end up with something similar to this:

 

Step 1: Setting up the model

Twitter exposes a search API that is pretty simple to use, and that can respond to search requests in either XML (Atom) or JSON format. We could let our client talk directly to the Twitter API, but in order to be able to improve the application with caching, some fallback if Twitter goes down etc., it’s probably a good idea to build our own model instead - and for that we are going to use a very simple ASP.NET MVC 3 application.

First, create an empty/default web application (with Razor as the chosen view engine) and add a controller - let’s call it TwitterController. Then rename the “Index” action that was automatically created with the new controller to “Search”. The controller should look something like this:

public class TwitterController : Controller
{
    [HttpGet]
    public ActionResult Search()
    {
        return View();
    }
}

Next, we’re going to create the action method that performs the actual Twitter search. For this lab, we will be using the Atom version of the search API. The request URL is on the format http://search.twitter.com/search.atom?q=[URL-encoded search parameters]. Since Atom is XML, we can use LINQ to XML to parse and transform the result set (Twitter returns a lot of data that we won’t be needing, so we’ll trim it down to a minimum).

But we don’t want to send XML down to the client - we wan’t JSON. Fortunately it’s easy to convert most objects to JSON, and in this case the simplest way is to use the built-in Json method that returns a JsonResult. All this boils down to:

[HttpPost]
public ActionResult Search(string query)
{
    var atomResult = XDocument.Load(string.Format(
        "http://search.twitter.com/search.atom?q={0}",
        HttpUtility.UrlEncode(query)));
    XNamespace ns = "http://www.w3.org/2005/Atom";
    var searchResult =
        from tweet in atomResult.Descendants(ns + "entry")
        let image = tweet.Elements(ns + "link")
                         .Where(e => e.Attributes()
                                      .Any(a => a.Name == "rel" &&
                                                a.Value == "image"))
                         .First().Attribute("href").Value
        let url =   tweet.Elements(ns + "link")
                         .Where(e => e.Attributes()
                                      .Any(a => a.Name == "rel" &&
                                                a.Value == "alternate"))
                         .First().Attribute("href").Value
        select new
        {
            id = tweet.Element(ns + "id").Value,
            author = tweet.Element(ns + "author").Element(ns + "name").Value,
            imageUrl = image,
            tweetUrl = url,
            tweetText = tweet.Element(ns + "title").Value
        };
    return Json(searchResult);
}

(The code above includes some parsing to extract the profile picture of the author and the link to each individual tweet.)

That’s it - we have built our model!

Step 2: Implementing the view model

Before we begin building the GUI, we’ll need to add some references to our solution. Either manually or via NuGet, download jQuery, Knockout and jQuery.tmpl. Then add a “Search.cshtml” view below Views/Twitter (don’t use a “master page” or layout - it’s easier to get an overview of things in a small lab like this if we’re not distributing the solution over too many separate files). Add the mentioned Javascript references. When you’re done, you should have something similar to this:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Twitter Live Search</title>
        <script src="@Url.Content("~/Scripts/jquery-1.6.2.js")"
                type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery.tmpl.min.js")"
                type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/knockout-1.2.1.js")"
                type="text/javascript"></script>
    </head>
    <body>
        <!-- TODO -->
    </body>
</html>

Now it’s time to build our view model. The view model contains the client-side application logic, if you like - it defines what the application can do and what properties it has, without directly referencing any markup or DOM elements.

The view model for this lab is very simple - it so far contains two properties and a method, used to store the current search query, the matching tweets and to perform a search against the model, respectively. It could look like this:

<script type="text/javascript">
    function SearchViewModel() {
        this.query = ko.observable('');
        this.tweets = ko.observableArray();
    }

    SearchViewModel.prototype.search = function () {
        var q = this.query();
        if (q.length > 0) {
            $.ajax({
                type: 'POST',
                url: '/Twitter/Search',
                data: { query: q },
                dataType: 'json',
                success: function (data) {
                    // TODO
                }.bind(this)
            });
        } else {
            // TODO
        }
    };
</script>

Not that much going on here; the query and tweets properties are empty initially, and the search method uses jQuery to post an AJAX request to our model on the server and retrieve the result as a JSON object (array of tweets).

How should we implement the “success” handler in the search method? We could clear the tweets array and add the new search results. But remember that we want to receive “updates” to our search results continuously - then ideally we would like to just add new tweets (and remove old ones), not replace the entire collection.

This is a great possibility to try out the Mapping plugin for Knockout. Once we’ve downloaded it and added the script reference, it enables us to implement the success handler of the AJAX request like this:

success: function (data) {
    ko.mapping.updateFromJS(this.tweets, data);
}.bind(this)

And then to set up a “key definition” for the tweets collection (which is used by Mapping to determine which items should be added, updated and removed), we just modify our constructor slightly:

function SearchViewModel() {
    this.query = ko.observable('');
    this.tweets = ko.mapping.fromJS(
        [],
        {
            key: function (tweet) { return ko.utils.unwrapObservable(tweet.id) }
        });
}

Note that we’re defining the key of the tweets collection to be the ID value that Twitter so conveniently provides. If you want more information about the Mapping plugin, the official Knockout site is a good resource.

Next: implementing the auto-refresh feature. Whenever a search query is present, the matching tweets should be updated every, say, three seconds to reflect changes in the model. One way of accomplishing this is to have the success callback of the AJAX search request call the search method recursively, with a delay. The complete view model then looks like this:

<script type="text/javascript">
    function SearchViewModel() {
        this.query = ko.observable('');
        this.tweets = ko.mapping.fromJS(
            [],
            {
                key: function (tweet) { return ko.utils.unwrapObservable(tweet.id) }
            });
        this.autoRefreshId = null;
    }

    SearchViewModel.prototype.search = function () {
        var timeoutId = this.autoRefreshId;
        if (timeoutId) {
            window.clearTimeout(timeoutId);
            this.autoRefreshId = null;
        }
        var q = this.query();
        if (q.length > 0) {
            $.ajax({
                type: 'POST',
                url: '/Twitter/Search',
                data: { query: q },
                dataType: 'json',
                success: function (data) {
                    ko.mapping.updateFromJS(this.tweets, data);
                    this.autoRefreshId = window.setTimeout(
                        this.search.bind(this), 3000);
                }.bind(this)
            });
        } else {
            ko.mapping.updateFromJS(this.tweets, []);
        }
    };
</script>

(I’ve added a couple of lines to disable the auto-refresh when the search box is empty, plus a check that disables any pending search when the user performs a new one. Also, the search results are emptied when a search is performed with an empty query.)

Step 3: Designing the view

To keep things clean and simple, the view will consist only of a search box and a list of tweets. We want to achieve something along this line:

<form>
    <input type="search" placeholder="Input #hashtag, @@username or something else" autofocus />
    <input type="submit" value="Search" />
</form>

<ul id="tweets">
    <li>
        <a href="[url-of-tweet]" title="[author-name]">
            <img src="[url-of-author-image]" alt="[author-name]" />
        </a>
        <h6>[author-name]</h6>
        <p>[tweet-text]</p>
    </li>
    <!-- More tweets... -->
</ul>

Let’s also add some declarative Knockout data bindings that tie the view to the view model. If we modify the search form just a bit...

<form data-bind="submit: search">
    <input type="search" data-bind="value: query, valueUpdate: 'afterkeydown'" placeholder="Input #hashtag, @@username or something else" autofocus />
    <input type="submit" value="Search" />
</form>

...Knockout will update the query property of the view model automatically when the user changes the text in the search box, and a search will be performed whenever the user presses the Enter key inside the search box.

What about the search results? A perfect scenario for using the Knockout template binding! First, create a template for how a single tweet should look like:

<script id="tweetTemplate" type="text/html">
    <li data-bind="attr: { id: id }">
        <a data-bind="attr: { href: tweetUrl, title: author }">
            <img data-bind="attr: { src: imageUrl, alt: author }" />
        </a>
        <h6 data-bind="text: author"></h6>
        <p data-bind="text: tweetText"></p>
    </li>
</script>

Then data-bind the HTML list against the tweets property of the view model, specifying our template as the way to render each item. It’s really easy:

<ul id="tweets"
    data-bind="template: {
                   name: 'tweetTemplate',
                   foreach: tweets
               }, visible: tweets().length > 0"></ul>

(Note that the list won’t be visible at all when the view model contains no tweets.)

Bonus: Add an animation that is used when new tweets are added to the list, using the afterAdd option of the template binding:

<ul id="tweets"
    data-bind="template: {
                   name: 'tweetTemplate',
                   foreach: tweets,
                   afterAdd: function (element) {
                       $(element).hide().fadeIn('fast');
                   }
               }, visible: tweets().length > 0"></ul>

At this point only one thing remains: some boostrapping logic. When the DOM has loaded, we need to create an instance of our view model and tell Knockout to connect the bindings to it. One line of code within the jQuery “document ready” event handler:

<script type="text/javascript">
    $(function () {
        ko.applyBindings(new SearchViewModel());
    });
</script>

And we’re done! Verified on my Windows 7 machine in Chrome 12, Internet Explorer 9 and Firefox 5. Here’s the complete solution (where I’ve added some CSS to make it look less horrible): TwitterLiveSearchLab.zip

Anders Fjeldstad
Consultant at Jayway

Tags: , , ,

8 comments ↓

#1 Nikunj Dhawan on 07.14.11 at 10:23

This is nicely written and explained! Thank you!

#2 Fata Haniche on 07.14.11 at 11:26

Great tutorial thank you very much for sharing.

#3 Zen on 07.14.11 at 14:15

Anders, thank you very much for sharing your article.

One thing that I do not understand is the purpose of bind(this) that you call on success callback? I must say I have seen it elsewhere (probably at knockoutjs.com), but I do not understand what does it do…

Cheers!

Zen

#4 Stephen on 07.14.11 at 21:37

Great tutorial mate! thanks a lot for sharing :)

#5 Henrik Feldt on 07.14.11 at 23:05

@Zen: the bind(this) binds the keyword ‘this’ in the success function to the current context’s this, i.e. the view model, which has the property ‘tweets’.

#6 Zen on 07.15.11 at 4:35

Thanks Henrik!

#7 Rashmi on 07.16.11 at 5:17

This is a great article. Will give it a try.

#8 Giuseppe Abbate on 07.16.11 at 13:19

Why at the end of success handler is called bind(this) ( I dont’t understand the method bid(this))
15 success: function (data) {
16 // TODO
17 }.bind(this)

Leave a Comment