|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionOver the years, I've written several applications that allow users to start long running processes. For a 1 or 2 second operation, you can get away with an hourglass, but for anything longer, you need a progress display. In desktop GUI, the standard progress display is a screen or dialog with a progress meter and a big fat cancel button. Usually there is some room on the dialog to show some text, a graphic or some other representation of what the computer is working on. Two classic examples of applications that revolved around a progress window are Windows Setup programs and the Windows Disk Defragmenter. I looked around for a way to create a progress display like this for my asp.net application. I found progress meters and progress dialogs, but nothing that really tied it all together. So, here is an example of a page that does. Some highlights of this design:
User Interface DesignSome screenshots demonstrate the example UI:
This example UI is a plain, trimmed down version of screen specific to a project I am working on. This is the editor screen for an MLS object. As you can see, I've integrated the Import Listings function into the MLS editor screen. I could have a separate screen (or tab) for Import Listings, but for this application I like having everything for an MLS on one page. Example Code HighlightsI've found that good progress displays depend on the process involved. Some progress displays can get away with just a simple text string for progress. Some can give good estimates for time remaining. Some can't estimate at all. So, I made my example code more of a design pattern (and example) than a reusable component. If you would prefer to have a fixed function, reusable Progress Dialog, it should be relatively straightforward programming exercise. To discuss the code, let's start from the bottom and work up. The first
class/file of interest is In this application, each time a job completes, the worker thread adds the
time the job took to
for (int i = 0; i < importedListings; i++)
{
DateTime start = DateTime.Now;
// Remove job from queue ... run job
_importStatus.JobsCompleted++;
_importStatus.JobsRemaining--;
_importStatus.TimeCompleted += DateTime.Now - start;
}
The remaining time is then estimated by computing an average time per job times the number of jobs remaining. We divide all this by the number threads running because 2 threads should complete jobs twice as fast, right?
private TimeSpan _timeCompleted = TimeSpan.Zero;
public TimeSpan TimeCompleted
{
get { return _timeCompleted; }
set
{
// MT: Obviously, this can get a little off
_timeCompleted = value;
_timeRemaining = new TimeSpan(0, 0, (int)((_timeCompleted.TotalSeconds * _jobsRemaining)
/ _jobsCompleted) / _threadsRunning);
}
}
I'm not doing any locking here, but it should be OK. This is progress information, not somebody's bank account balance. I didn't think it made sense to show all that busy work for a real job queue,
so I had
public void CancelImportListings()
{
_importCanceled = true;
}
And 2)
for (int i = 0; i < importedListings; i++)
{
if (_importCanceled)
{
_importStatus.Status = JobQueueEventArgs.StatusType.Idle;
_importStatus.QueueMessage = "MLS Listings Import canceled by user.";
return;
}
// Remove job from queue ... run job
}
Default.aspx is the sample page where everything comes together. Much of the code is typical asp.net code for a business object. Here is where the action starts:
protected void ImportListings_Click(object sender, EventArgs e)
{
MLS mls = GetMLS();
PageToMLS(mls);
mls.Update();
Session[_importSessionKey] = mls;
mls = (MLS)Session[_importSessionKey];
Thread thread = new Thread(new ThreadStart(mls.ImportListings));
thread.Start();
UpdateControls();
UpdateProgress();
AddMetaRefresh();
}
This looks simple and it mostly is, but there are a couple of things to watch out for: As you can see, the The other interesting thing going on in
protected void AddMetaRefresh()
{
MLS mls = (MLS)Session[_importSessionKey];
int refreshSeconds = mls != null && mls.ImportStatus.TimeRemaining.TotalSeconds > 10 ? 5 : 1;
Literal metaRefresh = new Literal();
metaRefresh.Text = string.Format("<meta http-equiv=\"refresh\" content=\"{0}\">", refreshSeconds);
Header.Controls.Add(metaRefresh);
}
You might have seem some other ways of doing this. This MSDN article uses JavaScript (I think because the author got stuck thinking you had to pass in a URL). I find the "meta refresh" method the cleanest. I tweak the refresh seconds, depending on how much estimated time is remaining to slow the refresh request rate when possible. This algorithm can be refined, but the estimated time is usually fairly crude, there is no point in getting fancy and you don't want to wait too long. Creating a Literal control and adding it to Header.Conrols.Add is nice because this page doesn't always need a "meta refresh" tag. Also, this code will still work when Default.aspx is changed to use a asp.net Master Page. So, how does Cancel work? Here is the code:
protected void CancelImportListings_Click(object sender, EventArgs e)
{
MLS mls = (MLS)Session[_importSessionKey];
if (mls != null)
{
mls.CancelImportListings();
UpdateProgress();
if (mls.ImportStatus.Status == JobQueueEventArgs.StatusType.Idle)
Session[_importSessionKey] = null;
else
AddMetaRefresh();
}
UpdateControls();
}
It would be nice if the call to Running the DemoOpen the project as a Web Site in Visual Studio 2005 and hit Ctrl+F5 to start without debugging. At first, you won't see the Import Listings button because the edit screen is in Insert mode and for this application, you can only import listings after the MLS has been added to the database (darn real world making everything so complicated) . To simulate the edit screen in Update Mode (and see the Import Listings button), append the query string "?MLSID=1" onto the URL. The full URL should look something like this:
http://localhost:37519/InPageProgressWithCancel/Default.aspx?MLSID=1
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||