Saturday, October 31, 2009

AJAX publish/subscribe

Calling long running web server process and locking the user for a while in web applications with the typical for the HTTP request/replay way can be critical for the user expirience. Even more, possibly we don't get back the data due to problems with the network connection or browser timeouts.

Even if on top of unreliable protocol like HTTP we can provide some abstraction level to ensure we'll get the result from the web server. We could simulate the publish/subscribe pattern in ajax. Furthermore our communication with the web server is asynchronous and allows our application to respond better to the user interactions.

On the web server we could implement multithreading and open working threads which get the calculations done.

With javascript we send request to the web server and then in some interval of time we again and again ask if the server has prepared the result. When the web server has collected the information we need, we take the data from the web server, display on the page and stop the repetative calls to the web server.

I will implement publish/subscribe with ASP.NET MVC and jQuery.

The CityWeather model



First i'll create model class CityWeather. The only public properties of CityWeather object are "City" and "Temperature".

After CityWeather class created, the constructor calls Measure() in thread. The Measure() function simulates long running process used to collect the weather information for that city.

We store all the requests for measurements in static collection _measurements. The keys of this collection are the city names.

The only public function of the model class is the static function GetCityWeather() which when called will check if in _measurements exists object CityWeather for this city. If not will create one. The function returns back the corresponding CityWeather object from the _measurements collection.



namespace PublishSubscribe.Models
{
// Measurements per city.
// Call class method CityWeather.GetCityWeather(city)
// to get CityWeather object with measurements
// for that city.
// Repead calling CityWeather.GetCityWeather(city)
// until city.Temperature is not null.
public class CityWeather
{
// Holds all requested measurements.
// Used from class method GetCityWeather()
// The keys are city names.
// The values are CityWeather objects.
private static Dictionary<string, CityWeather> _measurements =
new Dictionary<string, CityWeather>();

// public properties of CityWeather object:
public string City { get; private set; }
public int? Temperature { get; private set; }


// The constructor itself starts measurement
// in an asynchronous thread.
private CityWeather(string city)
{
this.City = city;

// after the object is constructed will start
// doing measurement in a thread, which respectively
// takes some time
ThreadStart ts = new ThreadStart(Measure);
Thread th = new Thread(ts);
th.Priority = ThreadPriority.Lowest;
th.Start();
}

// Worker method started in separate thread from
// the constructor
private void Measure()
{
// measurement takes some time - up to a minute
Thread.Sleep(new Random().Next(1,6) * 10000);

// lock the object alowing concurent access
// to the object properties
lock (this)
// degrees Celsius between 10 and 20
Temperature = new Random().Next(10, 20);
}


// The only public method that capsulates all the logic
// of keeping single instances - one per city -
// just as static memory for the purpose of the
// demonstration.
//
// Keep calling CityWeather.GetCityWeather(city) until
// you get back an CityWeather object with
// property Temperature which is not null.
public static CityWeather GetCityWeather(string city)
{
lock (_measurements)
{
if (! _measurements.ContainsKey(city))
{
CityWeather cw = new CityWeather(city);
_measurements.Add(city, cw);
}

return _measurements[city]; // check
}
}
}
}



The usage of this class is pretty simple. We call the static function CityWeather.GetCityWeather() with the name of a city to get weather measurements for that city. We repeat the call again and again until we get back CityWeather object where Temperature property is set - is not null, but contains value.



The Weather controller



Then I'll create the controller class:



namespace PublishSubscribe.Controllers
{
public class WeatherController : Controller
{
public JsonResult Measure(string city)
{
return Json(Models.CityWeather.GetCityWeather(city));
}

public ActionResult Index()
{
return View();
}
}
}



The controller has two methods. Measure gets weather measurements for one given city. We send back JSON result.

The Index method we will use to create page where we will demonstrate how the AJAX publish/subscribe calls will work.

We are ready to try how the Measure JSON handler will respond and to see the how the CityWeather model will work. We run the application and navigate to /weather/measure where we request measurements for a city and repetative hit repoad on the browser until we receive back the Temperature calculated.

Here is how example session with repetative calls looks like:




