From c5fba2473338660bf55a8905203fd5bae7513b5f Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Tue, 17 Jul 2007 19:33:18 +0000 Subject: [PATCH] Added a progress reporting form that provides a convenient way to lock up the user interface during all-exclusive background processes git-svn-id: file:///srv/devel/repo-conversion/nuwi@8 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Windows.Forms.csproj | 16 ++ .../ProgressReporterForm.Designer.cs | 99 +++++++ .../ProgressReporter/ProgressReporterForm.cs | 255 ++++++++++++++++++ .../ProgressReporterForm.resx | 123 +++++++++ 4 files changed, 493 insertions(+) create mode 100644 Source/ProgressReporter/ProgressReporterForm.Designer.cs create mode 100644 Source/ProgressReporter/ProgressReporterForm.cs create mode 100644 Source/ProgressReporter/ProgressReporterForm.resx diff --git a/Nuclex.Windows.Forms.csproj b/Nuclex.Windows.Forms.csproj index 1b69cdd..3f92341 100644 --- a/Nuclex.Windows.Forms.csproj +++ b/Nuclex.Windows.Forms.csproj @@ -84,6 +84,14 @@ false EmbeddedControlCollection + + Form + false + ProgressReporterForm + + + ProgressReporterForm.cs + false TrackingBar @@ -101,6 +109,14 @@ Nuclex.Support %28PC%29 + + + Designer + false + ProgressReporterForm + ProgressReporterForm.cs + + diff --git a/Source/ProgressReporter/ProgressReporterForm.Designer.cs b/Source/ProgressReporter/ProgressReporterForm.Designer.cs new file mode 100644 index 0000000..7768ada --- /dev/null +++ b/Source/ProgressReporter/ProgressReporterForm.Designer.cs @@ -0,0 +1,99 @@ +namespace Nuclex.Windows.Forms { + partial class ProgressReporterForm { + /// + /// 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 Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); + this.cancelButton = new System.Windows.Forms.Button(); + this.progressBar = new System.Windows.Forms.ProgressBar(); + this.statusLabel = new System.Windows.Forms.Label(); + this.controlCreationTimer = new System.Windows.Forms.Timer(this.components); + this.SuspendLayout(); + // + // cancelButton + // + this.cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Top; + this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.cancelButton.Location = new System.Drawing.Point(151, 55); + this.cancelButton.Name = "cancelButton"; + this.cancelButton.Size = new System.Drawing.Size(75, 23); + this.cancelButton.TabIndex = 0; + this.cancelButton.Text = "&Cancel"; + this.cancelButton.UseVisualStyleBackColor = true; + this.cancelButton.Click += new System.EventHandler(this.cancelClicked); + // + // progressBar + // + this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.progressBar.Location = new System.Drawing.Point(12, 26); + this.progressBar.Name = "progressBar"; + this.progressBar.Size = new System.Drawing.Size(352, 23); + this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.progressBar.TabIndex = 1; + // + // statusLabel + // + this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.statusLabel.Location = new System.Drawing.Point(12, 9); + this.statusLabel.Name = "statusLabel"; + this.statusLabel.Size = new System.Drawing.Size(352, 14); + this.statusLabel.TabIndex = 2; + this.statusLabel.Text = "Please Wait..."; + this.statusLabel.TextAlign = System.Drawing.ContentAlignment.TopCenter; + // + // controlCreationTimer + // + this.controlCreationTimer.Enabled = true; + this.controlCreationTimer.Interval = 1; + this.controlCreationTimer.Tick += new System.EventHandler(this.controlCreationTimerTicked); + // + // ProgressReporterForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.cancelButton; + this.ClientSize = new System.Drawing.Size(376, 90); + this.ControlBox = false; + this.Controls.Add(this.statusLabel); + this.Controls.Add(this.progressBar); + this.Controls.Add(this.cancelButton); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "ProgressReporterForm"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.Text = "Progress"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button cancelButton; + private System.Windows.Forms.ProgressBar progressBar; + private System.Windows.Forms.Label statusLabel; + private System.Windows.Forms.Timer controlCreationTimer; + } +} \ No newline at end of file diff --git a/Source/ProgressReporter/ProgressReporterForm.cs b/Source/ProgressReporter/ProgressReporterForm.cs new file mode 100644 index 0000000..736cb06 --- /dev/null +++ b/Source/ProgressReporter/ProgressReporterForm.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Text; +using System.Windows.Forms; +using System.Threading; + +using Nuclex.Support.Tracking; +using Nuclex.Support.Scheduling; + +namespace Nuclex.Windows.Forms { + + /// + /// Blocking progress dialog that prevents the user from accessing the application + /// window during all-blocking background processes. + /// + public partial class ProgressReporterForm : Form { + + /// Initializes a new progress reporter + internal ProgressReporterForm() { + InitializeComponent(); + + this.updateProgressDelegate = new MethodInvoker(updateProgress); + this.asyncEndedDelegate = new EventHandler(asyncEnded); + this.asyncProgressUpdatedDelegate = new EventHandler( + asyncProgressUpdated + ); + } + + /// + /// Shows the progress reporter until the specified progression has ended. + /// + /// + /// Progression for whose duration to show the progress reporter + /// + public static void Track(Progression progression) { + Track(null, progression); + } + + /// + /// Shows the progress reporter until the specified progression has ended. + /// + /// + /// Text to be shown in the progress reporter's title bar + /// + /// + /// Progression for whose duration to show the progress reporter + /// + public static void Track(string windowTitle, Progression progression) { + + // Small optimization to avoid the lengthy control creation when + // the progression has already ended + if(progression.Ended) + return; + + // Open the form and let it monitor the progression's state + using(ProgressReporterForm theForm = new ProgressReporterForm()) { + theForm.track(windowTitle, progression); + } + + } + + /// Called when the user tries to close the form manually + /// Contains flag that can be used to abort the close attempt + protected override void OnClosing(CancelEventArgs e) { + base.OnClosing(e); + + // 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); + } + + /// + /// Shows the progress reporter until the specified progression has ended. + /// + /// + /// Text to be shown in the progress reporter's title bar + /// + /// + /// Progression for whose duration to show the progress reporter + /// + private void track(string windowTitle, Progression progression) { + + // Set the window title if the user wants to use a custom one + if(windowTitle != null) + Text = windowTitle; + + // Only enable the cancel button if the progression can be aborted + this.cancelButton.Enabled = (progression is IAbortable); + + // Subscribe the form to the progression it is supposed to monitor + progression.AsyncEnded += this.asyncEndedDelegate; + progression.AsyncProgressUpdated += this.asyncProgressUpdatedDelegate; + + // The progression might have ended before this line was reached, if that's + // the case, we don't show the dialog at all. + if(!progression.Ended) + ShowDialog(); + + // We're done, unsubscribe from the progression's events + progression.AsyncProgressUpdated -= this.asyncProgressUpdatedDelegate; + progression.AsyncEnded -= this.asyncEndedDelegate; + + } + + /// Called when the progression has ended + /// Progression that has ended + /// Not used + private void asyncEnded(object sender, EventArgs arguments) { + + // If the new state is 2, the form was ready to close (since the state + // is incremented once when the form becomes ready to be closed) + int newState = Interlocked.Increment(ref this.state); + if(newState == 2) { + + // Close the dialog. Ensure the Close() method is invoked from the + // same thread the dialog was created in. + if(InvokeRequired) + Invoke(new MethodInvoker(Close)); + else + Close(); + + } + + } + + /// Called when the tracked progression's progress updates + /// Progression whose progress has been updated + /// + /// Contains the new progress achieved by the progression + /// + private void asyncProgressUpdated(object sender, ProgressUpdateEventArgs arguments) { + + // Set the new progress without any synchronization + this.currentProgress = arguments.Progress; + + // Another use of the double-checked locking idiom, here we're trying to optimize + // away the lock in case some "trigger-happy" progressions send way more + // progress updates than the poor control can process :) + if(!this.progressUpdatePending) { + lock(this) { + if(!this.progressUpdatePending) { + this.progressUpdatePending = true; + this.progressUpdateAsyncResult = BeginInvoke(this.updateProgressDelegate); + } + } // lock + } + + } + + /// Synchronously updates the value visualized in the progress bar + private void updateProgress() { + lock(this) { + + // Reset the update flag so incoming updates will cause the control to + // update itself another time. + this.progressUpdatePending = false; + EndInvoke(this.progressUpdateAsyncResult); + + // Until the first progress event is received, the progress reporter shows + // a marquee bar to entertain the user even when no progress reports are + // being made at all. + if(this.progressBar.Style == ProgressBarStyle.Marquee) + this.progressBar.Style = ProgressBarStyle.Blocks; + + // Transform the progress into an integer in the range of the progress bar's + // min and max values (these should normally be set to 0 and 100). + int min = this.progressBar.Minimum; + int max = this.progressBar.Maximum; + int progress = (int)(this.currentProgress * (max - min)) + min; + + // Update the control + 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! + //base.Invalidate(); + + } // lock + } + + /// + /// One-time timer callback that ensurs the form doesn't stay open when the + /// close request arrives at an inappropriate time. + /// + /// Timer that has ticked + /// Not used + private void controlCreationTimerTicked(object sender, EventArgs e) { + + // This timer is intended to run only once to find out when the dialog has + // been fully constructed and is running its message pump. So we'll disable + // it as soon as it has been triggered once. + this.controlCreationTimer.Enabled = false; + + // If the new state is 2, then the form was requested to close before it had + // been fully constructed, so we should close it now! + int newState = System.Threading.Interlocked.Increment(ref this.state); + if(newState == 2) + Close(); + + } + + /// + /// Aborts the background operation when the user clicks the cancel button + /// + /// Button that has been clicked + /// Not used + private void cancelClicked(object sender, EventArgs e) { + + if(this.abortReceiver != null) { + + // Do this first because the abort receiver might trigger the AsyncEnded + // event in the calling thread and thus destroy our window even in + // the safe and synchronous UI thread :) + this.cancelButton.Enabled = false; + + // Now we're ready to abort! + this.abortReceiver.AsyncAbort(); + this.abortReceiver = null; + + } + + } + + /// 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 the form can be closed and should be closed + /// + /// 0: Nothing happened yet + /// 1: Ready to close or close requested + /// 2: Ready to close and close requested, triggers close + /// + private int state; + /// + /// If set, reference to an object implementing IAbortable by which the + /// ongoing background process can be aborted. + /// + private IAbortable abortReceiver; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/ProgressReporter/ProgressReporterForm.resx b/Source/ProgressReporter/ProgressReporterForm.resx new file mode 100644 index 0000000..c709957 --- /dev/null +++ b/Source/ProgressReporter/ProgressReporterForm.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file