AJAX, ASP.NET, C#, Visual Studio

UpdatePanel Trigger Chainer

I present a little JavaScript class I wrote which allows you to queue up a list of UpdatePanel trigger events, which will each be executed in order, one after the other when the previous update finishes. For example, you may want to refresh UpdatePanel1 then UpdatePanel2 and so on.

With a bit of ingenuity you could string together updates in such a way to improve user load times and UX.

Some ideas may be:

  • Load sections of the page as the scroll into view.
  • Load panels in tabs one at a time. This would be good because the first tab would load very quickly, the rest would load in the background whilst user has attention on the first tab.
  • Pre-load other data in the background – e.g. Load a list of images, then background-load their detail into hidden DIV elements. Show the hidden div when user clicks.
  • A random looking recursively loading control that shows date times with random colours. Err… see an example of the UpdatePanelEx.Chainer class here: http://dev.webjak.net/updatepanelchainerexample/.

Download the class and example site here: UpdatePanelChainer.zip

I’m sure there are heaps of cool things you could do with this… it’s basically a great way of chaining up asynchronous post backs that run after the main page has loaded.

It works by queuing up the names of items in the page which can trigger an asynchronous postback, like buttons (hidden or visible) and UpdatePanels themselves.

Triggers may be added at any time, even if an UpdatePanelEx.Chainer object is already processing other items in the queue. Triggers may be added from both client side and from server side by filling a property on the UpdatePanelEx.Chainer default object (with a JSON array – example provided).

The UpdatePanelEx.Chainer creates a default known instance on startup so server side code as something to send trigger additions to, but you may create as many instances of the class yourself as you like. UpdatePanelEx.Chainer will continue to operate when multiple instances are attempting to update at once – they will each enter a one second timeout before retrying to initiate an async postback.

Usage is quite simple:

var chainer = new UpdatePanelEx.Chainer(["ItemOne", "ItemTwo"], true);

The first parameter is an array of items to update. These must be the full client ID, separated by the $ sign – e.g. MyControl$Button1.
The second parameter sets if the UpdatePanelEx.Chainer should automatically start posting back when items are added.

Items may also be added by calling the addTrigger method:

//Note: MyControl$Button1 is not required when the items are in the base page (only when the triggers are in a user control or other control container)
chainer.addTrigger("MyControl$Button1");

Full Property List

  • beginUpdating() – start the post backs. Use this when beginUpdateOnAddTrigger (set by property below, or in constructor) is set to False to being the post backs.
  • set_beginUpdateOnAddTrigger(boolean) – When set to true, post backs will automatically commence when the next item is added to the triggers array.
  • addTrigger(triggerName) – Add a trigger to the queue.
  • set_jsonUpdateTriggerList(jsonArrayString) – Adds a JSON array of objects to the trigger queue. More on this below.

NOTE: There are some properties and functions in the class beginning with _. These are not intended to be called directly – doing so may cause unpredictable results.

Updating the trigger list from the server

Updating the trigger list from the server is quite simple. Either you can use the default known “static” object called ChainerStatic, or you can use your own instance of the UpdatePanelEx.Chainer class – it doesn’t matter as long as you know the name.

System.Web.Script.Serialization.JavaScriptSerializer ser = new System.Web.Script.Serialization.JavaScriptSerializer();
string[] updates = { string.Format("{0}$btnHidden", this.ClientID.Replace('_', '$')) };
string json = ser.Serialize(updates);
ScriptManager.RegisterStartupScript(this, this.GetType(), "AddJsonTriggers_" + this.ClientID, string.Format("ChainerStatic.set_jsonUpdateTriggerList('{0}');", json), true);

This sample code is from the recursive random colour example app linked above. The full source code is available a the bottom of this article.

The first line creates a JavaScriptSerializer object which can be used to serialise .NET objects into a JSON notation string.
Next we create an array of names that are the triggers for the updates. In this example we are only adding one object to the array, but you could add as many as you like. Note here how the client ID is appended to the front of the name, and the _ are converted to $.
The next line serialises the object to the JSON string. This string will be set into the set_jsonUpdateTriggerList property on the line after using ScriptManager.RegisterStartupScript.

About the Sample

Link: http://dev.webjak.net/updatepanelchainerexample/

The sample app includes a WebUserControl called DataPiece which queues its own asynchronous update on initial load. Variables are set into the view state to ensure it only does this once. When the queue reaches the control, the post back is fired on the button. This creates two more instances of the control and adds them to the two panels (thus repeating the process). There is a counter to ensure this only happens four times.

The Default.aspx page then adds two of these Data Piece controls on to itself, kicking off the recursive control add processes.

Chainer class listing and explanation


Type.registerNamespace('UpdatePanelEx');

UpdatePanelEx.Chainer = function(updateTriggers, beginUpdateOnAddTrigger)
{
    //construct some initial values
    this._updateTriggers = updateTriggers;
    this._initialised = false;
    this._beginUpdateOnAddTrigger = beginUpdateOnAddTrigger;
    this._doing = false;
    this._jsonUpdateTriggerList = "";
}

First a new class is created and some constructor values are set. This class accepts two parameters: an array of items to add to the trigger collection and a boolean which sets the beginUpdateOnTrigger property.


