diff --git a/Nuclex.Windows.Forms.csproj b/Nuclex.Windows.Forms.csproj index 3f92341..7607c03 100644 --- a/Nuclex.Windows.Forms.csproj +++ b/Nuclex.Windows.Forms.csproj @@ -62,6 +62,16 @@ false AssemblyInfo + + Component + false + AsyncProgressBar + + + AsyncProgressBar.cs + false + AsyncProgressBar.Designer + Component false @@ -99,8 +109,6 @@ TrackingBar.cs - false - TrackingBar.Designer @@ -111,10 +119,8 @@ - Designer - false - ProgressReporterForm ProgressReporterForm.cs + Designer diff --git a/Source/AsyncProgressBar/AsyncProgressBar.Designer.cs b/Source/AsyncProgressBar/AsyncProgressBar.Designer.cs new file mode 100644 index 0000000..76d4688 --- /dev/null +++ b/Source/AsyncProgressBar/AsyncProgressBar.Designer.cs @@ -0,0 +1,33 @@ +namespace Nuclex.Windows.Forms { + + partial class AsyncProgressBar { + + /// Required designer variable. + private System.ComponentModel.IContainer components = null; + + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + /// + protected override void Dispose(bool disposing) { + if(disposing && (components != null)) { + components.Dispose(); + } + + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + components = new System.ComponentModel.Container(); + } + + #endregion + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/AsyncProgressBar/AsyncProgressBar.cs b/Source/AsyncProgressBar/AsyncProgressBar.cs new file mode 100644 index 0000000..607ceb8 --- /dev/null +++ b/Source/AsyncProgressBar/AsyncProgressBar.cs @@ -0,0 +1,101 @@ +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 this 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. + /// + public partial class AsyncProgressBar : ProgressBar { + + /// Initializes a new asynchronous progress bar + public AsyncProgressBar() { + InitializeComponent(); + + this.updateProgressDelegate = new MethodInvoker(updateProgress); + this.Disposed += new EventHandler(progressBarDisposed); + + 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) { + + // 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); + + // CHECK: This method is only called on an explicit Dispose() of the control. + // Microsoft officially states that it's allowed to call Control.BeginInvoke() + // without calling Control.EndInvoke(), so this code is quite correct, + // but is it also clean? :> + + } + + /// Asynchronously updates the value to be shown in the progress bar + /// New value to set the progress bar to + 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() { + + 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); + + // Convert the value to the progress bar's configured range and assign it + // to the progress bar + 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 diff --git a/Source/ProgressReporter/ProgressReporterForm.cs b/Source/ProgressReporter/ProgressReporterForm.cs index 736cb06..8016635 100644 --- a/Source/ProgressReporter/ProgressReporterForm.cs +++ b/Source/ProgressReporter/ProgressReporterForm.cs @@ -69,7 +69,7 @@ namespace Nuclex.Windows.Forms { // Only allow the form to close when the form is ready to close and the // progression being tracked has also finished. - e.Cancel = (this.state < 2); + e.Cancel = (Thread.VolatileRead(ref this.state) < 2); } /// @@ -99,7 +99,7 @@ namespace Nuclex.Windows.Forms { if(!progression.Ended) ShowDialog(); - // We're done, unsubscribe from the progression's events + // We're done, unsubscribe from the progression's events again progression.AsyncProgressUpdated -= this.asyncProgressUpdatedDelegate; progression.AsyncEnded -= this.asyncEndedDelegate; @@ -172,7 +172,7 @@ namespace Nuclex.Windows.Forms { int progress = (int)(this.currentProgress * (max - min)) + min; // Update the control - this.progressBar.Value = Math.Min(Math.Max(progress, min), max);; + this.progressBar.Value = Math.Min(Math.Max(progress, min), max); // Assigning the value sends PBM_SETPOS to the control which, // according to MSDN, already causes a redraw! @@ -224,19 +224,18 @@ namespace Nuclex.Windows.Forms { } - /// Whether an update of the control state is pending - private volatile bool progressUpdatePending; - /// Async result for the invoked control state update method - private volatile IAsyncResult progressUpdateAsyncResult; - /// Most recently reported progress of the tracker - private volatile float currentProgress; - /// Delegate for the asyncEnded() method private EventHandler asyncEndedDelegate; /// Delegate for the asyncProgressUpdated() method private EventHandler asyncProgressUpdatedDelegate; /// Delegate for the progress update method private MethodInvoker updateProgressDelegate; + /// Whether an update of the control state is pending + private volatile bool progressUpdatePending; + /// Async result for the invoked control state update method + private volatile IAsyncResult progressUpdateAsyncResult; + /// Most recently reported progress of the tracker + private volatile float currentProgress; /// Whether the form can be closed and should be closed /// /// 0: Nothing happened yet