Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

In Page Progress with Cancel Example

0.00/5 (No votes)
1 Mar 2006 2  
Example of an ASP.NET page with progress display and a Cancel button. A single page starts a long running process, shows the progress, and shows the process completion message.

Sample Image - import-running.jpg

Introduction

Over 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 revolve 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:

  • ASP.NET 2.0.
  • A single page starts the process, shows the progress, and shows the process completion message. I find this design to be a nice fit for my UI and programming style.
  • Cross-browser compatible (I have tried IE, Firefox, and Opera).
  • Easily skinned with CSS.
  • No JavaScript.

User Interface Design

Some screenshots demonstrate the example UI:

This example UI is a plain, trimmed down version of a 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 Highlights

I'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 a relatively straightforward programming exercise.

To discuss the code, let's start from the bottom and work up. The first class/file of interest is JobQueueEventArgs. In many of my applications, long processes are handled by a job queue. Jobs are placed in the job queue by some type of initialization routine. Then, a set of worker threads removes jobs from the queue, performs the job, and updates progress displays by updating a global JobQueueEventArgs object and firing a JobQueueEvent.

In this application, each time a job completes, the worker thread adds the time the job took to JobQueueEventArgs.TimeCompleted:

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 of threads running because two 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 MLS.ImportListings simulate a job queue with one worker thread. Also, since the ASP.NET application has to poll for status (it can't receive events from the job queue when the page is sitting in the user's browser), I don't bother creating and firing an actual event. If MLS were ever to be used in a GUI application, you would want to add this code. The ASP.NET application polls for status by reading the MLS.ImportStatus property. Also, in MLS are a few simple properties (MLSID, MLSListingsURL) and methods (Insert, Update) to simulate a typical business object. To support the cancel operation, two things happen:

  1. MLS.CancelImport sets a flag:
  2. public void CancelImportListings()
    {
        _importCanceled = true;
    }
  3. MLS.ImportListings checks this flag as frequently as possible:
  4. 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
    
    }

ProgressBar is the control that shows a progress meter. I started with ProgressBar from this CodeProject article. I mostly use flow layout, so I changed the control to create its table with width="100%". The row height is set from attributes or its contents (such as an image). For increased browser compatibility (Firefox), &nbsp; is always used in the cell contents.

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 MLS object is added to the Session. What's not so obvious is this will only work with the default sessionState mode of InProc. The other sessionState modes require that the object be serialized and deserialized. Although MLS can be serialized, you can't serialize a running thread (!), so any deserialized object wouldn't get the status updates. If you can't use InProc (say because you are running your application on a server farm), you'll need to move the ImportListings function into a separate process and work out some kind of IPC like SOAP. Think 3 tier architecture.

The other interesting thing going on in ImportListings_Click is the call to AddMetaRefresh. Here is what happens:

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 an 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 mls.CancelImportListings always worked right away, but typically, we have to wait for all the worker threads to shutdown. So, unless all the threads stopped right away (ImportStatus is Idle), we call AddMetaRefresh and let the browser keep refreshing until the import has been canceled.

Running the Demo

Open 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 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here