5,702,921 members and growing! (20,350 online)
Email Password   helpLost your password?
Web Development » ASP.NET » General     Intermediate

In Page Progress with Cancel Example

By P.J. Tezza

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.
C#, VC8.0, C++Windows, .NET, .NET 2.0, NT4, Win2K, WinXP, Win2003, Vista, TabletPC, ASP.NET, WebForms, VS2005, Visual Studio, Dev

Posted: 1 Mar 2006
Updated: 1 Mar 2006
Views: 33,631
Bookmarked: 44 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
11 votes for this Article.
Popularity: 3.86 Rating: 3.71 out of 5
2 votes, 18.2%
1
0 votes, 0.0%
2
1 vote, 9.1%
3
4 votes, 36.4%
4
4 votes, 36.4%
5
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

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

  • 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 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 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 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 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 MLS.ImportListings simulate a job queue with 1 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:

    public void CancelImportListings()
    {
        _importCanceled = true;
    }

And 2) MLS.ImportListings checks this flag as frequently as possible:

            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 Code Project 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 at 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 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 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 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

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

About the Author

P.J. Tezza



Location: United States United States

Other popular ASP.NET articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 8 of 8 (Total in Forum: 8) (Refresh)FirstPrevNext
GeneralRefresh page without meta http-equiv="refresh"memberSVY4:16 26 Jul '07  
GeneralGeneral Patternmemberchrisandharris4:00 30 Sep '06  
GeneralRe: General PatternmemberP.J. Tezza18:15 30 Sep '06  
GeneralWell WrittenmemberBrian Leach7:18 8 Mar '06  
GeneralRe: Well WrittenmemberP.J. Tezza7:54 8 Mar '06  
GeneralUsing AJAXmemberM Harris14:22 2 Mar '06  
GeneralRe: Using AJAXmemberP.J. Tezza17:20 2 Mar '06  
JokeRe: Using AJAXmemberM Harris17:29 2 Mar '06  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 1 Mar 2006
Editor:
Copyright 2006 by P.J. Tezza
Everything else Copyright © CodeProject, 1999-2008
Web12 | Advertise on the Code Project