Introduction
Imagine the scenario where you have multiple users viewing a page in your web application. You want to have each client page updated automatically when there are changes to the data the page is displaying.
Because of the request/response nature of web applications, we can not simply get our application to communicate directly to clients that are viewing our page and tell them to update their content as with a traditional client/server application. This article explores a way to synchronise your user's views using the XMLHTTP handler.
Why not use META HTTP-EQUIV="Refresh"?
If we use the META
tag in the header of our ASP.NET page, we could get a page to reload every few seconds, showing updated content since the last reload. So, what is wrong with doing this?
In a situation where we have a 100 users viewing a page with our META
tag set to reload the page every 5 seconds, each page would generate a request for an ASP.NET page, which in turn might be querying a database. That is a lot of page requests, doing a lot of work, which is mainly unnecessary seeing that the data is unlikely to be changing every 5 seconds.
We need a way for a client to ask our application if it is necessary to update its content ... We can do this using the XMLHTTP
object.
Components of the demo web app
There are four main components of the demonstration web application which are used to synchronise the ASP.NET page in our demo web app.
- DemoWebAppCore.cs
A controller class representing the bowels of the application (our business layer, if you like ...).
- Default.aspx
Our ASP.NET page displaying data we want synchronized across multiple client sessions. This contains a GridView
control displaying products with functions to add, edit, and remove products.
- CheckSync.js
JavaScript code using the XMLHTTP
object to perform 'behind the scenes' requests to CheckSync.ashx. This is included in the Default.aspx page.
- CheckSync.ashx
An HTTP handler with a simple job of responding to an XMLHTTP
request, with a flag determining if a page should update its content.
Synchronization using an incrementing number
To achieve our page synchronization, we are going to keep two references. One reference is going to be kept with the client (in the session state), and the other is going to be kept with the server (in the application state).
When any action takes place such as adding, editing, or deleting, the server will increment its reference. The client will regularly check its reference against the server reference. If the server reference is higher, the client will synchronize its content and copy the server reference to its own. This process repeats until the client is no longer viewing the data.
I have built a set of classes to accommodate simple synchronization in the demo.
ViewSynchronisation
Responsible for creating a server reference and ensuring that only one copy exists in the application.
ServerSynchronisationReference
Holds a server synchronization reference.
ClientSynchronisationReference
Holds a client synchronization reference.
The client synchronization process
The process of the client checking its reference against the server is detailed below.
On application startup
DemoWebAppCore
is instantiated in the Application_Start()
method of Global.asax.
protected void Application_Start(object sender, EventArgs e)
{
...
Application["Engine"] = new DemoWebAppCore();
...
}
DemoWebAppCore.ProductViewSync
is instantiated.
public class DemoWebAppCore
{
...
private ViewSynchronisation _productViewSync =
new ViewSynchronisation("Products");
public ViewSynchronisation ProductViewSync
{ get { return _productViewSync; } }
...
}
On every client connection
Session["ClientSyncRef"]
is instantiated with a ClientSynchronisationReference
using DemoWebAppCore.ProductViewSync.CreateClientReference()
in the Session_Start()
method of Global.asax.
protected void Session_Start(object sender, EventArgs e)
{
Session["ClientSyncRef"] =
Engine.ProductViewSync.CreateClientReference();
}
- Default.aspx loads and displays its content.
- The JavaScript in CheckSync.js runs and generates an
XMLHTTP
request for SyncCheck.ashx.
var pollInterval = 5000;
var checkStatusUrl = "CheckSync.ashx";
var pollID = window.setInterval(checkStatus, pollInterval);
function checkStatus()
{
req = createReq();
if(req != null)
{
req.onreadystatechange = process;
req.open("GET", checkStatusUrl, true);
req.send(null);
}
else
window.removeInterval(pollID);
}
function createReq()
{
try
{
req = new ActiveXObject("Msxml2.XMLHTTP");
}
catch(e)
{
try
{
req = new ActiveXObject("Microsoft.XMLHTTP");
}
catch(oc)
{
req = null;
}
}
if (!req && typeof XMLHttpRequest != "undefined")
{
req = new XMLHttpRequest();
}
return req;
}
- CheckSync.ashx checks if the
ClientSynchronisationReference
in Session["ClientSyncRef"]
is invalid using ClientSynchronisationReference.IsInvalid
. If it is, CheckSync.ashx returns the character "1" in its response. Otherwise, it will return "0".
public void ProcessRequest(HttpContext context)
{
context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
context.Response.ContentType = "text/plain";
if (ClientSyncRef(context) != null)
{
if (ClientSyncRef(context).IsInvalid)
{
context.Response.Write("1");
return;
}
}
context.Response.Write("0");
}
- CheckSync.js checks the response from SyncCheck.ashx and performs a reload on the page if the character "1" was returned; otherwise, it does nothing.
function process()
{
if (req.readyState == 4 && req.status == 200 &&
req.responseText == '1')
window.location.replace(window.location.href);
}
- The process repeats from step 3 until the page Default.aspx is no longer being viewed.
Invalidating client views on the server
As mentioned previously, whenever data changes, we need to invalidate our client views. To do this, we simply call the ViewSynchronisation.InvalidateClients()
method.
public void AddProduct()
{
...
ProductViewSync.InvalidateClients();
}
public void UpdateProduct(long Id, string Name,
string Description, decimal Price)
{
...
ProductViewSync.InvalidateClients();
}
public void RemoveProduct(long Id)
{
...
ProductViewSync.InvalidateClients();
}
By calling InvalidateClients()
, we are simply incrementing the reference stored in ServerSynchronisationReference._syncRef
.
public void InvalidateClients()
{
Interlocked.Increment(ref _syncRef);
}
So, when a client checks ClientSynchronisationReference.IsInvalid
, a simple comparison is made of its own client reference and the server reference.
public bool IsInvalid
{
get
{
long _serverSyncRef = _serverRef.Value;
if (_serverSyncRef > _clientSyncRef)
{
_clientSyncRef = _serverSyncRef;
return true;
}
return false;
}
}
Points to note and food for thought ...
- Yes, our client pages will still be sending a request every few seconds to the server - but, by using a combination
XMLHTTP
and an ASHX handler, we cut out most of the overheads involved with a full postback to an ASPX page. Also, we cut out the annoyance of full page refresh at the browser side. - Try to use on pages which display information only. It would be annoying if a user was in the middle of entering data into a form on the same page and it reloaded ...
- How often the client pages check for view invalidation depends on the application. In the demo app, we are checking every 5 seconds. If once every minute is enough, change the
var pollInterval
line in the CheckSync.js file. - In the demo app, when CheckSync.js receives "1" back from CheckSync.ashx, it just actions a full page reload to refresh the page. At this point, instead of the page refresh, we theoretically could use another
XMLHTTP
request to pull down modified data and update the page dynamically for a totally transparent update. - So far, our incrementing number synchronization is only used for the purpose of signaling an invalid client view. Why not attach in some way details of changes made with each server reference update so that the client page can specifically update only what has changed?
Remember also, CheckSync.ashx doesn't only have to return "1" and "0" ...
Server reference = 1
Product 234 added
Server reference = 2
Product 634 updated
Server reference = 3
Product 231 removed
History
- Version 1.0 (17 February 2008) - Initial article.
Started tinkering on an old BBC Microcomputer using BBC BASIC and progressed up to dabbling with C and ARMCode on an Acorn RiscPC. Moving to the PC platform I progressed from C++ to now a lot of C# and ASP.NET.