UpdatePanelEx.Chainer.prototype = {
    _init : function()
    {
        if(!this._initialised)
        {
            //hook up the events, ensure this only happens once
            this._initialised = true;
            var prm = Sys.WebForms.PageRequestManager.getInstance();
            var callBackDelegate = Function.createDelegate(this, this.endRequestHandler);
            prm.add_endRequest(callBackDelegate);
        }
    },

_init is automcatically called the first time doUpdate is called. This function hooks up the event which instructs the chainer when the previous update has completed. Take note of the call to Function.createDelegate(). This method ensures that the scope of the callback is set to “this”… meaning the callback can access all the local members as you would expect. Without this the callback would have another scope (probably the event handler or something) and you would not be able to call any methods on the chainer class.


    endRequestHandler : function(sender, args)
    {
        //continue the update chain
        this.doUpdate();
    },    

This function is called when an asynchronous postback has completed. It calls the doUpdate() method to continue on with the chain. Note that without the Function.createDelegate call above, this.doUpdate() would not be available.


    doUpdate : function()
    {
        if(!this._initialised)
        {
            this._init();
        }
        this._doing = true;
        //while there are triggers left, continue the update process
        if(this._updateTriggers.length > 0)
        {
            var prm = Sys.WebForms.PageRequestManager.getInstance();
            if(prm.get_isInAsyncPostBack())
            {
                var pnl = this._updateTriggers[0];
                Sys.Debug.trace("Update wait: " + pnl);
                this._doUpdateWait();
                return;
            }
            var panel = this._updateTriggers[0];
            Array.removeAt(this._updateTriggers, 0);
            prm._doPostBack(panel, '');
        }
        else
        {
            //if there are no triggers left, then end the process
            this._doing = false;
        }
    },

This function forms the central part to the chainer. First it checks if the init function has been run. The next part only runs if there are items in the _updateTriggers array. Next the PageRequestmanager object instance is populated in to the prm variable and a check is performed to see if there is already an asynchronous post back in operation. If there is then the _doUpdateWait() method is called. This provides some kind of concurrent chainer object protection.
If all is clear then the system will grab the next object from the _updateTriggers array, then remove it from the array and call the PageRequestManager’s _doPostBack method. The trigger array acts as a FIFO stack… first in first out (this is achieved by getting the first object out).


    _doUpdateWait : function()
    {
        //this performs some basic sychronisation between two or more Chainers running at one time
        var doUpdateDelegate = Function.createDelegate(this, this.doUpdate);
        window.setTimeout(doUpdateDelegate, '1000');
    },

This function is called when there was already an asynchronous post back running. It sets a one second timeout before retrying. Note another call to Function.createDelegate to ensure the callback has the correct scope.


    beginUpdating : function()
    {
        //_doing ensures we dont accidentally kick off two update chains.
        if(!this._doing)
        {
            this.doUpdate();
        }
    },

This function kicks off the chain of asynchronous post backs. This method should be used over the doUpdate() method as it checks first if this class is already running the update chain.


    addTrigger : function(triggerName)
    {
        //add more triggers to the update array
        Array.add(this._updateTriggers, triggerName);
        if(this._beginUpdateOnAddTrigger)
        {
            this.beginUpdating();
        }
    },

This function allows the addition of a trigger at any stage. If beginUpdateOnAddTrigger is set to true then the update chain is started.


    set_beginUpdateOnAddTrigger : function(autoBeginMode)
    {
        this._beginUpdateOnAddTrigger = autoBeginMode;
    },

Turns the auto start when add trigger on or off.


    set_jsonUpdateTriggerList : function(jsonString)
    {
        //this method handles adding triggers that have been sent from server
        var serializer = Sys.Serialization.JavaScriptSerializer;
        //convert the serialised json data into a proper array
        var arrAdd = serializer.deserialize(jsonString);
        //create a little delegate to pass into the Array.forEach below. There are easier ways, but this is good practice 🙂
        var arrayAdd = Function.createDelegate(this, function(item) {
            Array.add(this._updateTriggers, item);
            Sys.Debug.trace ("Added: " + item);
            });
        Array.forEach(arrAdd, arrayAdd);
        if(this._beginUpdateOnAddTrigger)
        {
            this.beginUpdating();
        }
    }

This method is used to add items to the trigger array from the server side. First a serializer object is created which is used to take the input JSON string and convert it to a JavaScript object. Next the array that was in the JSON string is copied into the triggers array. Note how I decided to use a function delegate and the Array.forEach method… there are simpler ways, but I wanted to try this out as I had not used it before :). Once again, if beginUpdateOnAddTrigger is set then this method will start the update chain process.

}

UpdatePanelEx.Chainer.registerClass('UpdatePanelEx.Chainer');

var ChainerStatic = new UpdatePanelEx.Chainer([], true);

The last part here registers the class with the ASP.NET AJAX type system and creates a known “static” type object mainly for use from the server (a known object is required from server to update using the set_jsonUpdateTriggerList function.

Known Issues

  • I noticed that the progress templates for the panels aren’t working – I will investigate and update the post.
  • Refreshing the sample app will not cause the updates to start again due to a bug where the viewstate isnt cleared out 🙂

So there you have it.

Questions and comments are welcome as always.
This work is licenced under a Creative Commons Licence.