using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace Nuclex.Windows.Forms {
/// Progress bar with optimized multi-threading behavior
///
///
/// If a background thread is generating lots of progress updates, using synchronized
/// calls can drastically reduce performance. This progress bar optimizes that case
/// by performing the update asynchronously and keeping only the most recent update
/// when multiple updates arrive while the asynchronous update call is still running.
///
///
/// This design eliminates useless queueing of progress updates, thereby reducing
/// CPU load occuring in the UI thread and at the same time avoids blocking the
/// worker thread, increasing its performance.
///
///
public partial class AsyncProgressBar : ProgressBar {
/// Initializes a new asynchronous progress bar
public AsyncProgressBar() {
InitializeComponent();
this.Disposed += new EventHandler(progressBarDisposed);
this.updateProgressDelegate = new MethodInvoker(updateProgress);
// Could probably use VolatileWrite() as well, but for consistency reasons
// this is an Interlocked call, too. Mixing different synchronization measures
// for a variable causes trouble so often that this raises a red flag
// whenever I see it :)
Interlocked.Exchange(ref this.newProgress, -1.0f);
}
/// Called when the progress bar is being disposed
/// Progress bar that is being disposed
/// Not used
private void progressBarDisposed(object sender, EventArgs arguments) {
// CHECK: This method is only called on an explicit Dispose() of the control.
// It is legal to call Control.BeginInvoke() without calling Control.EndInvoke(),
// so the code is quite correct even if no Dispose() occurs, but is it also clean?
// http://www.interact-sw.co.uk/iangblog/2005/05/16/endinvokerequired
// Since this has to occur in the UI thread, there's no way that updateProgress()
// could be executing just now. But the final call to updateProgress() will not
// have EndInvoke() called on it yet, so we do this here before the control
// is finally disposed.
if(this.progressUpdateAsyncResult != null) {
EndInvoke(this.progressUpdateAsyncResult);
this.progressUpdateAsyncResult = null;
}
}
/// Asynchronously updates the value to be shown in the progress bar
/// New value to set the progress bar to
///
/// This will schedule an asynchronous update of the progress bar in the UI thread.
/// If you change the progress value again before the progress bar has completed its
/// update cycle, the original progress value will be skipped and the progress bar
/// jumps directly to the latest progress value. Updates are not queued, there is
/// at most one update waiting on the UI thread. It is also strictly guaranteed that
/// the last most progress value set will be shown and never skipped.
///
public void AsyncSetValue(float value) {
// Update the value to be shown on the progress bar. If this happens multiple
// times, that's not a problem, the progress bar updates as fast as it can
// and always tries to show the most recent value assigned.
float oldValue = Interlocked.Exchange(ref this.newProgress, value);
// If the previous value was -1, the UI thread has already taken out the most recent
// value and assigned it (or is about to assign it) to the progress bar control.
// In this case, we'll wait until the current update has completed and immediately
// begin the next update - since we know that the value the UI thread has extracted
// is no longer the most recent one.
if(oldValue == -1.0f) {
if(this.progressUpdateAsyncResult != null)
EndInvoke(this.progressUpdateAsyncResult);
this.progressUpdateAsyncResult = BeginInvoke(this.updateProgressDelegate);
}
}
/// Synchronously updates the value visualized in the progress bar
private void updateProgress() {
// Cache these to shorten the code that follows :)
int minimum = base.Minimum;
int maximum = base.Maximum;
// Take out the most recent value that has been given to the asynchronous progress
// bar up until now and replace it by -1. This enables the updater to see when
// the update has actually been performed and whether it needs to start a new
// invocation to ensure the most recent value will remain at the end.
float progress = Interlocked.Exchange(ref this.newProgress, -1.0f);
// Restrain the value to the progress bar's configured range and assign it.
// This is done to prevent exceptions in the UI thread (theoretically the user
// could change the progress bar's min and max just before the UI thread executes
// this method, so we cannot validate the value in AsyncSetValue())
int value = (int)(progress * (maximum - minimum)) + minimum;
base.Value = Math.Min(Math.Max(value, minimum), maximum);
}
/// New progress being assigned to the progress bar
private float newProgress;
/// Delegate for the progress update method
private MethodInvoker updateProgressDelegate;
/// Async result for the invoked control state update method
private volatile IAsyncResult progressUpdateAsyncResult;
}
} // namespace Nuclex.Windows.Forms