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.

AJAX, ASP.NET, C#, Visual Studio

UpdatePanel Addiction

Update panel addiction is a nasty thing. Projects start, it’s decided they are to be “Ajaxified” – the answer: chuck in 50 million update panels to do the trick. The more the better is the general principal, as this means there are smaller sections of the page being updated. UpdatePanels are designed to be used to update small portions of the page – the less content encompassed by an UpdatePanel, the better.

With a sufficiently advanced application the need may arise to refresh update panels from other update panels by calling the Update() method.

One problem that can occur in such advanced applications (especially those which use some kind of framework for events and control instantiation and such) is that slowdowns can be caused when update panel refreshing is too liberal. Perhaps only a small section of the page should be being updated, but for some reason it’s parent UpdatePanel is also refreshing, or there are simply too many updates per post back. Slowdowns can also be caused when large amounts of content, like in an un-paged grid or table of data is sent back to the client for rendering… the delays are not only in the transmission of the data to client but also while the browser is rendering the new data (especially when tearing down the existing DOM elements, more on that in another post).

What ever the case, sometimes it’s nice to see what is going on under to hood during an async postback.

Ajax Response

The ASP.NET Ajax system sends back async updates as special tokenised text. E.g.

247|updatePanel|UpdatePanel1|
askdljfjlkasdf aklsdflkasfjlkadjkldas
<br /><br /><span id="Label1">29/04/2008 6:40:36 AM</span><br />
<input type="submit" name="Button1" value="Button" id="Button1" />
|128|hiddenField|__VIEWSTATE|/asdfasdf|48|hiddenField|__EVENTVALIDATION|/wEWAgLWzvmRBwKM54rGBpmraqo+tCbnoafT7bqYDaCZ2bH5|
0|asyncPostBackControlIDs|||0|postBackControlIDs|||13|updatePanelIDs||tUpdatePanel1|
0|childUpdatePanelIDs|||12|panelsToRefreshIDs||UpdatePanel1|2|asyncPostBackTimeout||90|12|formAction||Default.aspx|13|pageTitle||Untitled Page|

To view the tokenised output of your page, try something like the following code in your page:


protected override void Render(HtmlTextWriter writer)
{
   ScriptManager sm = ScriptManager.GetCurrent(this);
   if (sm == null || !sm.IsInAsyncPostBack)
   {
      //this is a normal postback
      base.Render(writer);
   }
   else
   {
      //this is an async post back (partial render) so lets see what is going to be output
      HtmlTextWriter textWrite = new HtmlTextWriter(new System.IO.StringWriter());
      base.Render(textWrite);
      //have a look at content variable after the next line has run
      string content = textWrite.InnerWriter.ToString();
      writer.Write(content);
   }
}

The basic breakdown of this output is: [SectionLength]|[TokenCommand]|[TargetID]|[Content]

As you can see there are various commands. Some of the more interesting ones are:

  • updatePanel – refreshes the target UpdatePanel with the new content. UpdatePanels are simply either DIVs or SPANs (depending on the the RenderMode attribute is set to Block or Inline) in the final rendered content – so this content will just overwrite the existing content.
  • hiddenField – update any field’s value with the new value here
  • expando – update any element’s attribute with a new value
  • pageTitle – updates the page title in the browser window
  • focus – after the render has completed this control will be given focus.

You can find lots more information and some cool tricks on Siderite Zackwehdex’s blog here: http://siderite.blogspot.com/2007/05/messing-with-updatepanel-to-speed-up.html.

Browser Add-ins

There are a number of ways to view the innards of a server round trip. For starters, every developer should have these tools: Firebug for Firefox and Web Development Helper for IE – you do test in at least these two browsers right?

Web Development Helper (Internet Explorer only) (by Nikhil Kothari of ASP.NET team fame) allows very close inspection of the AJAX response. Grab the installer from here: http://www.codeplex.com/webdevhelper.

Start the add-in by selecting View -> Explorer Bar -> Web Development Helper.

Fire up your latest AJAX based work of art, and select the “Enable Logging” checkbox. Perform an action that will kick off an UpdatePanel refresh, then double click the response in the logging window. Select the Response Content tab from the bottom section of the popup and voila – you have your AJAX information. Fold out updatePanel and select one of the items – you will see the content that was sent back to the client.

WebDevHelperSShot

Trace debugging UpdatePanel movements

In Visual Studio, it’s sometimes nice to write out some custom debugging information into the output window to get a broader view of what is happening with your program. Previously this has not been straight forward to achieve with client script (often JS debug sessions ended in “alert” style debugging) – but with the MS AJAX Client Library it’s a breeze.

Using the Web Development Helper add-in is a bit like bringing up a quick watch every time you want to view the information… but what if you want to survey an application over a period of time or you have a lot of action going on and want to see what’s happening without having to stop each time to bring up the debug results.

Luckily you can hook into the PageScriptManager object to receive notifications when the AJAX framework is doing things – then show some information about what is going on in the Visual Studio output window (and in the Script Console window in Web Development Helper).

Here is something I have put together for you to try.

First create a new JS file in your project and place the following code in it:


var updatePanelHook =
{
    initialised : false,
    init : function()
    {
        if (!this.initialised)
        {
            //hook up to the various page loading events to provide extended update panel functionality
            var prm = Sys.WebForms.PageRequestManager.getInstance();
            prm.add_pageLoading(pageLoadingHandler);
            this.initialised = true;
        }
        function pageLoadingHandler(sender, args)
        {
            var arr = args.get_panelsUpdating();
            for(var i =0; i< arr.length;i++)
            {
                var updatedPanel = arr[i];
                Sys.Debug.trace(String.format("Updated: {0}", updatedPanel.id));
            }
        }
    }
}

if (typeof(Sys) != 'undefined' )
{
    Sys.Application.notifyScriptLoaded();
}

Ensure the script is included in your page (or master page etc). It's best to use the ScriptManager object to do this:

Place the following code in your page to kick off the init function:


      updatePanelHook.init();

NOTE: The init() function checks that it has not already performed initiation, because the updatePanelHook variable and associated event subscriptions will persist though partial updates.

In Web Development Helper, switch to the Script Console view by clicking on HTTPLogging and selecting the option from the drop down.

Perform an action that causes an UpdatePanel post back and you will see the results of you postback in the script window. If you have run your project by debugging from Visual Studio, you will also see the results in the output window there. Slot that into a larger ASP.NET AJAX based app and it should assist you figuring out what your app is doing under the hood!

An interesting point here is that the objects in the “arr” variable in this code are DOM elements (DIVs and SPAN’s)… so you can do all sorts of things with them if you like.

This process helped me figure out why a large commercial web app I was working on was slowing down, and with some ingenuity I managed to get the number and size of the UpdatePanel requests right down, gaining more than a 2x speed improvement across the entire app.

One other small point: when performing HTTPLogging in Web Development Helper, keep an eye on the Response Size column… obviously the smaller you can get your responses the better.