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
This commit is contained in:
Markus Ewald 2007-07-17 19:33:18 +00:00
parent 19c55f370e
commit c5fba24733
4 changed files with 493 additions and 0 deletions

View File

@ -84,6 +84,14 @@
<XNAUseContentPipeline>false</XNAUseContentPipeline> <XNAUseContentPipeline>false</XNAUseContentPipeline>
<Name>EmbeddedControlCollection</Name> <Name>EmbeddedControlCollection</Name>
</Compile> </Compile>
<Compile Include="Source\ProgressReporter\ProgressReporterForm.cs">
<SubType>Form</SubType>
<XNAUseContentPipeline>false</XNAUseContentPipeline>
<Name>ProgressReporterForm</Name>
</Compile>
<Compile Include="Source\ProgressReporter\ProgressReporterForm.Designer.cs">
<DependentUpon>ProgressReporterForm.cs</DependentUpon>
</Compile>
<Compile Include="Source\TrackingBar\TrackingBar.cs"> <Compile Include="Source\TrackingBar\TrackingBar.cs">
<XNAUseContentPipeline>false</XNAUseContentPipeline> <XNAUseContentPipeline>false</XNAUseContentPipeline>
<Name>TrackingBar</Name> <Name>TrackingBar</Name>
@ -101,6 +109,14 @@
<Name>Nuclex.Support %28PC%29</Name> <Name>Nuclex.Support %28PC%29</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Source\ProgressReporter\ProgressReporterForm.resx">
<SubType>Designer</SubType>
<XNAUseContentPipeline>false</XNAUseContentPipeline>
<Name>ProgressReporterForm</Name>
<DependentUpon>ProgressReporterForm.cs</DependentUpon>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA\Game Studio Express\v1.0\Microsoft.Xna.ContentPipeline.targets" /> <Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA\Game Studio Express\v1.0\Microsoft.Xna.ContentPipeline.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA\Game Studio Express\v1.0\Microsoft.Xna.Common.targets" /> <Import Project="$(MSBuildExtensionsPath)\Microsoft\XNA\Game Studio Express\v1.0\Microsoft.Xna.Common.targets" />

View File

@ -0,0 +1,99 @@
namespace Nuclex.Windows.Forms {
partial class ProgressReporterForm {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if(disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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;
}
}

View File

@ -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 {
/// <summary>
/// Blocking progress dialog that prevents the user from accessing the application
/// window during all-blocking background processes.
/// </summary>
public partial class ProgressReporterForm : Form {
/// <summary>Initializes a new progress reporter</summary>
internal ProgressReporterForm() {
InitializeComponent();
this.updateProgressDelegate = new MethodInvoker(updateProgress);
this.asyncEndedDelegate = new EventHandler(asyncEnded);
this.asyncProgressUpdatedDelegate = new EventHandler<ProgressUpdateEventArgs>(
asyncProgressUpdated
);
}
/// <summary>
/// Shows the progress reporter until the specified progression has ended.
/// </summary>
/// <param name="progression">
/// Progression for whose duration to show the progress reporter
/// </param>
public static void Track(Progression progression) {
Track(null, progression);
}
/// <summary>
/// Shows the progress reporter until the specified progression has ended.
/// </summary>
/// <param name="windowTitle">
/// Text to be shown in the progress reporter's title bar
/// </param>
/// <param name="progression">
/// Progression for whose duration to show the progress reporter
/// </param>
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);
}
}
/// <summary>Called when the user tries to close the form manually</summary>
/// <param name="e">Contains flag that can be used to abort the close attempt</param>
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);
}
/// <summary>
/// Shows the progress reporter until the specified progression has ended.
/// </summary>
/// <param name="windowTitle">
/// Text to be shown in the progress reporter's title bar
/// </param>
/// <param name="progression">
/// Progression for whose duration to show the progress reporter
/// </param>
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;
}
/// <summary>Called when the progression has ended</summary>
/// <param name="sender">Progression that has ended</param>
/// <param name="arguments">Not used</param>
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();
}
}
/// <summary>Called when the tracked progression's progress updates</summary>
/// <param name="sender">Progression whose progress has been updated</param>
/// <param name="arguments">
/// Contains the new progress achieved by the progression
/// </param>
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
}
}
/// <summary>Synchronously updates the value visualized in the progress bar</summary>
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
}
/// <summary>
/// One-time timer callback that ensurs the form doesn't stay open when the
/// close request arrives at an inappropriate time.
/// </summary>
/// <param name="sender">Timer that has ticked</param>
/// <param name="e">Not used</param>
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();
}
/// <summary>
/// Aborts the background operation when the user clicks the cancel button
/// </summary>
/// <param name="sender">Button that has been clicked</param>
/// <param name="e">Not used</param>
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;
}
}
/// <summary>Whether an update of the control state is pending</summary>
private volatile bool progressUpdatePending;
/// <summary>Async result for the invoked control state update method</summary>
private volatile IAsyncResult progressUpdateAsyncResult;
/// <summary>Most recently reported progress of the tracker</summary>
private volatile float currentProgress;
/// <summary>Delegate for the asyncEnded() method</summary>
private EventHandler asyncEndedDelegate;
/// <summary>Delegate for the asyncProgressUpdated() method</summary>
private EventHandler<ProgressUpdateEventArgs> asyncProgressUpdatedDelegate;
/// <summary>Delegate for the progress update method</summary>
private MethodInvoker updateProgressDelegate;
/// <summary>Whether the form can be closed and should be closed</summary>
/// <remarks>
/// 0: Nothing happened yet
/// 1: Ready to close or close requested
/// 2: Ready to close and close requested, triggers close
/// </remarks>
private int state;
/// <summary>
/// If set, reference to an object implementing IAbortable by which the
/// ongoing background process can be aborted.
/// </summary>
private IAbortable abortReceiver;
}
} // namespace Nuclex.Windows.Forms

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="controlCreationTimer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>