Click here to Skip to main content
15,881,173 members
Articles / Web Development / ASP.NET

In Page Progress with Cancel Example

Rate me:
Please Sign up or sign in to vote.
4.29/5 (10 votes)
1 Mar 2006CPOL6 min read 177.7K   239   52   9
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:

Image 2

Image 3

Image 4

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:

C#
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?

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

C#
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:

C#
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:

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
General傻B Pin
Member 210028915-Sep-09 15:32
Member 210028915-Sep-09 15:32 
GeneralRefresh page without meta http-equiv="refresh" Pin
SVY26-Jul-07 3:16
SVY26-Jul-07 3:16 
GeneralGeneral Pattern Pin
chrisandharris30-Sep-06 3:00
chrisandharris30-Sep-06 3:00 
GeneralRe: General Pattern Pin
P.J. Tezza30-Sep-06 17:15
P.J. Tezza30-Sep-06 17:15 
GeneralWell Written Pin
Brian Leach8-Mar-06 6:18
Brian Leach8-Mar-06 6:18 
GeneralRe: Well Written Pin
P.J. Tezza8-Mar-06 6:54
P.J. Tezza8-Mar-06 6:54 
Thanks
GeneralUsing AJAX Pin
M Harris2-Mar-06 13:22
M Harris2-Mar-06 13:22 
GeneralRe: Using AJAX Pin
P.J. Tezza2-Mar-06 16:20
P.J. Tezza2-Mar-06 16:20 
JokeRe: Using AJAX Pin
M Harris2-Mar-06 16:29
M Harris2-Mar-06 16:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.