Patch Written for the UpdatePanelAnimationExtender
UPDATE: This patch was finally accepted!
While using the UpdatePanelAnimationExtender control from the Ajax Control Toolkit I decided that I didn’t like the behaviour of the control. My issue was that I had a update panel that I wanted to ‘collapse’ when an async postback started, and expand again once the postback had completed. If you view the controls’s sample page you can see this effect in operation. However, if the postback finishes before the ‘collapse’ animation has finished, the animation is aborted and the update panel will ‘jump’ to a height of zero before expanding again. I wanted the collapse animation to finish regardless of how quickly the server returned to ensure that the animation always appeared smoothly.
The way this is achieved on the sample page is by having a call to Thread.Sleep in the PageLoad method. I didn’t really want to waste resources on the server just to ensure a client-side animation appeared smoothly, so I set about writing a patch for the control.
Looking at the JavaScript behaviour for the control it was obvious why the control behaved the way it did. This is the JavaScript code fired when the async postback has completed:
_pageLoaded : function(sender, args) {
/// <summary>
/// Method that will be called when a partial update (via an UpdatePanel) finishes
/// </summary>
/// <param name="sender" type="Object">
/// Sender
/// </param>
/// <param name="args" type="Sys.WebForms.PageLoadedEventArgs">
/// Event arguments
/// </param>
if (this._postBackPending) {
this._postBackPending = false;
var element = this.get_element();
var panels = args.get_panelsUpdated();
for (var i = 0; i < panels.length; i++) {
if (panels[i].parentNode == element) {
this._onUpdating.quit();
this._onUpdated.play();
break;
}
}
}
}
As you can see, once this method is called the _onUpdating
animation is
cancelled immediately by the call to the quit()
method. What I needed
was a way to check that the animation has finished before playing the
_onUpdated
animation, and if not, wait until it has finished. The
first part was easily accomplished with a simple if:
if (this._onUpdating.get_animation().get_isPlaying()) {…}
The second part – waiting till it had finished – proved a bit harder
however. My initial thought was to use window.setTimeout to check later
if the animation had finished. However, the function supplied to
setTimeout runs in the context of the ‘window’ object, so I didn’t have
a reference to the this._onUpdated
or this._onUpdating
private
variables. A quick Google lead me to this page by K. Scott
Allen
which describes the use of the call()
and apply()
methods in
JavaScript. These methods are actually on the *function* object itself
and allow us to alter what ‘this’ refers to in a method call. Very
powerful – and definitely dangerous too – but exactly what I needed. I
added a new private method to the JavaScript class called
_tryAndStopOnUpdating
as follows:
_tryAndStopOnUpdating: function() {
if (this._onUpdating.get_animation().get_isPlaying()) {
var context = this;
window.setTimeout(function() { context._tryAndStopOnUpdating.apply(context); }, 200);
}
else {
this._onUpdating.quit();
this._onUpdated.play();
}
}
Firstly, this method checks if the first animation is still playing, and
if so uses window.setTimeout
to wait 200ms before calling itself to
check again. The use of ‘apply’ here ensures that when the method is
called again the ‘this’ keyword refers to our JavaScript class as
expected. Note that if I hadn’t saved ‘this’ to a local variable and
just referred to ‘this’ in the function passed to window.setTimeout,
then the call would fail as ‘this’ would then refer to the JavaScript
window object itself.
All that remained was to add a new property to the server control to
allow this alternative behaviour to be switched on or off and to modify
the body of the _pageLoaded
method to call my new method like so:
if (this._postBackPending) {
this._postBackPending = false;
var element = this.get_element();
var panels = args.get_panelsUpdated();
for (var i = 0; i < panels.length; i++) {
if (panels[i].parentNode == element) {
if (this._AlwaysFinishOnUpdatingAnimation) {
this._tryAndStopOnUpdating();
}
else {
this._onUpdating.quit();
this._onUpdated.play();
}
break;
}
}
}
You can see an example of this modified UpdatePanelAnimationExtender here. The bottom checkbox controls whether the first animation will always complete before the second one starts. Hopefully you’ll be able to see how much smoother the animation is with the bottom checkbox checked!
Unfortunately this patch hasn’t made it into the control toolkit yet, so if you would like to see it in there please vote for my patch here. Thanks!