Bug Bash: Enabling/Disabling the ASP.NET AJAX Timer using the Control's Client Side API
I know most dev's prefer writing new code over fixing/maintaining the existing stuff. And most days I feel the same way. But every once in a while it is kind of fun signing up for that one lingering defect that no one else can seem to fix. Maybe you know the one - its not reproducible, only occurs in production and brings the system to its knee's? Sound familiar?
Well I thought I might try something new and blog about some of the juicy bugs we have found hanging out in our app. This might turn into a blog series or something, or maybe not. Who knows.
The Bug
The app I am currently working on has a page that uses ASP.NET AJAX's new Timer control to automatically refresh the page every 30 seconds. The page is one of those status pages that lets you monitor the progress of some off-line processes. Under normal usage a user would make a request to run some off-line process and then be taken to this page where they can monitor the progress. The markup for the Timer looks something like this ...
<asp:Timer ID="timer" runat="server" Interval="30000" OnTick="Timer_Tick" />
When the server side Tick event fires we run a bit of code that gets the latest progress/status of the process and updates the UI. If the process has completed or failed, the Timer is turned off and the page quits automatically refreshing.
1: protected void Timer_Tick(object sender, EventArgs args)
2: {3: if (IsProcessComplete())
4: {5: // the work is done, turn off the timer
6: ((Timer)sender).Enabled = false;
7: } 8: }The content on the page is mostly read-only, but there is also a single INPUT element and a button that performs a postback. The problem is that sometimes the timer fires and causes the refresh to occur when the user is entering data into the INPUT box. And of course this can be a little irritating and confusing for the user.
This was the defect that rolled across my desk yesterday. So I took a look at the page, reproduced the problem and starting working out how I was going to fix it.
The Fix
I figured a sensible fix for this bug would be to disable the Timer when the INPUT's client side focus event fires and then re-enable it when blur fires. I described the solution to a few other dev's and everyone seemed to think it made sense. So I went off and started in on creating the fix.
The Implementation
I added a pageLoad event handler to my page and attached a bit of JavaScript to my INPUT element's focus and blur events that used the Timer component's client side API to enable and disable the timer.
1: function pageLoad(sender, args){
2: // fetch the timer components
3: var timer = $find('<%= this.timer.ClientID %>');
4: // fetch the INPUT element
5: var textbox = $get('<%= this.textbox.ClientID %>');
6: 7: $addHandler(textbox, 'focus', Function.createDelegate(this, function(){
8: // disable the Timer so we don't refresh the page
9: // while the user is entering the data
10: timer.set_enabled(false);
11: })); 12: 13: $addHandler(textbox, 'blur', Function.createDelegate(this, function(){
14: // re-enable the Timer
15: timer.set_enabled(true);
16: })); 17: }The Bug in the Fix
And holy crap I was surprised when this didn't fix the bug. I have worked with some of the other Timer controls before and unless I have misremembered, they all support this kind of thing - just in case you haven't noticed the .Net framework has no shortage of timer controls ;)
And I figured calling set_enabled(false) would do exactly what I want - turn off the timer. I spent ~10 minutes or so setting breakpoints and making sure my code wasn't faulty before I took a peek at the client side code for the Timer control. And guess what I found out - the Timer control doesn't check the enabled bit before it postback and raises the server side Tick event. No wonder my fix wasn't working!
The Fix to the Bug in the Fix
Is anyone else surprised by this? Well I was. So I nosed around the Timer's behavior a little bit more and to see if there was anything else here I could use. And it turns out there is. After a partial postback completes, the server side portion of the Timer control sends the client side 2 bits of data - a value for the timers interval and a boolean value that is true when the timer should be enabled and false otherwise. The client side timer component passes these 2 data items to it's _update function, which in turn either enables or disables the timer and updates the interval value. Here is the code for this function (I added the comments)
1: function Sys$UI$_Timer$_update(enabled,interval) {
2: // check to see if the timer is already disabled
3: var stopped = !this.get_enabled();
4: // check to see of the interval value has changed
5: var intervalChanged = (this.get_interval() !== interval);
6: // if we are not stopped already and either
7: // the interval value has changed or we are now
8: // disabling the timer, stop the timer
9: if ((!stopped) && ((!enabled)||(intervalChanged))){
10: this._stopTimer();
11: stopped = true;
12: } 13: // update the enabled bit and interval
14: this.set_enabled(enabled);
15: this.set_interval(interval);
16: // if the timer needs to be enabled and it is
17: // currently stopped, start the timer
18: if ((this.get_enabled()) && (stopped)){
19: this._startTimer();
20: } 21: }So I decided to update my focus and blur event handlers to call this method to enable/disable the Timer. And guess what - now my page functions exactly how I want it to. But of course there are few things that are untidy about this approach:
- The Timer's _update function is not meant to be invoked like this. Typically the '_' prefix indicates the member is non-public. I suppose this is a bit like using .Net's reflection to invoke a private method on some framework class - probably not a great idea.
- This implementation now allows me to interact with the Timer's client side behavior, but when the page postsback the new enabled and interval values that were set on the client are not sent back to the server. And when the page reloads either from a full or partial postback these values will go back to what the server thinks they should be - essentially overwriting any of the changes you may have made.
But I can live with both of these items. I am betting against Microsoft changing the _update function (if they do, maybe they will support this scenario) and I don't need the values I set on the client sent back to the server. All I want is to be able to control when the Timer fires, and this approach does it.
1: function pageLoad(sender, args){
2: // fetch the timer components
3: var timer = $find('<%= this.timer.ClientID %>');
4: // fetch the INPUT element
5: var textbox = $get('<%= this.textbox.ClientID %>');
6: 7: $addHandler(textbox, 'focus', Function.createDelegate(this, function(){
8: // disable the Timer so we don't refresh the page
9: // while the user is entering the data
10: timer._update(false, timer.get_interval());
11: })); 12: 13: $addHandler(textbox, 'blur', Function.createDelegate(this, function(){
14: // re-enable the Timer
15: timer._update(true, timer.get_interval());
16: })); 17: }Bug Fixed. Enjoy!
Comments
hi matt,
take a look to my TimerExtender: http://www.codeplex.com/MyAjaxControlToolkit
Could be a good solution for you ;)
cheers
Posted by: crino | May 8, 2008 01:34 AM
Cool Matt, I faced this issue last year. And I figured about _stopTimer & _startTimer private client side methods But it is great to know about the _update method. Good tip I guess this is much better than just using _stopTimer and _startTimer methods.
Thank you
Posted by: Muhammad Mosa | May 8, 2008 07:28 AM
Hi Matt,
I posted a similiar question over on this posting (http://mattberseth.com/blog/2008/05/bulk_inserting_data_with_the_l.html) about your use of Function.createDelegate(). I'm confused as to the purpose of creating the delegate, when you aren't using the delegate at all for disposal in your code. Right now you have:
$addHandler(textbox, 'focus', Function.createDelegate(this, function(){ timer.set_enabled(false); }));
When it seems like you could get away with:
$addHandler(textbox, 'focus', function(){ timer.set_enabled(false); });
Is there a reason you use the overhead of a delegate, when you aren't using it anywhere?
Posted by: Michael McGuire | May 8, 2008 09:43 AM
Hey Matt,
Great solution! I'm still trying to "reintroduce" myself to Javascript via all the "AJAXifications" you can do to tweak your own stuff.
What happens if you start to enter data into that input box, then decide to wait, figuring that the refresh will occur any second now and you may not need to submit your input afterall? You just sit there, not unfocusing the textbox. Does the timer stay permanently disabled at this point until you click somewhere else in the page?
What about having a TextChanged event simply reset the timer so that you have an additional 30 seconds. That way you can start typing, decide to wait, and still have things refresh after 30 seconds. But at any point in the game, you can start typing again and have your time back?
Just a thought. Thanks for such a great blog!!!
Posted by: Sean Patterson | May 8, 2008 10:37 AM
Wow. Talk about timeliness. You posted this last night and today, just this very minute I ran into the same issue. In my case I wanted to disable the timer while a modal popup dialog was activated.
I think I'm gonna start using Google as my *second* source of .NET isues/fixes, with quality content like this. :) Thanks for this info, as well as all the other great stuff on your blog!
Posted by: Kirk | May 8, 2008 02:58 PM
@crino -
Thanks for the link. I will check it out.
@Muhammad Mosa -
I wish I would have come across your page when I was debugging. I swear I thought I was going crazy when set_enabled(false); wasn't working ...
@Michael McGuire -
I often get pretty behind on comments so sorry about not responding to your previous one. I have got into the habbit of using Function.createDelegate (even when it isn't needed). You are right, for this example, and probably more than a few others this is just extra work. Thanks for pointing it out.
@Sean Patterson -
Good suggestion. Yep, with the changes I made to the page, the refresh will not occur as long as the textbox has focus. I think I like your approach better though. Its kind of a best of both worlds - reset the timer after every keystroke, and if 30 sec's pass without a keystroke go ahead and refresh the page. Thanks!
@Kirk -
Yea. It seems like kindof standard timer behavior right? I was so surprised by the behavior I figured other people would run into it too.
Posted by: Matt Berseth | May 8, 2008 05:49 PM
I have not tried the timer control but implement the same open ended continuous talk back to the server in a different way.
I added a call back to the web service as the last line of OnComplete, OnError and OnTimeout functions of the call.
Simple
Posted by: Deepak Chawla | May 12, 2008 04:15 AM
what about ?
timer._startTimer();
timer._stopTimer();
?
Posted by: Bassel | May 12, 2008 07:27 AM
Hi,
The Timer stopped calling its server side Tick method
my timer is inisde an updatepanel and the Tick method should update a Label with some calculated figures.
I Enabled the Timer from clinet-side API and i noticed that the timer starts working because the progressbar started to show on every 10 seconds (based on the timer interval) but Lable is not getting updated.
Posted by: Bassel | May 12, 2008 08:24 AM