Documentation
Home
Evaluation
Summary for programmers
Product limitations
Goals of Axisbase
Quick start
Installation
Using the launchpad and opening databases
Connecting to a sample database
Using building blocks
Planning
Define the purpose
Define the requirements
Borrow existing work
Determine the architecture
Design the data model
Design the process model
Deploy and maintain the product
Tutorials
building blocks
Performing a mailmerge
Bulk e-mailing
Programming
Single-threaded progress indicator in c#
Reference
Database menu items
Import XML
Save Copy As
Integrity Check
Change Password
Database Properties window
Opening the database properties window
Record types tab
Display types tab
Roles and Users tabs
Sidebar tab
Database ID/Links tab
Counters tab
Building blocks
Building blocks window
Editing grids and cells
Hyperlinks and nesting
Data Subset window
Data Outsource window
List window
Window window
Report window
Bulk Operation window
Label Printer window
Choosing a data source
Special topics
Expression syntax
Browse records
Storing building blocks within other building blocks
Programming
Using custom code in building blocks
Using Axisbase as an embedded database
Axis1.Util namespace reference
Axis1.Data namespace reference (Fishnets)
Axis1.Data namespace reference (other)
Axis1.Forms namespace reference
| Single-threaded progress indicator in c#Explains how the Axisbase progress indicator was written using a single thread. I had a coding situation where I needed to do some work that may take a long time and I wanted to force the user to wait while possibly giving them a progress indicator. There are many variants of the problem and several solutions. The code shown here is used in Axisbase (a free .NET database server and client). RequirementsNo matter what method is chosen, the UI should never freeze; if the user goes to another application's window then goes back to the working app, it should at least re-paint. (However, calling Application.DoEvents can lead to some very bad and hard to debug problems.) When using a progress indicator for a task that might be fast and might be slow, it is better to delay displaying it until after about a half second of the work. Users don't want to see a progress indicator flash for an instant and then go away. Background worker threads are often discussed as a solution to this, but when the work being done is closely related to the UI, the use of multiple threads creates the need for extra code. If the user has just asked to do one thing, and the UI doesn't block (because the request is being processed in the background), then should the user be able to click on other controls? In many cases, no. So you can disable all the controls during the background work. But you don't want the controls to repaint to disabled status if the duration of the background work turns out to be very short. Background threads are good for some things, but I wanted a progress indicator for a foreground process. A flight of fancyThere are two distinct kinds of UI operations: blocking and non-blocking. An example of a blocking (or sequential) operation is entering a character into a text box. You want the application to complete the action before moving on to the next action. If for some reason, it takes a long time to complete the action (network delay for example), you ideally want the window to block all other actions until that is done. Of course keystrokes can be queued up, but they are still processed sequentially. The other kind of operation - non-blocking - is painting or moving windows, or some other actions that cause repainting. These actions should be done even while the window is blocked for the next blocking action. Ideally we would have two threads to work with. One thread would allow the user to move windows and would repaint them, while the other thread would process sequential (blocking) user actions on the controls. If a long-running action was in progress, the painting thread could notice this, and change the window or cursor appearance as an indicator that the blocking thread is blocked. If the dual-UI-thread thing was available, you could do something like this, and push all the work of displaying progress onto the other thread in a generalized way:
But this is not available, so... Back to realityHere is my real-word answer: Create a progress form but don't show it when you start doing long-running work in the UI thread. Instead call it every once in a while while working with the progress status. The progress form shows itself if a half second has elapsed, and when it does, it blocks windows messages to other windows. So it isn't a dialog box, but it stays on top and blocks other windows from being used like a dialog box. The c# code for such a form is shown below. I made the progress form implement IMessageFilter. The filter tells windows to throw away messages that we don't want, like mouse clicks, during the work. However, it lets messages through for the cancel button and it lets all paint messages through. The ShowProgress method of the form should be called often during the work. It calls Application.DoEvents, but since event messages are being filtered, this doesn't cause problems. One downside of this approach is that when using the form, you must always call the Finish method to remove the message filter, and this HAS to be done in a finally clause and there MAY NOT be any dialog boxes or other interaction before Finish gets called. Otherwise the app will hang.
First: here is how you use the form:
try Here is the form code:using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace Axis1.Forms { /// <summary> /// Allows a progress indicator to pop up, blocking the user from /// doing anything (except possibly canceling the operation). /// To use you MUST MUST MUST call SetupAndShow, then call Finish in /// a finally block. You may call ShowProgress any number of times /// in between. If you allow cancellation, then you must trap errors, /// as a cancellation will throw an exception. /// </summary> public partial class ProgressForm : Form, IMessageFilter { const int WM_PAINT = 0x000F, WM_NCACTIVATE = 0x086; private Form caller; private DateTime showTime; //time when form should show, UTC private Cursor priorCursor = Cursors.Default; private bool allowCancel; //controls whether a manual close should cause an exception private bool pleaseAbort; //set when user presses cancel public ProgressForm() { InitializeComponent(); } /// <summary> /// Setup the form. /// </summary> /// <param name="msg1">the message describing the work being done</param> /// <param name="delayedDisplay">if true, the form is only shown if /// the operation takes longer than a half-second</param> public void SetupAndShow(Form caller, string msg1, bool delayedDisplay, bool allowCancel) { if (caller == null) { if (Application.OpenForms.Count == 0) return; caller = Application.OpenForms[0]; } this.allowCancel = allowCancel; eCancel.Visible = allowCancel; label1.Text = msg1; this.caller = caller; eBar.Value = 0; if (delayedDisplay) showTime = DateTime.UtcNow.AddSeconds(0.5); else { showTime = DateTime.MinValue; ShowProgress(0, null); } priorCursor = caller.Cursor; caller.Cursor = Cursors.WaitCursor; Application.AddMessageFilter(this); } /// <summary> /// Update progress bar and labels. If enough time has passed, /// make form visible. Call DoEvents, and if form is canceled, /// throw Exception. /// </summary> /// <param name="percent">a number between 0 and 1</param> /// <param name="displayValue"></param> public void ShowProgress(double percent, string displayValue) { if (percent > 0) { try { eBar.Value = (int)(percent * 100.0); } catch { } } if (displayValue != null) label2.Text = displayValue; //show/paint form if (!Visible && showTime < DateTime.UtcNow) { Show(); Cursor = Cursors.WaitCursor; if (eCancel.Visible) eCancel.Focus(); } //allow some events to occur during the work, but only those //that deal with the cancel button Application.DoEvents(); if (pleaseAbort) { Application.RemoveMessageFilter(this); Hide(); throw new Exception("Operation canceled"); } } //returns true if a message should be filtered out public bool PreFilterMessage(ref Message m) { bool isgoodtype = m.Msg == WM_PAINT; bool allowed = isgoodtype || (Visible && (m.HWnd == this.Handle || m.HWnd == eCancel.Handle)); return !allowed; } private void eCancel_Click(object sender, EventArgs e) { pleaseAbort = true; } /// <summary> /// Finish form - hides, unless there are errors, in which case it /// shows it as a dialog and waits for OK pressed /// </summary> public void Finish() { Application.DoEvents(); //this clears the event queue of unwanted clicks Hide(); Application.RemoveMessageFilter(this); Dispose(); caller.Cursor = priorCursor; } } } |