URL: http://localhost:3284/weather/measure/?city=berlin
Response: {"City":"berlin","Temperature":null}
...
URL: http://localhost:3284/weather/measure/?city=berlin
Response: {"City":"berlin","Temperature":null}
...
URL: http://localhost:3284/weather/measure/?city=berlin
Response: {"City":"berlin","Temperature":12}




AJAX publish/subscribe with jQuery



Now I'm going to implement the web page to demonstrate the idea behind the AJAX publish/subscribe.

The html body tag is very simple.



<body>

<label for="city">City:</label>
<input type="text" id="txtCity" />
<input type="button" value="measure" id="btnMeasure" />

<table id="cites">
<thead>
<tr><td style="width:200px">City</td><td>Temperature</td></tr>
</thead>
<tbody>
</tbody>
</table>

</body>



We write city name into the input box and click the "measure" button. We repeat that several times with other city names. We expect whenever the server has measured the weather for a city we asked for, the result will be added to the table as new row. The order of the cities we ask for measurements is not necessarily the order we'll get back calculated measurements. The time the server will spend to take weather measurements may vary from city to city and is not presumable.

This way we will have asynchronious communication with the server and the user will appreciate the better respond from the application.


Last, I'll put some javascript after the closing body task and will automate the html page with jQuery.

As we don't have multithreading in javascript we simulate it with setInterval and clearInterval. We start repetative calls as we run sendMeasureRequest(city) every second and ask the web server if the measurements for that city are calculated. Once the server gives back calculated results we stop the repetative calls.

The setInterval() function gives us interval ID which we later use at calling clearInterval() to stop repetative tasks. Then we also need to map somewhere interval ID to city name, thereof we create array measurements which keys are city names. The values of the measurements dictionary are data structures wich hold interval ID, city name and the temperature as measured from the web server.

Once the web server respond calculated measurement we stop the corresponding repetative calls to the web server and call the function cityMeasured(). We add new row into the table with the measurements.



<script type="text/javascript">

/* Hash array of all collected measurememts */
var measurements = new Array();

/* Does weather measurement for a city */
function measureCity(city) {
if (measurements[city] == undefined) {
// send request to the server every one second
var intervalID = setInterval(sendMeasureRequest, 1000, city);
measurements[city] = {
IntervalID: intervalID,
City: city,
Temperature: null
};
sendMeasureRequest(city);
}
cityMeasured(city);
}

/* Send GET request to the server to collect weather measurements for a city */
function sendMeasureRequest(city) {
$.ajax({
'url': '<%= Url.Action("Measure") %>',
'type': 'GET',
'dataType': 'json',
'data': { 'city': city },
'success': function(data) {
if (data.Temperature != null) {
clearInterval(measurements[data.City].IntervalID);
measurements[data.City].Temperature = data.Temperature;
cityMeasured(data.City);
}
}
});
}

/* Does weather measurement for a city */
function cityMeasured(city) {
if (measurements[city].Temperature != null) {
if ($('#tr_' + city).length == 0) {
$("#cites > tbody").append('<tr id="tr_' + city + '"><td>'
+ measurements[city].City
+ '</td><td>'
+ measurements[city].Temperature + ' grad'
+ '</td></tr>');
}
}
$('#tr_' + city).fadeOut();
$('#tr_' + city).fadeIn();
}

/* SetUp click handler */
$(document).ready(function() {
$("#btnMeasure").click(function() {
var city = $("#txtCity").val();
measureCity(city);
});
});
</script>



After I ran the application and navigated to /weather/ I asked for weather measurements for the following cities in the exact order: Paris, Boston, Rom, Hamburg

Then I waited few seconds and the weather measurements were displayed to the page one after other:




Well, we may consider as next to be done, once the weather in city from the table has charnged, we refresh the information on the table.

Atanas Hristov

kick it on DotNetKicks.com
Shout it

1 comment:

  1. Maybe it would be interesting to see what you would do in a situation where the server cannot hold all the values that are possibly requested by all users.
    For example users can request screen shots of their sites in several browsers. The rendering takes some time, there are a theoretical endless number of screen shots that are too big to save in memory for eternity.

    ReplyDelete