Sorry for the delay ... some things interceded, including (not just) a leaky shower stall and a range hood that came unstuck from its moorings, oops.
Part 1 of this adventure is here.
We continue. Having established that it wasn't practical to use two UpdatePanel controls, I experimented with using one UpdatePanel control for the long-running process (LRP) and plain client script, so to speak, to track progress. The idea is simple enough. The LRP does something in a loop; on every iteration, it stores its current status somewhere. The LRP is started from an UpdatePanel control. Then some client script (not in the panel) makes periodic calls to the server, reads the status info, and displays it in the page.
I figured there were two ways to call the server -- a Web service (.asmx) and a Page method. Page methods are normal methods in a page but that are a) static, b) public, and c) attributed with magic attributes that expose them (via proxies) to the client. I played with both of these. And ended up using a third option, sort of. :-)
I won't go into all the details of the various experiments or the emails that flew back and forth between Dave Reed and me. But I will tell you that the fundamental problem turned out to be where to store the status information. Let's review the choices:
Viewstate. Web services don't have viewstate. Although Page methods are on the page, they're static, so no viewstate.
Session state. Spent a lot of time here. Session is promising for a couple of reasons. One is that it's specific to the current user. Another is that it's Web-farm friendly, should that be an issue -- ie, the Session provider takes care of making sure it's available when and where it's needed.
However, Session gets locked by the current process when it's updated. The way that this manifested is that the LRP would dutifully update the counter. The client code would go read the current counter, no problem. However, because Session was locked, the client code wasn't actually getting the current value; it was getting the pre-locked value. When the LRP finished, Session was unlocked, and the client script got an updated value. Gee, thanks -- a progress report that works only when the process is done.
Dave suggested a workaround whereby the LRP doesn't directly update Session state. Instead, it calls a Web service and passes it the current status as a parameter. The Web service could update Session; it would then dispose itself, thereby unlocking Session. The client script could call a different method of the Web service and get the (now unlocked) Session value. Another complication: Web services require
special handling to have access to Session.
Application state. I know that Application state can be explicitly locked and unlocked (coz it's not thread-safe), so I thought about that. One problem would be that you'd have to have a unique identifier for the value (a per-user value) in case the LRP was set off by multiple users at the same time. Dave also noted that Application state, unlike Session state, wouldn't cross servers in a Web farm. Neither of these is actually an issue for my little utility, but one does want to use best practices and all, or one's best shot at those.
Cache. Cache is potentially a good solution -- it's fast and it cleans itself up nicely. However, you would have the unique-user problem. Plus it's not Web-farm friendly. In fact, I started by using Cache, and it worked quite well. Except for those limitations just noted.
Database. While discussing the various ways in which information would not be shared in a Web farm, Dave said some sort of persistent storage would work -- ie, a database. That seemed like a lot of overhead for a process that was already long by definition. And anyway, that's a lot of code. However, an easy way to store stuff in a database is ...
Profile properties. Hmm. Profile properties provide persistent/cross-farm storage that's moreover user-specific. There's still read/write overhead, but by the time we got this far in the list, I was willing to give it a shot. And ASP.NET AJAX has a Profile application service that is already exposed to client script. (Although it's easy enough to just access it in a Page method.)
So that's what I did. As the LRP plugs along, it writes its status to a Profile property. Entirely separate client script in the page uses the client Profile application service to read the profile property. To simplify things (sort of), the status information is just a string that the client script displays in the page. (You can of course store discrete status values in the Profile property and then parse them out or whatever.)
The client script that checks status is on a manually-constructed timer -- ie, I use a window.setTimeout call to have the status checker call itself every second. (Note that making Web service calls every second might not scale well -- then again, Google Suggest calls a Web service on every keystroke, so ...).
I fancied things up (and thereby further delayed this post) by adding the ability to cancel the counters and cancel the LRP. And manage the display thereof, etc. Canceling the display of the counter wasn't so hard -- kill the timeout and clear the display. (The status itself keeps going on the server, so you can re-enable it and be right on track.) Canceling the LRP was also easy -- all it requires is a button inside the UpdatePanel control that does an async postback -- IOW, any button. Any subsequent postback kills a postback in process, even if the second postback doesn't do anything.
There was some messiness in coordinating the output displayed by two separate async processes. The status display based on the timeout was often a second behind the actual status. And canceling the LRP had to somehow communicate this fact to the counter display. In the interests of expediency, I just had the LRP set the counter value to various magic values ("done", "canceled") to indicate something other than a current counter value. Not elegant, and this needs some thinking.
Anyway, the ASP.NET code is here; the .js code is here; a sample page is here. The faked-up LRP uses a Thread.Sleep call; my actual app, as noted, makes Web requests to a list of URLs.
As you see, I put all the client script into a .js file (source) and then registered the .js file using the ScriptManager control. Not strictly necessary, but it has some advantages ... I'll be able to reuse the code for my next adventure! You bet. I don't happen to have code that runs as soon as the page loads (mine is all gesture-driven), but this line makes sure that any ASP.NET AJAX code in your .js file doesn't outrun the library when getting loaded:if(typeof(Sys) !== "undefined") Sys.Application.notifyScriptLoaded();
Is that it? I think so. Jeez, long post. Two more coming up soon, one with an alternative approach, and one with some mercifully short thoughts on using AJAX for this particular task.