📜 ⬆️ ⬇️

Developing a Twitter live search application using Knockout, jQuery and ASP.NET MVC 3

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

One way to add some structure and order on the client side is to use a framework like Knockout. Knockout is an open source free Javascript library that helps to implement the Model-View-View Model (MVVM) pattern on the client. It is well documented and the official web site is a great starting point with a bunch of real-life examples that not only demonstrate the use of built-in library features, but also show how to extend it with your own features.

In this post, we will look at how Knockout can be used in conjunction with a mapping plugin, some jQuery, and an ASP.NET MVC 3 backend (or model, as you like) to build a simple but effective Twitter search app.
')

purpose


Our goal is simple: the user should be able to enter some search criteria (such as a hashtag or Twitter username), then the application should submit the most relevant tweets. Also, a lot of results should be automatically updated in a certain background procedure on the client so that the GUI always shows the most recent tweets found. (This can be useful, for example, when tracking some event that occurs here and now.) Ultimately, we would like to get something like this:


Step 1: Model Definition


Twitter introduces a search API that is simple enough to use and that can return a response to search queries in XML (Atom) or JSON format. We could allow our client to communicate directly with the Twitter API, but this would deprive us of the opportunity to improve the application by adding caching, some kind of stub in case of a Twitter crash, etc .; so a good idea would be to create our own model instead, and for this we are going to use a very simple ASP.NET MVC 3 application.

First, create a standard empty web application (with Razor as the display engine) and add a controller - let's call it TwitterController . Then rename the “Index” action, which was automatically created with the new controller, to “Search”. The controller will look something like this:
public class TwitterController : Controller
{
[HttpGet]
public ActionResult Search()
{
return View();
}
}


* This source code was highlighted with Source Code Highlighter .

Next, we are going to create an action method that performs a current Twitter search. For this task, we will use the Atom version of the search API. The request URL matches the following search.twitter.com/search.atom?q= [URL-encoded search parameters] format. Because Atom is XML, we can use LINQ to XML to parse and transform a lot of results (Twitter returns a lot of data that we don’t need, so we reduce it to a minimum).

But we have no desire to send XML to the client - better than JSON. Fortunately, it is easy to convert most objects into JSON, and in this case the easiest way is to use the built-in Json method, which is returned by JsonResult. It all comes 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);
}


* This source code was highlighted with Source Code Highlighter .

(The code above includes a parsing procedure to highlight an avatar in the author’s profile and a link to each individual tweet.)

That's all - we built our model!

Step 2: Implementing the View Model


Before we start developing the GUI, we will need to add some links to our solution. Either via NuGet , or manually loading jQuery, Knockout, and jQuery.tmpl. Then add a “Search.cshtml” view in Views / Twitter (do not use the “master page” or layout - it’s easier to get an idea of ​​the essence in a small task, similar to ours, if we don’t smear the solution over many separate files). Add the mentioned javascript links. When you do this, you will have something like 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 >


* This source code was highlighted with Source Code Highlighter .

It is time to build our view model. The view model includes the application logic on the client side or, if you like, it determines what the application can do and what properties it has, without directly binding to any markup element or DOM elements.

For this task, the view model is extremely simple: it contains 2 properties used to store the current search query and found tweets, and, accordingly, a method for performing a search by model. It might 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>


* This source code was highlighted with Source Code Highlighter .

It is difficult to say that there is something significant here; query properties and tweets are initially empty, the search method uses jQuery to send an AJAX request to our model on the server and return the results as a JSON object (array of tweets).

How should we implement a “successful” handler in the search method? We can clear the tweet array and add new search results. But remember that we want to constantly receive “updates” for our search results, ideally we would just like to add new tweets (and delete old ones) without replacing the entire collection.

This is a great opportunity to try out the mapping plugin for knockout . After we have loaded it and added a link to the script, we can implement a successful AJAX request handler:
success: function (data) {
ko.mapping.updateFromJS( this .tweets, data);
}.bind( this )


* This source code was highlighted with Source Code Highlighter .

And then set the “ key definition ” for the tweet collection (which is used by the display to determine which elements should be added, updated or deleted), we just rewrite our constructor a bit:
function SearchViewModel() {
this .query = ko.observable( '' );
this .tweets = ko.mapping.fromJS(
[],
{
key: function (tweet) { return ko.utils.unwrapObservable(tweet.id) }
});
}


* This source code was highlighted with Source Code Highlighter .

Notice that we define the key for the collection of tweets as an ID value, which Twitter also conveniently provides. If you need more information about the Mapping plugin, visit the official Knockout website.

Next: implement auto-update features. Every time there is a search query, the corresponding tweets should be updated, say, every 3 seconds to reflect changes in the model. One way to accomplish this is to have a successful calback for a caller's AJAX search query recursive, but delayed, search method. The full view model in this case 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>


* This source code was highlighted with Source Code Highlighter .

(I added a couple of lines to disable auto-refresh when the search query input line is empty; plus a check that interrupts any running search when the user starts a new one. Also, the search results are zeroed if you search for an empty query.)

Step 3: View Design


In order to keep things simple and obvious, the view will consist only of a search query input line and a list of tweets. We want to achieve something in this spirit:
< 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 >


* This source code was highlighted with Source Code Highlighter .

Let's also add some declarative Knockout data bindings that will link the view and the view model. If we modify the search form 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 >


* This source code was highlighted with Source Code Highlighter .


... Knockout will update the query property of the view model automatically when the user changes the text in the search query field, and the search will be performed as soon as the user presses the Enter key.

What about search results ? Excellent script to use Knockout template binding ! First, create a template for each tweet:
< 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 >


* This source code was highlighted with Source Code Highlighter .

Then the data-bind property of the HTML list of tweets in the view model, setting your template as a way to display each of them. Relatively easy:
< ul id ="tweets"
data-bind =" template: {
name: 'tweetTemplate' ,
foreach: tweets
}, visible: tweets (). length > 0" >
</ ul >


* This source code was highlighted with Source Code Highlighter .

(Note that the list will not be visible if the view model does not contain tweets)

Bonus: Add an animation that is used when new tweets are added to the list using the afterAdd template binding option:
< ul id ="tweets"
data-bind =" template: {
name: 'tweetTemplate' ,
foreach: tweets ,
afterAdd: function ( element ) {
$( element ). hide (). fadeIn ( 'fast' );
}
}, visible: tweets (). length > 0" >
</ ul >


* This source code was highlighted with Source Code Highlighter .

At this point, it remains to finish only one thing - the bootstrap logic. When the DOM has loaded, we need to create an instance of our view model and ask Knockout to attach our bindings to it. One line of code in the jQuery handler for the “document ready” event:
<script type= "text/javascript" >
$( function () {
ko.applyBindings( new SearchViewModel());
});
</script>


* This source code was highlighted with Source Code Highlighter .

And done! Checked on my Windows 7 machine in Chrome 12, Internet Explorer 9 and Firefox 5. Here is a complete solution (where I added some CSS to make it look less awful): TwitterLiveSearchLab.zip

Source: https://habr.com/ru/post/123692/


All Articles