Changed license to Apache License 2.0
This commit is contained in:
parent
857917aad5
commit
1bb2363a07
|
@ -1,31 +1,50 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Nuclex.Windows.Forms")]
|
||||
[assembly: AssemblyProduct("Nuclex.Windows.Forms")]
|
||||
[assembly: AssemblyDescription("Lean and elegant MVVM library with extras for WinForms")]
|
||||
[assembly: AssemblyCompany("Nuclex Development Labs")]
|
||||
[assembly: AssemblyCopyright("Copyright © Nuclex Development Labs 2019")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("08254ad9-394e-4638-8412-098c8c4a4c39")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Nuclex.Windows.Forms")]
|
||||
[assembly: AssemblyProduct("Nuclex.Windows.Forms")]
|
||||
[assembly: AssemblyDescription("Lean and elegant MVVM library with extras for WinForms")]
|
||||
[assembly: AssemblyCompany("Nuclex Development Labs")]
|
||||
[assembly: AssemblyCopyright("Copyright © Markus Ewald / Nuclex Development Labs 2002-2024")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("08254ad9-394e-4638-8412-098c8c4a4c39")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
|
|
105
Source/AsyncProgressBar/AsyncProgressBar.Designer.cs
generated
105
Source/AsyncProgressBar/AsyncProgressBar.Designer.cs
generated
|
@ -1,53 +1,52 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
partial class AsyncProgressBar {
|
||||
|
||||
/// <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 Component 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() {
|
||||
components = new System.ComponentModel.Container();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
partial class AsyncProgressBar {
|
||||
|
||||
/// <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 Component 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() {
|
||||
components = new System.ComponentModel.Container();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,71 +1,70 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Unit Test for the asynchronously updating progress bar</summary>
|
||||
[TestFixture, Explicit]
|
||||
public class AsyncProgressBarTest {
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that asynchronous progress assignment is working
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestProgressAssignment() {
|
||||
using(AsyncProgressBar progressBar = new AsyncProgressBar()) {
|
||||
|
||||
// Let the control create its window handle
|
||||
progressBar.CreateControl();
|
||||
progressBar.Minimum = 0;
|
||||
progressBar.Maximum = 100;
|
||||
|
||||
Assert.AreEqual(0, progressBar.Value);
|
||||
|
||||
// Assign the new value. This will be done asynchronously, so we call
|
||||
// Application.DoEvents() to execute the message pump once, guaranteeing
|
||||
// that the call will have been executed after Application.DoEvents() returns.
|
||||
progressBar.AsyncSetValue(0.33f);
|
||||
Application.DoEvents();
|
||||
|
||||
Assert.AreEqual(33, progressBar.Value);
|
||||
|
||||
progressBar.AsyncSetValue(0.66f);
|
||||
Application.DoEvents();
|
||||
|
||||
Assert.AreEqual(66, progressBar.Value);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
||||
#endif // UNITTEST
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Unit Test for the asynchronously updating progress bar</summary>
|
||||
[TestFixture, Explicit]
|
||||
public class AsyncProgressBarTest {
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that asynchronous progress assignment is working
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestProgressAssignment() {
|
||||
using(AsyncProgressBar progressBar = new AsyncProgressBar()) {
|
||||
|
||||
// Let the control create its window handle
|
||||
progressBar.CreateControl();
|
||||
progressBar.Minimum = 0;
|
||||
progressBar.Maximum = 100;
|
||||
|
||||
Assert.AreEqual(0, progressBar.Value);
|
||||
|
||||
// Assign the new value. This will be done asynchronously, so we call
|
||||
// Application.DoEvents() to execute the message pump once, guaranteeing
|
||||
// that the call will have been executed after Application.DoEvents() returns.
|
||||
progressBar.AsyncSetValue(0.33f);
|
||||
Application.DoEvents();
|
||||
|
||||
Assert.AreEqual(33, progressBar.Value);
|
||||
|
||||
progressBar.AsyncSetValue(0.66f);
|
||||
Application.DoEvents();
|
||||
|
||||
Assert.AreEqual(66, progressBar.Value);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
||||
#endif // UNITTEST
|
||||
|
|
|
@ -1,140 +1,139 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Progress bar with optimized multi-threading behavior</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public partial class AsyncProgressBar : ProgressBar {
|
||||
|
||||
/// <summary>Initializes a new asynchronous progress bar</summary>
|
||||
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 raises a red flag whenever I see it :)
|
||||
Interlocked.Exchange(ref this.newProgress, -1.0f);
|
||||
}
|
||||
|
||||
/// <summary>Called when the progress bar is being disposed</summary>
|
||||
/// <param name="sender">Progress bar that is being disposed</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously updates the value to be shown in the progress bar</summary>
|
||||
/// <param name="value">New value to set the progress bar to</param>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Synchronously updates the value visualized in the progress bar</summary>
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>New progress being assigned to the progress bar</summary>
|
||||
private float newProgress;
|
||||
/// <summary>Delegate for the progress update method</summary>
|
||||
private MethodInvoker updateProgressDelegate;
|
||||
/// <summary>Async result for the invoked control state update method</summary>
|
||||
private volatile IAsyncResult progressUpdateAsyncResult;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Progress bar with optimized multi-threading behavior</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public partial class AsyncProgressBar : ProgressBar {
|
||||
|
||||
/// <summary>Initializes a new asynchronous progress bar</summary>
|
||||
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 raises a red flag whenever I see it :)
|
||||
Interlocked.Exchange(ref this.newProgress, -1.0f);
|
||||
}
|
||||
|
||||
/// <summary>Called when the progress bar is being disposed</summary>
|
||||
/// <param name="sender">Progress bar that is being disposed</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously updates the value to be shown in the progress bar</summary>
|
||||
/// <param name="value">New value to set the progress bar to</param>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Synchronously updates the value visualized in the progress bar</summary>
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>New progress being assigned to the progress bar</summary>
|
||||
private float newProgress;
|
||||
/// <summary>Delegate for the progress update method</summary>
|
||||
private MethodInvoker updateProgressDelegate;
|
||||
/// <summary>Async result for the invoked control state update method</summary>
|
||||
private volatile IAsyncResult progressUpdateAsyncResult;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,47 +1,66 @@
|
|||
using Nuclex.Windows.Forms.Views;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.AutoBinding {
|
||||
|
||||
/// <summary>
|
||||
/// Binds a view to its model using a convention-over-configuration approach
|
||||
/// </summary>
|
||||
public class ConventionBinder : IAutoBinder {
|
||||
|
||||
/// <summary>Binds the specified view to an explicitly selected view model</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model the view will be bound to
|
||||
/// </typeparam>
|
||||
/// <param name="view">View that will be bound to a view model</param>
|
||||
/// <param name="viewModel">View model the view will be bound to</param>
|
||||
public void Bind<TViewModel>(Control view, TViewModel viewModel)
|
||||
where TViewModel : class {
|
||||
bind(view, viewModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the specified view to the view model specified in its DataContext
|
||||
/// </summary>
|
||||
/// <param name="viewControl">View that will be bound</param>
|
||||
public void Bind(Control viewControl) {
|
||||
IView viewControlAsView = viewControl as IView;
|
||||
if(viewControlAsView == null) {
|
||||
throw new InvalidOperationException(
|
||||
"The specified view has no view model associated. Either assign your " +
|
||||
"view model to the view's data context beforehand or use the overload " +
|
||||
"of Bind() that allows you to explicitly specify the view model."
|
||||
);
|
||||
}
|
||||
|
||||
bind(viewControl, viewControlAsView.DataContext);
|
||||
}
|
||||
|
||||
/// <summary>Binds a view to a view model</summary>
|
||||
/// <param name="view">View that will be bound</param>
|
||||
/// <param name="viewModel">View model the view will be bound to</param>
|
||||
private void bind(Control view, object viewModel) {
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace Nuclex.Windows.Forms.AutoBinding
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using Nuclex.Windows.Forms.Views;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.AutoBinding {
|
||||
|
||||
/// <summary>
|
||||
/// Binds a view to its model using a convention-over-configuration approach
|
||||
/// </summary>
|
||||
public class ConventionBinder : IAutoBinder {
|
||||
|
||||
/// <summary>Binds the specified view to an explicitly selected view model</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model the view will be bound to
|
||||
/// </typeparam>
|
||||
/// <param name="view">View that will be bound to a view model</param>
|
||||
/// <param name="viewModel">View model the view will be bound to</param>
|
||||
public void Bind<TViewModel>(Control view, TViewModel viewModel)
|
||||
where TViewModel : class {
|
||||
bind(view, viewModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the specified view to the view model specified in its DataContext
|
||||
/// </summary>
|
||||
/// <param name="viewControl">View that will be bound</param>
|
||||
public void Bind(Control viewControl) {
|
||||
IView viewControlAsView = viewControl as IView;
|
||||
if(viewControlAsView == null) {
|
||||
throw new InvalidOperationException(
|
||||
"The specified view has no view model associated. Either assign your " +
|
||||
"view model to the view's data context beforehand or use the overload " +
|
||||
"of Bind() that allows you to explicitly specify the view model."
|
||||
);
|
||||
}
|
||||
|
||||
bind(viewControl, viewControlAsView.DataContext);
|
||||
}
|
||||
|
||||
/// <summary>Binds a view to a view model</summary>
|
||||
/// <param name="view">View that will be bound</param>
|
||||
/// <param name="viewModel">View model the view will be bound to</param>
|
||||
private void bind(Control view, object viewModel) {
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace Nuclex.Windows.Forms.AutoBinding
|
||||
|
|
|
@ -1,28 +1,47 @@
|
|||
using System;
|
||||
|
||||
using System.Windows.Forms;
|
||||
using Nuclex.Windows.Forms.Views;
|
||||
|
||||
namespace Nuclex.Windows.Forms.AutoBinding {
|
||||
|
||||
/// <summary>Binds views to their view models</summary>
|
||||
public interface IAutoBinder {
|
||||
|
||||
/// <summary>Binds the specified view to an explicitly selected view model</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model the view will be bound to
|
||||
/// </typeparam>
|
||||
/// <param name="view">View that will be bound to a view model</param>
|
||||
/// <param name="viewModel">View model the view will be bound to</param>
|
||||
void Bind<TViewModel>(Control view, TViewModel viewModel)
|
||||
where TViewModel : class;
|
||||
|
||||
/// <summary>
|
||||
/// Binds the specified view to the view model specified in its DataContext
|
||||
/// </summary>
|
||||
/// <param name="view">View that will be bound</param>
|
||||
void Bind(Control view);
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.AutoBinding
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
|
||||
using System.Windows.Forms;
|
||||
using Nuclex.Windows.Forms.Views;
|
||||
|
||||
namespace Nuclex.Windows.Forms.AutoBinding {
|
||||
|
||||
/// <summary>Binds views to their view models</summary>
|
||||
public interface IAutoBinder {
|
||||
|
||||
/// <summary>Binds the specified view to an explicitly selected view model</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model the view will be bound to
|
||||
/// </typeparam>
|
||||
/// <param name="view">View that will be bound to a view model</param>
|
||||
/// <param name="viewModel">View model the view will be bound to</param>
|
||||
void Bind<TViewModel>(Control view, TViewModel viewModel)
|
||||
where TViewModel : class;
|
||||
|
||||
/// <summary>
|
||||
/// Binds the specified view to the view model specified in its DataContext
|
||||
/// </summary>
|
||||
/// <param name="view">View that will be bound</param>
|
||||
void Bind(Control view);
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.AutoBinding
|
||||
|
|
|
@ -1,187 +1,186 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.CommonDialogs {
|
||||
|
||||
/// <summary>Displays common dialogs for selecting files and directories</summary>
|
||||
public class CommonDialogManager : ICommonDialogService {
|
||||
|
||||
/// <summary>Initializes a new task dialog message service</summary>
|
||||
public CommonDialogManager() : this(NullActiveWindowTracker.Default) { }
|
||||
|
||||
/// <summary>Initializes a new task dialog message service</summary>
|
||||
/// <param name="tracker">
|
||||
/// Active window tracker used to obtain the parent window for message boxes
|
||||
/// </param>
|
||||
public CommonDialogManager(IActiveWindowTracker tracker) {
|
||||
this.tracker = tracker;
|
||||
}
|
||||
|
||||
/// <summary>Asks the user for a location to save a file under</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of the file the user wishes to save as</returns>
|
||||
public string AskForSaveLocation(string caption, params string[] masks) {
|
||||
var saveDialog = new SaveFileDialog() {
|
||||
Title = caption,
|
||||
Filter = combineMasks(masks)
|
||||
};
|
||||
|
||||
DialogResult result;
|
||||
{
|
||||
Form activeWindow = this.tracker.ActiveWindow;
|
||||
if(activeWindow == null) {
|
||||
result = saveDialog.ShowDialog();
|
||||
} else {
|
||||
result = saveDialog.ShowDialog(activeWindow);
|
||||
}
|
||||
}
|
||||
|
||||
if(result == DialogResult.OK) {
|
||||
return saveDialog.FileName;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Asks the user to select a file to open</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of the file the user selected</returns>
|
||||
public string AskForFileToOpen(string caption, params string[] masks) {
|
||||
var openDialog = new OpenFileDialog() {
|
||||
Title = caption,
|
||||
Filter = combineMasks(masks),
|
||||
CheckFileExists = true,
|
||||
CheckPathExists = true,
|
||||
Multiselect = false
|
||||
};
|
||||
|
||||
DialogResult result;
|
||||
{
|
||||
Form activeWindow = this.tracker.ActiveWindow;
|
||||
if(activeWindow == null) {
|
||||
result = openDialog.ShowDialog();
|
||||
} else {
|
||||
result = openDialog.ShowDialog(activeWindow);
|
||||
}
|
||||
}
|
||||
|
||||
if(result == DialogResult.OK) {
|
||||
return openDialog.FileName;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Asks the user to select one or more files to open</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of all files the user selected</returns>
|
||||
public string[] AskForFilesToOpen(string caption, params string[] masks) {
|
||||
var openDialog = new OpenFileDialog() {
|
||||
Title = caption,
|
||||
Filter = combineMasks(masks),
|
||||
CheckFileExists = true,
|
||||
CheckPathExists = true,
|
||||
Multiselect = true
|
||||
};
|
||||
|
||||
DialogResult result;
|
||||
{
|
||||
Form activeWindow = this.tracker.ActiveWindow;
|
||||
if(activeWindow == null) {
|
||||
result = openDialog.ShowDialog();
|
||||
} else {
|
||||
result = openDialog.ShowDialog(activeWindow);
|
||||
}
|
||||
}
|
||||
|
||||
if(result == DialogResult.OK) {
|
||||
return openDialog.FileNames;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Asks the user to select a directory</summary>
|
||||
/// <returns>The directory the user has selected</returns>
|
||||
public string AskForDirectory(string caption) {
|
||||
var folderDialog = new System.Windows.Forms.FolderBrowserDialog() {
|
||||
Description = caption
|
||||
};
|
||||
|
||||
DialogResult result;
|
||||
{
|
||||
Form activeWindow = this.tracker.ActiveWindow;
|
||||
if(activeWindow == null) {
|
||||
result = folderDialog.ShowDialog();
|
||||
} else {
|
||||
result = folderDialog.ShowDialog(activeWindow);
|
||||
}
|
||||
}
|
||||
|
||||
if(result == DialogResult.OK) {
|
||||
return folderDialog.SelectedPath;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Combines an array of file masks into a single mask</summary>
|
||||
/// <param name="masks">Masks that will be combined</param>
|
||||
/// <returns>That combined masks</returns>
|
||||
private static string combineMasks(string[] masks) {
|
||||
if((masks == null) || (masks.Length == 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int requiredCapacity = 0;
|
||||
for(int index = 0; index < masks.Length; ++index) {
|
||||
requiredCapacity += masks[index].Length;
|
||||
requiredCapacity += 1;
|
||||
}
|
||||
|
||||
var maskBuilder = new StringBuilder(requiredCapacity);
|
||||
maskBuilder.Append(masks[0]);
|
||||
for(int index = 1; index < masks.Length; ++index) {
|
||||
maskBuilder.Append('|');
|
||||
maskBuilder.Append(masks[index]);
|
||||
}
|
||||
|
||||
return maskBuilder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Provides the currently active top-level window</summary>
|
||||
private IActiveWindowTracker tracker;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.CommonDialogs
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.CommonDialogs {
|
||||
|
||||
/// <summary>Displays common dialogs for selecting files and directories</summary>
|
||||
public class CommonDialogManager : ICommonDialogService {
|
||||
|
||||
/// <summary>Initializes a new task dialog message service</summary>
|
||||
public CommonDialogManager() : this(NullActiveWindowTracker.Default) { }
|
||||
|
||||
/// <summary>Initializes a new task dialog message service</summary>
|
||||
/// <param name="tracker">
|
||||
/// Active window tracker used to obtain the parent window for message boxes
|
||||
/// </param>
|
||||
public CommonDialogManager(IActiveWindowTracker tracker) {
|
||||
this.tracker = tracker;
|
||||
}
|
||||
|
||||
/// <summary>Asks the user for a location to save a file under</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of the file the user wishes to save as</returns>
|
||||
public string AskForSaveLocation(string caption, params string[] masks) {
|
||||
var saveDialog = new SaveFileDialog() {
|
||||
Title = caption,
|
||||
Filter = combineMasks(masks)
|
||||
};
|
||||
|
||||
DialogResult result;
|
||||
{
|
||||
Form activeWindow = this.tracker.ActiveWindow;
|
||||
if(activeWindow == null) {
|
||||
result = saveDialog.ShowDialog();
|
||||
} else {
|
||||
result = saveDialog.ShowDialog(activeWindow);
|
||||
}
|
||||
}
|
||||
|
||||
if(result == DialogResult.OK) {
|
||||
return saveDialog.FileName;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Asks the user to select a file to open</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of the file the user selected</returns>
|
||||
public string AskForFileToOpen(string caption, params string[] masks) {
|
||||
var openDialog = new OpenFileDialog() {
|
||||
Title = caption,
|
||||
Filter = combineMasks(masks),
|
||||
CheckFileExists = true,
|
||||
CheckPathExists = true,
|
||||
Multiselect = false
|
||||
};
|
||||
|
||||
DialogResult result;
|
||||
{
|
||||
Form activeWindow = this.tracker.ActiveWindow;
|
||||
if(activeWindow == null) {
|
||||
result = openDialog.ShowDialog();
|
||||
} else {
|
||||
result = openDialog.ShowDialog(activeWindow);
|
||||
}
|
||||
}
|
||||
|
||||
if(result == DialogResult.OK) {
|
||||
return openDialog.FileName;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Asks the user to select one or more files to open</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of all files the user selected</returns>
|
||||
public string[] AskForFilesToOpen(string caption, params string[] masks) {
|
||||
var openDialog = new OpenFileDialog() {
|
||||
Title = caption,
|
||||
Filter = combineMasks(masks),
|
||||
CheckFileExists = true,
|
||||
CheckPathExists = true,
|
||||
Multiselect = true
|
||||
};
|
||||
|
||||
DialogResult result;
|
||||
{
|
||||
Form activeWindow = this.tracker.ActiveWindow;
|
||||
if(activeWindow == null) {
|
||||
result = openDialog.ShowDialog();
|
||||
} else {
|
||||
result = openDialog.ShowDialog(activeWindow);
|
||||
}
|
||||
}
|
||||
|
||||
if(result == DialogResult.OK) {
|
||||
return openDialog.FileNames;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Asks the user to select a directory</summary>
|
||||
/// <returns>The directory the user has selected</returns>
|
||||
public string AskForDirectory(string caption) {
|
||||
var folderDialog = new System.Windows.Forms.FolderBrowserDialog() {
|
||||
Description = caption
|
||||
};
|
||||
|
||||
DialogResult result;
|
||||
{
|
||||
Form activeWindow = this.tracker.ActiveWindow;
|
||||
if(activeWindow == null) {
|
||||
result = folderDialog.ShowDialog();
|
||||
} else {
|
||||
result = folderDialog.ShowDialog(activeWindow);
|
||||
}
|
||||
}
|
||||
|
||||
if(result == DialogResult.OK) {
|
||||
return folderDialog.SelectedPath;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Combines an array of file masks into a single mask</summary>
|
||||
/// <param name="masks">Masks that will be combined</param>
|
||||
/// <returns>That combined masks</returns>
|
||||
private static string combineMasks(string[] masks) {
|
||||
if((masks == null) || (masks.Length == 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int requiredCapacity = 0;
|
||||
for(int index = 0; index < masks.Length; ++index) {
|
||||
requiredCapacity += masks[index].Length;
|
||||
requiredCapacity += 1;
|
||||
}
|
||||
|
||||
var maskBuilder = new StringBuilder(requiredCapacity);
|
||||
maskBuilder.Append(masks[0]);
|
||||
for(int index = 1; index < masks.Length; ++index) {
|
||||
maskBuilder.Append('|');
|
||||
maskBuilder.Append(masks[index]);
|
||||
}
|
||||
|
||||
return maskBuilder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Provides the currently active top-level window</summary>
|
||||
private IActiveWindowTracker tracker;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.CommonDialogs
|
||||
|
|
|
@ -1,58 +1,57 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.CommonDialogs {
|
||||
|
||||
/// <summary>Displays common dialogs for selecting files and directories</summary>
|
||||
public interface ICommonDialogService {
|
||||
|
||||
/// <summary>Asks the user for a location to save a file under</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of the file the user wishes to save as</returns>
|
||||
string AskForSaveLocation(string caption, params string[] masks);
|
||||
|
||||
/// <summary>Asks the user to select a file to open</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of the file the user selected</returns>
|
||||
string AskForFileToOpen(string caption, params string[] masks);
|
||||
|
||||
/// <summary>Asks the user to select one or more files to open</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of all files the user selected</returns>
|
||||
string[] AskForFilesToOpen(string caption, params string[] masks);
|
||||
|
||||
/// <summary>Asks the user to select a directory</summary>
|
||||
/// <returns>The directory the user has selected</returns>
|
||||
string AskForDirectory(string caption);
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.CommonDialogs
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.CommonDialogs {
|
||||
|
||||
/// <summary>Displays common dialogs for selecting files and directories</summary>
|
||||
public interface ICommonDialogService {
|
||||
|
||||
/// <summary>Asks the user for a location to save a file under</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of the file the user wishes to save as</returns>
|
||||
string AskForSaveLocation(string caption, params string[] masks);
|
||||
|
||||
/// <summary>Asks the user to select a file to open</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of the file the user selected</returns>
|
||||
string AskForFileToOpen(string caption, params string[] masks);
|
||||
|
||||
/// <summary>Asks the user to select one or more files to open</summary>
|
||||
/// <param name="caption">Caption of the dialog</param>
|
||||
/// <param name="masks">
|
||||
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
|
||||
/// </param>
|
||||
/// <returns>The full path of all files the user selected</returns>
|
||||
string[] AskForFilesToOpen(string caption, params string[] masks);
|
||||
|
||||
/// <summary>Asks the user to select a directory</summary>
|
||||
/// <returns>The directory the user has selected</returns>
|
||||
string AskForDirectory(string caption);
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.CommonDialogs
|
||||
|
|
105
Source/ContainerListView/ContainerListView.Designer.cs
generated
105
Source/ContainerListView/ContainerListView.Designer.cs
generated
|
@ -1,53 +1,52 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
partial class ContainerListView {
|
||||
|
||||
/// <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 Component 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() {
|
||||
components = new System.ComponentModel.Container();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
partial class ContainerListView {
|
||||
|
||||
/// <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 Component 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() {
|
||||
components = new System.ComponentModel.Container();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,83 +1,82 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Unit Test for the control container list view</summary>
|
||||
[TestFixture, Explicit]
|
||||
public class ContainerListViewTest {
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the asynchronous progress bar's constructor is working
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestConstructor() {
|
||||
using(ContainerListView listView = new ContainerListView()) {
|
||||
|
||||
// Let the control create its window handle
|
||||
listView.CreateControl();
|
||||
listView.Columns.Add("Numeric");
|
||||
listView.Columns.Add("Spelled");
|
||||
listView.Columns.Add("Nonsense");
|
||||
|
||||
addRow(listView, "1", "One");
|
||||
addRow(listView, "2", "Two");
|
||||
addRow(listView, "3", "Three");
|
||||
|
||||
using(CheckBox checkBox = new CheckBox()) {
|
||||
listView.EmbeddedControls.Add(new ListViewEmbeddedControl(checkBox, 2, 0));
|
||||
listView.EmbeddedControls.Clear();
|
||||
|
||||
listView.Refresh();
|
||||
|
||||
ListViewEmbeddedControl embeddedControl = new ListViewEmbeddedControl(
|
||||
checkBox, 2, 0
|
||||
);
|
||||
listView.EmbeddedControls.Add(embeddedControl);
|
||||
listView.EmbeddedControls.Remove(embeddedControl);
|
||||
|
||||
listView.Refresh();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds a row to a control container list view</summary>
|
||||
/// <param name="listView">List view control the row will be added to</param>
|
||||
/// <param name="columns">Values that will appear in the individual columns</param>
|
||||
private void addRow(ContainerListView listView, params string[] columns) {
|
||||
listView.Items.Add(new ListViewItem(columns));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
||||
#endif // UNITTEST
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Unit Test for the control container list view</summary>
|
||||
[TestFixture, Explicit]
|
||||
public class ContainerListViewTest {
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the asynchronous progress bar's constructor is working
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestConstructor() {
|
||||
using(ContainerListView listView = new ContainerListView()) {
|
||||
|
||||
// Let the control create its window handle
|
||||
listView.CreateControl();
|
||||
listView.Columns.Add("Numeric");
|
||||
listView.Columns.Add("Spelled");
|
||||
listView.Columns.Add("Nonsense");
|
||||
|
||||
addRow(listView, "1", "One");
|
||||
addRow(listView, "2", "Two");
|
||||
addRow(listView, "3", "Three");
|
||||
|
||||
using(CheckBox checkBox = new CheckBox()) {
|
||||
listView.EmbeddedControls.Add(new ListViewEmbeddedControl(checkBox, 2, 0));
|
||||
listView.EmbeddedControls.Clear();
|
||||
|
||||
listView.Refresh();
|
||||
|
||||
ListViewEmbeddedControl embeddedControl = new ListViewEmbeddedControl(
|
||||
checkBox, 2, 0
|
||||
);
|
||||
listView.EmbeddedControls.Add(embeddedControl);
|
||||
listView.EmbeddedControls.Remove(embeddedControl);
|
||||
|
||||
listView.Refresh();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds a row to a control container list view</summary>
|
||||
/// <param name="listView">List view control the row will be added to</param>
|
||||
/// <param name="columns">Values that will appear in the individual columns</param>
|
||||
private void addRow(ContainerListView listView, params string[] columns) {
|
||||
listView.Items.Add(new ListViewItem(columns));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
||||
#endif // UNITTEST
|
||||
|
|
|
@ -1,247 +1,246 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Nuclex.Support.Collections;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>ListView allowing for other controls to be embedded in its cells</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// There basically were two possible design choices: Provide a specialized
|
||||
/// ListViewSubItem that carries a Control instead of a string or manage the
|
||||
/// embedded controls seperate of the ListView's items.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The first option requires a complete rewrite of the ListViewItem class
|
||||
/// and its related support classes, all of which are surprisingly large and
|
||||
/// complex. Thus, I chose the less clean but more doable latter option.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This control is useful for simple item lists where you want to provide
|
||||
/// a combobox, checkbox or other control to the user for a certain column.
|
||||
/// It will not perform well for lists with hundreds of items since it
|
||||
/// requires a control to be created per row and management of the embedded
|
||||
/// controls is designed for limited usage.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public partial class ContainerListView : System.Windows.Forms.ListView {
|
||||
|
||||
/// <summary>Message sent to a control to let it paint itself</summary>
|
||||
private const int WM_PAINT = 0x000F;
|
||||
|
||||
/// <summary>Initializes a new ContainerListView</summary>
|
||||
public ContainerListView() {
|
||||
this.embeddedControlClickedDelegate = new EventHandler(embeddedControlClicked);
|
||||
|
||||
this.embeddedControls = new ObservableList<ListViewEmbeddedControl>();
|
||||
this.embeddedControls.ItemAdded +=
|
||||
new EventHandler<ItemEventArgs<ListViewEmbeddedControl>>(embeddedControlAdded);
|
||||
this.embeddedControls.ItemRemoved +=
|
||||
new EventHandler<ItemEventArgs<ListViewEmbeddedControl>>(embeddedControlRemoved);
|
||||
this.embeddedControls.Clearing += new EventHandler(embeddedControlsClearing);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// Eliminate flickering
|
||||
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
|
||||
|
||||
base.View = View.Details;
|
||||
|
||||
this.columnHeaderHeight = Font.Height;
|
||||
}
|
||||
|
||||
/// <summary>Controls being embedded in the ListView</summary>
|
||||
public ICollection<ListViewEmbeddedControl> EmbeddedControls {
|
||||
get { return this.embeddedControls; }
|
||||
}
|
||||
|
||||
/// <summary>Updates the controls embeded into the list view</summary>
|
||||
public void UpdateEmbeddedControls() {
|
||||
if(View != View.Details) {
|
||||
for(int index = 0; index < this.embeddedControls.Count; ++index) {
|
||||
this.embeddedControls[index].Control.Visible = false;
|
||||
}
|
||||
} else {
|
||||
for(int index = 0; index < this.embeddedControls.Count; ++index) {
|
||||
ListViewEmbeddedControl embeddedControl = this.embeddedControls[index];
|
||||
|
||||
Rectangle cellBounds = this.GetSubItemBounds(
|
||||
Items[embeddedControl.Row], embeddedControl.Column
|
||||
);
|
||||
|
||||
bool intersectsColumnHeader =
|
||||
(base.HeaderStyle != ColumnHeaderStyle.None) &&
|
||||
(cellBounds.Top < base.Font.Height);
|
||||
|
||||
embeddedControl.Control.Visible = !intersectsColumnHeader;
|
||||
embeddedControl.Control.Bounds = cellBounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculates the boundaries of a cell in the list view</summary>
|
||||
/// <param name="item">Item in the list view from which to calculate the cell</param>
|
||||
/// <param name="subItem">Index der cell whose boundaries to calculate</param>
|
||||
/// <returns>The boundaries of the specified list view cell</returns>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// When the specified sub item index is not in the range of valid sub items
|
||||
/// </exception>
|
||||
protected Rectangle GetSubItemBounds(ListViewItem item, int subItem) {
|
||||
int[] order = GetColumnOrder();
|
||||
if(order == null) { // No Columns
|
||||
return Rectangle.Empty;
|
||||
}
|
||||
|
||||
if(subItem >= order.Length) {
|
||||
throw new IndexOutOfRangeException("SubItem " + subItem + " out of range");
|
||||
}
|
||||
|
||||
// Determine the border of the entire ListViewItem, including all sub items
|
||||
Rectangle itemBounds = item.GetBounds(ItemBoundsPortion.Entire);
|
||||
int subItemX = itemBounds.Left;
|
||||
|
||||
// Find the horizontal position of the sub item. Because the column order can vary,
|
||||
// we need to use Columns[order[i]] instead of simply doing Columns[i] here!
|
||||
ColumnHeader columnHeader;
|
||||
int i;
|
||||
for(i = 0; i < order.Length; ++i) {
|
||||
columnHeader = this.Columns[order[i]];
|
||||
if(columnHeader.Index == subItem) {
|
||||
break;
|
||||
}
|
||||
|
||||
subItemX += columnHeader.Width;
|
||||
}
|
||||
|
||||
return new Rectangle(
|
||||
subItemX, itemBounds.Top, this.Columns[order[i]].Width, itemBounds.Height
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Responds to window messages sent by the operating system</summary>
|
||||
/// <param name="message">Window message that will be processed</param>
|
||||
protected override void WndProc(ref Message message) {
|
||||
switch(message.Msg) {
|
||||
case WM_PAINT: {
|
||||
UpdateEmbeddedControls();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
base.WndProc(ref message);
|
||||
}
|
||||
|
||||
/// <summary>Called when the list of embedded controls has been cleared</summary>
|
||||
/// <param name="sender">Collection that has been cleared of its controls</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void embeddedControlsClearing(object sender, EventArgs arguments) {
|
||||
this.BeginUpdate();
|
||||
try {
|
||||
foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) {
|
||||
embeddedControl.Control.Click -= this.embeddedControlClickedDelegate;
|
||||
this.Controls.Remove(embeddedControl.Control);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.EndUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when a control gets removed from the embedded controls list</summary>
|
||||
/// <param name="sender">List from which the control has been removed</param>
|
||||
/// <param name="arguments">
|
||||
/// Event arguments providing a reference to the removed control
|
||||
/// </param>
|
||||
private void embeddedControlAdded(
|
||||
object sender, ItemEventArgs<ListViewEmbeddedControl> arguments
|
||||
) {
|
||||
arguments.Item.Control.Click += this.embeddedControlClickedDelegate;
|
||||
this.Controls.Add(arguments.Item.Control);
|
||||
}
|
||||
|
||||
/// <summary>Called when a control gets added to the embedded controls list</summary>
|
||||
/// <param name="sender">List to which the control has been added</param>
|
||||
/// <param name="arguments">
|
||||
/// Event arguments providing a reference to the added control
|
||||
/// </param>
|
||||
private void embeddedControlRemoved(
|
||||
object sender, ItemEventArgs<ListViewEmbeddedControl> arguments
|
||||
) {
|
||||
if(this.Controls.Contains(arguments.Item.Control)) {
|
||||
arguments.Item.Control.Click -= this.embeddedControlClickedDelegate;
|
||||
this.Controls.Remove(arguments.Item.Control);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when an embedded control has been clicked on</summary>
|
||||
/// <param name="sender">Embedded control that has been clicked</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void embeddedControlClicked(object sender, EventArgs arguments) {
|
||||
this.BeginUpdate();
|
||||
|
||||
try {
|
||||
SelectedItems.Clear();
|
||||
|
||||
foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) {
|
||||
if(ReferenceEquals(embeddedControl.Control, sender)) {
|
||||
if((embeddedControl.Row > 0) && (embeddedControl.Row < Items.Count)) {
|
||||
Items[embeddedControl.Row].Selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.EndUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Obtains the current column order of the list</summary>
|
||||
/// <returns>An array indicating the order of the list's columns</returns>
|
||||
private int[] GetColumnOrder() {
|
||||
int[] order = new int[this.Columns.Count];
|
||||
|
||||
for(int index = 0; index < this.Columns.Count; ++index) {
|
||||
order[this.Columns[index].DisplayIndex] = index;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
/// <summary>Height of the list view's column header</summary>
|
||||
private int columnHeaderHeight;
|
||||
/// <summary>Event handler for when embedded controls are clicked on</summary>
|
||||
private EventHandler embeddedControlClickedDelegate;
|
||||
/// <summary>Controls being embedded in this ListView</summary>
|
||||
private ObservableList<ListViewEmbeddedControl> embeddedControls;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Data;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Nuclex.Support.Collections;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>ListView allowing for other controls to be embedded in its cells</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// There basically were two possible design choices: Provide a specialized
|
||||
/// ListViewSubItem that carries a Control instead of a string or manage the
|
||||
/// embedded controls seperate of the ListView's items.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The first option requires a complete rewrite of the ListViewItem class
|
||||
/// and its related support classes, all of which are surprisingly large and
|
||||
/// complex. Thus, I chose the less clean but more doable latter option.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This control is useful for simple item lists where you want to provide
|
||||
/// a combobox, checkbox or other control to the user for a certain column.
|
||||
/// It will not perform well for lists with hundreds of items since it
|
||||
/// requires a control to be created per row and management of the embedded
|
||||
/// controls is designed for limited usage.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public partial class ContainerListView : System.Windows.Forms.ListView {
|
||||
|
||||
/// <summary>Message sent to a control to let it paint itself</summary>
|
||||
private const int WM_PAINT = 0x000F;
|
||||
|
||||
/// <summary>Initializes a new ContainerListView</summary>
|
||||
public ContainerListView() {
|
||||
this.embeddedControlClickedDelegate = new EventHandler(embeddedControlClicked);
|
||||
|
||||
this.embeddedControls = new ObservableList<ListViewEmbeddedControl>();
|
||||
this.embeddedControls.ItemAdded +=
|
||||
new EventHandler<ItemEventArgs<ListViewEmbeddedControl>>(embeddedControlAdded);
|
||||
this.embeddedControls.ItemRemoved +=
|
||||
new EventHandler<ItemEventArgs<ListViewEmbeddedControl>>(embeddedControlRemoved);
|
||||
this.embeddedControls.Clearing += new EventHandler(embeddedControlsClearing);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// Eliminate flickering
|
||||
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
|
||||
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
|
||||
|
||||
base.View = View.Details;
|
||||
|
||||
this.columnHeaderHeight = Font.Height;
|
||||
}
|
||||
|
||||
/// <summary>Controls being embedded in the ListView</summary>
|
||||
public ICollection<ListViewEmbeddedControl> EmbeddedControls {
|
||||
get { return this.embeddedControls; }
|
||||
}
|
||||
|
||||
/// <summary>Updates the controls embeded into the list view</summary>
|
||||
public void UpdateEmbeddedControls() {
|
||||
if(View != View.Details) {
|
||||
for(int index = 0; index < this.embeddedControls.Count; ++index) {
|
||||
this.embeddedControls[index].Control.Visible = false;
|
||||
}
|
||||
} else {
|
||||
for(int index = 0; index < this.embeddedControls.Count; ++index) {
|
||||
ListViewEmbeddedControl embeddedControl = this.embeddedControls[index];
|
||||
|
||||
Rectangle cellBounds = this.GetSubItemBounds(
|
||||
Items[embeddedControl.Row], embeddedControl.Column
|
||||
);
|
||||
|
||||
bool intersectsColumnHeader =
|
||||
(base.HeaderStyle != ColumnHeaderStyle.None) &&
|
||||
(cellBounds.Top < base.Font.Height);
|
||||
|
||||
embeddedControl.Control.Visible = !intersectsColumnHeader;
|
||||
embeddedControl.Control.Bounds = cellBounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculates the boundaries of a cell in the list view</summary>
|
||||
/// <param name="item">Item in the list view from which to calculate the cell</param>
|
||||
/// <param name="subItem">Index der cell whose boundaries to calculate</param>
|
||||
/// <returns>The boundaries of the specified list view cell</returns>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// When the specified sub item index is not in the range of valid sub items
|
||||
/// </exception>
|
||||
protected Rectangle GetSubItemBounds(ListViewItem item, int subItem) {
|
||||
int[] order = GetColumnOrder();
|
||||
if(order == null) { // No Columns
|
||||
return Rectangle.Empty;
|
||||
}
|
||||
|
||||
if(subItem >= order.Length) {
|
||||
throw new IndexOutOfRangeException("SubItem " + subItem + " out of range");
|
||||
}
|
||||
|
||||
// Determine the border of the entire ListViewItem, including all sub items
|
||||
Rectangle itemBounds = item.GetBounds(ItemBoundsPortion.Entire);
|
||||
int subItemX = itemBounds.Left;
|
||||
|
||||
// Find the horizontal position of the sub item. Because the column order can vary,
|
||||
// we need to use Columns[order[i]] instead of simply doing Columns[i] here!
|
||||
ColumnHeader columnHeader;
|
||||
int i;
|
||||
for(i = 0; i < order.Length; ++i) {
|
||||
columnHeader = this.Columns[order[i]];
|
||||
if(columnHeader.Index == subItem) {
|
||||
break;
|
||||
}
|
||||
|
||||
subItemX += columnHeader.Width;
|
||||
}
|
||||
|
||||
return new Rectangle(
|
||||
subItemX, itemBounds.Top, this.Columns[order[i]].Width, itemBounds.Height
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Responds to window messages sent by the operating system</summary>
|
||||
/// <param name="message">Window message that will be processed</param>
|
||||
protected override void WndProc(ref Message message) {
|
||||
switch(message.Msg) {
|
||||
case WM_PAINT: {
|
||||
UpdateEmbeddedControls();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
base.WndProc(ref message);
|
||||
}
|
||||
|
||||
/// <summary>Called when the list of embedded controls has been cleared</summary>
|
||||
/// <param name="sender">Collection that has been cleared of its controls</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void embeddedControlsClearing(object sender, EventArgs arguments) {
|
||||
this.BeginUpdate();
|
||||
try {
|
||||
foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) {
|
||||
embeddedControl.Control.Click -= this.embeddedControlClickedDelegate;
|
||||
this.Controls.Remove(embeddedControl.Control);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.EndUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when a control gets removed from the embedded controls list</summary>
|
||||
/// <param name="sender">List from which the control has been removed</param>
|
||||
/// <param name="arguments">
|
||||
/// Event arguments providing a reference to the removed control
|
||||
/// </param>
|
||||
private void embeddedControlAdded(
|
||||
object sender, ItemEventArgs<ListViewEmbeddedControl> arguments
|
||||
) {
|
||||
arguments.Item.Control.Click += this.embeddedControlClickedDelegate;
|
||||
this.Controls.Add(arguments.Item.Control);
|
||||
}
|
||||
|
||||
/// <summary>Called when a control gets added to the embedded controls list</summary>
|
||||
/// <param name="sender">List to which the control has been added</param>
|
||||
/// <param name="arguments">
|
||||
/// Event arguments providing a reference to the added control
|
||||
/// </param>
|
||||
private void embeddedControlRemoved(
|
||||
object sender, ItemEventArgs<ListViewEmbeddedControl> arguments
|
||||
) {
|
||||
if(this.Controls.Contains(arguments.Item.Control)) {
|
||||
arguments.Item.Control.Click -= this.embeddedControlClickedDelegate;
|
||||
this.Controls.Remove(arguments.Item.Control);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when an embedded control has been clicked on</summary>
|
||||
/// <param name="sender">Embedded control that has been clicked</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void embeddedControlClicked(object sender, EventArgs arguments) {
|
||||
this.BeginUpdate();
|
||||
|
||||
try {
|
||||
SelectedItems.Clear();
|
||||
|
||||
foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) {
|
||||
if(ReferenceEquals(embeddedControl.Control, sender)) {
|
||||
if((embeddedControl.Row > 0) && (embeddedControl.Row < Items.Count)) {
|
||||
Items[embeddedControl.Row].Selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.EndUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Obtains the current column order of the list</summary>
|
||||
/// <returns>An array indicating the order of the list's columns</returns>
|
||||
private int[] GetColumnOrder() {
|
||||
int[] order = new int[this.Columns.Count];
|
||||
|
||||
for(int index = 0; index < this.Columns.Count; ++index) {
|
||||
order[this.Columns[index].DisplayIndex] = index;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
/// <summary>Height of the list view's column header</summary>
|
||||
private int columnHeaderHeight;
|
||||
/// <summary>Event handler for when embedded controls are clicked on</summary>
|
||||
private EventHandler embeddedControlClickedDelegate;
|
||||
/// <summary>Controls being embedded in this ListView</summary>
|
||||
private ObservableList<ListViewEmbeddedControl> embeddedControls;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,64 +1,63 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Stores informations about an embedded control</summary>
|
||||
public class ListViewEmbeddedControl {
|
||||
|
||||
/// <summary>Initializes a new embedded control holder</summary>
|
||||
/// <param name="control">Control being embedded in a list view</param>
|
||||
/// <param name="row">List row at which the control will be embedded</param>
|
||||
/// <param name="column">List column at which the control will be embedded</param>
|
||||
public ListViewEmbeddedControl(Control control, int row, int column) {
|
||||
this.control = control;
|
||||
this.row = row;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
/// <summary>Control that is being embedded in the ListView</summary>
|
||||
public Control Control {
|
||||
get { return this.control; }
|
||||
}
|
||||
|
||||
/// <summary>Row the control has been embedded in</summary>
|
||||
public int Row {
|
||||
get { return this.row; }
|
||||
}
|
||||
|
||||
/// <summary>Column the control has been embedded in</summary>
|
||||
public int Column {
|
||||
get { return this.column; }
|
||||
}
|
||||
|
||||
/// <summary>Embedded control</summary>
|
||||
private Control control;
|
||||
/// <summary>Row where the control is embedded</summary>
|
||||
private int row;
|
||||
/// <summary>Column where the control is embedded</summary>
|
||||
private int column;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Stores informations about an embedded control</summary>
|
||||
public class ListViewEmbeddedControl {
|
||||
|
||||
/// <summary>Initializes a new embedded control holder</summary>
|
||||
/// <param name="control">Control being embedded in a list view</param>
|
||||
/// <param name="row">List row at which the control will be embedded</param>
|
||||
/// <param name="column">List column at which the control will be embedded</param>
|
||||
public ListViewEmbeddedControl(Control control, int row, int column) {
|
||||
this.control = control;
|
||||
this.row = row;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
/// <summary>Control that is being embedded in the ListView</summary>
|
||||
public Control Control {
|
||||
get { return this.control; }
|
||||
}
|
||||
|
||||
/// <summary>Row the control has been embedded in</summary>
|
||||
public int Row {
|
||||
get { return this.row; }
|
||||
}
|
||||
|
||||
/// <summary>Column the control has been embedded in</summary>
|
||||
public int Column {
|
||||
get { return this.column; }
|
||||
}
|
||||
|
||||
/// <summary>Embedded control</summary>
|
||||
private Control control;
|
||||
/// <summary>Row where the control is embedded</summary>
|
||||
private int row;
|
||||
/// <summary>Column where the control is embedded</summary>
|
||||
private int column;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
117
Source/Controls/ProgressSpinner.Designer.cs
generated
117
Source/Controls/ProgressSpinner.Designer.cs
generated
|
@ -1,49 +1,68 @@
|
|||
namespace Nuclex.Windows.Forms.Controls {
|
||||
|
||||
partial class ProgressSpinner {
|
||||
|
||||
/// <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 Component 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.animationUpdateTimer = new System.Windows.Forms.Timer();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// animationUpdateTimer
|
||||
//
|
||||
this.animationUpdateTimer.Tick += new System.EventHandler(this.animationTimerTicked);
|
||||
//
|
||||
// ProgressSpinner
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.Transparent;
|
||||
this.DoubleBuffered = true;
|
||||
this.Name = "ProgressSpinner";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Timer used to update the progress animation</summary>
|
||||
private System.Windows.Forms.Timer animationUpdateTimer;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Controls
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
namespace Nuclex.Windows.Forms.Controls {
|
||||
|
||||
partial class ProgressSpinner {
|
||||
|
||||
/// <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 Component 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.animationUpdateTimer = new System.Windows.Forms.Timer();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// animationUpdateTimer
|
||||
//
|
||||
this.animationUpdateTimer.Tick += new System.EventHandler(this.animationTimerTicked);
|
||||
//
|
||||
// ProgressSpinner
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.BackColor = System.Drawing.Color.Transparent;
|
||||
this.DoubleBuffered = true;
|
||||
this.Name = "ProgressSpinner";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>Timer used to update the progress animation</summary>
|
||||
private System.Windows.Forms.Timer animationUpdateTimer;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Controls
|
||||
|
|
|
@ -1,297 +1,316 @@
|
|||
using System;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Controls {
|
||||
|
||||
/// <summary>Displays a progress spinner to entertain the user while waiting</summary>
|
||||
public partial class ProgressSpinner : UserControl {
|
||||
|
||||
/// <summary>Number of dots the progress spinner will display</summary>
|
||||
private const int DotCount = 8;
|
||||
/// <summary>Size of a normal dot (only ever assumed by the trailing dot)</summary>
|
||||
private const int DotRadius = 4;
|
||||
/// <summary>
|
||||
/// The leading dot will be DotCount times this larger than a normal dot
|
||||
/// </summary>
|
||||
private const int ScaleFactor = 1;
|
||||
|
||||
/// <summary>Initializes a new progress spinner</summary>
|
||||
public ProgressSpinner() {
|
||||
SetStyle(
|
||||
(
|
||||
ControlStyles.AllPaintingInWmPaint |
|
||||
ControlStyles.OptimizedDoubleBuffer |
|
||||
ControlStyles.ResizeRedraw | ControlStyles.UserPaint |
|
||||
ControlStyles.SupportsTransparentBackColor
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Disposed += new EventHandler(OnDisposed);
|
||||
|
||||
if(!DesignMode) {
|
||||
StartSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Releases all resources owned by the control when it is destroyed</summary>
|
||||
/// <param name="sender">Control that is being destroyed</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void OnDisposed(object sender, EventArgs arguments) {
|
||||
if(this.dotOutlinePen != null) {
|
||||
this.dotOutlinePen.Dispose();
|
||||
this.dotOutlinePen = null;
|
||||
}
|
||||
if(this.dotFillBrush != null) {
|
||||
this.dotFillBrush.Dispose();
|
||||
this.dotFillBrush = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starts the spinner's animation</summary>
|
||||
public void StartSpinner() {
|
||||
this.spinnerRunning = true;
|
||||
this.animationUpdateTimer.Enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>Stops the spinner's animation</summary>
|
||||
public void StopSpinner() {
|
||||
this.animationUpdateTimer.Enabled = false;
|
||||
this.spinnerRunning = false;
|
||||
}
|
||||
|
||||
/// <summary>Color used to fill the dots</summary>
|
||||
public Color DotFillColor {
|
||||
get { return this.dotFillColor; }
|
||||
set {
|
||||
if(value != this.dotFillColor) {
|
||||
this.dotFillColor = value;
|
||||
if(this.dotFillBrush != null) {
|
||||
this.dotFillBrush.Dispose();
|
||||
this.dotFillBrush = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Color used for the dots' outline</summary>
|
||||
public Color DotOutlineColor {
|
||||
get { return this.dotOutlineColor; }
|
||||
set {
|
||||
if(value != this.dotOutlineColor) {
|
||||
this.dotOutlineColor = value;
|
||||
if(this.dotOutlinePen != null) {
|
||||
this.dotOutlinePen.Dispose();
|
||||
this.dotOutlinePen = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculates the optimal size for the spinner control</summary>
|
||||
/// <returns>The optimal size for the spinner control to have</returns>
|
||||
/// <remarks>
|
||||
/// Thanks to WinForms limited control transparency, the progress spinner needs to
|
||||
/// redraw every control behind it each time it updates. Thus it's wise to keep it
|
||||
/// as small as possible, but wide enough to fit the status text, if any.
|
||||
/// </remarks>
|
||||
public Size GetOptimalSize() {
|
||||
SizeF textRectangle;
|
||||
using(var dummyImage = new Bitmap(1, 1)) {
|
||||
using(Graphics graphics = Graphics.FromImage(dummyImage)) {
|
||||
textRectangle = graphics.MeasureString(
|
||||
this.statusText, this.statusFont
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Size(
|
||||
Math.Max(128, (int)(textRectangle.Width + 2.0f)),
|
||||
this.statusFont.Height + 128
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Font that is used to display the status text</summary>
|
||||
public Font StatusFont {
|
||||
get { return this.statusFont; }
|
||||
set { this.statusFont = value; }
|
||||
}
|
||||
|
||||
/// <summary>Text that will be displayed as the control's status</summary>
|
||||
public string StatusText {
|
||||
get { return this.statusText; }
|
||||
set { this.statusText = value; }
|
||||
}
|
||||
|
||||
/// <summary>Called when the control is hidden or shown</summary>
|
||||
/// <param name="arguments">Not used</param>
|
||||
protected override void OnVisibleChanged(EventArgs arguments) {
|
||||
base.OnVisibleChanged(arguments);
|
||||
this.animationUpdateTimer.Enabled = this.spinnerRunning && Visible;
|
||||
}
|
||||
|
||||
/// <summary>Called when the control should redraw itself</summary>
|
||||
/// <param name="arguments">Provides access to the drawing surface and tools</param>
|
||||
protected override void OnPaint(PaintEventArgs arguments) {
|
||||
paintControlsBehindMe(arguments);
|
||||
paintAnimatedDots(arguments);
|
||||
paintStatusMessage(arguments);
|
||||
}
|
||||
|
||||
/// <summary>Forcefully redraws the controls below this one</summary>
|
||||
/// <param name="arguments">Provides access to the drawing surface and tools</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// WinForms has very poor transparency support. A transparent control will only
|
||||
/// be transparent to its immediate parent (so the parent needs to be a container
|
||||
/// control and hold the transparent control as its preferrably only child).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Worse yet, if you manually establish this relationship in your .Designer.cs
|
||||
/// file, the Visual Studio WinForms designer will dismantle it next time you
|
||||
/// edit something. This method fixes those issues by repainting all controls
|
||||
/// that are behind this control and whose bounding box intersect this control.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void paintControlsBehindMe(PaintEventArgs arguments) {
|
||||
if(Parent != null && this.BackColor == Color.Transparent) {
|
||||
using(var bmp = new Bitmap(Parent.Width, Parent.Height)) {
|
||||
Parent.Controls.Cast<Control>()
|
||||
.Where(c => Parent.Controls.GetChildIndex(c) > Parent.Controls.GetChildIndex(this))
|
||||
.Where(c => c.Bounds.IntersectsWith(this.Bounds))
|
||||
.OrderByDescending(c => Parent.Controls.GetChildIndex(c))
|
||||
.ToList()
|
||||
.ForEach(c => c.DrawToBitmap(bmp, c.Bounds));
|
||||
|
||||
arguments.Graphics.DrawImage(bmp, -Left, -Top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Draws a simple animated dots animation</summary>
|
||||
/// <param name="arguments">Provides access to the drawing surface and tools</param>
|
||||
private void paintAnimatedDots(PaintEventArgs arguments) {
|
||||
if(this.dotOutlinePen == null) {
|
||||
this.dotOutlinePen = new Pen(this.dotOutlineColor);
|
||||
}
|
||||
if(this.dotFillBrush == null) {
|
||||
this.dotFillBrush = new SolidBrush(this.dotFillColor);
|
||||
}
|
||||
|
||||
SmoothingMode prevousSmoothingMode = arguments.Graphics.SmoothingMode;
|
||||
arguments.Graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
try {
|
||||
PointF center = new PointF(Width / 2.0f, (Height - this.statusFont.Height - 2) / 2.0f);
|
||||
|
||||
int diameter = Math.Min(Width, Height - this.statusFont.Height - 2);
|
||||
int bigRadius = diameter / 2 - DotRadius - (DotCount - 1) * ScaleFactor;
|
||||
|
||||
// Draw the dots
|
||||
float unitAngle = 360.0f / DotCount;
|
||||
for(int index = 0; index < DotCount; ++index) {
|
||||
int dotIndex = (index + leadingDotIndex) % DotCount;
|
||||
|
||||
var dotPosition = new PointF(
|
||||
center.X + (float)(bigRadius * Math.Cos(unitAngle * dotIndex * Math.PI / 180.0f)),
|
||||
center.Y + (float)(bigRadius * Math.Sin(unitAngle * dotIndex * Math.PI / 180.0f))
|
||||
);
|
||||
|
||||
int currentDotRadius = DotRadius + index * ScaleFactor;
|
||||
|
||||
var corner = new PointF(
|
||||
dotPosition.X - currentDotRadius, dotPosition.Y - currentDotRadius
|
||||
);
|
||||
arguments.Graphics.FillEllipse(
|
||||
this.dotFillBrush, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius
|
||||
);
|
||||
arguments.Graphics.DrawEllipse(
|
||||
this.dotOutlinePen, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius
|
||||
);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
arguments.Graphics.SmoothingMode = prevousSmoothingMode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Draws the status message under the animated dots</summary>
|
||||
/// <param name="arguments">Provides access to the drawing surface and tools</param>
|
||||
private void paintStatusMessage(PaintEventArgs arguments) {
|
||||
if(!string.IsNullOrEmpty(this.statusText)) {
|
||||
SizeF textRectangle = arguments.Graphics.MeasureString(
|
||||
this.statusText, this.statusFont
|
||||
);
|
||||
|
||||
var messageArea = new RectangleF(
|
||||
(Width - textRectangle.Width) / 2.0f,
|
||||
Height - this.statusFont.Height - 1.0f,
|
||||
textRectangle.Width,
|
||||
this.statusFont.Height
|
||||
);
|
||||
|
||||
// Draw text with a white halo. This is a little bit ugly...
|
||||
{
|
||||
messageArea.Offset(-1.0f, 0.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, Brushes.White, messageArea
|
||||
);
|
||||
|
||||
messageArea.Offset(2.0f, 0.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, Brushes.White, messageArea
|
||||
);
|
||||
|
||||
messageArea.Offset(-1.0f, -1.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, Brushes.White, messageArea
|
||||
);
|
||||
|
||||
messageArea.Offset(0.0f, 2.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, Brushes.White, messageArea
|
||||
);
|
||||
|
||||
messageArea.Offset(0.0f, -1.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, this.dotFillBrush, messageArea
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Called when the animation timer ticks to update the animation state</summary>
|
||||
/// <param name="sender">Animation timer that has ticked</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void animationTimerTicked(object sender, EventArgs arguments) {
|
||||
this.leadingDotIndex = (this.leadingDotIndex + 1) % DotCount; // Advance the animation
|
||||
Invalidate(); // Request a redraw at the earliest opportune time
|
||||
}
|
||||
|
||||
/// <summary>Whether the spinner has been started</summary>
|
||||
private bool spinnerRunning;
|
||||
/// <summary>Index of the currently leading dot</summary>
|
||||
private int leadingDotIndex = 0;
|
||||
/// <summary>Text that will be displayed under the control as the current status</summary>
|
||||
private string statusText;
|
||||
|
||||
/// <summary>Color in which the dots will be filled</summary>
|
||||
private Color dotFillColor = Color.RoyalBlue;
|
||||
/// <summary>Color that will be used for the dots' outline</summary>
|
||||
private Color dotOutlineColor = Color.White;
|
||||
/// <summary>Brush used to fill the dots</summary>
|
||||
private Brush dotFillBrush;
|
||||
/// <summary>Brush used for the dots' outline</summary>
|
||||
private Pen dotOutlinePen;
|
||||
/// <summary>Font that is used to display the status text</summary>
|
||||
private Font statusFont = SystemFonts.SmallCaptionFont;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Controls
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Controls {
|
||||
|
||||
/// <summary>Displays a progress spinner to entertain the user while waiting</summary>
|
||||
public partial class ProgressSpinner : UserControl {
|
||||
|
||||
/// <summary>Number of dots the progress spinner will display</summary>
|
||||
private const int DotCount = 8;
|
||||
/// <summary>Size of a normal dot (only ever assumed by the trailing dot)</summary>
|
||||
private const int DotRadius = 4;
|
||||
/// <summary>
|
||||
/// The leading dot will be DotCount times this larger than a normal dot
|
||||
/// </summary>
|
||||
private const int ScaleFactor = 1;
|
||||
|
||||
/// <summary>Initializes a new progress spinner</summary>
|
||||
public ProgressSpinner() {
|
||||
SetStyle(
|
||||
(
|
||||
ControlStyles.AllPaintingInWmPaint |
|
||||
ControlStyles.OptimizedDoubleBuffer |
|
||||
ControlStyles.ResizeRedraw | ControlStyles.UserPaint |
|
||||
ControlStyles.SupportsTransparentBackColor
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
Disposed += new EventHandler(OnDisposed);
|
||||
|
||||
if(!DesignMode) {
|
||||
StartSpinner();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Releases all resources owned by the control when it is destroyed</summary>
|
||||
/// <param name="sender">Control that is being destroyed</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void OnDisposed(object sender, EventArgs arguments) {
|
||||
if(this.dotOutlinePen != null) {
|
||||
this.dotOutlinePen.Dispose();
|
||||
this.dotOutlinePen = null;
|
||||
}
|
||||
if(this.dotFillBrush != null) {
|
||||
this.dotFillBrush.Dispose();
|
||||
this.dotFillBrush = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starts the spinner's animation</summary>
|
||||
public void StartSpinner() {
|
||||
this.spinnerRunning = true;
|
||||
this.animationUpdateTimer.Enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>Stops the spinner's animation</summary>
|
||||
public void StopSpinner() {
|
||||
this.animationUpdateTimer.Enabled = false;
|
||||
this.spinnerRunning = false;
|
||||
}
|
||||
|
||||
/// <summary>Color used to fill the dots</summary>
|
||||
public Color DotFillColor {
|
||||
get { return this.dotFillColor; }
|
||||
set {
|
||||
if(value != this.dotFillColor) {
|
||||
this.dotFillColor = value;
|
||||
if(this.dotFillBrush != null) {
|
||||
this.dotFillBrush.Dispose();
|
||||
this.dotFillBrush = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Color used for the dots' outline</summary>
|
||||
public Color DotOutlineColor {
|
||||
get { return this.dotOutlineColor; }
|
||||
set {
|
||||
if(value != this.dotOutlineColor) {
|
||||
this.dotOutlineColor = value;
|
||||
if(this.dotOutlinePen != null) {
|
||||
this.dotOutlinePen.Dispose();
|
||||
this.dotOutlinePen = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculates the optimal size for the spinner control</summary>
|
||||
/// <returns>The optimal size for the spinner control to have</returns>
|
||||
/// <remarks>
|
||||
/// Thanks to WinForms limited control transparency, the progress spinner needs to
|
||||
/// redraw every control behind it each time it updates. Thus it's wise to keep it
|
||||
/// as small as possible, but wide enough to fit the status text, if any.
|
||||
/// </remarks>
|
||||
public Size GetOptimalSize() {
|
||||
SizeF textRectangle;
|
||||
using(var dummyImage = new Bitmap(1, 1)) {
|
||||
using(Graphics graphics = Graphics.FromImage(dummyImage)) {
|
||||
textRectangle = graphics.MeasureString(
|
||||
this.statusText, this.statusFont
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Size(
|
||||
Math.Max(128, (int)(textRectangle.Width + 2.0f)),
|
||||
this.statusFont.Height + 128
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Font that is used to display the status text</summary>
|
||||
public Font StatusFont {
|
||||
get { return this.statusFont; }
|
||||
set { this.statusFont = value; }
|
||||
}
|
||||
|
||||
/// <summary>Text that will be displayed as the control's status</summary>
|
||||
public string StatusText {
|
||||
get { return this.statusText; }
|
||||
set { this.statusText = value; }
|
||||
}
|
||||
|
||||
/// <summary>Called when the control is hidden or shown</summary>
|
||||
/// <param name="arguments">Not used</param>
|
||||
protected override void OnVisibleChanged(EventArgs arguments) {
|
||||
base.OnVisibleChanged(arguments);
|
||||
this.animationUpdateTimer.Enabled = this.spinnerRunning && Visible;
|
||||
}
|
||||
|
||||
/// <summary>Called when the control should redraw itself</summary>
|
||||
/// <param name="arguments">Provides access to the drawing surface and tools</param>
|
||||
protected override void OnPaint(PaintEventArgs arguments) {
|
||||
paintControlsBehindMe(arguments);
|
||||
paintAnimatedDots(arguments);
|
||||
paintStatusMessage(arguments);
|
||||
}
|
||||
|
||||
/// <summary>Forcefully redraws the controls below this one</summary>
|
||||
/// <param name="arguments">Provides access to the drawing surface and tools</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// WinForms has very poor transparency support. A transparent control will only
|
||||
/// be transparent to its immediate parent (so the parent needs to be a container
|
||||
/// control and hold the transparent control as its preferrably only child).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Worse yet, if you manually establish this relationship in your .Designer.cs
|
||||
/// file, the Visual Studio WinForms designer will dismantle it next time you
|
||||
/// edit something. This method fixes those issues by repainting all controls
|
||||
/// that are behind this control and whose bounding box intersect this control.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void paintControlsBehindMe(PaintEventArgs arguments) {
|
||||
if(Parent != null && this.BackColor == Color.Transparent) {
|
||||
using(var bmp = new Bitmap(Parent.Width, Parent.Height)) {
|
||||
Parent.Controls.Cast<Control>()
|
||||
.Where(c => Parent.Controls.GetChildIndex(c) > Parent.Controls.GetChildIndex(this))
|
||||
.Where(c => c.Bounds.IntersectsWith(this.Bounds))
|
||||
.OrderByDescending(c => Parent.Controls.GetChildIndex(c))
|
||||
.ToList()
|
||||
.ForEach(c => c.DrawToBitmap(bmp, c.Bounds));
|
||||
|
||||
arguments.Graphics.DrawImage(bmp, -Left, -Top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Draws a simple animated dots animation</summary>
|
||||
/// <param name="arguments">Provides access to the drawing surface and tools</param>
|
||||
private void paintAnimatedDots(PaintEventArgs arguments) {
|
||||
if(this.dotOutlinePen == null) {
|
||||
this.dotOutlinePen = new Pen(this.dotOutlineColor);
|
||||
}
|
||||
if(this.dotFillBrush == null) {
|
||||
this.dotFillBrush = new SolidBrush(this.dotFillColor);
|
||||
}
|
||||
|
||||
SmoothingMode prevousSmoothingMode = arguments.Graphics.SmoothingMode;
|
||||
arguments.Graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
try {
|
||||
PointF center = new PointF(Width / 2.0f, (Height - this.statusFont.Height - 2) / 2.0f);
|
||||
|
||||
int diameter = Math.Min(Width, Height - this.statusFont.Height - 2);
|
||||
int bigRadius = diameter / 2 - DotRadius - (DotCount - 1) * ScaleFactor;
|
||||
|
||||
// Draw the dots
|
||||
float unitAngle = 360.0f / DotCount;
|
||||
for(int index = 0; index < DotCount; ++index) {
|
||||
int dotIndex = (index + leadingDotIndex) % DotCount;
|
||||
|
||||
var dotPosition = new PointF(
|
||||
center.X + (float)(bigRadius * Math.Cos(unitAngle * dotIndex * Math.PI / 180.0f)),
|
||||
center.Y + (float)(bigRadius * Math.Sin(unitAngle * dotIndex * Math.PI / 180.0f))
|
||||
);
|
||||
|
||||
int currentDotRadius = DotRadius + index * ScaleFactor;
|
||||
|
||||
var corner = new PointF(
|
||||
dotPosition.X - currentDotRadius, dotPosition.Y - currentDotRadius
|
||||
);
|
||||
arguments.Graphics.FillEllipse(
|
||||
this.dotFillBrush, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius
|
||||
);
|
||||
arguments.Graphics.DrawEllipse(
|
||||
this.dotOutlinePen, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius
|
||||
);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
arguments.Graphics.SmoothingMode = prevousSmoothingMode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Draws the status message under the animated dots</summary>
|
||||
/// <param name="arguments">Provides access to the drawing surface and tools</param>
|
||||
private void paintStatusMessage(PaintEventArgs arguments) {
|
||||
if(!string.IsNullOrEmpty(this.statusText)) {
|
||||
SizeF textRectangle = arguments.Graphics.MeasureString(
|
||||
this.statusText, this.statusFont
|
||||
);
|
||||
|
||||
var messageArea = new RectangleF(
|
||||
(Width - textRectangle.Width) / 2.0f,
|
||||
Height - this.statusFont.Height - 1.0f,
|
||||
textRectangle.Width,
|
||||
this.statusFont.Height
|
||||
);
|
||||
|
||||
// Draw text with a white halo. This is a little bit ugly...
|
||||
{
|
||||
messageArea.Offset(-1.0f, 0.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, Brushes.White, messageArea
|
||||
);
|
||||
|
||||
messageArea.Offset(2.0f, 0.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, Brushes.White, messageArea
|
||||
);
|
||||
|
||||
messageArea.Offset(-1.0f, -1.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, Brushes.White, messageArea
|
||||
);
|
||||
|
||||
messageArea.Offset(0.0f, 2.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, Brushes.White, messageArea
|
||||
);
|
||||
|
||||
messageArea.Offset(0.0f, -1.0f);
|
||||
arguments.Graphics.DrawString(
|
||||
this.statusText, this.statusFont, this.dotFillBrush, messageArea
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Called when the animation timer ticks to update the animation state</summary>
|
||||
/// <param name="sender">Animation timer that has ticked</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void animationTimerTicked(object sender, EventArgs arguments) {
|
||||
this.leadingDotIndex = (this.leadingDotIndex + 1) % DotCount; // Advance the animation
|
||||
Invalidate(); // Request a redraw at the earliest opportune time
|
||||
}
|
||||
|
||||
/// <summary>Whether the spinner has been started</summary>
|
||||
private bool spinnerRunning;
|
||||
/// <summary>Index of the currently leading dot</summary>
|
||||
private int leadingDotIndex = 0;
|
||||
/// <summary>Text that will be displayed under the control as the current status</summary>
|
||||
private string statusText;
|
||||
|
||||
/// <summary>Color in which the dots will be filled</summary>
|
||||
private Color dotFillColor = Color.RoyalBlue;
|
||||
/// <summary>Color that will be used for the dots' outline</summary>
|
||||
private Color dotOutlineColor = Color.White;
|
||||
/// <summary>Brush used to fill the dots</summary>
|
||||
private Brush dotFillBrush;
|
||||
/// <summary>Brush used for the dots' outline</summary>
|
||||
private Pen dotOutlinePen;
|
||||
/// <summary>Font that is used to display the status text</summary>
|
||||
private Font statusFont = SystemFonts.SmallCaptionFont;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Controls
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Enables consumer to look up the currently active window</summary>
|
||||
public interface IActiveWindowTracker {
|
||||
|
||||
/// <summary>The currently active top-level or modal window</summary>
|
||||
/// <remarks>
|
||||
/// If windows live in multiple threads, the property change notification for
|
||||
/// this property, if supported, might be fired from a different thread.
|
||||
/// </remarks>
|
||||
Form ActiveWindow { get; }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Enables consumer to look up the currently active window</summary>
|
||||
public interface IActiveWindowTracker {
|
||||
|
||||
/// <summary>The currently active top-level or modal window</summary>
|
||||
/// <remarks>
|
||||
/// If windows live in multiple threads, the property change notification for
|
||||
/// this property, if supported, might be fired from a different thread.
|
||||
/// </remarks>
|
||||
Form ActiveWindow { get; }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,93 +1,92 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Interface for a window manager used in an MVVM environment</summary>
|
||||
public interface IWindowManager : IActiveWindowTracker {
|
||||
|
||||
/// <summary>Opens a view as a new root window of the application</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model a root window will be opened for
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a window will be opened for. If null, the view model will be
|
||||
/// created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <param name="disposeOnClose">
|
||||
/// Whether the view model should be disposed when the view is closed
|
||||
/// </param>
|
||||
/// <returns>The window that has been opened by the window manager</returns>
|
||||
Form OpenRoot<TViewModel>(
|
||||
TViewModel viewModel = null, bool disposeOnClose = true
|
||||
) where TViewModel : class;
|
||||
|
||||
/// <summary>Displays a view as a modal window</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of the view model for which a view will be displayed
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a modal window will be displayed for. If null, the view model will
|
||||
/// be created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <param name="disposeOnClose">
|
||||
/// Whether the view model should be disposed when the view is closed
|
||||
/// </param>
|
||||
/// <returns>The return value of the modal window</returns>
|
||||
bool? ShowModal<TViewModel>(
|
||||
TViewModel viewModel = null, bool disposeOnClose = true
|
||||
) where TViewModel : class;
|
||||
|
||||
/// <summary>Creates the view for the specified view model</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model for which a view will be created
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a view will be created for. If null, the view model will be
|
||||
/// created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <returns>The view for the specified view model</returns>
|
||||
Control CreateView<TViewModel>(TViewModel viewModel = null)
|
||||
where TViewModel : class;
|
||||
|
||||
/// <summary>Creates a view model without a matching view</summary>
|
||||
/// <typeparam name="TViewModel">Type of view model that will be created</typeparam>
|
||||
/// <returns>The new view model</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is useful if a view model needs to create child view models (i.e. paged container
|
||||
/// and wants to ensure the same dependency injector (if any) if used as the window
|
||||
/// manager uses for other view models it creates.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This way, view models can set up their child view models without having to immediately
|
||||
/// bind a view to them. Later on, views can use the window manager to create a matching
|
||||
/// child view and store it in a container.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
TViewModel CreateViewModel<TViewModel>()
|
||||
where TViewModel : class;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Interface for a window manager used in an MVVM environment</summary>
|
||||
public interface IWindowManager : IActiveWindowTracker {
|
||||
|
||||
/// <summary>Opens a view as a new root window of the application</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model a root window will be opened for
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a window will be opened for. If null, the view model will be
|
||||
/// created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <param name="disposeOnClose">
|
||||
/// Whether the view model should be disposed when the view is closed
|
||||
/// </param>
|
||||
/// <returns>The window that has been opened by the window manager</returns>
|
||||
Form OpenRoot<TViewModel>(
|
||||
TViewModel viewModel = null, bool disposeOnClose = true
|
||||
) where TViewModel : class;
|
||||
|
||||
/// <summary>Displays a view as a modal window</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of the view model for which a view will be displayed
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a modal window will be displayed for. If null, the view model will
|
||||
/// be created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <param name="disposeOnClose">
|
||||
/// Whether the view model should be disposed when the view is closed
|
||||
/// </param>
|
||||
/// <returns>The return value of the modal window</returns>
|
||||
bool? ShowModal<TViewModel>(
|
||||
TViewModel viewModel = null, bool disposeOnClose = true
|
||||
) where TViewModel : class;
|
||||
|
||||
/// <summary>Creates the view for the specified view model</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model for which a view will be created
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a view will be created for. If null, the view model will be
|
||||
/// created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <returns>The view for the specified view model</returns>
|
||||
Control CreateView<TViewModel>(TViewModel viewModel = null)
|
||||
where TViewModel : class;
|
||||
|
||||
/// <summary>Creates a view model without a matching view</summary>
|
||||
/// <typeparam name="TViewModel">Type of view model that will be created</typeparam>
|
||||
/// <returns>The new view model</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is useful if a view model needs to create child view models (i.e. paged container
|
||||
/// and wants to ensure the same dependency injector (if any) if used as the window
|
||||
/// manager uses for other view models it creates.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This way, view models can set up their child view models without having to immediately
|
||||
/// bind a view to them. Later on, views can use the window manager to create a matching
|
||||
/// child view and store it in a container.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
TViewModel CreateViewModel<TViewModel>()
|
||||
where TViewModel : class;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,157 +1,156 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>
|
||||
/// Proxy stand-in to delay checking for the main window until it has been created
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The issue: when the view model for the main window is created, the main window
|
||||
/// may only exist as a .NET object, without the underlying operating system window
|
||||
/// (done in <see cref="System.Windows.Forms.Control.CreateControl" /> checkable via
|
||||
/// <see cref="System.Windows.Forms.Control.IsHandleCreated" />). Not only will things
|
||||
/// like <see cref="System.Windows.Forms.Control.Invoke(Delegate)" /> fail, we can't
|
||||
/// even locate the main window at that stage.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Thus, if the main window cannot be found at the time a view model is created,
|
||||
/// this late-checking synchronizer will jump into its place and re-check for
|
||||
/// the main window only when something needs to be executed in the UI thread.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
class LateCheckedSynchronizer : ISynchronizeInvoke {
|
||||
|
||||
/// <summary>Initializes a new late-checked main window synchronizer</summary>
|
||||
/// <param name="uiContextFoundCallback"></param>
|
||||
public LateCheckedSynchronizer(Action<ISynchronizeInvoke> uiContextFoundCallback) {
|
||||
this.uiContextFoundCallback = uiContextFoundCallback;
|
||||
}
|
||||
|
||||
/// <summary>Finds the application's main window</summary>
|
||||
/// <returns>Main window of the application or null if none has been created</returns>
|
||||
/// <remarks>
|
||||
/// The application's main window, if it has been created yet
|
||||
/// </remarks>
|
||||
public static Form GetMainWindow() {
|
||||
IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
|
||||
|
||||
// We can get two things: a list of all open windows and the handle of
|
||||
// the window that the process has registered as main window. Use the latter
|
||||
// to pick the correct window from the former.
|
||||
FormCollection openForms = Application.OpenForms;
|
||||
int openFormCount = openForms.Count;
|
||||
for(int index = 0; index < openFormCount; ++index) {
|
||||
Form form = openForms[index];
|
||||
|
||||
IntPtr handle;
|
||||
if(form.InvokeRequired) {
|
||||
handle = (IntPtr)form.Invoke(new Func<Form, IntPtr>(getWindowHandle), form);
|
||||
} else {
|
||||
handle = getWindowHandle(form);
|
||||
}
|
||||
if(handle != IntPtr.Zero) {
|
||||
if(handle == mainWindowHandle) {
|
||||
return form;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matching main window found: use the first one in good faith or fail.
|
||||
if(openFormCount > 0) {
|
||||
return openForms[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Checks whether the calling thread needs to use Invoke()</summary>
|
||||
public bool InvokeRequired {
|
||||
get { return getMainWindowOrFail().InvokeRequired; }
|
||||
}
|
||||
|
||||
/// <summary>Schedules a method to be run by the main UI thread</summary>
|
||||
/// <param name="method">Method that will be scheduled to run</param>
|
||||
/// <param name="args">Arguments that will be passed to the method</param>
|
||||
/// <returns>An asynchronous result handle that can be used to track the call</returns>
|
||||
public IAsyncResult BeginInvoke(Delegate method, object[] args) {
|
||||
return getMainWindowOrFail().BeginInvoke(method, args);
|
||||
}
|
||||
|
||||
/// <summary>Waits for a call scheduled on the main UI thread to complete</summary>
|
||||
/// <param name="result">Asynchronous result handle returned by BeginInvoke()</param>
|
||||
/// <returns>The value returned by the method ran in the main UI thread</returns>
|
||||
public object EndInvoke(IAsyncResult result) {
|
||||
return getMainWindowOrFail().EndInvoke(result);
|
||||
}
|
||||
|
||||
/// <summary>Executes a method on the main UI thread and waits for it to complete</summary>
|
||||
/// <param name="method">Method that will be run by the main UI thread</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>The value returned by the method</returns>
|
||||
public object Invoke(Delegate method, object[] arguments) {
|
||||
return getMainWindowOrFail().Invoke(method, arguments);
|
||||
}
|
||||
|
||||
/// <summary>Retrieves the application's current main window</summary>
|
||||
/// <returns>The application's current main window</returns>
|
||||
/// <remarks>
|
||||
/// If there is no main window, an exception will be thrown
|
||||
/// </remarks>
|
||||
private Form getMainWindowOrFail() {
|
||||
Form mainWindow = GetMainWindow();
|
||||
if(mainWindow == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Could not schedule work for the UI thread because no WinForms UI main window " +
|
||||
"was found. Create a main window first or specify the UI synchronization context " +
|
||||
"explicitly to the view model."
|
||||
);
|
||||
}
|
||||
|
||||
if(this.uiContextFoundCallback != null) {
|
||||
this.uiContextFoundCallback(mainWindow);
|
||||
this.uiContextFoundCallback = null;
|
||||
}
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
/// <summary>Returns a Form's window handle without forcing its creation</summary>
|
||||
/// <param name="form">Form whose window handle will be returned</param>
|
||||
/// <returns>The form's window handle of IntPtr.Zero if it has none</returns>
|
||||
private static IntPtr getWindowHandle(Form form) {
|
||||
if(form.IsHandleCreated) {
|
||||
return form.Handle;
|
||||
} else {
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when the late-checked synchronizer finds the main window</summary>
|
||||
private Action<ISynchronizeInvoke> uiContextFoundCallback;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>
|
||||
/// Proxy stand-in to delay checking for the main window until it has been created
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The issue: when the view model for the main window is created, the main window
|
||||
/// may only exist as a .NET object, without the underlying operating system window
|
||||
/// (done in <see cref="System.Windows.Forms.Control.CreateControl" /> checkable via
|
||||
/// <see cref="System.Windows.Forms.Control.IsHandleCreated" />). Not only will things
|
||||
/// like <see cref="System.Windows.Forms.Control.Invoke(Delegate)" /> fail, we can't
|
||||
/// even locate the main window at that stage.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Thus, if the main window cannot be found at the time a view model is created,
|
||||
/// this late-checking synchronizer will jump into its place and re-check for
|
||||
/// the main window only when something needs to be executed in the UI thread.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
class LateCheckedSynchronizer : ISynchronizeInvoke {
|
||||
|
||||
/// <summary>Initializes a new late-checked main window synchronizer</summary>
|
||||
/// <param name="uiContextFoundCallback"></param>
|
||||
public LateCheckedSynchronizer(Action<ISynchronizeInvoke> uiContextFoundCallback) {
|
||||
this.uiContextFoundCallback = uiContextFoundCallback;
|
||||
}
|
||||
|
||||
/// <summary>Finds the application's main window</summary>
|
||||
/// <returns>Main window of the application or null if none has been created</returns>
|
||||
/// <remarks>
|
||||
/// The application's main window, if it has been created yet
|
||||
/// </remarks>
|
||||
public static Form GetMainWindow() {
|
||||
IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
|
||||
|
||||
// We can get two things: a list of all open windows and the handle of
|
||||
// the window that the process has registered as main window. Use the latter
|
||||
// to pick the correct window from the former.
|
||||
FormCollection openForms = Application.OpenForms;
|
||||
int openFormCount = openForms.Count;
|
||||
for(int index = 0; index < openFormCount; ++index) {
|
||||
Form form = openForms[index];
|
||||
|
||||
IntPtr handle;
|
||||
if(form.InvokeRequired) {
|
||||
handle = (IntPtr)form.Invoke(new Func<Form, IntPtr>(getWindowHandle), form);
|
||||
} else {
|
||||
handle = getWindowHandle(form);
|
||||
}
|
||||
if(handle != IntPtr.Zero) {
|
||||
if(handle == mainWindowHandle) {
|
||||
return form;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matching main window found: use the first one in good faith or fail.
|
||||
if(openFormCount > 0) {
|
||||
return openForms[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Checks whether the calling thread needs to use Invoke()</summary>
|
||||
public bool InvokeRequired {
|
||||
get { return getMainWindowOrFail().InvokeRequired; }
|
||||
}
|
||||
|
||||
/// <summary>Schedules a method to be run by the main UI thread</summary>
|
||||
/// <param name="method">Method that will be scheduled to run</param>
|
||||
/// <param name="args">Arguments that will be passed to the method</param>
|
||||
/// <returns>An asynchronous result handle that can be used to track the call</returns>
|
||||
public IAsyncResult BeginInvoke(Delegate method, object[] args) {
|
||||
return getMainWindowOrFail().BeginInvoke(method, args);
|
||||
}
|
||||
|
||||
/// <summary>Waits for a call scheduled on the main UI thread to complete</summary>
|
||||
/// <param name="result">Asynchronous result handle returned by BeginInvoke()</param>
|
||||
/// <returns>The value returned by the method ran in the main UI thread</returns>
|
||||
public object EndInvoke(IAsyncResult result) {
|
||||
return getMainWindowOrFail().EndInvoke(result);
|
||||
}
|
||||
|
||||
/// <summary>Executes a method on the main UI thread and waits for it to complete</summary>
|
||||
/// <param name="method">Method that will be run by the main UI thread</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>The value returned by the method</returns>
|
||||
public object Invoke(Delegate method, object[] arguments) {
|
||||
return getMainWindowOrFail().Invoke(method, arguments);
|
||||
}
|
||||
|
||||
/// <summary>Retrieves the application's current main window</summary>
|
||||
/// <returns>The application's current main window</returns>
|
||||
/// <remarks>
|
||||
/// If there is no main window, an exception will be thrown
|
||||
/// </remarks>
|
||||
private Form getMainWindowOrFail() {
|
||||
Form mainWindow = GetMainWindow();
|
||||
if(mainWindow == null) {
|
||||
throw new InvalidOperationException(
|
||||
"Could not schedule work for the UI thread because no WinForms UI main window " +
|
||||
"was found. Create a main window first or specify the UI synchronization context " +
|
||||
"explicitly to the view model."
|
||||
);
|
||||
}
|
||||
|
||||
if(this.uiContextFoundCallback != null) {
|
||||
this.uiContextFoundCallback(mainWindow);
|
||||
this.uiContextFoundCallback = null;
|
||||
}
|
||||
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
/// <summary>Returns a Form's window handle without forcing its creation</summary>
|
||||
/// <param name="form">Form whose window handle will be returned</param>
|
||||
/// <returns>The form's window handle of IntPtr.Zero if it has none</returns>
|
||||
private static IntPtr getWindowHandle(Form form) {
|
||||
if(form.IsHandleCreated) {
|
||||
return form.Handle;
|
||||
} else {
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when the late-checked synchronizer finds the main window</summary>
|
||||
private Action<ISynchronizeInvoke> uiContextFoundCallback;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,55 +1,54 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Performs simple user interaction</summary>
|
||||
/// <remarks>
|
||||
/// Methods provided by this service can be covered using plain old message boxes
|
||||
/// and do not require special dialogs or calls to the task dialog API.
|
||||
/// </remarks>
|
||||
public interface IMessageService {
|
||||
|
||||
/// <summary>Triggered when a message is about to be displayed to the user</summary>
|
||||
event EventHandler<MessageEventArgs> MessageDisplaying;
|
||||
|
||||
/// <summary>Triggered when the user has acknowledged the current message</summary>
|
||||
event EventHandler MessageAcknowledged;
|
||||
|
||||
/// <summary>Asks the user a question that can be answered via several buttons</summary>
|
||||
/// <param name="image">Image that will be shown on the message box</param>
|
||||
/// <param name="text">Text that will be shown to the user</param>
|
||||
/// <param name="buttons">Buttons available for the user to click on</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
DialogResult ShowQuestion(
|
||||
MessageBoxIcon image, MessageText text, MessageBoxButtons buttons
|
||||
);
|
||||
|
||||
/// <summary>Displays a notification to the user</summary>
|
||||
/// <param name="image">Image that will be shown on the message bx</param>
|
||||
/// <param name="text">Text that will be shown to the user</param>
|
||||
void ShowNotification(MessageBoxIcon image, MessageText text);
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Performs simple user interaction</summary>
|
||||
/// <remarks>
|
||||
/// Methods provided by this service can be covered using plain old message boxes
|
||||
/// and do not require special dialogs or calls to the task dialog API.
|
||||
/// </remarks>
|
||||
public interface IMessageService {
|
||||
|
||||
/// <summary>Triggered when a message is about to be displayed to the user</summary>
|
||||
event EventHandler<MessageEventArgs> MessageDisplaying;
|
||||
|
||||
/// <summary>Triggered when the user has acknowledged the current message</summary>
|
||||
event EventHandler MessageAcknowledged;
|
||||
|
||||
/// <summary>Asks the user a question that can be answered via several buttons</summary>
|
||||
/// <param name="image">Image that will be shown on the message box</param>
|
||||
/// <param name="text">Text that will be shown to the user</param>
|
||||
/// <param name="buttons">Buttons available for the user to click on</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
DialogResult ShowQuestion(
|
||||
MessageBoxIcon image, MessageText text, MessageBoxButtons buttons
|
||||
);
|
||||
|
||||
/// <summary>Displays a notification to the user</summary>
|
||||
/// <param name="image">Image that will be shown on the message bx</param>
|
||||
/// <param name="text">Text that will be shown to the user</param>
|
||||
void ShowNotification(MessageBoxIcon image, MessageText text);
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
|
|
|
@ -1,49 +1,48 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Unit tests for the message box event argument container</summary>
|
||||
[TestFixture]
|
||||
internal class MessageEventArgsTest {
|
||||
|
||||
/// <summary>Verifies that the image associated with the message gets stored</summary>
|
||||
[Test]
|
||||
public void ImageIsStored() {
|
||||
var arguments = new MessageEventArgs(MessageBoxIcon.Exclamation, null);
|
||||
Assert.AreEqual(MessageBoxIcon.Exclamation, arguments.Image);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the text associated with the message gets stored</summary>
|
||||
[Test]
|
||||
public void TextIsStored() {
|
||||
var text = new MessageText();
|
||||
var arguments = new MessageEventArgs(MessageBoxIcon.None, text);
|
||||
Assert.AreSame(text, arguments.Text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Unit tests for the message box event argument container</summary>
|
||||
[TestFixture]
|
||||
internal class MessageEventArgsTest {
|
||||
|
||||
/// <summary>Verifies that the image associated with the message gets stored</summary>
|
||||
[Test]
|
||||
public void ImageIsStored() {
|
||||
var arguments = new MessageEventArgs(MessageBoxIcon.Exclamation, null);
|
||||
Assert.AreEqual(MessageBoxIcon.Exclamation, arguments.Image);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the text associated with the message gets stored</summary>
|
||||
[Test]
|
||||
public void TextIsStored() {
|
||||
var text = new MessageText();
|
||||
var arguments = new MessageEventArgs(MessageBoxIcon.None, text);
|
||||
Assert.AreSame(text, arguments.Text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
|
|
|
@ -1,54 +1,53 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Provides a displayed message and its severity to event subscribers</summary>
|
||||
public class MessageEventArgs : EventArgs {
|
||||
|
||||
/// <summary>Initializes a new message box event argument container</summary>
|
||||
/// <param name="image">Image the message box will be displaying</param>
|
||||
/// <param name="text">Text that will be displayed in the message box</param>
|
||||
public MessageEventArgs(MessageBoxIcon image, MessageText text) {
|
||||
this.image = image;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/// <summary>Image that indicates the severity of the message being displayed</summary>
|
||||
public MessageBoxIcon Image {
|
||||
get { return this.image; }
|
||||
}
|
||||
|
||||
/// <summary>Text that is being displayed in the message box</summary>
|
||||
public MessageText Text {
|
||||
get { return this.text; }
|
||||
}
|
||||
|
||||
/// <summary>Image that indicates the severity of the message being displayed</summary>
|
||||
private MessageBoxIcon image;
|
||||
/// <summary>Text that is being displayed in the message box</summary>
|
||||
private MessageText text;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Provides a displayed message and its severity to event subscribers</summary>
|
||||
public class MessageEventArgs : EventArgs {
|
||||
|
||||
/// <summary>Initializes a new message box event argument container</summary>
|
||||
/// <param name="image">Image the message box will be displaying</param>
|
||||
/// <param name="text">Text that will be displayed in the message box</param>
|
||||
public MessageEventArgs(MessageBoxIcon image, MessageText text) {
|
||||
this.image = image;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/// <summary>Image that indicates the severity of the message being displayed</summary>
|
||||
public MessageBoxIcon Image {
|
||||
get { return this.image; }
|
||||
}
|
||||
|
||||
/// <summary>Text that is being displayed in the message box</summary>
|
||||
public MessageText Text {
|
||||
get { return this.text; }
|
||||
}
|
||||
|
||||
/// <summary>Image that indicates the severity of the message being displayed</summary>
|
||||
private MessageBoxIcon image;
|
||||
/// <summary>Text that is being displayed in the message box</summary>
|
||||
private MessageText text;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
|
|
|
@ -1,108 +1,107 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Contains helper methods for the message service</summary>
|
||||
public static class MessageServiceHelper {
|
||||
|
||||
/// <summary>Asks the user a question that can be answered with yes or no</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the question
|
||||
/// </param>
|
||||
/// <param name="text">Text that will be shown on the message box</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
public static DialogResult AskYesNo(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
return messageService.ShowQuestion(
|
||||
MessageBoxIcon.Question, text, MessageBoxButtons.YesNo
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Asks the user a question that can be answered with ok or cancel</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the question
|
||||
/// </param>
|
||||
/// <param name="text">Text that will be shown on the message box</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
public static DialogResult AskOkCancel(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
return messageService.ShowQuestion(
|
||||
MessageBoxIcon.Question, text, MessageBoxButtons.OKCancel
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks the user a question that can be answered with yes, no or cancel
|
||||
/// </summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the question
|
||||
/// </param>
|
||||
/// <param name="text">Text that will be shown on the message box</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
public static DialogResult AskYesNoCancel(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
return messageService.ShowQuestion(
|
||||
MessageBoxIcon.Question, text, MessageBoxButtons.YesNoCancel
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Displays an informative message</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the warning
|
||||
/// </param>
|
||||
/// <param name="text">Text to be displayed on the warning message</param>
|
||||
public static void Inform(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
messageService.ShowNotification(MessageBoxIcon.Information, text);
|
||||
}
|
||||
|
||||
/// <summary>Displays a warning</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the warning
|
||||
/// </param>
|
||||
/// <param name="text">Text to be displayed on the warning message</param>
|
||||
public static void Warn(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
messageService.ShowNotification(MessageBoxIcon.Warning, text);
|
||||
}
|
||||
|
||||
/// <summary>Reports an error</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the warning
|
||||
/// </param>
|
||||
/// <param name="text">Text to be displayed on the warning message</param>
|
||||
public static void ReportError(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
messageService.ShowNotification(MessageBoxIcon.Error, text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Contains helper methods for the message service</summary>
|
||||
public static class MessageServiceHelper {
|
||||
|
||||
/// <summary>Asks the user a question that can be answered with yes or no</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the question
|
||||
/// </param>
|
||||
/// <param name="text">Text that will be shown on the message box</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
public static DialogResult AskYesNo(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
return messageService.ShowQuestion(
|
||||
MessageBoxIcon.Question, text, MessageBoxButtons.YesNo
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Asks the user a question that can be answered with ok or cancel</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the question
|
||||
/// </param>
|
||||
/// <param name="text">Text that will be shown on the message box</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
public static DialogResult AskOkCancel(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
return messageService.ShowQuestion(
|
||||
MessageBoxIcon.Question, text, MessageBoxButtons.OKCancel
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks the user a question that can be answered with yes, no or cancel
|
||||
/// </summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the question
|
||||
/// </param>
|
||||
/// <param name="text">Text that will be shown on the message box</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
public static DialogResult AskYesNoCancel(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
return messageService.ShowQuestion(
|
||||
MessageBoxIcon.Question, text, MessageBoxButtons.YesNoCancel
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Displays an informative message</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the warning
|
||||
/// </param>
|
||||
/// <param name="text">Text to be displayed on the warning message</param>
|
||||
public static void Inform(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
messageService.ShowNotification(MessageBoxIcon.Information, text);
|
||||
}
|
||||
|
||||
/// <summary>Displays a warning</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the warning
|
||||
/// </param>
|
||||
/// <param name="text">Text to be displayed on the warning message</param>
|
||||
public static void Warn(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
messageService.ShowNotification(MessageBoxIcon.Warning, text);
|
||||
}
|
||||
|
||||
/// <summary>Reports an error</summary>
|
||||
/// <param name="messageService">
|
||||
/// Message service that will be used to display the warning
|
||||
/// </param>
|
||||
/// <param name="text">Text to be displayed on the warning message</param>
|
||||
public static void ReportError(
|
||||
this IMessageService messageService, MessageText text
|
||||
) {
|
||||
messageService.ShowNotification(MessageBoxIcon.Error, text);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
|
|
|
@ -1,51 +1,50 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Unit tests for the message text container</summary>
|
||||
[TestFixture]
|
||||
internal class MessageTextTest {
|
||||
|
||||
/// <summary>Ensures that the message text class provides a copy constructor</summary>
|
||||
[Test]
|
||||
public void HasCopyConstructor() {
|
||||
var text = new MessageText() {
|
||||
Caption = "Caption",
|
||||
Message = "Message",
|
||||
Details = "Details",
|
||||
ExpandedDetails = "ExpandedDetails"
|
||||
};
|
||||
var copy = new MessageText(text);
|
||||
|
||||
Assert.AreEqual(text.Caption, copy.Caption);
|
||||
Assert.AreEqual(text.Message, copy.Message);
|
||||
Assert.AreEqual(text.Details, copy.Details);
|
||||
Assert.AreEqual(text.ExpandedDetails, copy.ExpandedDetails);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Unit tests for the message text container</summary>
|
||||
[TestFixture]
|
||||
internal class MessageTextTest {
|
||||
|
||||
/// <summary>Ensures that the message text class provides a copy constructor</summary>
|
||||
[Test]
|
||||
public void HasCopyConstructor() {
|
||||
var text = new MessageText() {
|
||||
Caption = "Caption",
|
||||
Message = "Message",
|
||||
Details = "Details",
|
||||
ExpandedDetails = "ExpandedDetails"
|
||||
};
|
||||
var copy = new MessageText(text);
|
||||
|
||||
Assert.AreEqual(text.Caption, copy.Caption);
|
||||
Assert.AreEqual(text.Message, copy.Message);
|
||||
Assert.AreEqual(text.Details, copy.Details);
|
||||
Assert.AreEqual(text.ExpandedDetails, copy.ExpandedDetails);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
|
|
|
@ -1,55 +1,54 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Text that will be displayed in a message box</summary>
|
||||
public class MessageText {
|
||||
|
||||
/// <summary>Initializs a new message text</summary>
|
||||
public MessageText() { }
|
||||
|
||||
/// <summary>Initializes a new message text by copying another instance</summary>
|
||||
/// <param name="other">Instance that will be copied</param>
|
||||
public MessageText(MessageText other) {
|
||||
Caption = other.Caption;
|
||||
Message = other.Message;
|
||||
Details = other.Details;
|
||||
ExpandedDetails = other.ExpandedDetails;
|
||||
}
|
||||
|
||||
/// <summary>The caption used when the is displayed in a message box</summary>
|
||||
public string Caption { get; set; }
|
||||
/// <summary>Main message being displayed to the user</summary>
|
||||
public string Message { get; set; }
|
||||
/// <summary>Message details shown below the main message</summary>
|
||||
public string Details { get; set; }
|
||||
/// <summary>
|
||||
/// Additional informations the user can display by expanding
|
||||
/// the message dialog. Can be null, in which case the message dialog
|
||||
/// will not be expandable.
|
||||
/// </summary>
|
||||
public string ExpandedDetails { get; set; }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Text that will be displayed in a message box</summary>
|
||||
public class MessageText {
|
||||
|
||||
/// <summary>Initializs a new message text</summary>
|
||||
public MessageText() { }
|
||||
|
||||
/// <summary>Initializes a new message text by copying another instance</summary>
|
||||
/// <param name="other">Instance that will be copied</param>
|
||||
public MessageText(MessageText other) {
|
||||
Caption = other.Caption;
|
||||
Message = other.Message;
|
||||
Details = other.Details;
|
||||
ExpandedDetails = other.ExpandedDetails;
|
||||
}
|
||||
|
||||
/// <summary>The caption used when the is displayed in a message box</summary>
|
||||
public string Caption { get; set; }
|
||||
/// <summary>Main message being displayed to the user</summary>
|
||||
public string Message { get; set; }
|
||||
/// <summary>Message details shown below the main message</summary>
|
||||
public string Details { get; set; }
|
||||
/// <summary>
|
||||
/// Additional informations the user can display by expanding
|
||||
/// the message dialog. Can be null, in which case the message dialog
|
||||
/// will not be expandable.
|
||||
/// </summary>
|
||||
public string ExpandedDetails { get; set; }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
|
|
|
@ -1,162 +1,161 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Uses task dialogs to display message boxes</summary>
|
||||
public class StandardMessageBoxManager : IMessageService {
|
||||
|
||||
#region class MessageScope
|
||||
|
||||
/// <summary>Triggers the message displayed and acknowledged events</summary>
|
||||
private class MessageScope : IDisposable {
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new message scope, triggering the message displayed event
|
||||
/// </summary>
|
||||
/// <param name="self">Message service the scope belongs to</param>
|
||||
/// <param name="image">Image of the message being displayed</param>
|
||||
/// <param name="text">Text contained in the message being displayed</param>
|
||||
public MessageScope(
|
||||
StandardMessageBoxManager self, MessageBoxIcon image, MessageText text
|
||||
) {
|
||||
EventHandler<MessageEventArgs> messageDisplayed = self.MessageDisplaying;
|
||||
if(messageDisplayed != null) {
|
||||
messageDisplayed(this, new MessageEventArgs(image, text));
|
||||
}
|
||||
|
||||
this.self = self;
|
||||
}
|
||||
|
||||
/// <summary>Triggers the message acknowledged event</summary>
|
||||
public void Dispose() {
|
||||
EventHandler messageAcknowledged = self.MessageAcknowledged;
|
||||
if(messageAcknowledged != null) {
|
||||
messageAcknowledged(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Message service the scope belongs to</summary>
|
||||
private StandardMessageBoxManager self;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class MessageScope
|
||||
|
||||
/// <summary>Delegate for the standard message box show function</summary>
|
||||
/// <param name="owner">Window that will modally display the message box</param>
|
||||
/// <param name="text">Text that will be presented to the user</param>
|
||||
/// <param name="caption">Contents of the message box' title bar</param>
|
||||
/// <param name="buttons">Buttons available for the user to choose from</param>
|
||||
/// <param name="icon">Icon that will be displayed next to the text</param>
|
||||
/// <returns>The choice made by the user if multiple buttons were provided</returns>
|
||||
private delegate DialogResult ShowMessageBoxDelegate(
|
||||
IWin32Window owner,
|
||||
string text,
|
||||
string caption,
|
||||
MessageBoxButtons buttons,
|
||||
MessageBoxIcon icon
|
||||
);
|
||||
|
||||
/// <summary>Triggered when a message is displayed to the user</summary>
|
||||
public event EventHandler<MessageEventArgs> MessageDisplaying;
|
||||
|
||||
/// <summary>Triggered when the user has acknowledged the current message</summary>
|
||||
public event EventHandler MessageAcknowledged;
|
||||
|
||||
/// <summary>Initializes a new task dialog message service</summary>
|
||||
public StandardMessageBoxManager() : this(NullActiveWindowTracker.Default) { }
|
||||
|
||||
/// <summary>Initializes a new task dialog message service</summary>
|
||||
/// <param name="tracker">
|
||||
/// Active window tracker used to obtain the parent window for message boxes
|
||||
/// </param>
|
||||
public StandardMessageBoxManager(IActiveWindowTracker tracker) {
|
||||
this.tracker = tracker;
|
||||
this.showMessageDelegate = new ShowMessageBoxDelegate(MessageBox.Show);
|
||||
}
|
||||
|
||||
/// <summary>Asks the user a question that can be answered via several buttons</summary>
|
||||
/// <param name="image">Image that will be shown on the message box</param>
|
||||
/// <param name="text">Text that will be shown to the user</param>
|
||||
/// <param name="buttons">Buttons available for the user to click on</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
public DialogResult ShowQuestion(
|
||||
MessageBoxIcon image, MessageText text, MessageBoxButtons buttons
|
||||
) {
|
||||
using(var scope = new MessageScope(this, image, text)) {
|
||||
return showMessageBoxInActiveUiThread(
|
||||
text.Message,
|
||||
text.Caption,
|
||||
buttons,
|
||||
image
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Displays a notification to the user</summary>
|
||||
/// <param name="image">Image that will be shown on the message bx</param>
|
||||
/// <param name="text">Text that will be shown to the user</param>
|
||||
public void ShowNotification(MessageBoxIcon image, MessageText text) {
|
||||
using(var scope = new MessageScope(this, image, text)) {
|
||||
showMessageBoxInActiveUiThread(
|
||||
text.Message,
|
||||
text.Caption,
|
||||
MessageBoxButtons.OK,
|
||||
image
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Displays the message box in the active view's thread</summary>
|
||||
/// <param name="message">Text that will be presented to the user</param>
|
||||
/// <param name="caption">Contents of the message box' title bar</param>
|
||||
/// <param name="buttons">Buttons available for the user to choose from</param>
|
||||
/// <param name="image">Image that will be displayed next to the text</param>
|
||||
/// <returns></returns>
|
||||
private DialogResult showMessageBoxInActiveUiThread(
|
||||
string message,
|
||||
string caption,
|
||||
MessageBoxButtons buttons,
|
||||
MessageBoxIcon image
|
||||
) {
|
||||
Form mainWindow = this.tracker.ActiveWindow;
|
||||
if(mainWindow != null) {
|
||||
return (DialogResult)mainWindow.Invoke(
|
||||
this.showMessageDelegate,
|
||||
(IWin32Window)mainWindow, message, caption, buttons, image
|
||||
);
|
||||
}
|
||||
|
||||
// No window tracker or unknown main window -- just show the message box
|
||||
return MessageBox.Show(message, caption, buttons, image);
|
||||
}
|
||||
|
||||
/// <summary>Provides the currently active top-level window</summary>
|
||||
private IActiveWindowTracker tracker;
|
||||
/// <summary>Delegate for the MessageBox.Show() method</summary>
|
||||
private ShowMessageBoxDelegate showMessageDelegate;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Messages {
|
||||
|
||||
/// <summary>Uses task dialogs to display message boxes</summary>
|
||||
public class StandardMessageBoxManager : IMessageService {
|
||||
|
||||
#region class MessageScope
|
||||
|
||||
/// <summary>Triggers the message displayed and acknowledged events</summary>
|
||||
private class MessageScope : IDisposable {
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new message scope, triggering the message displayed event
|
||||
/// </summary>
|
||||
/// <param name="self">Message service the scope belongs to</param>
|
||||
/// <param name="image">Image of the message being displayed</param>
|
||||
/// <param name="text">Text contained in the message being displayed</param>
|
||||
public MessageScope(
|
||||
StandardMessageBoxManager self, MessageBoxIcon image, MessageText text
|
||||
) {
|
||||
EventHandler<MessageEventArgs> messageDisplayed = self.MessageDisplaying;
|
||||
if(messageDisplayed != null) {
|
||||
messageDisplayed(this, new MessageEventArgs(image, text));
|
||||
}
|
||||
|
||||
this.self = self;
|
||||
}
|
||||
|
||||
/// <summary>Triggers the message acknowledged event</summary>
|
||||
public void Dispose() {
|
||||
EventHandler messageAcknowledged = self.MessageAcknowledged;
|
||||
if(messageAcknowledged != null) {
|
||||
messageAcknowledged(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Message service the scope belongs to</summary>
|
||||
private StandardMessageBoxManager self;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class MessageScope
|
||||
|
||||
/// <summary>Delegate for the standard message box show function</summary>
|
||||
/// <param name="owner">Window that will modally display the message box</param>
|
||||
/// <param name="text">Text that will be presented to the user</param>
|
||||
/// <param name="caption">Contents of the message box' title bar</param>
|
||||
/// <param name="buttons">Buttons available for the user to choose from</param>
|
||||
/// <param name="icon">Icon that will be displayed next to the text</param>
|
||||
/// <returns>The choice made by the user if multiple buttons were provided</returns>
|
||||
private delegate DialogResult ShowMessageBoxDelegate(
|
||||
IWin32Window owner,
|
||||
string text,
|
||||
string caption,
|
||||
MessageBoxButtons buttons,
|
||||
MessageBoxIcon icon
|
||||
);
|
||||
|
||||
/// <summary>Triggered when a message is displayed to the user</summary>
|
||||
public event EventHandler<MessageEventArgs> MessageDisplaying;
|
||||
|
||||
/// <summary>Triggered when the user has acknowledged the current message</summary>
|
||||
public event EventHandler MessageAcknowledged;
|
||||
|
||||
/// <summary>Initializes a new task dialog message service</summary>
|
||||
public StandardMessageBoxManager() : this(NullActiveWindowTracker.Default) { }
|
||||
|
||||
/// <summary>Initializes a new task dialog message service</summary>
|
||||
/// <param name="tracker">
|
||||
/// Active window tracker used to obtain the parent window for message boxes
|
||||
/// </param>
|
||||
public StandardMessageBoxManager(IActiveWindowTracker tracker) {
|
||||
this.tracker = tracker;
|
||||
this.showMessageDelegate = new ShowMessageBoxDelegate(MessageBox.Show);
|
||||
}
|
||||
|
||||
/// <summary>Asks the user a question that can be answered via several buttons</summary>
|
||||
/// <param name="image">Image that will be shown on the message box</param>
|
||||
/// <param name="text">Text that will be shown to the user</param>
|
||||
/// <param name="buttons">Buttons available for the user to click on</param>
|
||||
/// <returns>The button the user has clicked on</returns>
|
||||
public DialogResult ShowQuestion(
|
||||
MessageBoxIcon image, MessageText text, MessageBoxButtons buttons
|
||||
) {
|
||||
using(var scope = new MessageScope(this, image, text)) {
|
||||
return showMessageBoxInActiveUiThread(
|
||||
text.Message,
|
||||
text.Caption,
|
||||
buttons,
|
||||
image
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Displays a notification to the user</summary>
|
||||
/// <param name="image">Image that will be shown on the message bx</param>
|
||||
/// <param name="text">Text that will be shown to the user</param>
|
||||
public void ShowNotification(MessageBoxIcon image, MessageText text) {
|
||||
using(var scope = new MessageScope(this, image, text)) {
|
||||
showMessageBoxInActiveUiThread(
|
||||
text.Message,
|
||||
text.Caption,
|
||||
MessageBoxButtons.OK,
|
||||
image
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Displays the message box in the active view's thread</summary>
|
||||
/// <param name="message">Text that will be presented to the user</param>
|
||||
/// <param name="caption">Contents of the message box' title bar</param>
|
||||
/// <param name="buttons">Buttons available for the user to choose from</param>
|
||||
/// <param name="image">Image that will be displayed next to the text</param>
|
||||
/// <returns></returns>
|
||||
private DialogResult showMessageBoxInActiveUiThread(
|
||||
string message,
|
||||
string caption,
|
||||
MessageBoxButtons buttons,
|
||||
MessageBoxIcon image
|
||||
) {
|
||||
Form mainWindow = this.tracker.ActiveWindow;
|
||||
if(mainWindow != null) {
|
||||
return (DialogResult)mainWindow.Invoke(
|
||||
this.showMessageDelegate,
|
||||
(IWin32Window)mainWindow, message, caption, buttons, image
|
||||
);
|
||||
}
|
||||
|
||||
// No window tracker or unknown main window -- just show the message box
|
||||
return MessageBox.Show(message, caption, buttons, image);
|
||||
}
|
||||
|
||||
/// <summary>Provides the currently active top-level window</summary>
|
||||
private IActiveWindowTracker tracker;
|
||||
/// <summary>Delegate for the MessageBox.Show() method</summary>
|
||||
private ShowMessageBoxDelegate showMessageDelegate;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Messages
|
||||
|
|
|
@ -1,37 +1,36 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Dummy implementation of the active window tracker service</summary>
|
||||
internal class NullActiveWindowTracker : IActiveWindowTracker {
|
||||
|
||||
/// <summary>The default instance of the dummy window tracker</summary>
|
||||
public static readonly NullActiveWindowTracker Default = new NullActiveWindowTracker();
|
||||
|
||||
/// <summary>The currently active top-level or modal window</summary>
|
||||
public Form ActiveWindow { get { return null; } }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Dummy implementation of the active window tracker service</summary>
|
||||
internal class NullActiveWindowTracker : IActiveWindowTracker {
|
||||
|
||||
/// <summary>The default instance of the dummy window tracker</summary>
|
||||
public static readonly NullActiveWindowTracker Default = new NullActiveWindowTracker();
|
||||
|
||||
/// <summary>The currently active top-level or modal window</summary>
|
||||
public Form ActiveWindow { get { return null; } }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
237
Source/ProgressReporter/ProgressReporterForm.Designer.cs
generated
237
Source/ProgressReporter/ProgressReporterForm.Designer.cs
generated
|
@ -1,120 +1,119 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
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 Nuclex.Windows.Forms.AsyncProgressBar();
|
||||
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.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
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 Nuclex.Windows.Forms.AsyncProgressBar progressBar;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
private System.Windows.Forms.Timer controlCreationTimer;
|
||||
}
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
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 Nuclex.Windows.Forms.AsyncProgressBar();
|
||||
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.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
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 Nuclex.Windows.Forms.AsyncProgressBar progressBar;
|
||||
private System.Windows.Forms.Label statusLabel;
|
||||
private System.Windows.Forms.Timer controlCreationTimer;
|
||||
}
|
||||
}
|
|
@ -1,273 +1,272 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support.Scheduling;
|
||||
using Nuclex.Support.Tracking;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>
|
||||
/// Blocking progress dialog that prevents the user from accessing the application
|
||||
/// window during a modal asynchronous processes.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// class Test : Nuclex.Support.Scheduling.ThreadOperation {
|
||||
///
|
||||
/// static void Main() {
|
||||
/// Test myTest = new Test();
|
||||
/// myTest.Begin();
|
||||
/// Nuclex.Windows.Forms.ProgressReporterForm.Track(myTest);
|
||||
/// myTest.End();
|
||||
/// }
|
||||
///
|
||||
/// protected override void Execute() {
|
||||
/// for(int i = 0; i < 10000000; ++i) {
|
||||
/// OnAsyncProgressUpdated((float)i / 10000000.0f);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public partial class ProgressReporterForm : Form {
|
||||
|
||||
/// <summary>Initializes a new progress reporter</summary>
|
||||
internal ProgressReporterForm() {
|
||||
InitializeComponent();
|
||||
|
||||
this.asyncEndedDelegate = new EventHandler(asyncEnded);
|
||||
this.asyncProgressChangedDelegate = new EventHandler<ProgressReportEventArgs>(
|
||||
asyncProgressChanged
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the progress reporter until the specified transaction has ended.
|
||||
/// </summary>
|
||||
/// <param name="transaction">
|
||||
/// Transaction for whose duration to show the progress reporter
|
||||
/// </param>
|
||||
public static void Track(Transaction transaction) {
|
||||
Track(null, transaction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the progress reporter until the specified transaction has ended.
|
||||
/// </summary>
|
||||
/// <param name="windowTitle">
|
||||
/// Text to be shown in the progress reporter's title bar
|
||||
/// </param>
|
||||
/// <param name="transaction">
|
||||
/// Process for whose duration to show the progress reporter
|
||||
/// </param>
|
||||
public static void Track(string windowTitle, Transaction transaction) {
|
||||
|
||||
// Small optimization to avoid the lengthy control creation when the background
|
||||
// process has already ended. This is an accepted race condition: If the process
|
||||
// finishes right after this line, it doesn't change the outcome, it just
|
||||
// causes the progress dialog to be constructed needlessly.
|
||||
if(transaction.Ended) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the form and let it monitor the transaction's state
|
||||
using(ProgressReporterForm theForm = new ProgressReporterForm()) {
|
||||
theForm.track(windowTitle, transaction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Called when the user tries to close the form manually</summary>
|
||||
/// <param name="arguments">
|
||||
/// Contains a flag that can be used to abort the close attempt
|
||||
/// </param>
|
||||
protected override void OnClosing(CancelEventArgs arguments) {
|
||||
base.OnClosing(arguments);
|
||||
|
||||
// Only allow the form to close when the form is ready to close and the
|
||||
// transaction being tracked has also finished.
|
||||
arguments.Cancel = (Thread.VolatileRead(ref this.state) < 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the progress reporter until the specified transaction has ended.
|
||||
/// </summary>
|
||||
/// <param name="windowTitle">
|
||||
/// Text to be shown in the progress reporter's title bar
|
||||
/// </param>
|
||||
/// <param name="transaction">
|
||||
/// Transaction for whose duration to show the progress reporter
|
||||
/// </param>
|
||||
private void track(string windowTitle, Transaction transaction) {
|
||||
|
||||
// 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 transaction can be aborted
|
||||
this.abortReceiver = (transaction as IAbortable);
|
||||
this.cancelButton.Enabled = (this.abortReceiver != null);
|
||||
|
||||
// Make sure the progress bar control has been created (otherwise, we've got
|
||||
// a chance that BeginInvoke() would fail if the first progress notification
|
||||
// arrived before we called ShowDialog()!)
|
||||
{ IntPtr tempDummy = this.progressBar.Handle; }
|
||||
|
||||
// Subscribe the form to the transaction it is supposed to monitor.
|
||||
// Careful: With the new design, this can cause the asyncEndedDelegate()
|
||||
// callback to be called immediately and synchronously!
|
||||
transaction.AsyncEnded += this.asyncEndedDelegate;
|
||||
IProgressReporter progressReporter = transaction as IProgressReporter;
|
||||
if(progressReporter != null) {
|
||||
progressReporter.AsyncProgressChanged += this.asyncProgressChangedDelegate;
|
||||
}
|
||||
|
||||
// The transaction might have ended before this line was reached, if that's
|
||||
// the case, we don't show the dialog at all.
|
||||
if(!transaction.Ended)
|
||||
ShowDialog();
|
||||
|
||||
// We're done, unsubscribe from the transaction's events again
|
||||
progressReporter = transaction as IProgressReporter;
|
||||
if(progressReporter != null) {
|
||||
progressReporter.AsyncProgressChanged -= this.asyncProgressChangedDelegate;
|
||||
}
|
||||
transaction.AsyncEnded -= this.asyncEndedDelegate;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Called when the transaction has ended</summary>
|
||||
/// <param name="sender">Transaction 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)
|
||||
if(Interlocked.Increment(ref this.state) == 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 transaction's progress updates</summary>
|
||||
/// <param name="sender">Transaction whose progress has been updated</param>
|
||||
/// <param name="arguments">
|
||||
/// Contains the new progress achieved by the transaction
|
||||
/// </param>
|
||||
private void asyncProgressChanged(object sender, ProgressReportEventArgs arguments) {
|
||||
|
||||
// See if this is the first progress update we're receiving. If yes, we need to
|
||||
// switch the progress bar from marquee into its normal mode!
|
||||
int haveProgress = Interlocked.Exchange(ref this.areProgressUpdatesIncoming, 1);
|
||||
if(haveProgress == 0) {
|
||||
this.progressBar.BeginInvoke(
|
||||
(MethodInvoker)delegate() { this.progressBar.Style = ProgressBarStyle.Blocks; }
|
||||
);
|
||||
}
|
||||
|
||||
// Send the new progress to the progress bar
|
||||
this.progressBar.AsyncSetValue(arguments.Progress);
|
||||
|
||||
}
|
||||
|
||||
/// <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="arguments">Not used</param>
|
||||
private void controlCreationTimerTicked(object sender, EventArgs arguments) {
|
||||
|
||||
// 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!
|
||||
if(Interlocked.Increment(ref this.state) == 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="arguments">Not used</param>
|
||||
private void cancelClicked(object sender, EventArgs arguments) {
|
||||
|
||||
if(this.abortReceiver != null) {
|
||||
|
||||
// Do this first because the abort receiver might trigger the AsyncEnded()
|
||||
// event in the calling thread (us!) 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>Delegate for the asyncEnded() method</summary>
|
||||
private EventHandler asyncEndedDelegate;
|
||||
/// <summary>Delegate for the asyncProgressUpdated() method</summary>
|
||||
private EventHandler<ProgressReportEventArgs> asyncProgressChangedDelegate;
|
||||
/// <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>Whether we're receiving progress updates from the transaction</summary>
|
||||
/// <remarks>
|
||||
/// 0: No progress updates have arrived so far
|
||||
/// 1: We have received at least one progress update from the transaction
|
||||
/// </remarks>
|
||||
private int areProgressUpdatesIncoming;
|
||||
/// <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
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support.Scheduling;
|
||||
using Nuclex.Support.Tracking;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>
|
||||
/// Blocking progress dialog that prevents the user from accessing the application
|
||||
/// window during a modal asynchronous processes.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// class Test : Nuclex.Support.Scheduling.ThreadOperation {
|
||||
///
|
||||
/// static void Main() {
|
||||
/// Test myTest = new Test();
|
||||
/// myTest.Begin();
|
||||
/// Nuclex.Windows.Forms.ProgressReporterForm.Track(myTest);
|
||||
/// myTest.End();
|
||||
/// }
|
||||
///
|
||||
/// protected override void Execute() {
|
||||
/// for(int i = 0; i < 10000000; ++i) {
|
||||
/// OnAsyncProgressUpdated((float)i / 10000000.0f);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public partial class ProgressReporterForm : Form {
|
||||
|
||||
/// <summary>Initializes a new progress reporter</summary>
|
||||
internal ProgressReporterForm() {
|
||||
InitializeComponent();
|
||||
|
||||
this.asyncEndedDelegate = new EventHandler(asyncEnded);
|
||||
this.asyncProgressChangedDelegate = new EventHandler<ProgressReportEventArgs>(
|
||||
asyncProgressChanged
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the progress reporter until the specified transaction has ended.
|
||||
/// </summary>
|
||||
/// <param name="transaction">
|
||||
/// Transaction for whose duration to show the progress reporter
|
||||
/// </param>
|
||||
public static void Track(Transaction transaction) {
|
||||
Track(null, transaction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the progress reporter until the specified transaction has ended.
|
||||
/// </summary>
|
||||
/// <param name="windowTitle">
|
||||
/// Text to be shown in the progress reporter's title bar
|
||||
/// </param>
|
||||
/// <param name="transaction">
|
||||
/// Process for whose duration to show the progress reporter
|
||||
/// </param>
|
||||
public static void Track(string windowTitle, Transaction transaction) {
|
||||
|
||||
// Small optimization to avoid the lengthy control creation when the background
|
||||
// process has already ended. This is an accepted race condition: If the process
|
||||
// finishes right after this line, it doesn't change the outcome, it just
|
||||
// causes the progress dialog to be constructed needlessly.
|
||||
if(transaction.Ended) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the form and let it monitor the transaction's state
|
||||
using(ProgressReporterForm theForm = new ProgressReporterForm()) {
|
||||
theForm.track(windowTitle, transaction);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Called when the user tries to close the form manually</summary>
|
||||
/// <param name="arguments">
|
||||
/// Contains a flag that can be used to abort the close attempt
|
||||
/// </param>
|
||||
protected override void OnClosing(CancelEventArgs arguments) {
|
||||
base.OnClosing(arguments);
|
||||
|
||||
// Only allow the form to close when the form is ready to close and the
|
||||
// transaction being tracked has also finished.
|
||||
arguments.Cancel = (Thread.VolatileRead(ref this.state) < 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the progress reporter until the specified transaction has ended.
|
||||
/// </summary>
|
||||
/// <param name="windowTitle">
|
||||
/// Text to be shown in the progress reporter's title bar
|
||||
/// </param>
|
||||
/// <param name="transaction">
|
||||
/// Transaction for whose duration to show the progress reporter
|
||||
/// </param>
|
||||
private void track(string windowTitle, Transaction transaction) {
|
||||
|
||||
// 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 transaction can be aborted
|
||||
this.abortReceiver = (transaction as IAbortable);
|
||||
this.cancelButton.Enabled = (this.abortReceiver != null);
|
||||
|
||||
// Make sure the progress bar control has been created (otherwise, we've got
|
||||
// a chance that BeginInvoke() would fail if the first progress notification
|
||||
// arrived before we called ShowDialog()!)
|
||||
{ IntPtr tempDummy = this.progressBar.Handle; }
|
||||
|
||||
// Subscribe the form to the transaction it is supposed to monitor.
|
||||
// Careful: With the new design, this can cause the asyncEndedDelegate()
|
||||
// callback to be called immediately and synchronously!
|
||||
transaction.AsyncEnded += this.asyncEndedDelegate;
|
||||
IProgressReporter progressReporter = transaction as IProgressReporter;
|
||||
if(progressReporter != null) {
|
||||
progressReporter.AsyncProgressChanged += this.asyncProgressChangedDelegate;
|
||||
}
|
||||
|
||||
// The transaction might have ended before this line was reached, if that's
|
||||
// the case, we don't show the dialog at all.
|
||||
if(!transaction.Ended)
|
||||
ShowDialog();
|
||||
|
||||
// We're done, unsubscribe from the transaction's events again
|
||||
progressReporter = transaction as IProgressReporter;
|
||||
if(progressReporter != null) {
|
||||
progressReporter.AsyncProgressChanged -= this.asyncProgressChangedDelegate;
|
||||
}
|
||||
transaction.AsyncEnded -= this.asyncEndedDelegate;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Called when the transaction has ended</summary>
|
||||
/// <param name="sender">Transaction 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)
|
||||
if(Interlocked.Increment(ref this.state) == 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 transaction's progress updates</summary>
|
||||
/// <param name="sender">Transaction whose progress has been updated</param>
|
||||
/// <param name="arguments">
|
||||
/// Contains the new progress achieved by the transaction
|
||||
/// </param>
|
||||
private void asyncProgressChanged(object sender, ProgressReportEventArgs arguments) {
|
||||
|
||||
// See if this is the first progress update we're receiving. If yes, we need to
|
||||
// switch the progress bar from marquee into its normal mode!
|
||||
int haveProgress = Interlocked.Exchange(ref this.areProgressUpdatesIncoming, 1);
|
||||
if(haveProgress == 0) {
|
||||
this.progressBar.BeginInvoke(
|
||||
(MethodInvoker)delegate() { this.progressBar.Style = ProgressBarStyle.Blocks; }
|
||||
);
|
||||
}
|
||||
|
||||
// Send the new progress to the progress bar
|
||||
this.progressBar.AsyncSetValue(arguments.Progress);
|
||||
|
||||
}
|
||||
|
||||
/// <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="arguments">Not used</param>
|
||||
private void controlCreationTimerTicked(object sender, EventArgs arguments) {
|
||||
|
||||
// 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!
|
||||
if(Interlocked.Increment(ref this.state) == 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="arguments">Not used</param>
|
||||
private void cancelClicked(object sender, EventArgs arguments) {
|
||||
|
||||
if(this.abortReceiver != null) {
|
||||
|
||||
// Do this first because the abort receiver might trigger the AsyncEnded()
|
||||
// event in the calling thread (us!) 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>Delegate for the asyncEnded() method</summary>
|
||||
private EventHandler asyncEndedDelegate;
|
||||
/// <summary>Delegate for the asyncProgressUpdated() method</summary>
|
||||
private EventHandler<ProgressReportEventArgs> asyncProgressChangedDelegate;
|
||||
/// <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>Whether we're receiving progress updates from the transaction</summary>
|
||||
/// <remarks>
|
||||
/// 0: No progress updates have arrived so far
|
||||
/// 1: We have received at least one progress update from the transaction
|
||||
/// </remarks>
|
||||
private int areProgressUpdatesIncoming;
|
||||
/// <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
|
||||
|
|
|
@ -1,114 +1,113 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support.Tracking;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Tracking bar that can be hosted in a tool strip container</summary>
|
||||
public class ToolStripTrackingBar : ToolStripControlHost {
|
||||
|
||||
/// <summary>Initializes a new tool strip tracking bar</summary>
|
||||
public ToolStripTrackingBar() : base(createTrackingBar()) {
|
||||
hideControlAtRuntime();
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new tool strip tracking bar with a name</summary>
|
||||
/// <param name="name">Name of the tracking bar control</param>
|
||||
public ToolStripTrackingBar(string name) : base(createTrackingBar(), name) {
|
||||
hideControlAtRuntime();
|
||||
}
|
||||
|
||||
/// <summary>The tracking bar control being hosted by the tool strip host</summary>
|
||||
public TrackingBar TrackingBarControl {
|
||||
get { return base.Control as TrackingBar; }
|
||||
}
|
||||
|
||||
/// <summary>Tracks the specified transaction in the tracking bar</summary>
|
||||
/// <param name="transaction">Transaction to be tracked</param>
|
||||
public void Track(Transaction transaction) {
|
||||
TrackingBarControl.Track(transaction);
|
||||
}
|
||||
|
||||
/// <summary>Tracks the specified transaction in the tracking bar</summary>
|
||||
/// <param name="transaction">Transaction to be tracked</param>
|
||||
/// <param name="weight">Weight of this transaction in the total progress</param>
|
||||
public void Track(Transaction transaction, float weight) {
|
||||
TrackingBarControl.Track(transaction, weight);
|
||||
}
|
||||
|
||||
/// <summary>Stops tracking the specified transaction</summary>
|
||||
/// <param name="transaction">Transaction to stop tracking</param>
|
||||
public void Untrack(Transaction transaction) {
|
||||
TrackingBarControl.Untrack(transaction);
|
||||
}
|
||||
|
||||
/// <summary>Default size of the hosted control</summary>
|
||||
protected override Size DefaultSize {
|
||||
get { return new Size(100, 15); }
|
||||
}
|
||||
|
||||
/// <summary>Default margin to leave around the control in the tool strip</summary>
|
||||
protected override Padding DefaultMargin {
|
||||
get {
|
||||
if((base.Owner != null) && (base.Owner is StatusStrip)) {
|
||||
return new Padding(1, 3, 1, 3);
|
||||
}
|
||||
|
||||
return new Padding(1, 2, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a new tracking bar</summary>
|
||||
/// <returns>A new tracking bar</returns>
|
||||
private static TrackingBar createTrackingBar() {
|
||||
TrackingBar trackingBar = new TrackingBar();
|
||||
trackingBar.Size = new Size(100, 15);
|
||||
return trackingBar;
|
||||
}
|
||||
|
||||
/// <summary>Hides the control during runtime usage</summary>
|
||||
private void hideControlAtRuntime() {
|
||||
TrackingBarControl.VisibleChanged += new EventHandler(trackingBarVisibleChanged);
|
||||
|
||||
LicenseUsageMode usageMode = System.ComponentModel.LicenseManager.UsageMode;
|
||||
if(usageMode == LicenseUsageMode.Runtime) {
|
||||
base.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the visibility of the tool strip host when the tracking bar control's
|
||||
/// visibility changes.
|
||||
/// </summary>
|
||||
/// <param name="sender">Tracking bar control whose visiblity has changed</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void trackingBarVisibleChanged(object sender, EventArgs arguments) {
|
||||
base.Visible = TrackingBarControl.Visible;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support.Tracking;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Tracking bar that can be hosted in a tool strip container</summary>
|
||||
public class ToolStripTrackingBar : ToolStripControlHost {
|
||||
|
||||
/// <summary>Initializes a new tool strip tracking bar</summary>
|
||||
public ToolStripTrackingBar() : base(createTrackingBar()) {
|
||||
hideControlAtRuntime();
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new tool strip tracking bar with a name</summary>
|
||||
/// <param name="name">Name of the tracking bar control</param>
|
||||
public ToolStripTrackingBar(string name) : base(createTrackingBar(), name) {
|
||||
hideControlAtRuntime();
|
||||
}
|
||||
|
||||
/// <summary>The tracking bar control being hosted by the tool strip host</summary>
|
||||
public TrackingBar TrackingBarControl {
|
||||
get { return base.Control as TrackingBar; }
|
||||
}
|
||||
|
||||
/// <summary>Tracks the specified transaction in the tracking bar</summary>
|
||||
/// <param name="transaction">Transaction to be tracked</param>
|
||||
public void Track(Transaction transaction) {
|
||||
TrackingBarControl.Track(transaction);
|
||||
}
|
||||
|
||||
/// <summary>Tracks the specified transaction in the tracking bar</summary>
|
||||
/// <param name="transaction">Transaction to be tracked</param>
|
||||
/// <param name="weight">Weight of this transaction in the total progress</param>
|
||||
public void Track(Transaction transaction, float weight) {
|
||||
TrackingBarControl.Track(transaction, weight);
|
||||
}
|
||||
|
||||
/// <summary>Stops tracking the specified transaction</summary>
|
||||
/// <param name="transaction">Transaction to stop tracking</param>
|
||||
public void Untrack(Transaction transaction) {
|
||||
TrackingBarControl.Untrack(transaction);
|
||||
}
|
||||
|
||||
/// <summary>Default size of the hosted control</summary>
|
||||
protected override Size DefaultSize {
|
||||
get { return new Size(100, 15); }
|
||||
}
|
||||
|
||||
/// <summary>Default margin to leave around the control in the tool strip</summary>
|
||||
protected override Padding DefaultMargin {
|
||||
get {
|
||||
if((base.Owner != null) && (base.Owner is StatusStrip)) {
|
||||
return new Padding(1, 3, 1, 3);
|
||||
}
|
||||
|
||||
return new Padding(1, 2, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a new tracking bar</summary>
|
||||
/// <returns>A new tracking bar</returns>
|
||||
private static TrackingBar createTrackingBar() {
|
||||
TrackingBar trackingBar = new TrackingBar();
|
||||
trackingBar.Size = new Size(100, 15);
|
||||
return trackingBar;
|
||||
}
|
||||
|
||||
/// <summary>Hides the control during runtime usage</summary>
|
||||
private void hideControlAtRuntime() {
|
||||
TrackingBarControl.VisibleChanged += new EventHandler(trackingBarVisibleChanged);
|
||||
|
||||
LicenseUsageMode usageMode = System.ComponentModel.LicenseManager.UsageMode;
|
||||
if(usageMode == LicenseUsageMode.Runtime) {
|
||||
base.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the visibility of the tool strip host when the tracking bar control's
|
||||
/// visibility changes.
|
||||
/// </summary>
|
||||
/// <param name="sender">Tracking bar control whose visiblity has changed</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void trackingBarVisibleChanged(object sender, EventArgs arguments) {
|
||||
base.Visible = TrackingBarControl.Visible;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
101
Source/TrackingBar/TrackingBar.Designer.cs
generated
101
Source/TrackingBar/TrackingBar.Designer.cs
generated
|
@ -1,51 +1,50 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
partial class TrackingBar {
|
||||
/// <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 Component 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() {
|
||||
components = new System.ComponentModel.Container();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
partial class TrackingBar {
|
||||
/// <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 Component 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() {
|
||||
components = new System.ComponentModel.Container();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,134 +1,133 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
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;
|
||||
|
||||
using Nuclex.Support.Tracking;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Progress bar for tracking the progress of background operations</summary>
|
||||
public partial class TrackingBar : AsyncProgressBar {
|
||||
|
||||
/// <summary>Initializes a new tracking bar</summary>
|
||||
public TrackingBar() {
|
||||
InitializeComponent();
|
||||
|
||||
// We start off being in the idle state (and thus, being invisible)
|
||||
this.isIdle = true;
|
||||
this.Visible = false;
|
||||
|
||||
// Initialize the delegates we use to update the control's state and those
|
||||
// we use to register ourselfes to the tracker's events
|
||||
this.updateIdleStateDelegate = new MethodInvoker(updateIdleState);
|
||||
this.asyncIdleStateChangedDelegate = new EventHandler<IdleStateEventArgs>(
|
||||
asyncIdleStateChanged
|
||||
);
|
||||
this.asyncProgressUpdateDelegate = new EventHandler<ProgressReportEventArgs>(
|
||||
asyncProgressUpdated
|
||||
);
|
||||
|
||||
// Create the tracker and attach ourselfes to its events
|
||||
this.tracker = new ProgressTracker();
|
||||
this.tracker.AsyncIdleStateChanged += this.asyncIdleStateChangedDelegate;
|
||||
this.tracker.AsyncProgressChanged += this.asyncProgressUpdateDelegate;
|
||||
}
|
||||
|
||||
/// <summary>Tracks the specified transaction in the tracking bar</summary>
|
||||
/// <param name="transaction">Transaction to be tracked</param>
|
||||
public void Track(Transaction transaction) {
|
||||
this.tracker.Track(transaction);
|
||||
}
|
||||
|
||||
/// <summary>Tracks the specified transaction in the tracking bar</summary>
|
||||
/// <param name="transaction">Transaction to be tracked</param>
|
||||
/// <param name="weight">Weight of this transaction in the total progress</param>
|
||||
public void Track(Transaction transaction, float weight) {
|
||||
this.tracker.Track(transaction, weight);
|
||||
}
|
||||
|
||||
/// <summary>Stops tracking the specified transaction</summary>
|
||||
/// <param name="transaction">Transaction to stop tracking</param>
|
||||
public void Untrack(Transaction transaction) {
|
||||
this.tracker.Untrack(transaction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the summed progressed of the tracked transaction has changed
|
||||
/// </summary>
|
||||
/// <param name="sender">Transaction whose progress has changed</param>
|
||||
/// <param name="arguments">Contains the progress achieved by the transaction</param>
|
||||
private void asyncProgressUpdated(
|
||||
object sender, ProgressReportEventArgs arguments
|
||||
) {
|
||||
AsyncSetValue(arguments.Progress);
|
||||
}
|
||||
|
||||
/// <summary>Called when the tracker becomes enters of leaves the idle state</summary>
|
||||
/// <param name="sender">Tracker that has entered or left the idle state</param>
|
||||
/// <param name="arguments">Contains the new idle state</param>
|
||||
private void asyncIdleStateChanged(object sender, IdleStateEventArgs arguments) {
|
||||
|
||||
// Do a fully synchronous update of the idle state. This update must not be
|
||||
// lost because otherwise, the progress bar might stay on-screen when in fact,
|
||||
// the background operation has already finished and nothing is happening anymore.
|
||||
this.isIdle = arguments.Idle;
|
||||
|
||||
// Update the bar's idle state
|
||||
if(InvokeRequired) {
|
||||
Invoke(this.updateIdleStateDelegate);
|
||||
} else {
|
||||
updateIdleState();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the idle state of the progress bar
|
||||
/// (controls whether the progress bar is shown or invisible)
|
||||
/// </summary>
|
||||
private void updateIdleState() {
|
||||
|
||||
// Only show the progress bar when something is happening
|
||||
base.Visible = !this.isIdle;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Whether the progress bar is in the idle state</summary>
|
||||
private volatile bool isIdle;
|
||||
/// <summary>Tracker used to sum and update the total progress</summary>
|
||||
private ProgressTracker tracker;
|
||||
/// <summary>Delegate for the idle state update method</summary>
|
||||
private MethodInvoker updateIdleStateDelegate;
|
||||
/// <summary>Delegate for the asyncIdleStateChanged() method</summary>
|
||||
private EventHandler<IdleStateEventArgs> asyncIdleStateChangedDelegate;
|
||||
/// <summary>Delegate for the asyncProgressUpdate() method</summary>
|
||||
private EventHandler<ProgressReportEventArgs> asyncProgressUpdateDelegate;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
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;
|
||||
|
||||
using Nuclex.Support.Tracking;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Progress bar for tracking the progress of background operations</summary>
|
||||
public partial class TrackingBar : AsyncProgressBar {
|
||||
|
||||
/// <summary>Initializes a new tracking bar</summary>
|
||||
public TrackingBar() {
|
||||
InitializeComponent();
|
||||
|
||||
// We start off being in the idle state (and thus, being invisible)
|
||||
this.isIdle = true;
|
||||
this.Visible = false;
|
||||
|
||||
// Initialize the delegates we use to update the control's state and those
|
||||
// we use to register ourselfes to the tracker's events
|
||||
this.updateIdleStateDelegate = new MethodInvoker(updateIdleState);
|
||||
this.asyncIdleStateChangedDelegate = new EventHandler<IdleStateEventArgs>(
|
||||
asyncIdleStateChanged
|
||||
);
|
||||
this.asyncProgressUpdateDelegate = new EventHandler<ProgressReportEventArgs>(
|
||||
asyncProgressUpdated
|
||||
);
|
||||
|
||||
// Create the tracker and attach ourselfes to its events
|
||||
this.tracker = new ProgressTracker();
|
||||
this.tracker.AsyncIdleStateChanged += this.asyncIdleStateChangedDelegate;
|
||||
this.tracker.AsyncProgressChanged += this.asyncProgressUpdateDelegate;
|
||||
}
|
||||
|
||||
/// <summary>Tracks the specified transaction in the tracking bar</summary>
|
||||
/// <param name="transaction">Transaction to be tracked</param>
|
||||
public void Track(Transaction transaction) {
|
||||
this.tracker.Track(transaction);
|
||||
}
|
||||
|
||||
/// <summary>Tracks the specified transaction in the tracking bar</summary>
|
||||
/// <param name="transaction">Transaction to be tracked</param>
|
||||
/// <param name="weight">Weight of this transaction in the total progress</param>
|
||||
public void Track(Transaction transaction, float weight) {
|
||||
this.tracker.Track(transaction, weight);
|
||||
}
|
||||
|
||||
/// <summary>Stops tracking the specified transaction</summary>
|
||||
/// <param name="transaction">Transaction to stop tracking</param>
|
||||
public void Untrack(Transaction transaction) {
|
||||
this.tracker.Untrack(transaction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the summed progressed of the tracked transaction has changed
|
||||
/// </summary>
|
||||
/// <param name="sender">Transaction whose progress has changed</param>
|
||||
/// <param name="arguments">Contains the progress achieved by the transaction</param>
|
||||
private void asyncProgressUpdated(
|
||||
object sender, ProgressReportEventArgs arguments
|
||||
) {
|
||||
AsyncSetValue(arguments.Progress);
|
||||
}
|
||||
|
||||
/// <summary>Called when the tracker becomes enters of leaves the idle state</summary>
|
||||
/// <param name="sender">Tracker that has entered or left the idle state</param>
|
||||
/// <param name="arguments">Contains the new idle state</param>
|
||||
private void asyncIdleStateChanged(object sender, IdleStateEventArgs arguments) {
|
||||
|
||||
// Do a fully synchronous update of the idle state. This update must not be
|
||||
// lost because otherwise, the progress bar might stay on-screen when in fact,
|
||||
// the background operation has already finished and nothing is happening anymore.
|
||||
this.isIdle = arguments.Idle;
|
||||
|
||||
// Update the bar's idle state
|
||||
if(InvokeRequired) {
|
||||
Invoke(this.updateIdleStateDelegate);
|
||||
} else {
|
||||
updateIdleState();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the idle state of the progress bar
|
||||
/// (controls whether the progress bar is shown or invisible)
|
||||
/// </summary>
|
||||
private void updateIdleState() {
|
||||
|
||||
// Only show the progress bar when something is happening
|
||||
base.Visible = !this.isIdle;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Whether the progress bar is in the idle state</summary>
|
||||
private volatile bool isIdle;
|
||||
/// <summary>Tracker used to sum and update the total progress</summary>
|
||||
private ProgressTracker tracker;
|
||||
/// <summary>Delegate for the idle state update method</summary>
|
||||
private MethodInvoker updateIdleStateDelegate;
|
||||
/// <summary>Delegate for the asyncIdleStateChanged() method</summary>
|
||||
private EventHandler<IdleStateEventArgs> asyncIdleStateChangedDelegate;
|
||||
/// <summary>Delegate for the asyncProgressUpdate() method</summary>
|
||||
private EventHandler<ProgressReportEventArgs> asyncProgressUpdateDelegate;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,156 +1,155 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Unit test for the dialog view model</summary>
|
||||
[TestFixture]
|
||||
public class DialogViewModelTest {
|
||||
|
||||
#region class DialogViewModelSubscriber
|
||||
|
||||
/// <summary>Subscriber for the events offered by a dialog view model</summary>
|
||||
private class DialogViewModelSubscriber {
|
||||
|
||||
/// <summary>Indicates that the user has accepted the dialog</summary>
|
||||
public void Confirmed(object sender, EventArgs arguments) {
|
||||
++this.confirmCallCount;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the user has cancelled the dialog</summary>
|
||||
public void Cancelled(object sender, EventArgs arguments) {
|
||||
++this.cancelCallCount;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the dialog was simply closed</summary>
|
||||
public void Submitted(object sender, EventArgs arguments) {
|
||||
++this.submitCallCount;
|
||||
}
|
||||
|
||||
/// <summary>How many times the Confirmed() method was called</summary>
|
||||
public int ConfirmCallCount {
|
||||
get { return this.confirmCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Cancelled() method was called</summary>
|
||||
public int CancelCallCount {
|
||||
get { return this.cancelCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Submitted() method was called</summary>
|
||||
public int SubmitCallCount {
|
||||
get { return this.submitCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Confirmed() method was called</summary>
|
||||
private int confirmCallCount;
|
||||
/// <summary>How many times the Cancelled() method was called</summary>
|
||||
private int cancelCallCount;
|
||||
/// <summary>How many times the Submitted() method was called</summary>
|
||||
private int submitCallCount;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DialogViewModelSubscriber
|
||||
|
||||
/// <summary>Verifies that the dialog view model has a default constructor</summary>
|
||||
[Test]
|
||||
public void HasDefaultConstructor() {
|
||||
Assert.DoesNotThrow(
|
||||
delegate() { new DialogViewModel(); }
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Confirm() on the dialog view model triggers
|
||||
/// the 'Confirmed' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConfirmTriggersConfirmedEvent() {
|
||||
var viewModel = new DialogViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Confirm();
|
||||
Assert.AreEqual(1, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Cancel() on the dialog view model triggers
|
||||
/// the 'Cancelled' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CancelTriggersCancelledEvent() {
|
||||
var viewModel = new DialogViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Cancel();
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(1, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Submitm() on the dialog view model triggers
|
||||
/// the 'Submitted' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SubmitTriggersSubmittedEvent() {
|
||||
var viewModel = new DialogViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Submit();
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(1, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
|
||||
/// <param name="viewModel">View model a subscriber will be created for</param>
|
||||
/// <returns>A subscriber for the events of the specified view model</returns>
|
||||
private DialogViewModelSubscriber createSubscriber(DialogViewModel viewModel) {
|
||||
var subscriber = new DialogViewModelSubscriber();
|
||||
viewModel.Confirmed += subscriber.Confirmed;
|
||||
viewModel.Canceled += subscriber.Cancelled;
|
||||
viewModel.Submitted += subscriber.Submitted;
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
||||
#endif // UNITTEST
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Unit test for the dialog view model</summary>
|
||||
[TestFixture]
|
||||
public class DialogViewModelTest {
|
||||
|
||||
#region class DialogViewModelSubscriber
|
||||
|
||||
/// <summary>Subscriber for the events offered by a dialog view model</summary>
|
||||
private class DialogViewModelSubscriber {
|
||||
|
||||
/// <summary>Indicates that the user has accepted the dialog</summary>
|
||||
public void Confirmed(object sender, EventArgs arguments) {
|
||||
++this.confirmCallCount;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the user has cancelled the dialog</summary>
|
||||
public void Cancelled(object sender, EventArgs arguments) {
|
||||
++this.cancelCallCount;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the dialog was simply closed</summary>
|
||||
public void Submitted(object sender, EventArgs arguments) {
|
||||
++this.submitCallCount;
|
||||
}
|
||||
|
||||
/// <summary>How many times the Confirmed() method was called</summary>
|
||||
public int ConfirmCallCount {
|
||||
get { return this.confirmCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Cancelled() method was called</summary>
|
||||
public int CancelCallCount {
|
||||
get { return this.cancelCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Submitted() method was called</summary>
|
||||
public int SubmitCallCount {
|
||||
get { return this.submitCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Confirmed() method was called</summary>
|
||||
private int confirmCallCount;
|
||||
/// <summary>How many times the Cancelled() method was called</summary>
|
||||
private int cancelCallCount;
|
||||
/// <summary>How many times the Submitted() method was called</summary>
|
||||
private int submitCallCount;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DialogViewModelSubscriber
|
||||
|
||||
/// <summary>Verifies that the dialog view model has a default constructor</summary>
|
||||
[Test]
|
||||
public void HasDefaultConstructor() {
|
||||
Assert.DoesNotThrow(
|
||||
delegate() { new DialogViewModel(); }
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Confirm() on the dialog view model triggers
|
||||
/// the 'Confirmed' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConfirmTriggersConfirmedEvent() {
|
||||
var viewModel = new DialogViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Confirm();
|
||||
Assert.AreEqual(1, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Cancel() on the dialog view model triggers
|
||||
/// the 'Cancelled' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CancelTriggersCancelledEvent() {
|
||||
var viewModel = new DialogViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Cancel();
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(1, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Submitm() on the dialog view model triggers
|
||||
/// the 'Submitted' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SubmitTriggersSubmittedEvent() {
|
||||
var viewModel = new DialogViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Submit();
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(1, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
|
||||
/// <param name="viewModel">View model a subscriber will be created for</param>
|
||||
/// <returns>A subscriber for the events of the specified view model</returns>
|
||||
private DialogViewModelSubscriber createSubscriber(DialogViewModel viewModel) {
|
||||
var subscriber = new DialogViewModelSubscriber();
|
||||
viewModel.Confirmed += subscriber.Confirmed;
|
||||
viewModel.Canceled += subscriber.Cancelled;
|
||||
viewModel.Submitted += subscriber.Submitted;
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
||||
#endif // UNITTEST
|
||||
|
|
|
@ -1,76 +1,75 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Base class for the view model of dialogs (typically modal ones)</summary>
|
||||
public class DialogViewModel : Observable {
|
||||
|
||||
/// <summary>Indicates that the view should close with a positive result</summary>
|
||||
/// <remarks>
|
||||
/// This event typically corresponds to the 'Ok' button in a dialog.
|
||||
/// </remarks>
|
||||
public event EventHandler Confirmed;
|
||||
|
||||
/// <summary>Indicates that the view should close with a negative result</summary>
|
||||
/// <remarks>
|
||||
/// This event typically corresponds to the 'Cancel' button in a dialog.
|
||||
/// </remarks>
|
||||
public event EventHandler Canceled;
|
||||
|
||||
/// <summary>Indicates that the view should close</summary>
|
||||
/// <remarks>
|
||||
/// This closes the view with a neutral result, used when the view doesn't follow
|
||||
/// an ok/cancel scheme or the result is transmitted in some other way.
|
||||
/// </remarks>
|
||||
public event EventHandler Submitted;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the dialog should be closed with a positive outcome
|
||||
/// </summary>
|
||||
public virtual void Confirm() {
|
||||
if(Confirmed != null) {
|
||||
Confirmed(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the dialog should be closed with a negative outcome
|
||||
/// </summary>
|
||||
public virtual void Cancel() {
|
||||
if(Canceled != null) {
|
||||
Canceled(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the dialog should be closed</summary>
|
||||
public virtual void Submit() {
|
||||
if(Submitted != null) {
|
||||
Submitted(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Base class for the view model of dialogs (typically modal ones)</summary>
|
||||
public class DialogViewModel : Observable {
|
||||
|
||||
/// <summary>Indicates that the view should close with a positive result</summary>
|
||||
/// <remarks>
|
||||
/// This event typically corresponds to the 'Ok' button in a dialog.
|
||||
/// </remarks>
|
||||
public event EventHandler Confirmed;
|
||||
|
||||
/// <summary>Indicates that the view should close with a negative result</summary>
|
||||
/// <remarks>
|
||||
/// This event typically corresponds to the 'Cancel' button in a dialog.
|
||||
/// </remarks>
|
||||
public event EventHandler Canceled;
|
||||
|
||||
/// <summary>Indicates that the view should close</summary>
|
||||
/// <remarks>
|
||||
/// This closes the view with a neutral result, used when the view doesn't follow
|
||||
/// an ok/cancel scheme or the result is transmitted in some other way.
|
||||
/// </remarks>
|
||||
public event EventHandler Submitted;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the dialog should be closed with a positive outcome
|
||||
/// </summary>
|
||||
public virtual void Confirm() {
|
||||
if(Confirmed != null) {
|
||||
Confirmed(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the dialog should be closed with a negative outcome
|
||||
/// </summary>
|
||||
public virtual void Cancel() {
|
||||
if(Canceled != null) {
|
||||
Canceled(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the dialog should be closed</summary>
|
||||
public virtual void Submit() {
|
||||
if(Submitted != null) {
|
||||
Submitted(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
|
|
@ -1,14 +1,33 @@
|
|||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Interface for vew models that can switch between different pages</summary>
|
||||
public interface IMultiPageViewModel {
|
||||
|
||||
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
|
||||
/// <returns>A view model for the active page on the multi-page view model</returns>
|
||||
object GetActivePageViewModel();
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Interface for vew models that can switch between different pages</summary>
|
||||
public interface IMultiPageViewModel {
|
||||
|
||||
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
|
||||
/// <returns>A view model for the active page on the multi-page view model</returns>
|
||||
object GetActivePageViewModel();
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
|
|
@ -1,130 +1,129 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Base class for view models that have multiple child view models</summary>
|
||||
/// <typeparam name="TPageEnumeration">Enum type by which pages can be indicated</typeparam>
|
||||
public abstract class MultiPageViewModel<TPageEnumeration> :
|
||||
Observable, IMultiPageViewModel, IDisposable {
|
||||
|
||||
/// <summary>Initializes a new multi-page view model</summary>
|
||||
/// <param name="windowManager">
|
||||
/// Window manager the view model uses to create child views
|
||||
/// </param>
|
||||
/// <param name="cachePageViewModels">
|
||||
/// Whether child view models will be kept alive and reused
|
||||
/// </param>
|
||||
public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) {
|
||||
this.windowManager = windowManager;
|
||||
if(cachePageViewModels) {
|
||||
this.cachedViewModels = new ConcurrentDictionary<TPageEnumeration, object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public virtual void Dispose() {
|
||||
if(this.cachedViewModels != null) {
|
||||
foreach(object cacheViewModel in this.cachedViewModels.Values) {
|
||||
disposeIfSupported(cacheViewModel);
|
||||
}
|
||||
this.activePageViewModel = null;
|
||||
|
||||
this.cachedViewModels.Clear();
|
||||
this.cachedViewModels = null;
|
||||
} else if(this.activePageViewModel != null) {
|
||||
disposeIfSupported(this.activePageViewModel);
|
||||
this.activePageViewModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Child page that is currently being displayed by the view model</summary>
|
||||
public TPageEnumeration ActivePage {
|
||||
get { return this.activePage; }
|
||||
set {
|
||||
if(!this.activePage.Equals(value)) {
|
||||
this.activePage = value;
|
||||
if(this.activePageViewModel != null) {
|
||||
if(this.cachedViewModels == null) {
|
||||
disposeIfSupported(this.activePageViewModel);
|
||||
}
|
||||
this.activePageViewModel = null;
|
||||
}
|
||||
OnPropertyChanged(nameof(ActivePage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
|
||||
/// <returns>A view model for the active page on the multi-page view model</returns>
|
||||
public object GetActivePageViewModel() {
|
||||
if(this.cachedViewModels == null) {
|
||||
if(this.activePageViewModel == null) {
|
||||
this.activePageViewModel = CreateViewModelForPage(this.activePage);
|
||||
}
|
||||
} else if(this.activePageViewModel == null) {
|
||||
this.activePageViewModel = this.cachedViewModels.GetOrAdd(
|
||||
this.activePage,
|
||||
delegate(TPageEnumeration activePage) {
|
||||
return CreateViewModelForPage(this.activePage);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return this.activePageViewModel;
|
||||
}
|
||||
|
||||
/// <summary>Windowmanager that can create view models and display other views</summary>
|
||||
protected IWindowManager WindowManager {
|
||||
get { return this.windowManager; }
|
||||
}
|
||||
|
||||
/// <summary>Creates a view model for the specified page</summary>
|
||||
/// <param name="page">Page for which a view model will be created</param>
|
||||
/// <returns>The view model for the specified page</returns>
|
||||
protected abstract object CreateViewModelForPage(TPageEnumeration page);
|
||||
|
||||
/// <summary>Disposes the specified object if it is disposable</summary>
|
||||
/// <param name="potentiallyDisposable">Object that will be disposed if supported</param>
|
||||
private static void disposeIfSupported(object potentiallyDisposable) {
|
||||
var disposable = potentiallyDisposable as IDisposable;
|
||||
if(disposable != null) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Page that is currently active in the multi-page view model</summary>
|
||||
private TPageEnumeration activePage;
|
||||
/// <summary>Window manager that can be used to display other views</summary>
|
||||
private IWindowManager windowManager;
|
||||
|
||||
/// <summary>View model for the active page</summary>
|
||||
private object activePageViewModel;
|
||||
/// <summary>Cached page view models, if caching is enabled</summary>
|
||||
private ConcurrentDictionary<TPageEnumeration, object> cachedViewModels;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Base class for view models that have multiple child view models</summary>
|
||||
/// <typeparam name="TPageEnumeration">Enum type by which pages can be indicated</typeparam>
|
||||
public abstract class MultiPageViewModel<TPageEnumeration> :
|
||||
Observable, IMultiPageViewModel, IDisposable {
|
||||
|
||||
/// <summary>Initializes a new multi-page view model</summary>
|
||||
/// <param name="windowManager">
|
||||
/// Window manager the view model uses to create child views
|
||||
/// </param>
|
||||
/// <param name="cachePageViewModels">
|
||||
/// Whether child view models will be kept alive and reused
|
||||
/// </param>
|
||||
public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) {
|
||||
this.windowManager = windowManager;
|
||||
if(cachePageViewModels) {
|
||||
this.cachedViewModels = new ConcurrentDictionary<TPageEnumeration, object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public virtual void Dispose() {
|
||||
if(this.cachedViewModels != null) {
|
||||
foreach(object cacheViewModel in this.cachedViewModels.Values) {
|
||||
disposeIfSupported(cacheViewModel);
|
||||
}
|
||||
this.activePageViewModel = null;
|
||||
|
||||
this.cachedViewModels.Clear();
|
||||
this.cachedViewModels = null;
|
||||
} else if(this.activePageViewModel != null) {
|
||||
disposeIfSupported(this.activePageViewModel);
|
||||
this.activePageViewModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Child page that is currently being displayed by the view model</summary>
|
||||
public TPageEnumeration ActivePage {
|
||||
get { return this.activePage; }
|
||||
set {
|
||||
if(!this.activePage.Equals(value)) {
|
||||
this.activePage = value;
|
||||
if(this.activePageViewModel != null) {
|
||||
if(this.cachedViewModels == null) {
|
||||
disposeIfSupported(this.activePageViewModel);
|
||||
}
|
||||
this.activePageViewModel = null;
|
||||
}
|
||||
OnPropertyChanged(nameof(ActivePage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
|
||||
/// <returns>A view model for the active page on the multi-page view model</returns>
|
||||
public object GetActivePageViewModel() {
|
||||
if(this.cachedViewModels == null) {
|
||||
if(this.activePageViewModel == null) {
|
||||
this.activePageViewModel = CreateViewModelForPage(this.activePage);
|
||||
}
|
||||
} else if(this.activePageViewModel == null) {
|
||||
this.activePageViewModel = this.cachedViewModels.GetOrAdd(
|
||||
this.activePage,
|
||||
delegate(TPageEnumeration activePage) {
|
||||
return CreateViewModelForPage(this.activePage);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return this.activePageViewModel;
|
||||
}
|
||||
|
||||
/// <summary>Windowmanager that can create view models and display other views</summary>
|
||||
protected IWindowManager WindowManager {
|
||||
get { return this.windowManager; }
|
||||
}
|
||||
|
||||
/// <summary>Creates a view model for the specified page</summary>
|
||||
/// <param name="page">Page for which a view model will be created</param>
|
||||
/// <returns>The view model for the specified page</returns>
|
||||
protected abstract object CreateViewModelForPage(TPageEnumeration page);
|
||||
|
||||
/// <summary>Disposes the specified object if it is disposable</summary>
|
||||
/// <param name="potentiallyDisposable">Object that will be disposed if supported</param>
|
||||
private static void disposeIfSupported(object potentiallyDisposable) {
|
||||
var disposable = potentiallyDisposable as IDisposable;
|
||||
if(disposable != null) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Page that is currently active in the multi-page view model</summary>
|
||||
private TPageEnumeration activePage;
|
||||
/// <summary>Window manager that can be used to display other views</summary>
|
||||
private IWindowManager windowManager;
|
||||
|
||||
/// <summary>View model for the active page</summary>
|
||||
private object activePageViewModel;
|
||||
/// <summary>Cached page view models, if caching is enabled</summary>
|
||||
private ConcurrentDictionary<TPageEnumeration, object> cachedViewModels;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
|
|
@ -1,271 +1,270 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Unit test for the threaded action class</summary>
|
||||
[TestFixture]
|
||||
public class ThreadedActionTest {
|
||||
|
||||
#region class DummyContext
|
||||
|
||||
/// <summary>Synchronization context that does absolutely nothing</summary>
|
||||
private class DummyContext : ISynchronizeInvoke {
|
||||
|
||||
#region class SimpleAsyncResult
|
||||
|
||||
/// <summary>Barebones implementation of an asynchronous result</summary>
|
||||
private class SimpleAsyncResult : IAsyncResult {
|
||||
|
||||
/// <summary>Ehether the asynchronous operation is complete</summary>
|
||||
/// <remarks>
|
||||
/// Always true because it completes synchronously
|
||||
/// </remarks>
|
||||
public bool IsCompleted { get { return true; } }
|
||||
|
||||
/// <summary>
|
||||
/// Wait handle that can be used to wait for the asynchronous operation
|
||||
/// </summary>
|
||||
public WaitHandle AsyncWaitHandle {
|
||||
get { throw new NotImplementedException("Not implemented"); }
|
||||
}
|
||||
|
||||
/// <summary>Custom state that can be used to pass information around</summary>
|
||||
public object AsyncState {
|
||||
get { throw new NotImplementedException("Not implemented"); }
|
||||
}
|
||||
|
||||
/// <summary>Whether the asynchronous operation completed synchronously</summary>
|
||||
public bool CompletedSynchronously { get { return true; } }
|
||||
|
||||
/// <summary>The value returned from the asynchronous operation</summary>
|
||||
public object ReturnedValue;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class SimpleAsyncResult
|
||||
|
||||
/// <summary>Whether the calling thread needs to use Invoke()</summary>
|
||||
public bool InvokeRequired {
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>Schedules the specified method for execution in the target thread</summary>
|
||||
/// <param name="method">Method the target thread will execute when it is idle</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>
|
||||
/// An asynchronous result handle that can be used to check on the status of
|
||||
/// the call and wait for its completion
|
||||
/// </returns>
|
||||
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
|
||||
var asyncResult = new SimpleAsyncResult();
|
||||
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
|
||||
return asyncResult;
|
||||
}
|
||||
|
||||
/// <summary>Waits for the asychronous call to complete</summary>
|
||||
/// <param name="result">
|
||||
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
|
||||
/// </param>
|
||||
/// <returns>The original result returned by the asychronously called method</returns>
|
||||
public object EndInvoke(IAsyncResult result) {
|
||||
return ((SimpleAsyncResult)result).ReturnedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules the specified method for execution in the target thread and waits
|
||||
/// for it to complete
|
||||
/// </summary>
|
||||
/// <param name="method">Method that will be executed by the target thread</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>The result returned by the specified method</returns>
|
||||
public object Invoke(Delegate method, object[] arguments) {
|
||||
return method.Method.Invoke(method.Target, arguments);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DummyContext
|
||||
|
||||
#region class DummyThreadedAction
|
||||
|
||||
/// <summary>Implementation of a threaded action for the unit test</summary>
|
||||
private class DummyThreadedAction : ThreadedAction {
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new threaded action, letting the base class figure out the UI thread
|
||||
/// </summary>
|
||||
public DummyThreadedAction() : base() {
|
||||
this.finishedGate = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new view model using the specified UI context explicitly
|
||||
/// </summary>
|
||||
public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) {
|
||||
this.finishedGate = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public override void Dispose() {
|
||||
base.Dispose();
|
||||
|
||||
if(this.finishedGate != null) {
|
||||
this.finishedGate.Dispose();
|
||||
this.finishedGate = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Waits until the first background operation is finished</summary>
|
||||
/// <returns>
|
||||
/// True if the background operation is finished, false if it is ongoing
|
||||
/// </returns>
|
||||
public bool WaitUntilFinished() {
|
||||
return this.finishedGate.WaitOne(100);
|
||||
}
|
||||
|
||||
/// <summary>Selects the value that will be assigned when the action runs</summary>
|
||||
/// <param name="valueToAssign">Value the action will assigned when it runs</param>
|
||||
public void SetValueToAssign(int valueToAssign) {
|
||||
this.valueToAssign = valueToAssign;
|
||||
}
|
||||
|
||||
/// <summary>Sets up an error the action will fail with when run</summary>
|
||||
/// <param name="errorToFailWith">Error the action will fail with</param>
|
||||
public void SetErrorToFailWith(Exception errorToFailWith) {
|
||||
this.errorToFailWith = errorToFailWith;
|
||||
}
|
||||
|
||||
/// <summary>Last error that was reported by the threaded view model</summary>
|
||||
public Exception ReportedError {
|
||||
get { return this.reportedError; }
|
||||
}
|
||||
|
||||
/// <summary>Value that has been assigned from the background thread</summary>
|
||||
public int AssignedValue {
|
||||
get { return this.assignedValue; }
|
||||
}
|
||||
|
||||
/// <summary>Executes the threaded action from the background thread</summary>
|
||||
/// <param name="cancellationToken">Token by which execution can be canceled</param>
|
||||
protected override void Run(CancellationToken cancellationToken) {
|
||||
if(this.errorToFailWith != null) {
|
||||
throw this.errorToFailWith;
|
||||
}
|
||||
|
||||
this.assignedValue = this.valueToAssign;
|
||||
this.finishedGate.Set();
|
||||
}
|
||||
|
||||
/// <summary>Called when an error occurs in the background thread</summary>
|
||||
/// <param name="exception">Exception that was thrown in the background thread</param>
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.reportedError = exception;
|
||||
this.finishedGate.Set();
|
||||
}
|
||||
|
||||
/// <summary>Error the action will fail with, if set</summary>
|
||||
private Exception errorToFailWith;
|
||||
/// <summary>Value the action will assign to its same-named field</summary>
|
||||
private int valueToAssign;
|
||||
|
||||
/// <summary>Last error that was reported by the threaded view model</summary>
|
||||
private volatile Exception reportedError;
|
||||
/// <summary>Triggered when the </summary>
|
||||
private ManualResetEvent finishedGate;
|
||||
/// <summary>Value that is assigned through the background thread</summary>
|
||||
private volatile int assignedValue;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DummyThreadedAction
|
||||
|
||||
/// <summary>Verifies that the threaded action has a default constructor</summary>
|
||||
[Test, Explicit]
|
||||
public void HasDefaultConstructor() {
|
||||
using(var mainForm = new System.Windows.Forms.Form()) {
|
||||
mainForm.Show();
|
||||
try {
|
||||
mainForm.Visible = false;
|
||||
using(new DummyThreadedAction()) { }
|
||||
}
|
||||
finally {
|
||||
mainForm.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the threaded action can be constructed with a custom UI context
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasCustomSychronizationContextConstructor() {
|
||||
using(new DummyThreadedAction(new DummyContext())) { }
|
||||
}
|
||||
|
||||
/// <summary>Checks that a new threadd action starts out idle and not busy</summary>
|
||||
[Test]
|
||||
public void NewInstanceIsNotBusy() {
|
||||
using(var action = new DummyThreadedAction(new DummyContext())) {
|
||||
Assert.IsFalse(action.IsBusy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that errors happening in the background processing threads are
|
||||
/// reported to the main thread
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ErrorsInBackgroundThreadAreReported() {
|
||||
using(var action = new DummyThreadedAction(new DummyContext())) {
|
||||
var testError = new ArgumentException("Mooh");
|
||||
action.SetErrorToFailWith(testError);
|
||||
action.Start();
|
||||
action.WaitUntilFinished();
|
||||
Assert.AreSame(testError, action.ReportedError);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the background thread actually executes and can do work
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void BackgroundThreadExecutesTasks() {
|
||||
using(var action = new DummyThreadedAction(new DummyContext())) {
|
||||
action.SetValueToAssign(42001);
|
||||
action.Start();
|
||||
action.WaitUntilFinished();
|
||||
Assert.AreEqual(42001, action.AssignedValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Unit test for the threaded action class</summary>
|
||||
[TestFixture]
|
||||
public class ThreadedActionTest {
|
||||
|
||||
#region class DummyContext
|
||||
|
||||
/// <summary>Synchronization context that does absolutely nothing</summary>
|
||||
private class DummyContext : ISynchronizeInvoke {
|
||||
|
||||
#region class SimpleAsyncResult
|
||||
|
||||
/// <summary>Barebones implementation of an asynchronous result</summary>
|
||||
private class SimpleAsyncResult : IAsyncResult {
|
||||
|
||||
/// <summary>Ehether the asynchronous operation is complete</summary>
|
||||
/// <remarks>
|
||||
/// Always true because it completes synchronously
|
||||
/// </remarks>
|
||||
public bool IsCompleted { get { return true; } }
|
||||
|
||||
/// <summary>
|
||||
/// Wait handle that can be used to wait for the asynchronous operation
|
||||
/// </summary>
|
||||
public WaitHandle AsyncWaitHandle {
|
||||
get { throw new NotImplementedException("Not implemented"); }
|
||||
}
|
||||
|
||||
/// <summary>Custom state that can be used to pass information around</summary>
|
||||
public object AsyncState {
|
||||
get { throw new NotImplementedException("Not implemented"); }
|
||||
}
|
||||
|
||||
/// <summary>Whether the asynchronous operation completed synchronously</summary>
|
||||
public bool CompletedSynchronously { get { return true; } }
|
||||
|
||||
/// <summary>The value returned from the asynchronous operation</summary>
|
||||
public object ReturnedValue;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class SimpleAsyncResult
|
||||
|
||||
/// <summary>Whether the calling thread needs to use Invoke()</summary>
|
||||
public bool InvokeRequired {
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>Schedules the specified method for execution in the target thread</summary>
|
||||
/// <param name="method">Method the target thread will execute when it is idle</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>
|
||||
/// An asynchronous result handle that can be used to check on the status of
|
||||
/// the call and wait for its completion
|
||||
/// </returns>
|
||||
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
|
||||
var asyncResult = new SimpleAsyncResult();
|
||||
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
|
||||
return asyncResult;
|
||||
}
|
||||
|
||||
/// <summary>Waits for the asychronous call to complete</summary>
|
||||
/// <param name="result">
|
||||
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
|
||||
/// </param>
|
||||
/// <returns>The original result returned by the asychronously called method</returns>
|
||||
public object EndInvoke(IAsyncResult result) {
|
||||
return ((SimpleAsyncResult)result).ReturnedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules the specified method for execution in the target thread and waits
|
||||
/// for it to complete
|
||||
/// </summary>
|
||||
/// <param name="method">Method that will be executed by the target thread</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>The result returned by the specified method</returns>
|
||||
public object Invoke(Delegate method, object[] arguments) {
|
||||
return method.Method.Invoke(method.Target, arguments);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DummyContext
|
||||
|
||||
#region class DummyThreadedAction
|
||||
|
||||
/// <summary>Implementation of a threaded action for the unit test</summary>
|
||||
private class DummyThreadedAction : ThreadedAction {
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new threaded action, letting the base class figure out the UI thread
|
||||
/// </summary>
|
||||
public DummyThreadedAction() : base() {
|
||||
this.finishedGate = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new view model using the specified UI context explicitly
|
||||
/// </summary>
|
||||
public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) {
|
||||
this.finishedGate = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public override void Dispose() {
|
||||
base.Dispose();
|
||||
|
||||
if(this.finishedGate != null) {
|
||||
this.finishedGate.Dispose();
|
||||
this.finishedGate = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Waits until the first background operation is finished</summary>
|
||||
/// <returns>
|
||||
/// True if the background operation is finished, false if it is ongoing
|
||||
/// </returns>
|
||||
public bool WaitUntilFinished() {
|
||||
return this.finishedGate.WaitOne(100);
|
||||
}
|
||||
|
||||
/// <summary>Selects the value that will be assigned when the action runs</summary>
|
||||
/// <param name="valueToAssign">Value the action will assigned when it runs</param>
|
||||
public void SetValueToAssign(int valueToAssign) {
|
||||
this.valueToAssign = valueToAssign;
|
||||
}
|
||||
|
||||
/// <summary>Sets up an error the action will fail with when run</summary>
|
||||
/// <param name="errorToFailWith">Error the action will fail with</param>
|
||||
public void SetErrorToFailWith(Exception errorToFailWith) {
|
||||
this.errorToFailWith = errorToFailWith;
|
||||
}
|
||||
|
||||
/// <summary>Last error that was reported by the threaded view model</summary>
|
||||
public Exception ReportedError {
|
||||
get { return this.reportedError; }
|
||||
}
|
||||
|
||||
/// <summary>Value that has been assigned from the background thread</summary>
|
||||
public int AssignedValue {
|
||||
get { return this.assignedValue; }
|
||||
}
|
||||
|
||||
/// <summary>Executes the threaded action from the background thread</summary>
|
||||
/// <param name="cancellationToken">Token by which execution can be canceled</param>
|
||||
protected override void Run(CancellationToken cancellationToken) {
|
||||
if(this.errorToFailWith != null) {
|
||||
throw this.errorToFailWith;
|
||||
}
|
||||
|
||||
this.assignedValue = this.valueToAssign;
|
||||
this.finishedGate.Set();
|
||||
}
|
||||
|
||||
/// <summary>Called when an error occurs in the background thread</summary>
|
||||
/// <param name="exception">Exception that was thrown in the background thread</param>
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.reportedError = exception;
|
||||
this.finishedGate.Set();
|
||||
}
|
||||
|
||||
/// <summary>Error the action will fail with, if set</summary>
|
||||
private Exception errorToFailWith;
|
||||
/// <summary>Value the action will assign to its same-named field</summary>
|
||||
private int valueToAssign;
|
||||
|
||||
/// <summary>Last error that was reported by the threaded view model</summary>
|
||||
private volatile Exception reportedError;
|
||||
/// <summary>Triggered when the </summary>
|
||||
private ManualResetEvent finishedGate;
|
||||
/// <summary>Value that is assigned through the background thread</summary>
|
||||
private volatile int assignedValue;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DummyThreadedAction
|
||||
|
||||
/// <summary>Verifies that the threaded action has a default constructor</summary>
|
||||
[Test, Explicit]
|
||||
public void HasDefaultConstructor() {
|
||||
using(var mainForm = new System.Windows.Forms.Form()) {
|
||||
mainForm.Show();
|
||||
try {
|
||||
mainForm.Visible = false;
|
||||
using(new DummyThreadedAction()) { }
|
||||
}
|
||||
finally {
|
||||
mainForm.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the threaded action can be constructed with a custom UI context
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasCustomSychronizationContextConstructor() {
|
||||
using(new DummyThreadedAction(new DummyContext())) { }
|
||||
}
|
||||
|
||||
/// <summary>Checks that a new threadd action starts out idle and not busy</summary>
|
||||
[Test]
|
||||
public void NewInstanceIsNotBusy() {
|
||||
using(var action = new DummyThreadedAction(new DummyContext())) {
|
||||
Assert.IsFalse(action.IsBusy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that errors happening in the background processing threads are
|
||||
/// reported to the main thread
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ErrorsInBackgroundThreadAreReported() {
|
||||
using(var action = new DummyThreadedAction(new DummyContext())) {
|
||||
var testError = new ArgumentException("Mooh");
|
||||
action.SetErrorToFailWith(testError);
|
||||
action.Start();
|
||||
action.WaitUntilFinished();
|
||||
Assert.AreSame(testError, action.ReportedError);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the background thread actually executes and can do work
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void BackgroundThreadExecutesTasks() {
|
||||
using(var action = new DummyThreadedAction(new DummyContext())) {
|
||||
action.SetValueToAssign(42001);
|
||||
action.Start();
|
||||
action.WaitUntilFinished();
|
||||
Assert.AreEqual(42001, action.AssignedValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
||||
#endif // UNITTEST
|
|
@ -1,398 +1,397 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
using Nuclex.Support.Threading;
|
||||
|
||||
// Possible problem:
|
||||
//
|
||||
// After Run() is called, the action may not actually run if
|
||||
// it is using another thread runner and that one is cancelled.
|
||||
//
|
||||
// Thus, a second call to Run() has to schedule the action again,
|
||||
// even if it might already be scheduled, but should also not execute
|
||||
// the action a second time if is was indeed still scheduled.
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Encapsulates an action that can run in a thread</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Sometimes a view model wants to allow multiple actions to take place
|
||||
/// at the same time. Think multiple panels on the view require updating
|
||||
/// from a web service - you can make both requests at the same time
|
||||
/// instead of sequentially.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This class is also of use for things that need to be done sequentially
|
||||
/// by sharing the thread runner of the threaded view model. That way,
|
||||
/// you still have cancellable actions you can run at will and they
|
||||
/// automatically queue themselves to be executed one after another.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class ThreadedAction : Observable, IDisposable {
|
||||
|
||||
#region class ThreadedActionThreadRunner
|
||||
|
||||
/// <summary>Thread runner for the threaded action</summary>
|
||||
private class ThreadedActionThreadRunner : ThreadRunner {
|
||||
|
||||
/// <summary>Initializes a new thread runner for the threaded view model</summary>
|
||||
public ThreadedActionThreadRunner(ThreadedAction viewModel) {
|
||||
this.threadedAction = viewModel;
|
||||
}
|
||||
|
||||
/// <summary>Reports an error</summary>
|
||||
/// <param name="exception">Error that will be reported</param>
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.threadedAction.reportErrorFromThread(exception);
|
||||
}
|
||||
|
||||
/// <summary>Called when the status of the busy flag changes</summary>
|
||||
protected override void BusyChanged() {
|
||||
// Narf. Can't use this.
|
||||
}
|
||||
|
||||
/// <summary>View model the thread runner belongs to</summary>
|
||||
private ThreadedAction threadedAction;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class ThreadedActionThreadRunner
|
||||
|
||||
/// <summary>Initializes all common fields of the instance</summary>
|
||||
private ThreadedAction() {
|
||||
this.callRunIfNotCancelledDelegate = new Action<CancellationTokenSource>(
|
||||
callThreadedExecuteIfNotCancelled
|
||||
);
|
||||
this.reportErrorDelegate = new Action<Exception>(ReportError);
|
||||
}
|
||||
|
||||
/// <summary>Initializes a threaded action that uses its own thread runner</summary>
|
||||
public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() {
|
||||
if(uiContext == null) {
|
||||
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
|
||||
if(this.uiContext == null) {
|
||||
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
|
||||
}
|
||||
} else {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
this.ownThreadRunner = new ThreadedActionThreadRunner(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a threaded action that uses the view model's thread runner
|
||||
/// </summary>
|
||||
/// <param name="viewModel">View model whose thread runner will be used</param>
|
||||
/// <param name="uiContext">
|
||||
/// UI dispatcher that can be used to run callbacks in the UI thread
|
||||
/// </param>
|
||||
public ThreadedAction(
|
||||
ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null
|
||||
) : this() {
|
||||
if(uiContext == null) {
|
||||
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
|
||||
if(this.uiContext == null) {
|
||||
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
|
||||
}
|
||||
} else {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
this.externalThreadRunner = viewModel.ThreadRunner;
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public virtual void Dispose() {
|
||||
if(this.isBusy) {
|
||||
Cancel();
|
||||
}
|
||||
if(this.ownThreadRunner != null) {
|
||||
this.ownThreadRunner.Dispose();
|
||||
this.ownThreadRunner = null;
|
||||
}
|
||||
if(this.currentCancellationTokenSource != null) {
|
||||
this.currentCancellationTokenSource.Dispose();
|
||||
this.currentCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether the view model is currently busy executing a task</summary>
|
||||
public bool IsBusy {
|
||||
get { return this.isBusy; }
|
||||
private set {
|
||||
if(value != this.isBusy) {
|
||||
this.isBusy = value;
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cancels the running background task, if any</summary>
|
||||
public void Cancel() {
|
||||
lock(this.runningTaskSyncRoot) {
|
||||
|
||||
// If the background task is not running, do nothing. This also allows
|
||||
// us to avoid needless recreation of the same cancellation token source.
|
||||
if(!this.isBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a task is currently running, cancel it
|
||||
if(this.isRunning) {
|
||||
if(this.currentCancellationTokenSource != null) {
|
||||
this.currentCancellationTokenSource.Cancel();
|
||||
this.currentCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If the task was scheduled to be repeated, we also have to mark
|
||||
// the upcoming cancellation token source as canceled because the scheduled
|
||||
// run will still be happening (it will just cancel out immediately).
|
||||
if(this.nextCancellationTokenSource != null) {
|
||||
this.nextCancellationTokenSource.Cancel();
|
||||
this.nextCancellationTokenSource = null;
|
||||
}
|
||||
this.isScheduledAgain = false;
|
||||
|
||||
// If the task was not running, we can clear the busy state because it
|
||||
// is not going to reach the running state.
|
||||
if(!this.isRunning) {
|
||||
this.isBusy = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the task, cancelling the running task before doing so
|
||||
/// </summary>
|
||||
public void Restart() {
|
||||
bool reportBusyChange = false;
|
||||
|
||||
lock(this.runningTaskSyncRoot) {
|
||||
|
||||
// If we're already in the execution phase, schedule another execution right
|
||||
// after this one is finished (because now, data might have changed after
|
||||
// execution has finished).
|
||||
if(this.isRunning) {
|
||||
//System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution");
|
||||
if(this.currentCancellationTokenSource != null) {
|
||||
this.currentCancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
this.currentCancellationTokenSource = this.nextCancellationTokenSource;
|
||||
this.nextCancellationTokenSource = null;
|
||||
this.isScheduledAgain = false;
|
||||
}
|
||||
|
||||
// If there's no cancellation token source, create one. If an execution
|
||||
// was already scheduled and the cancellation token source is still valid,
|
||||
// then reuse that in order to be able to cancel all scheduled executions.
|
||||
if(this.currentCancellationTokenSource == null) {
|
||||
//System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token");
|
||||
this.currentCancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
// Schedule another execution of the action
|
||||
scheduleExecution();
|
||||
|
||||
reportBusyChange = (this.isBusy == false);
|
||||
this.isBusy = true;
|
||||
}
|
||||
|
||||
if(reportBusyChange) {
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starts the task</summary>
|
||||
public void Start() {
|
||||
bool reportBusyChange = false;
|
||||
|
||||
lock(this.runningTaskSyncRoot) {
|
||||
|
||||
// If we're already in the execution phase, schedule another execution right
|
||||
// after this one is finished (because now, data might have changed after
|
||||
// execution has finished).
|
||||
if(this.isRunning) {
|
||||
|
||||
// If we already created a new cancellation token source, keep it,
|
||||
// otherwise create a new one for the next execution
|
||||
if(!this.isScheduledAgain) {
|
||||
this.nextCancellationTokenSource = new CancellationTokenSource();
|
||||
this.isScheduledAgain = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// If there's no cancellation token source, create one. If an execution
|
||||
// was already scheduled and the cancellation token source is still valid,
|
||||
// then reuse that in order to be able to cancel all scheduled executions.
|
||||
if(this.currentCancellationTokenSource == null) {
|
||||
this.currentCancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
// Schedule another execution of the action
|
||||
scheduleExecution();
|
||||
|
||||
}
|
||||
|
||||
reportBusyChange = (this.isBusy == false);
|
||||
this.isBusy = true;
|
||||
}
|
||||
|
||||
if(reportBusyChange) {
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reports an error</summary>
|
||||
/// <param name="exception">Error that will be reported</param>
|
||||
protected abstract void ReportError(Exception exception);
|
||||
|
||||
/// <summary>Executes the threaded action from the background thread</summary>
|
||||
/// <param name="cancellationToken">Token by which execution can be canceled</param>
|
||||
protected abstract void Run(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Calls the Run() method from the background thread and manages the flags
|
||||
/// </summary>
|
||||
/// <param name="cancellationTokenSource"></param>
|
||||
private void callThreadedExecuteIfNotCancelled(
|
||||
CancellationTokenSource cancellationTokenSource
|
||||
) {
|
||||
lock(this) {
|
||||
if(cancellationTokenSource.Token.IsCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
try {
|
||||
Run(cancellationTokenSource.Token);
|
||||
}
|
||||
finally {
|
||||
bool reportBusyChange = false;
|
||||
|
||||
lock(this) {
|
||||
this.isRunning = false;
|
||||
|
||||
// Cancel the current cancellation token because this execution may have
|
||||
// been scheduled multiple times (there's no way for the Run() method to
|
||||
// know if the currently scheduled execution was cancelled, so it is forced
|
||||
// to reschedule on each call - accepting redundant schedules).
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Pick the next cancellation token source. Normally it is null, but this
|
||||
// is more elegant because we can avoid an while if statement this way :)
|
||||
this.currentCancellationTokenSource = nextCancellationTokenSource;
|
||||
this.nextCancellationTokenSource = null;
|
||||
|
||||
// If Start() was called while we were executing, another execution is required
|
||||
// (because the data may have changed during the call to Start()).
|
||||
if(this.isScheduledAgain) {
|
||||
this.isScheduledAgain = false;
|
||||
scheduleExecution();
|
||||
} else { // We're idle now
|
||||
reportBusyChange = (this.isBusy == true);
|
||||
this.isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(reportBusyChange) {
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Schedules one execution of the action</summary>
|
||||
private void scheduleExecution() {
|
||||
//System.Diagnostics.Debug.WriteLine("Scheduling execution");
|
||||
|
||||
ThreadRunner runner = this.externalThreadRunner;
|
||||
if(runner != null) {
|
||||
runner.RunInBackground(
|
||||
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
|
||||
);
|
||||
}
|
||||
|
||||
runner = this.ownThreadRunner;
|
||||
if(runner != null) {
|
||||
runner.RunInBackground(
|
||||
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reports an error that occurred in the runner's background thread</summary>
|
||||
/// <param name="exception">Exception that the thread has encountered</param>
|
||||
private void reportErrorFromThread(Exception exception) {
|
||||
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
|
||||
}
|
||||
|
||||
/// <summary>Sets the UI context that will be used by the threaded action</summary>
|
||||
/// <param name="uiContext">The UI context the threaded action will use</param>
|
||||
private void updateUiContext(ISynchronizeInvoke uiContext) {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
/// <summary>Synchronization context of the thread in which the view runs</summary>
|
||||
private ISynchronizeInvoke uiContext;
|
||||
/// <summary>Delegate for the ReportError() method</summary>
|
||||
private Action<Exception> reportErrorDelegate;
|
||||
/// <summary>Delegate for the callThreadedExecuteIfNotCancelled() method</summary>
|
||||
private Action<CancellationTokenSource> callRunIfNotCancelledDelegate;
|
||||
|
||||
/// <summary>Thread runner on which the action can run its background task</summary>
|
||||
private ThreadedActionThreadRunner ownThreadRunner;
|
||||
/// <summary>
|
||||
/// External thread runner on which the action runs its background task if assigned
|
||||
/// </summary>
|
||||
private ThreadRunner externalThreadRunner;
|
||||
|
||||
/// <summary>Synchronization root for the threaded execute method</summary>
|
||||
private object runningTaskSyncRoot = new object();
|
||||
/// <summary>Used to cancel the currently running task</summary>
|
||||
private CancellationTokenSource currentCancellationTokenSource;
|
||||
/// <summary>Used to cancel the upcoming task if a re-run was scheduled</summary>
|
||||
private CancellationTokenSource nextCancellationTokenSource;
|
||||
/// <summary>Whether the background task is running or waiting to run</summary>
|
||||
private volatile bool isBusy;
|
||||
/// <summary>Whether execution is taking place right now</summary>
|
||||
/// <remarks>
|
||||
/// If this flag is set and the Start() method is called, another run needs to
|
||||
/// be scheduled.
|
||||
/// </remarks>
|
||||
private bool isRunning;
|
||||
/// <summary>Whether run was called while the action was already running</summary>
|
||||
private bool isScheduledAgain;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
using Nuclex.Support.Threading;
|
||||
|
||||
// Possible problem:
|
||||
//
|
||||
// After Run() is called, the action may not actually run if
|
||||
// it is using another thread runner and that one is cancelled.
|
||||
//
|
||||
// Thus, a second call to Run() has to schedule the action again,
|
||||
// even if it might already be scheduled, but should also not execute
|
||||
// the action a second time if is was indeed still scheduled.
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Encapsulates an action that can run in a thread</summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Sometimes a view model wants to allow multiple actions to take place
|
||||
/// at the same time. Think multiple panels on the view require updating
|
||||
/// from a web service - you can make both requests at the same time
|
||||
/// instead of sequentially.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This class is also of use for things that need to be done sequentially
|
||||
/// by sharing the thread runner of the threaded view model. That way,
|
||||
/// you still have cancellable actions you can run at will and they
|
||||
/// automatically queue themselves to be executed one after another.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class ThreadedAction : Observable, IDisposable {
|
||||
|
||||
#region class ThreadedActionThreadRunner
|
||||
|
||||
/// <summary>Thread runner for the threaded action</summary>
|
||||
private class ThreadedActionThreadRunner : ThreadRunner {
|
||||
|
||||
/// <summary>Initializes a new thread runner for the threaded view model</summary>
|
||||
public ThreadedActionThreadRunner(ThreadedAction viewModel) {
|
||||
this.threadedAction = viewModel;
|
||||
}
|
||||
|
||||
/// <summary>Reports an error</summary>
|
||||
/// <param name="exception">Error that will be reported</param>
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.threadedAction.reportErrorFromThread(exception);
|
||||
}
|
||||
|
||||
/// <summary>Called when the status of the busy flag changes</summary>
|
||||
protected override void BusyChanged() {
|
||||
// Narf. Can't use this.
|
||||
}
|
||||
|
||||
/// <summary>View model the thread runner belongs to</summary>
|
||||
private ThreadedAction threadedAction;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class ThreadedActionThreadRunner
|
||||
|
||||
/// <summary>Initializes all common fields of the instance</summary>
|
||||
private ThreadedAction() {
|
||||
this.callRunIfNotCancelledDelegate = new Action<CancellationTokenSource>(
|
||||
callThreadedExecuteIfNotCancelled
|
||||
);
|
||||
this.reportErrorDelegate = new Action<Exception>(ReportError);
|
||||
}
|
||||
|
||||
/// <summary>Initializes a threaded action that uses its own thread runner</summary>
|
||||
public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() {
|
||||
if(uiContext == null) {
|
||||
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
|
||||
if(this.uiContext == null) {
|
||||
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
|
||||
}
|
||||
} else {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
this.ownThreadRunner = new ThreadedActionThreadRunner(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a threaded action that uses the view model's thread runner
|
||||
/// </summary>
|
||||
/// <param name="viewModel">View model whose thread runner will be used</param>
|
||||
/// <param name="uiContext">
|
||||
/// UI dispatcher that can be used to run callbacks in the UI thread
|
||||
/// </param>
|
||||
public ThreadedAction(
|
||||
ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null
|
||||
) : this() {
|
||||
if(uiContext == null) {
|
||||
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
|
||||
if(this.uiContext == null) {
|
||||
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
|
||||
}
|
||||
} else {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
this.externalThreadRunner = viewModel.ThreadRunner;
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public virtual void Dispose() {
|
||||
if(this.isBusy) {
|
||||
Cancel();
|
||||
}
|
||||
if(this.ownThreadRunner != null) {
|
||||
this.ownThreadRunner.Dispose();
|
||||
this.ownThreadRunner = null;
|
||||
}
|
||||
if(this.currentCancellationTokenSource != null) {
|
||||
this.currentCancellationTokenSource.Dispose();
|
||||
this.currentCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether the view model is currently busy executing a task</summary>
|
||||
public bool IsBusy {
|
||||
get { return this.isBusy; }
|
||||
private set {
|
||||
if(value != this.isBusy) {
|
||||
this.isBusy = value;
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cancels the running background task, if any</summary>
|
||||
public void Cancel() {
|
||||
lock(this.runningTaskSyncRoot) {
|
||||
|
||||
// If the background task is not running, do nothing. This also allows
|
||||
// us to avoid needless recreation of the same cancellation token source.
|
||||
if(!this.isBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a task is currently running, cancel it
|
||||
if(this.isRunning) {
|
||||
if(this.currentCancellationTokenSource != null) {
|
||||
this.currentCancellationTokenSource.Cancel();
|
||||
this.currentCancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If the task was scheduled to be repeated, we also have to mark
|
||||
// the upcoming cancellation token source as canceled because the scheduled
|
||||
// run will still be happening (it will just cancel out immediately).
|
||||
if(this.nextCancellationTokenSource != null) {
|
||||
this.nextCancellationTokenSource.Cancel();
|
||||
this.nextCancellationTokenSource = null;
|
||||
}
|
||||
this.isScheduledAgain = false;
|
||||
|
||||
// If the task was not running, we can clear the busy state because it
|
||||
// is not going to reach the running state.
|
||||
if(!this.isRunning) {
|
||||
this.isBusy = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the task, cancelling the running task before doing so
|
||||
/// </summary>
|
||||
public void Restart() {
|
||||
bool reportBusyChange = false;
|
||||
|
||||
lock(this.runningTaskSyncRoot) {
|
||||
|
||||
// If we're already in the execution phase, schedule another execution right
|
||||
// after this one is finished (because now, data might have changed after
|
||||
// execution has finished).
|
||||
if(this.isRunning) {
|
||||
//System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution");
|
||||
if(this.currentCancellationTokenSource != null) {
|
||||
this.currentCancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
this.currentCancellationTokenSource = this.nextCancellationTokenSource;
|
||||
this.nextCancellationTokenSource = null;
|
||||
this.isScheduledAgain = false;
|
||||
}
|
||||
|
||||
// If there's no cancellation token source, create one. If an execution
|
||||
// was already scheduled and the cancellation token source is still valid,
|
||||
// then reuse that in order to be able to cancel all scheduled executions.
|
||||
if(this.currentCancellationTokenSource == null) {
|
||||
//System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token");
|
||||
this.currentCancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
// Schedule another execution of the action
|
||||
scheduleExecution();
|
||||
|
||||
reportBusyChange = (this.isBusy == false);
|
||||
this.isBusy = true;
|
||||
}
|
||||
|
||||
if(reportBusyChange) {
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starts the task</summary>
|
||||
public void Start() {
|
||||
bool reportBusyChange = false;
|
||||
|
||||
lock(this.runningTaskSyncRoot) {
|
||||
|
||||
// If we're already in the execution phase, schedule another execution right
|
||||
// after this one is finished (because now, data might have changed after
|
||||
// execution has finished).
|
||||
if(this.isRunning) {
|
||||
|
||||
// If we already created a new cancellation token source, keep it,
|
||||
// otherwise create a new one for the next execution
|
||||
if(!this.isScheduledAgain) {
|
||||
this.nextCancellationTokenSource = new CancellationTokenSource();
|
||||
this.isScheduledAgain = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// If there's no cancellation token source, create one. If an execution
|
||||
// was already scheduled and the cancellation token source is still valid,
|
||||
// then reuse that in order to be able to cancel all scheduled executions.
|
||||
if(this.currentCancellationTokenSource == null) {
|
||||
this.currentCancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
// Schedule another execution of the action
|
||||
scheduleExecution();
|
||||
|
||||
}
|
||||
|
||||
reportBusyChange = (this.isBusy == false);
|
||||
this.isBusy = true;
|
||||
}
|
||||
|
||||
if(reportBusyChange) {
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reports an error</summary>
|
||||
/// <param name="exception">Error that will be reported</param>
|
||||
protected abstract void ReportError(Exception exception);
|
||||
|
||||
/// <summary>Executes the threaded action from the background thread</summary>
|
||||
/// <param name="cancellationToken">Token by which execution can be canceled</param>
|
||||
protected abstract void Run(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Calls the Run() method from the background thread and manages the flags
|
||||
/// </summary>
|
||||
/// <param name="cancellationTokenSource"></param>
|
||||
private void callThreadedExecuteIfNotCancelled(
|
||||
CancellationTokenSource cancellationTokenSource
|
||||
) {
|
||||
lock(this) {
|
||||
if(cancellationTokenSource.Token.IsCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
}
|
||||
|
||||
try {
|
||||
Run(cancellationTokenSource.Token);
|
||||
}
|
||||
finally {
|
||||
bool reportBusyChange = false;
|
||||
|
||||
lock(this) {
|
||||
this.isRunning = false;
|
||||
|
||||
// Cancel the current cancellation token because this execution may have
|
||||
// been scheduled multiple times (there's no way for the Run() method to
|
||||
// know if the currently scheduled execution was cancelled, so it is forced
|
||||
// to reschedule on each call - accepting redundant schedules).
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Pick the next cancellation token source. Normally it is null, but this
|
||||
// is more elegant because we can avoid an while if statement this way :)
|
||||
this.currentCancellationTokenSource = nextCancellationTokenSource;
|
||||
this.nextCancellationTokenSource = null;
|
||||
|
||||
// If Start() was called while we were executing, another execution is required
|
||||
// (because the data may have changed during the call to Start()).
|
||||
if(this.isScheduledAgain) {
|
||||
this.isScheduledAgain = false;
|
||||
scheduleExecution();
|
||||
} else { // We're idle now
|
||||
reportBusyChange = (this.isBusy == true);
|
||||
this.isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(reportBusyChange) {
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Schedules one execution of the action</summary>
|
||||
private void scheduleExecution() {
|
||||
//System.Diagnostics.Debug.WriteLine("Scheduling execution");
|
||||
|
||||
ThreadRunner runner = this.externalThreadRunner;
|
||||
if(runner != null) {
|
||||
runner.RunInBackground(
|
||||
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
|
||||
);
|
||||
}
|
||||
|
||||
runner = this.ownThreadRunner;
|
||||
if(runner != null) {
|
||||
runner.RunInBackground(
|
||||
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reports an error that occurred in the runner's background thread</summary>
|
||||
/// <param name="exception">Exception that the thread has encountered</param>
|
||||
private void reportErrorFromThread(Exception exception) {
|
||||
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
|
||||
}
|
||||
|
||||
/// <summary>Sets the UI context that will be used by the threaded action</summary>
|
||||
/// <param name="uiContext">The UI context the threaded action will use</param>
|
||||
private void updateUiContext(ISynchronizeInvoke uiContext) {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
/// <summary>Synchronization context of the thread in which the view runs</summary>
|
||||
private ISynchronizeInvoke uiContext;
|
||||
/// <summary>Delegate for the ReportError() method</summary>
|
||||
private Action<Exception> reportErrorDelegate;
|
||||
/// <summary>Delegate for the callThreadedExecuteIfNotCancelled() method</summary>
|
||||
private Action<CancellationTokenSource> callRunIfNotCancelledDelegate;
|
||||
|
||||
/// <summary>Thread runner on which the action can run its background task</summary>
|
||||
private ThreadedActionThreadRunner ownThreadRunner;
|
||||
/// <summary>
|
||||
/// External thread runner on which the action runs its background task if assigned
|
||||
/// </summary>
|
||||
private ThreadRunner externalThreadRunner;
|
||||
|
||||
/// <summary>Synchronization root for the threaded execute method</summary>
|
||||
private object runningTaskSyncRoot = new object();
|
||||
/// <summary>Used to cancel the currently running task</summary>
|
||||
private CancellationTokenSource currentCancellationTokenSource;
|
||||
/// <summary>Used to cancel the upcoming task if a re-run was scheduled</summary>
|
||||
private CancellationTokenSource nextCancellationTokenSource;
|
||||
/// <summary>Whether the background task is running or waiting to run</summary>
|
||||
private volatile bool isBusy;
|
||||
/// <summary>Whether execution is taking place right now</summary>
|
||||
/// <remarks>
|
||||
/// If this flag is set and the Start() method is called, another run needs to
|
||||
/// be scheduled.
|
||||
/// </remarks>
|
||||
private bool isRunning;
|
||||
/// <summary>Whether run was called while the action was already running</summary>
|
||||
private bool isScheduledAgain;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
|
|
@ -1,174 +1,173 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Unit test for the threaded dialog view model</summary>
|
||||
[TestFixture]
|
||||
public class ThreadedDialogViewModelTest {
|
||||
|
||||
#region class DialogViewModelSubscriber
|
||||
|
||||
/// <summary>Subscriber for the events offered by a dialog view model</summary>
|
||||
private class DialogViewModelSubscriber {
|
||||
|
||||
/// <summary>Indicates that the user has accepted the dialog</summary>
|
||||
public void Confirmed(object sender, EventArgs arguments) {
|
||||
++this.confirmCallCount;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the user has cancelled the dialog</summary>
|
||||
public void Cancelled(object sender, EventArgs arguments) {
|
||||
++this.cancelCallCount;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the dialog was simply closed</summary>
|
||||
public void Submitted(object sender, EventArgs arguments) {
|
||||
++this.submitCallCount;
|
||||
}
|
||||
|
||||
/// <summary>How many times the Confirmed() method was called</summary>
|
||||
public int ConfirmCallCount {
|
||||
get { return this.confirmCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Cancelled() method was called</summary>
|
||||
public int CancelCallCount {
|
||||
get { return this.cancelCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Submitted() method was called</summary>
|
||||
public int SubmitCallCount {
|
||||
get { return this.submitCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Confirmed() method was called</summary>
|
||||
private int confirmCallCount;
|
||||
/// <summary>How many times the Cancelled() method was called</summary>
|
||||
private int cancelCallCount;
|
||||
/// <summary>How many times the Submitted() method was called</summary>
|
||||
private int submitCallCount;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DialogViewModelSubscriber
|
||||
|
||||
#region class TestViewModel
|
||||
|
||||
private class TestViewModel : ThreadedDialogViewModel {
|
||||
|
||||
public Exception ReportedError {
|
||||
get { return this.reportedError; }
|
||||
}
|
||||
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.reportedError = exception;
|
||||
}
|
||||
|
||||
private Exception reportedError;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class TestViewModel
|
||||
|
||||
/// <summary>Verifies that the dialog view model has a default constructor</summary>
|
||||
[Test]
|
||||
public void HasDefaultConstructor() {
|
||||
Assert.DoesNotThrow(
|
||||
delegate() { new TestViewModel(); }
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Confirm() on the dialog view model triggers
|
||||
/// the 'Confirmed' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConfirmTriggersConfirmedEvent() {
|
||||
var viewModel = new TestViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Confirm();
|
||||
Assert.AreEqual(1, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Cancel() on the dialog view model triggers
|
||||
/// the 'Cancelled' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CancelTriggersCancelledEvent() {
|
||||
var viewModel = new TestViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Cancel();
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(1, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Submitm() on the dialog view model triggers
|
||||
/// the 'Submitted' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SubmitTriggersSubmittedEvent() {
|
||||
var viewModel = new TestViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Submit();
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(1, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
|
||||
/// <param name="viewModel">View model a subscriber will be created for</param>
|
||||
/// <returns>A subscriber for the events of the specified view model</returns>
|
||||
private DialogViewModelSubscriber createSubscriber(ThreadedDialogViewModel viewModel) {
|
||||
var subscriber = new DialogViewModelSubscriber();
|
||||
viewModel.Confirmed += subscriber.Confirmed;
|
||||
viewModel.Canceled += subscriber.Cancelled;
|
||||
viewModel.Submitted += subscriber.Submitted;
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
||||
#endif // UNITTEST
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Unit test for the threaded dialog view model</summary>
|
||||
[TestFixture]
|
||||
public class ThreadedDialogViewModelTest {
|
||||
|
||||
#region class DialogViewModelSubscriber
|
||||
|
||||
/// <summary>Subscriber for the events offered by a dialog view model</summary>
|
||||
private class DialogViewModelSubscriber {
|
||||
|
||||
/// <summary>Indicates that the user has accepted the dialog</summary>
|
||||
public void Confirmed(object sender, EventArgs arguments) {
|
||||
++this.confirmCallCount;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the user has cancelled the dialog</summary>
|
||||
public void Cancelled(object sender, EventArgs arguments) {
|
||||
++this.cancelCallCount;
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the dialog was simply closed</summary>
|
||||
public void Submitted(object sender, EventArgs arguments) {
|
||||
++this.submitCallCount;
|
||||
}
|
||||
|
||||
/// <summary>How many times the Confirmed() method was called</summary>
|
||||
public int ConfirmCallCount {
|
||||
get { return this.confirmCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Cancelled() method was called</summary>
|
||||
public int CancelCallCount {
|
||||
get { return this.cancelCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Submitted() method was called</summary>
|
||||
public int SubmitCallCount {
|
||||
get { return this.submitCallCount; }
|
||||
}
|
||||
|
||||
/// <summary>How many times the Confirmed() method was called</summary>
|
||||
private int confirmCallCount;
|
||||
/// <summary>How many times the Cancelled() method was called</summary>
|
||||
private int cancelCallCount;
|
||||
/// <summary>How many times the Submitted() method was called</summary>
|
||||
private int submitCallCount;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DialogViewModelSubscriber
|
||||
|
||||
#region class TestViewModel
|
||||
|
||||
private class TestViewModel : ThreadedDialogViewModel {
|
||||
|
||||
public Exception ReportedError {
|
||||
get { return this.reportedError; }
|
||||
}
|
||||
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.reportedError = exception;
|
||||
}
|
||||
|
||||
private Exception reportedError;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class TestViewModel
|
||||
|
||||
/// <summary>Verifies that the dialog view model has a default constructor</summary>
|
||||
[Test]
|
||||
public void HasDefaultConstructor() {
|
||||
Assert.DoesNotThrow(
|
||||
delegate() { new TestViewModel(); }
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Confirm() on the dialog view model triggers
|
||||
/// the 'Confirmed' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ConfirmTriggersConfirmedEvent() {
|
||||
var viewModel = new TestViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Confirm();
|
||||
Assert.AreEqual(1, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Cancel() on the dialog view model triggers
|
||||
/// the 'Cancelled' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void CancelTriggersCancelledEvent() {
|
||||
var viewModel = new TestViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Cancel();
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(1, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that calling Submitm() on the dialog view model triggers
|
||||
/// the 'Submitted' event
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void SubmitTriggersSubmittedEvent() {
|
||||
var viewModel = new TestViewModel();
|
||||
var subscriber = createSubscriber(viewModel);
|
||||
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(0, subscriber.SubmitCallCount);
|
||||
viewModel.Submit();
|
||||
Assert.AreEqual(0, subscriber.ConfirmCallCount);
|
||||
Assert.AreEqual(0, subscriber.CancelCallCount);
|
||||
Assert.AreEqual(1, subscriber.SubmitCallCount);
|
||||
}
|
||||
|
||||
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
|
||||
/// <param name="viewModel">View model a subscriber will be created for</param>
|
||||
/// <returns>A subscriber for the events of the specified view model</returns>
|
||||
private DialogViewModelSubscriber createSubscriber(ThreadedDialogViewModel viewModel) {
|
||||
var subscriber = new DialogViewModelSubscriber();
|
||||
viewModel.Confirmed += subscriber.Confirmed;
|
||||
viewModel.Canceled += subscriber.Cancelled;
|
||||
viewModel.Submitted += subscriber.Submitted;
|
||||
return subscriber;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
||||
#endif // UNITTEST
|
||||
|
|
|
@ -1,74 +1,73 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>View model for a dialog that can execute tasks in a background thread</summary>
|
||||
public abstract class ThreadedDialogViewModel : ThreadedViewModel {
|
||||
|
||||
/// <summary>Indicates that the view should close with a positive result</summary>
|
||||
/// <remarks>
|
||||
/// This event typically corresponds to the 'Ok' button in a dialog.
|
||||
/// </remarks>
|
||||
public event EventHandler Confirmed;
|
||||
|
||||
/// <summary>Indicates that the view should close with a negative result</summary>
|
||||
/// <remarks>
|
||||
/// This event typically corresponds to the 'Cancel' button in a dialog.
|
||||
/// </remarks>
|
||||
public event EventHandler Canceled;
|
||||
|
||||
/// <summary>Indicates that the view should close</summary>
|
||||
/// <remarks>
|
||||
/// This closes the view with a neutral result, used when the view doesn't follow
|
||||
/// an ok/cancel scheme or the result is transmitted in some other way.
|
||||
/// </remarks>
|
||||
public event EventHandler Submitted;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the dialog should be closed with a positive outcome
|
||||
/// </summary>
|
||||
public virtual void Confirm() {
|
||||
if(Confirmed != null) {
|
||||
Confirmed(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the dialog should be closed with a negative outcome
|
||||
/// </summary>
|
||||
public virtual void Cancel() {
|
||||
if(Canceled != null) {
|
||||
Canceled(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the dialog should be closed</summary>
|
||||
public virtual void Submit() {
|
||||
if(Submitted != null) {
|
||||
Submitted(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>View model for a dialog that can execute tasks in a background thread</summary>
|
||||
public abstract class ThreadedDialogViewModel : ThreadedViewModel {
|
||||
|
||||
/// <summary>Indicates that the view should close with a positive result</summary>
|
||||
/// <remarks>
|
||||
/// This event typically corresponds to the 'Ok' button in a dialog.
|
||||
/// </remarks>
|
||||
public event EventHandler Confirmed;
|
||||
|
||||
/// <summary>Indicates that the view should close with a negative result</summary>
|
||||
/// <remarks>
|
||||
/// This event typically corresponds to the 'Cancel' button in a dialog.
|
||||
/// </remarks>
|
||||
public event EventHandler Canceled;
|
||||
|
||||
/// <summary>Indicates that the view should close</summary>
|
||||
/// <remarks>
|
||||
/// This closes the view with a neutral result, used when the view doesn't follow
|
||||
/// an ok/cancel scheme or the result is transmitted in some other way.
|
||||
/// </remarks>
|
||||
public event EventHandler Submitted;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the dialog should be closed with a positive outcome
|
||||
/// </summary>
|
||||
public virtual void Confirm() {
|
||||
if(Confirmed != null) {
|
||||
Confirmed(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the dialog should be closed with a negative outcome
|
||||
/// </summary>
|
||||
public virtual void Cancel() {
|
||||
if(Canceled != null) {
|
||||
Canceled(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Indicates that the dialog should be closed</summary>
|
||||
public virtual void Submit() {
|
||||
if(Submitted != null) {
|
||||
Submitted(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
|
|
@ -1,262 +1,261 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Unit test for the threaded view model base class</summary>
|
||||
[TestFixture]
|
||||
public class ThreadedViewModelTest {
|
||||
|
||||
#region class DummyContext
|
||||
|
||||
/// <summary>Synchronization context that does absolutely nothing</summary>
|
||||
private class DummyContext : ISynchronizeInvoke {
|
||||
|
||||
#region class SimpleAsyncResult
|
||||
|
||||
/// <summary>Barebones implementation of an asynchronous result</summary>
|
||||
private class SimpleAsyncResult : IAsyncResult {
|
||||
|
||||
/// <summary>Ehether the asynchronous operation is complete</summary>
|
||||
/// <remarks>
|
||||
/// Always true because it completes synchronously
|
||||
/// </remarks>
|
||||
public bool IsCompleted { get { return true; } }
|
||||
|
||||
/// <summary>
|
||||
/// Wait handle that can be used to wait for the asynchronous operation
|
||||
/// </summary>
|
||||
public WaitHandle AsyncWaitHandle {
|
||||
get { throw new NotImplementedException("Not implemented"); }
|
||||
}
|
||||
|
||||
/// <summary>Custom state that can be used to pass information around</summary>
|
||||
public object AsyncState {
|
||||
get { throw new NotImplementedException("Not implemented"); }
|
||||
}
|
||||
|
||||
/// <summary>Whether the asynchronous operation completed synchronously</summary>
|
||||
public bool CompletedSynchronously { get { return true; } }
|
||||
|
||||
/// <summary>The value returned from the asynchronous operation</summary>
|
||||
public object ReturnedValue;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class SimpleAsyncResult
|
||||
|
||||
/// <summary>Whether the calling thread needs to use Invoke()</summary>
|
||||
public bool InvokeRequired {
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>Schedules the specified method for execution in the target thread</summary>
|
||||
/// <param name="method">Method the target thread will execute when it is idle</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>
|
||||
/// An asynchronous result handle that can be used to check on the status of
|
||||
/// the call and wait for its completion
|
||||
/// </returns>
|
||||
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
|
||||
var asyncResult = new SimpleAsyncResult();
|
||||
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
|
||||
return asyncResult;
|
||||
}
|
||||
|
||||
/// <summary>Waits for the asychronous call to complete</summary>
|
||||
/// <param name="result">
|
||||
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
|
||||
/// </param>
|
||||
/// <returns>The original result returned by the asychronously called method</returns>
|
||||
public object EndInvoke(IAsyncResult result) {
|
||||
return ((SimpleAsyncResult)result).ReturnedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules the specified method for execution in the target thread and waits
|
||||
/// for it to complete
|
||||
/// </summary>
|
||||
/// <param name="method">Method that will be executed by the target thread</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>The result returned by the specified method</returns>
|
||||
public object Invoke(Delegate method, object[] arguments) {
|
||||
return method.Method.Invoke(method.Target, arguments);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DummyContext
|
||||
|
||||
#region class TestViewModel
|
||||
|
||||
/// <summary>View model used to unit test the threaded view model base class</summary>
|
||||
private class TestViewModel : ThreadedViewModel {
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new view model, letting the base class figure out the UI thread
|
||||
/// </summary>
|
||||
public TestViewModel() : base() {
|
||||
this.finishedGate = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new view model, using the specified context for the UI thread
|
||||
/// </summary>
|
||||
/// <param name="uiContext">Synchronization context of the UI thread</param>
|
||||
public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) {
|
||||
this.finishedGate = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public override void Dispose() {
|
||||
base.Dispose();
|
||||
|
||||
if(this.finishedGate != null) {
|
||||
this.finishedGate.Dispose();
|
||||
this.finishedGate = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Waits until the first background operation is finished</summary>
|
||||
/// <returns>
|
||||
/// True if the background operation is finished, false if it is ongoing
|
||||
/// </returns>
|
||||
public bool WaitUntilFinished() {
|
||||
return this.finishedGate.WaitOne(100);
|
||||
}
|
||||
|
||||
/// <summary>Runs a background process that causes the specified error</summary>
|
||||
/// <param name="error">Error that will be caused in the background process</param>
|
||||
public void CauseErrorInBackgroundThread(Exception error) {
|
||||
RunInBackground(
|
||||
delegate() { throw error; }
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns the specified value to the same-named property from a background thread
|
||||
/// </summary>
|
||||
/// <param name="value">Value that will be assigned to the same-named property</param>
|
||||
public void AssignValueInBackgroundThread(int value) {
|
||||
RunInBackground(
|
||||
delegate () {
|
||||
this.assignedValue = value;
|
||||
this.finishedGate.Set();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Last error that was reported by the threaded view model</summary>
|
||||
public Exception ReportedError {
|
||||
get { return this.reportedError; }
|
||||
}
|
||||
|
||||
/// <summary>Value that has been assigned from the background thread</summary>
|
||||
public int AssignedValue {
|
||||
get { return this.assignedValue; }
|
||||
}
|
||||
|
||||
/// <summary>Called when an error occurs in the background thread</summary>
|
||||
/// <param name="exception">Exception that was thrown in the background thread</param>
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.reportedError = exception;
|
||||
this.finishedGate.Set();
|
||||
}
|
||||
|
||||
/// <summary>Last error that was reported by the threaded view model</summary>
|
||||
private volatile Exception reportedError;
|
||||
/// <summary>Triggered when the </summary>
|
||||
private ManualResetEvent finishedGate;
|
||||
/// <summary>Value that is assigned through the background thread</summary>
|
||||
private volatile int assignedValue;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class TestViewModel
|
||||
|
||||
/// <summary>Verifies that the threaded view model has a default constructor</summary>
|
||||
[Test, Explicit]
|
||||
public void HasDefaultConstructor() {
|
||||
using(var mainForm = new System.Windows.Forms.Form()) {
|
||||
mainForm.Show();
|
||||
try {
|
||||
mainForm.Visible = false;
|
||||
using(new TestViewModel()) { }
|
||||
}
|
||||
finally {
|
||||
mainForm.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the threaded view model can be constructed with a custom UI context
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasCustomSychronizationContextConstructor() {
|
||||
using(new TestViewModel(new DummyContext())) { }
|
||||
}
|
||||
|
||||
/// <summary>Checks that a new view model starts out idle and not busy</summary>
|
||||
[Test]
|
||||
public void NewInstanceIsNotBusy() {
|
||||
using(var viewModel = new TestViewModel(new DummyContext())) {
|
||||
Assert.IsFalse(viewModel.IsBusy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that errors happening in the background processing threads are
|
||||
/// reported to the main thread
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ErrorsInBackgroundThreadAreReported() {
|
||||
using(var viewModel = new TestViewModel(new DummyContext())) {
|
||||
var testError = new ArgumentException("Mooh");
|
||||
viewModel.CauseErrorInBackgroundThread(testError);
|
||||
viewModel.WaitUntilFinished();
|
||||
Assert.AreSame(testError, viewModel.ReportedError);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the background thread actually executes and can do work
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void BackgroundThreadExecutesTasks() {
|
||||
using(var viewModel = new TestViewModel(new DummyContext())) {
|
||||
viewModel.AssignValueInBackgroundThread(10042);
|
||||
viewModel.WaitUntilFinished();
|
||||
Assert.AreEqual(10042, viewModel.AssignedValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
#if UNITTEST
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>Unit test for the threaded view model base class</summary>
|
||||
[TestFixture]
|
||||
public class ThreadedViewModelTest {
|
||||
|
||||
#region class DummyContext
|
||||
|
||||
/// <summary>Synchronization context that does absolutely nothing</summary>
|
||||
private class DummyContext : ISynchronizeInvoke {
|
||||
|
||||
#region class SimpleAsyncResult
|
||||
|
||||
/// <summary>Barebones implementation of an asynchronous result</summary>
|
||||
private class SimpleAsyncResult : IAsyncResult {
|
||||
|
||||
/// <summary>Ehether the asynchronous operation is complete</summary>
|
||||
/// <remarks>
|
||||
/// Always true because it completes synchronously
|
||||
/// </remarks>
|
||||
public bool IsCompleted { get { return true; } }
|
||||
|
||||
/// <summary>
|
||||
/// Wait handle that can be used to wait for the asynchronous operation
|
||||
/// </summary>
|
||||
public WaitHandle AsyncWaitHandle {
|
||||
get { throw new NotImplementedException("Not implemented"); }
|
||||
}
|
||||
|
||||
/// <summary>Custom state that can be used to pass information around</summary>
|
||||
public object AsyncState {
|
||||
get { throw new NotImplementedException("Not implemented"); }
|
||||
}
|
||||
|
||||
/// <summary>Whether the asynchronous operation completed synchronously</summary>
|
||||
public bool CompletedSynchronously { get { return true; } }
|
||||
|
||||
/// <summary>The value returned from the asynchronous operation</summary>
|
||||
public object ReturnedValue;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class SimpleAsyncResult
|
||||
|
||||
/// <summary>Whether the calling thread needs to use Invoke()</summary>
|
||||
public bool InvokeRequired {
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
/// <summary>Schedules the specified method for execution in the target thread</summary>
|
||||
/// <param name="method">Method the target thread will execute when it is idle</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>
|
||||
/// An asynchronous result handle that can be used to check on the status of
|
||||
/// the call and wait for its completion
|
||||
/// </returns>
|
||||
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
|
||||
var asyncResult = new SimpleAsyncResult();
|
||||
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
|
||||
return asyncResult;
|
||||
}
|
||||
|
||||
/// <summary>Waits for the asychronous call to complete</summary>
|
||||
/// <param name="result">
|
||||
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
|
||||
/// </param>
|
||||
/// <returns>The original result returned by the asychronously called method</returns>
|
||||
public object EndInvoke(IAsyncResult result) {
|
||||
return ((SimpleAsyncResult)result).ReturnedValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules the specified method for execution in the target thread and waits
|
||||
/// for it to complete
|
||||
/// </summary>
|
||||
/// <param name="method">Method that will be executed by the target thread</param>
|
||||
/// <param name="arguments">Arguments that will be passed to the method</param>
|
||||
/// <returns>The result returned by the specified method</returns>
|
||||
public object Invoke(Delegate method, object[] arguments) {
|
||||
return method.Method.Invoke(method.Target, arguments);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion // class DummyContext
|
||||
|
||||
#region class TestViewModel
|
||||
|
||||
/// <summary>View model used to unit test the threaded view model base class</summary>
|
||||
private class TestViewModel : ThreadedViewModel {
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new view model, letting the base class figure out the UI thread
|
||||
/// </summary>
|
||||
public TestViewModel() : base() {
|
||||
this.finishedGate = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new view model, using the specified context for the UI thread
|
||||
/// </summary>
|
||||
/// <param name="uiContext">Synchronization context of the UI thread</param>
|
||||
public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) {
|
||||
this.finishedGate = new ManualResetEvent(initialState: false);
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public override void Dispose() {
|
||||
base.Dispose();
|
||||
|
||||
if(this.finishedGate != null) {
|
||||
this.finishedGate.Dispose();
|
||||
this.finishedGate = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Waits until the first background operation is finished</summary>
|
||||
/// <returns>
|
||||
/// True if the background operation is finished, false if it is ongoing
|
||||
/// </returns>
|
||||
public bool WaitUntilFinished() {
|
||||
return this.finishedGate.WaitOne(100);
|
||||
}
|
||||
|
||||
/// <summary>Runs a background process that causes the specified error</summary>
|
||||
/// <param name="error">Error that will be caused in the background process</param>
|
||||
public void CauseErrorInBackgroundThread(Exception error) {
|
||||
RunInBackground(
|
||||
delegate() { throw error; }
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns the specified value to the same-named property from a background thread
|
||||
/// </summary>
|
||||
/// <param name="value">Value that will be assigned to the same-named property</param>
|
||||
public void AssignValueInBackgroundThread(int value) {
|
||||
RunInBackground(
|
||||
delegate () {
|
||||
this.assignedValue = value;
|
||||
this.finishedGate.Set();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Last error that was reported by the threaded view model</summary>
|
||||
public Exception ReportedError {
|
||||
get { return this.reportedError; }
|
||||
}
|
||||
|
||||
/// <summary>Value that has been assigned from the background thread</summary>
|
||||
public int AssignedValue {
|
||||
get { return this.assignedValue; }
|
||||
}
|
||||
|
||||
/// <summary>Called when an error occurs in the background thread</summary>
|
||||
/// <param name="exception">Exception that was thrown in the background thread</param>
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.reportedError = exception;
|
||||
this.finishedGate.Set();
|
||||
}
|
||||
|
||||
/// <summary>Last error that was reported by the threaded view model</summary>
|
||||
private volatile Exception reportedError;
|
||||
/// <summary>Triggered when the </summary>
|
||||
private ManualResetEvent finishedGate;
|
||||
/// <summary>Value that is assigned through the background thread</summary>
|
||||
private volatile int assignedValue;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class TestViewModel
|
||||
|
||||
/// <summary>Verifies that the threaded view model has a default constructor</summary>
|
||||
[Test, Explicit]
|
||||
public void HasDefaultConstructor() {
|
||||
using(var mainForm = new System.Windows.Forms.Form()) {
|
||||
mainForm.Show();
|
||||
try {
|
||||
mainForm.Visible = false;
|
||||
using(new TestViewModel()) { }
|
||||
}
|
||||
finally {
|
||||
mainForm.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the threaded view model can be constructed with a custom UI context
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasCustomSychronizationContextConstructor() {
|
||||
using(new TestViewModel(new DummyContext())) { }
|
||||
}
|
||||
|
||||
/// <summary>Checks that a new view model starts out idle and not busy</summary>
|
||||
[Test]
|
||||
public void NewInstanceIsNotBusy() {
|
||||
using(var viewModel = new TestViewModel(new DummyContext())) {
|
||||
Assert.IsFalse(viewModel.IsBusy);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that errors happening in the background processing threads are
|
||||
/// reported to the main thread
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void ErrorsInBackgroundThreadAreReported() {
|
||||
using(var viewModel = new TestViewModel(new DummyContext())) {
|
||||
var testError = new ArgumentException("Mooh");
|
||||
viewModel.CauseErrorInBackgroundThread(testError);
|
||||
viewModel.WaitUntilFinished();
|
||||
Assert.AreSame(testError, viewModel.ReportedError);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the background thread actually executes and can do work
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void BackgroundThreadExecutesTasks() {
|
||||
using(var viewModel = new TestViewModel(new DummyContext())) {
|
||||
viewModel.AssignValueInBackgroundThread(10042);
|
||||
viewModel.WaitUntilFinished();
|
||||
Assert.AreEqual(10042, viewModel.AssignedValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
||||
#endif // UNITTEST
|
|
@ -1,232 +1,231 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
using Nuclex.Support.Threading;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>View model that can execute tasks in a background thread</summary>
|
||||
public abstract class ThreadedViewModel : Observable, IDisposable {
|
||||
|
||||
#region class ViewModelThreadRunner
|
||||
|
||||
/// <summary>Thread runner for the threaded view model</summary>
|
||||
private class ViewModelThreadRunner : ThreadRunner {
|
||||
|
||||
/// <summary>Initializes a new thread runner for the threaded view model</summary>
|
||||
public ViewModelThreadRunner(ThreadedViewModel viewModel) {
|
||||
this.viewModel = viewModel;
|
||||
}
|
||||
|
||||
/// <summary>Reports an error</summary>
|
||||
/// <param name="exception">Error that will be reported</param>
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.viewModel.reportErrorFromThread(exception);
|
||||
}
|
||||
|
||||
/// <summary>Called when the status of the busy flag changes</summary>
|
||||
protected override void BusyChanged() {
|
||||
this.viewModel.OnIsBusyChanged();
|
||||
}
|
||||
|
||||
/// <summary>View model the thread runner belongs to</summary>
|
||||
private ThreadedViewModel viewModel;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class ViewModelThreadRunner
|
||||
|
||||
/// <summary>Initializes a new view model for background processing</summary>
|
||||
/// <param name="uiContext">
|
||||
/// UI dispatcher that can be used to run callbacks in the UI thread
|
||||
/// </param>
|
||||
protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) {
|
||||
if(uiContext == null) {
|
||||
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
|
||||
if(this.uiContext == null) {
|
||||
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
|
||||
}
|
||||
} else {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
this.reportErrorDelegate = new Action<Exception>(ReportError);
|
||||
|
||||
this.threadRunner = new ViewModelThreadRunner(this);
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public virtual void Dispose() {
|
||||
if(this.threadRunner != null) {
|
||||
this.threadRunner.Dispose();
|
||||
this.threadRunner = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether the view model is currently busy executing a task</summary>
|
||||
public bool IsBusy {
|
||||
get { return this.threadRunner.IsBusy; }
|
||||
}
|
||||
|
||||
/// <summary>Reports an error to the user</summary>
|
||||
/// <param name="exception">Error that will be reported</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// You can use this method as a default handling method for your own error reporting
|
||||
/// (displaying the error to the user, logging it or whatever else is appropriate).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When <see cref="RunInBackground(System.Action)" /> is used, this method will also
|
||||
/// be called in case an exception within the asynchronously running code goes unhandled.
|
||||
/// This choice was made because, in the context of UI code, you would wrap any
|
||||
/// operations that might fail in a try..catch pair anyway in order to inform
|
||||
/// the user instead of aborting the entire application.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected abstract void ReportError(Exception exception);
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
protected void RunInBackground(Action action) {
|
||||
this.threadRunner.RunInBackground(action);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
protected void RunInBackground(CancellableAction action) {
|
||||
this.threadRunner.RunInBackground(action);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
/// <param name="parameter1">Parameter that will be passed to the action</param>
|
||||
protected void RunInBackground<P1>(Action<P1> action, P1 parameter1) {
|
||||
this.threadRunner.RunInBackground(action, parameter1);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
/// <param name="parameter1">Parameter that will be passed to the action</param>
|
||||
protected void RunInBackground<P1>(CancellableAction<P1> action, P1 parameter1) {
|
||||
this.threadRunner.RunInBackground(action, parameter1);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
/// <param name="parameter1">First parameter that will be passed to the action</param>
|
||||
/// <param name="parameter2">Second parameter that will be passed to the action</param>
|
||||
protected void RunInBackground<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) {
|
||||
this.threadRunner.RunInBackground(action, parameter1, parameter2);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
/// <param name="parameter1">First parameter that will be passed to the action</param>
|
||||
/// <param name="parameter2">Second parameter that will be passed to the action</param>
|
||||
protected void RunInBackground<P1, P2>(
|
||||
CancellableAction<P1, P2> action, P1 parameter1, P2 parameter2
|
||||
) {
|
||||
this.threadRunner.RunInBackground(action, parameter1, parameter2);
|
||||
}
|
||||
|
||||
/// <summary>Cancels the currently running background operation</summary>
|
||||
protected void CancelBackgroundOperation() {
|
||||
this.threadRunner.CancelBackgroundOperation();
|
||||
}
|
||||
|
||||
/// <summary>Cancels all queued and the currently running background operation</summary>
|
||||
protected void CancelAllBackgroundOperations() {
|
||||
this.threadRunner.CancelAllBackgroundOperations();
|
||||
}
|
||||
|
||||
/// <summary>Whether the background operation has been cancelled</summary>
|
||||
//[Obsolete("Please use a method accepting a cancellation token instead of using this")]
|
||||
protected bool IsBackgroundOperationCancelled {
|
||||
get { return this.threadRunner.IsBackgroundOperationCancelled; }
|
||||
}
|
||||
|
||||
/// <summary>Throws an exception if the background operation was cancelled</summary>
|
||||
//[Obsolete("Please use a method accepting a cancellation token instead of using this")]
|
||||
protected void ThrowIfBackgroundOperationCancelled() {
|
||||
this.threadRunner.ThrowIfBackgroundOperationCancelled();
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified action in the UI thread</summary>
|
||||
/// <param name="action">Action that will be executed in the UI thread</param>
|
||||
protected void RunInUIThread(Action action) {
|
||||
this.uiContext.Invoke(action, EmptyObjectArray);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified action in the UI thread</summary>
|
||||
/// <param name="action">Action that will be executed in the UI thread</param>
|
||||
/// <param name="parameter1">Parameter that will be passed to the action</param>
|
||||
protected void RunInUIThread<P1>(Action<P1> action, P1 parameter1) {
|
||||
this.uiContext.Invoke(action, new object[1] { parameter1 });
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified action in the UI thread</summary>
|
||||
/// <param name="action">Action that will be executed in the UI thread</param>
|
||||
/// <param name="parameter1">First parameter that will be passed to the action</param>
|
||||
/// <param name="parameter2">Second parameter that will be passed to the action</param>
|
||||
protected void RunInUIThread<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) {
|
||||
this.uiContext.Invoke(action, new object[2] { parameter1, parameter2 });
|
||||
}
|
||||
|
||||
/// <summary>Called when the thread runner's busy flag changes</summary>
|
||||
protected virtual void OnIsBusyChanged() {
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
|
||||
// For the ThreadedAction class - there should be a better way!
|
||||
/// <summary>Thread runner that manages the view model's thread</summary>
|
||||
internal ThreadRunner ThreadRunner {
|
||||
get { return this.threadRunner; }
|
||||
}
|
||||
|
||||
/// <summary>Reports an error that occurred in the runner's background thread</summary>
|
||||
/// <param name="exception">Exception that the thread has encountered</param>
|
||||
private void reportErrorFromThread(Exception exception) {
|
||||
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
|
||||
}
|
||||
|
||||
/// <summary>Sets the UI context that will be used by the threaded action</summary>
|
||||
/// <param name="uiContext">The UI context the threaded action will use</param>
|
||||
private void updateUiContext(ISynchronizeInvoke uiContext) {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
/// <summary>An array of zero objects</summary>
|
||||
private static readonly object[] EmptyObjectArray = new object[0];
|
||||
|
||||
/// <summary>UI dispatcher of the thread in which the view runs</summary>
|
||||
private ISynchronizeInvoke uiContext;
|
||||
/// <summary>Delegate for the ReportError() method</summary>
|
||||
private Action<Exception> reportErrorDelegate;
|
||||
/// <summary>Thread runner that manages the view model's thread</summary>
|
||||
private ViewModelThreadRunner threadRunner;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
using Nuclex.Support.Threading;
|
||||
|
||||
namespace Nuclex.Windows.Forms.ViewModels {
|
||||
|
||||
/// <summary>View model that can execute tasks in a background thread</summary>
|
||||
public abstract class ThreadedViewModel : Observable, IDisposable {
|
||||
|
||||
#region class ViewModelThreadRunner
|
||||
|
||||
/// <summary>Thread runner for the threaded view model</summary>
|
||||
private class ViewModelThreadRunner : ThreadRunner {
|
||||
|
||||
/// <summary>Initializes a new thread runner for the threaded view model</summary>
|
||||
public ViewModelThreadRunner(ThreadedViewModel viewModel) {
|
||||
this.viewModel = viewModel;
|
||||
}
|
||||
|
||||
/// <summary>Reports an error</summary>
|
||||
/// <param name="exception">Error that will be reported</param>
|
||||
protected override void ReportError(Exception exception) {
|
||||
this.viewModel.reportErrorFromThread(exception);
|
||||
}
|
||||
|
||||
/// <summary>Called when the status of the busy flag changes</summary>
|
||||
protected override void BusyChanged() {
|
||||
this.viewModel.OnIsBusyChanged();
|
||||
}
|
||||
|
||||
/// <summary>View model the thread runner belongs to</summary>
|
||||
private ThreadedViewModel viewModel;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class ViewModelThreadRunner
|
||||
|
||||
/// <summary>Initializes a new view model for background processing</summary>
|
||||
/// <param name="uiContext">
|
||||
/// UI dispatcher that can be used to run callbacks in the UI thread
|
||||
/// </param>
|
||||
protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) {
|
||||
if(uiContext == null) {
|
||||
this.uiContext = LateCheckedSynchronizer.GetMainWindow();
|
||||
if(this.uiContext == null) {
|
||||
this.uiContext = new LateCheckedSynchronizer(updateUiContext);
|
||||
}
|
||||
} else {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
this.reportErrorDelegate = new Action<Exception>(ReportError);
|
||||
|
||||
this.threadRunner = new ViewModelThreadRunner(this);
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public virtual void Dispose() {
|
||||
if(this.threadRunner != null) {
|
||||
this.threadRunner.Dispose();
|
||||
this.threadRunner = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether the view model is currently busy executing a task</summary>
|
||||
public bool IsBusy {
|
||||
get { return this.threadRunner.IsBusy; }
|
||||
}
|
||||
|
||||
/// <summary>Reports an error to the user</summary>
|
||||
/// <param name="exception">Error that will be reported</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// You can use this method as a default handling method for your own error reporting
|
||||
/// (displaying the error to the user, logging it or whatever else is appropriate).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// When <see cref="RunInBackground(System.Action)" /> is used, this method will also
|
||||
/// be called in case an exception within the asynchronously running code goes unhandled.
|
||||
/// This choice was made because, in the context of UI code, you would wrap any
|
||||
/// operations that might fail in a try..catch pair anyway in order to inform
|
||||
/// the user instead of aborting the entire application.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected abstract void ReportError(Exception exception);
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
protected void RunInBackground(Action action) {
|
||||
this.threadRunner.RunInBackground(action);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
protected void RunInBackground(CancellableAction action) {
|
||||
this.threadRunner.RunInBackground(action);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
/// <param name="parameter1">Parameter that will be passed to the action</param>
|
||||
protected void RunInBackground<P1>(Action<P1> action, P1 parameter1) {
|
||||
this.threadRunner.RunInBackground(action, parameter1);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
/// <param name="parameter1">Parameter that will be passed to the action</param>
|
||||
protected void RunInBackground<P1>(CancellableAction<P1> action, P1 parameter1) {
|
||||
this.threadRunner.RunInBackground(action, parameter1);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
/// <param name="parameter1">First parameter that will be passed to the action</param>
|
||||
/// <param name="parameter2">Second parameter that will be passed to the action</param>
|
||||
protected void RunInBackground<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) {
|
||||
this.threadRunner.RunInBackground(action, parameter1, parameter2);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified operation in the background</summary>
|
||||
/// <param name="action">Action that will be executed in the background</param>
|
||||
/// <param name="parameter1">First parameter that will be passed to the action</param>
|
||||
/// <param name="parameter2">Second parameter that will be passed to the action</param>
|
||||
protected void RunInBackground<P1, P2>(
|
||||
CancellableAction<P1, P2> action, P1 parameter1, P2 parameter2
|
||||
) {
|
||||
this.threadRunner.RunInBackground(action, parameter1, parameter2);
|
||||
}
|
||||
|
||||
/// <summary>Cancels the currently running background operation</summary>
|
||||
protected void CancelBackgroundOperation() {
|
||||
this.threadRunner.CancelBackgroundOperation();
|
||||
}
|
||||
|
||||
/// <summary>Cancels all queued and the currently running background operation</summary>
|
||||
protected void CancelAllBackgroundOperations() {
|
||||
this.threadRunner.CancelAllBackgroundOperations();
|
||||
}
|
||||
|
||||
/// <summary>Whether the background operation has been cancelled</summary>
|
||||
//[Obsolete("Please use a method accepting a cancellation token instead of using this")]
|
||||
protected bool IsBackgroundOperationCancelled {
|
||||
get { return this.threadRunner.IsBackgroundOperationCancelled; }
|
||||
}
|
||||
|
||||
/// <summary>Throws an exception if the background operation was cancelled</summary>
|
||||
//[Obsolete("Please use a method accepting a cancellation token instead of using this")]
|
||||
protected void ThrowIfBackgroundOperationCancelled() {
|
||||
this.threadRunner.ThrowIfBackgroundOperationCancelled();
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified action in the UI thread</summary>
|
||||
/// <param name="action">Action that will be executed in the UI thread</param>
|
||||
protected void RunInUIThread(Action action) {
|
||||
this.uiContext.Invoke(action, EmptyObjectArray);
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified action in the UI thread</summary>
|
||||
/// <param name="action">Action that will be executed in the UI thread</param>
|
||||
/// <param name="parameter1">Parameter that will be passed to the action</param>
|
||||
protected void RunInUIThread<P1>(Action<P1> action, P1 parameter1) {
|
||||
this.uiContext.Invoke(action, new object[1] { parameter1 });
|
||||
}
|
||||
|
||||
/// <summary>Executes the specified action in the UI thread</summary>
|
||||
/// <param name="action">Action that will be executed in the UI thread</param>
|
||||
/// <param name="parameter1">First parameter that will be passed to the action</param>
|
||||
/// <param name="parameter2">Second parameter that will be passed to the action</param>
|
||||
protected void RunInUIThread<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) {
|
||||
this.uiContext.Invoke(action, new object[2] { parameter1, parameter2 });
|
||||
}
|
||||
|
||||
/// <summary>Called when the thread runner's busy flag changes</summary>
|
||||
protected virtual void OnIsBusyChanged() {
|
||||
OnPropertyChanged(nameof(IsBusy));
|
||||
}
|
||||
|
||||
// For the ThreadedAction class - there should be a better way!
|
||||
/// <summary>Thread runner that manages the view model's thread</summary>
|
||||
internal ThreadRunner ThreadRunner {
|
||||
get { return this.threadRunner; }
|
||||
}
|
||||
|
||||
/// <summary>Reports an error that occurred in the runner's background thread</summary>
|
||||
/// <param name="exception">Exception that the thread has encountered</param>
|
||||
private void reportErrorFromThread(Exception exception) {
|
||||
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
|
||||
}
|
||||
|
||||
/// <summary>Sets the UI context that will be used by the threaded action</summary>
|
||||
/// <param name="uiContext">The UI context the threaded action will use</param>
|
||||
private void updateUiContext(ISynchronizeInvoke uiContext) {
|
||||
this.uiContext = uiContext;
|
||||
}
|
||||
|
||||
/// <summary>An array of zero objects</summary>
|
||||
private static readonly object[] EmptyObjectArray = new object[0];
|
||||
|
||||
/// <summary>UI dispatcher of the thread in which the view runs</summary>
|
||||
private ISynchronizeInvoke uiContext;
|
||||
/// <summary>Delegate for the ReportError() method</summary>
|
||||
private Action<Exception> reportErrorDelegate;
|
||||
/// <summary>Thread runner that manages the view model's thread</summary>
|
||||
private ViewModelThreadRunner threadRunner;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.ViewModels
|
||||
|
|
|
@ -1,38 +1,37 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Enables consumer to look up the currently active window</summary>
|
||||
public interface IActiveWindowTracker {
|
||||
|
||||
/// <summary>The currently active top-level or modal window</summary>
|
||||
/// <remarks>
|
||||
/// If windows live in multiple threads, the property change notification for
|
||||
/// this property, if supported, might be fired from a different thread.
|
||||
/// </remarks>
|
||||
Form ActiveWindow { get; }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Enables consumer to look up the currently active window</summary>
|
||||
public interface IActiveWindowTracker {
|
||||
|
||||
/// <summary>The currently active top-level or modal window</summary>
|
||||
/// <remarks>
|
||||
/// If windows live in multiple threads, the property change notification for
|
||||
/// this property, if supported, might be fired from a different thread.
|
||||
/// </remarks>
|
||||
Form ActiveWindow { get; }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,41 +1,40 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Views {
|
||||
|
||||
/// <summary>View with support for data binding</summary>
|
||||
public interface IView {
|
||||
|
||||
/// <summary>Provides the data binding target for the view</summary>
|
||||
/// <remarks>
|
||||
/// This property is identical to the same-named one in WPF, it provides
|
||||
/// the view model to which the view should bind its controls.
|
||||
/// </remarks>
|
||||
object DataContext { get; set; }
|
||||
|
||||
// Whether the view owns its view model and it needs to be disposed after
|
||||
// the view ceases to exist
|
||||
//bool IsOwnedByView { get; set; }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Views
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Views {
|
||||
|
||||
/// <summary>View with support for data binding</summary>
|
||||
public interface IView {
|
||||
|
||||
/// <summary>Provides the data binding target for the view</summary>
|
||||
/// <remarks>
|
||||
/// This property is identical to the same-named one in WPF, it provides
|
||||
/// the view model to which the view should bind its controls.
|
||||
/// </remarks>
|
||||
object DataContext { get; set; }
|
||||
|
||||
// Whether the view owns its view model and it needs to be disposed after
|
||||
// the view ceases to exist
|
||||
//bool IsOwnedByView { get; set; }
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Views
|
||||
|
|
|
@ -1,366 +1,385 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
using Nuclex.Windows.Forms.ViewModels;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Views {
|
||||
|
||||
/// <summary>Special view form that can display different child views</summary>
|
||||
public class MultiPageViewForm : ViewForm {
|
||||
|
||||
#region struct RedrawLockScope
|
||||
|
||||
/// <summary>Prevents controls from redrawing themselves for a while</summary>
|
||||
private struct RedrawLockScope : IDisposable {
|
||||
|
||||
/// <summary>Window message that enables or disables control redraw</summary>
|
||||
private const int WM_SETREDRAW = 11;
|
||||
|
||||
/// <summary>Sends a window message to the specified window</summary>
|
||||
/// <param name="windowHandle">Window a message will be sent to</param>
|
||||
/// <param name="messageId">ID of the message that will be sent</param>
|
||||
/// <param name="firstArgument">First argument to the window procedure</param>
|
||||
/// <param name="secondArgument">Second argument to the window procedure</param>
|
||||
/// <returns>The return value of the window procedure</returns>
|
||||
[DllImport("user32")]
|
||||
public static extern int SendMessage(
|
||||
IntPtr windowHandle, int messageId, bool firstArgument, int secondArgument
|
||||
);
|
||||
|
||||
/// <summary>Stops redrawing the specified control</summary>
|
||||
/// <param name="control">Control to stop redrawing</param>
|
||||
public RedrawLockScope(Control control) {
|
||||
if(Environment.OSVersion.Platform == PlatformID.Win32NT) {
|
||||
SendMessage(control.Handle, WM_SETREDRAW, false, 0);
|
||||
this.control = control;
|
||||
} else {
|
||||
this.control = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Enables redrawing again when the lock scope is disposed</summary>
|
||||
public void Dispose() {
|
||||
if(this.control != null) {
|
||||
SendMessage(this.control.Handle, WM_SETREDRAW, true, 0);
|
||||
this.control.Invalidate(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Control that has been stopped from redrawing itself</summary>
|
||||
private Control control;
|
||||
|
||||
}
|
||||
|
||||
#endregion // struct RedrawLockScope
|
||||
|
||||
/// <summary>Initializes a new multi page view window for the windows forms designer</summary>
|
||||
public MultiPageViewForm() {
|
||||
this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView));
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new multi page view window</summary>
|
||||
/// <param name="windowManager">
|
||||
/// Window manager that is used to set up the child views
|
||||
/// </param>
|
||||
/// <param name="cachePageViews">Whether page views should be kept alive and reused</param>
|
||||
public MultiPageViewForm(IWindowManager windowManager, bool cachePageViews = false) {
|
||||
this.windowManager = windowManager;
|
||||
this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView));
|
||||
|
||||
if(cachePageViews) {
|
||||
this.cachedViews = new Dictionary<Type, Control>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when the control is being disposed</summary>
|
||||
/// <param name="calledExplicitly">
|
||||
/// Whether the call was made by user code (vs. the garbage collector)
|
||||
/// </param>
|
||||
protected override void Dispose(bool calledExplicitly) {
|
||||
if(calledExplicitly) {
|
||||
|
||||
// Disable the active view, if any
|
||||
if(this.activePageView != null) {
|
||||
if(this.childViewContainer != null) {
|
||||
this.childViewContainer.Controls.Remove(this.activePageView);
|
||||
}
|
||||
}
|
||||
|
||||
// If caching is disabled, dispose of the active child view, if any
|
||||
if(this.cachedViews == null) {
|
||||
if(this.activePageView != null) {
|
||||
disposeIfSupported(this.activePageView);
|
||||
this.activePageView = null;
|
||||
}
|
||||
} else { // Caching is enabled, dispose of any cached child views
|
||||
foreach(Control childView in this.cachedViews.Values) {
|
||||
disposeIfSupported(childView);
|
||||
}
|
||||
this.cachedViews.Clear();
|
||||
this.cachedViews = null;
|
||||
this.activePageView = null;
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(calledExplicitly);
|
||||
}
|
||||
|
||||
/// <summary>Discovers the container control used to host the child views</summary>
|
||||
/// <returns>The container control is which the child views will be hosted</returns>
|
||||
/// <remarks>
|
||||
/// This is supposed to be overriden by the user, simply returning the container
|
||||
/// control that should host the page views. If it isn't, however, we use some
|
||||
/// heuristics to figure out the most likely candidate: it should be a container,
|
||||
/// and it should cover most of the window's client area.
|
||||
/// </remarks>
|
||||
protected virtual Control IdentifyPageContainer() {
|
||||
Size halfWindowSize = Size;
|
||||
halfWindowSize.Width /= 2;
|
||||
halfWindowSize.Height /= 2;
|
||||
|
||||
// First container control we found -- if we find no likely candidate,
|
||||
// we simply use the first
|
||||
Control firstContainer = null;
|
||||
|
||||
// Check all top-level controls in the window. If there's a container that
|
||||
// covers most of the window, it's our best bet
|
||||
int controlCount = Controls.Count;
|
||||
for(int index = 0; index < controlCount; ++index) {
|
||||
Control control = Controls[index];
|
||||
|
||||
// Only check container controls
|
||||
if((control is ContainerControl) || (control is Panel)) {
|
||||
if(firstContainer == null) {
|
||||
firstContainer = control;
|
||||
}
|
||||
|
||||
// If this control covers most of the view, it's our candidate!
|
||||
Size controlSize = control.Size;
|
||||
bool goodCandidate = (
|
||||
(controlSize.Width > halfWindowSize.Width) &&
|
||||
(controlSize.Height > halfWindowSize.Height)
|
||||
);
|
||||
if(goodCandidate) {
|
||||
return control;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no candidate was found, return the first container control we encountered
|
||||
// or create a new UserControl as the container if nothing was found at all.
|
||||
if(firstContainer == null) {
|
||||
firstContainer = new Panel();
|
||||
Controls.Add(firstContainer);
|
||||
firstContainer.Dock = DockStyle.Fill;
|
||||
}
|
||||
|
||||
return firstContainer;
|
||||
}
|
||||
|
||||
/// <summary>Called when the window's data context is changed</summary>
|
||||
/// <param name="sender">Window whose data context was changed</param>
|
||||
/// <param name="oldDataContext">Data context that was previously used</param>
|
||||
/// <param name="newDataContext">Data context that will be used from now on</param>
|
||||
protected override void OnDataContextChanged(
|
||||
object sender, object oldDataContext, object newDataContext
|
||||
) {
|
||||
|
||||
// Kill the currently active view if there was an old view model.
|
||||
if(oldDataContext != null) {
|
||||
disableActivePageView();
|
||||
}
|
||||
|
||||
base.OnDataContextChanged(sender, oldDataContext, newDataContext);
|
||||
|
||||
// If a valid view model was assigned, create a new view its active page view model
|
||||
if(newDataContext != null) {
|
||||
var dataContextAsMultiPageViewModel = newDataContext as IMultiPageViewModel;
|
||||
if(dataContextAsMultiPageViewModel != null) {
|
||||
activatePageView(dataContextAsMultiPageViewModel.GetActivePageViewModel());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Called when a property of the view model is changed</summary>
|
||||
/// <param name="sender">View model in which a property was changed</param>
|
||||
/// <param name="arguments">Contains the name of the property that has changed</param>
|
||||
protected override void OnViewModelPropertyChanged(
|
||||
object sender, PropertyChangedEventArgs arguments
|
||||
) {
|
||||
base.OnViewModelPropertyChanged(sender, arguments);
|
||||
|
||||
if(arguments.AreAffecting(nameof(MultiPageViewModel<object>.ActivePage))) {
|
||||
var viewModelAsMultiPageviewModel = DataContext as IMultiPageViewModel;
|
||||
if(viewModelAsMultiPageviewModel != null) {
|
||||
if(InvokeRequired) {
|
||||
Invoke(
|
||||
new Action<object>(activatePageView),
|
||||
viewModelAsMultiPageviewModel.GetActivePageViewModel()
|
||||
);
|
||||
} else {
|
||||
activatePageView(viewModelAsMultiPageviewModel.GetActivePageViewModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Currently active page view control</summary>
|
||||
protected Control ActivePageView {
|
||||
get { return this.activePageView; }
|
||||
}
|
||||
|
||||
/// <summary>The view model running the currently active page</summary>
|
||||
protected object ActivePageViewModel {
|
||||
get {
|
||||
var activePageViewAsView = this.activePageView as IView;
|
||||
if(activePageViewAsView == null) {
|
||||
return null;
|
||||
} else {
|
||||
return activePageViewAsView.DataContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Activates the page view for the specified page view model</summary>
|
||||
/// <param name="pageViewModel">
|
||||
/// Page view model for which the page view will be activated
|
||||
/// </param>
|
||||
private void activatePageView(object pageViewModel) {
|
||||
object activePageViewModel = null;
|
||||
{
|
||||
var activePageViewAsView = this.activePageView as IView;
|
||||
if(activePageViewAsView != null) {
|
||||
activePageViewModel = activePageViewAsView.DataContext;
|
||||
}
|
||||
}
|
||||
|
||||
// Try from the cheapest to the most expensive way to get to our goal,
|
||||
// an activated view suiting the specified view model.
|
||||
|
||||
// If we already have the target view model selected, do nothing
|
||||
if(activePageViewModel == pageViewModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the page view model for the old and the new page are of the same
|
||||
// type, we can reuse the currently active page view
|
||||
if((activePageViewModel != null) && (pageViewModel != null)) {
|
||||
if(pageViewModel.GetType() == this.activePageView.GetType()) {
|
||||
var activePageViewAsView = this.activePageView as IView;
|
||||
if(activePageViewAsView != null) {
|
||||
activePageViewAsView.DataContext = pageViewModel;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Worst, but usual, case: the new page view model might require
|
||||
// a different view. Create or look up the new view and put it in the container
|
||||
{
|
||||
if(pageViewModel == null) {
|
||||
disableActivePageView();
|
||||
} else {
|
||||
Control pageViewContainer = getPageViewContainer();
|
||||
using(new RedrawLockScope(pageViewContainer)) {
|
||||
disableActivePageView();
|
||||
|
||||
this.activePageView = getOrCreatePageView(pageViewModel);
|
||||
pageViewContainer.Controls.Add(this.activePageView);
|
||||
this.activePageView.Dock = DockStyle.Fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the cached child view or creates a new one if not cached</summary>
|
||||
/// <param name="viewModel">View model for which a child view will be returned</param>
|
||||
/// <returns>A child view suitable for the specified view model</returns>
|
||||
private Control getOrCreatePageView(object viewModel) {
|
||||
Type viewModelType = viewModel.GetType();
|
||||
|
||||
Control view;
|
||||
|
||||
// If caching is enabled, check if we have a cached view
|
||||
if(this.cachedViews != null) {
|
||||
if(this.cachedViews.TryGetValue(viewModelType, out view)) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, call the window manager's CreateView() method
|
||||
MethodInfo specializedCreateViewMethod = (
|
||||
this.createViewMethod.MakeGenericMethod(viewModelType)
|
||||
);
|
||||
view = (Control)specializedCreateViewMethod.Invoke(
|
||||
this.windowManager, new object[1] { viewModel }
|
||||
);
|
||||
|
||||
// If caching is enabled, register the view in the cache
|
||||
if(this.cachedViews != null) {
|
||||
this.cachedViews.Add(viewModelType, view);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/// <summary>Disables the currently active page view control</summary>
|
||||
private void disableActivePageView() {
|
||||
if(this.activePageView != null) {
|
||||
Control container = getPageViewContainer();
|
||||
container.Controls.Remove(this.activePageView);
|
||||
|
||||
// If we don't reuse views, kill it now
|
||||
if(this.cachedViews == null) {
|
||||
disposeIfSupported(this.activePageView);
|
||||
this.activePageView = null;
|
||||
} else {
|
||||
var activePageViewAsView = this.activePageView as IView;
|
||||
if(activePageViewAsView != null) {
|
||||
activePageViewAsView.DataContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fetches the container that holds the child views</summary>
|
||||
/// <returns>The container for the child views</returns>
|
||||
private Control getPageViewContainer() {
|
||||
if(this.childViewContainer == null) {
|
||||
this.childViewContainer = IdentifyPageContainer();
|
||||
}
|
||||
|
||||
return this.childViewContainer;
|
||||
}
|
||||
|
||||
/// <summary>Disposes the specified object if it is disposable</summary>
|
||||
/// <param name="potentiallyDisposable">Object that will be disposed if supported</param>
|
||||
private static void disposeIfSupported(object potentiallyDisposable) {
|
||||
var disposable = potentiallyDisposable as IDisposable;
|
||||
if(disposable != null) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Window manager through which the child views are created</summary>
|
||||
private IWindowManager windowManager;
|
||||
/// <summary>Reflection info for the createView() method of the window manager</summary>
|
||||
private MethodInfo createViewMethod;
|
||||
|
||||
/// <summary>Container in which the child views will be hosted</summary>
|
||||
private Control childViewContainer;
|
||||
/// <summary>Cached views that will be reused when the view model activates them</summary>
|
||||
private Dictionary<Type, Control> cachedViews;
|
||||
/// <summary>The currently active child view</summary>
|
||||
private Control activePageView;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Views
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Drawing;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
using Nuclex.Windows.Forms.ViewModels;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Views {
|
||||
|
||||
/// <summary>Special view form that can display different child views</summary>
|
||||
public class MultiPageViewForm : ViewForm {
|
||||
|
||||
#region struct RedrawLockScope
|
||||
|
||||
/// <summary>Prevents controls from redrawing themselves for a while</summary>
|
||||
private struct RedrawLockScope : IDisposable {
|
||||
|
||||
/// <summary>Window message that enables or disables control redraw</summary>
|
||||
private const int WM_SETREDRAW = 11;
|
||||
|
||||
/// <summary>Sends a window message to the specified window</summary>
|
||||
/// <param name="windowHandle">Window a message will be sent to</param>
|
||||
/// <param name="messageId">ID of the message that will be sent</param>
|
||||
/// <param name="firstArgument">First argument to the window procedure</param>
|
||||
/// <param name="secondArgument">Second argument to the window procedure</param>
|
||||
/// <returns>The return value of the window procedure</returns>
|
||||
[DllImport("user32")]
|
||||
public static extern int SendMessage(
|
||||
IntPtr windowHandle, int messageId, bool firstArgument, int secondArgument
|
||||
);
|
||||
|
||||
/// <summary>Stops redrawing the specified control</summary>
|
||||
/// <param name="control">Control to stop redrawing</param>
|
||||
public RedrawLockScope(Control control) {
|
||||
if(Environment.OSVersion.Platform == PlatformID.Win32NT) {
|
||||
SendMessage(control.Handle, WM_SETREDRAW, false, 0);
|
||||
this.control = control;
|
||||
} else {
|
||||
this.control = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Enables redrawing again when the lock scope is disposed</summary>
|
||||
public void Dispose() {
|
||||
if(this.control != null) {
|
||||
SendMessage(this.control.Handle, WM_SETREDRAW, true, 0);
|
||||
this.control.Invalidate(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Control that has been stopped from redrawing itself</summary>
|
||||
private Control control;
|
||||
|
||||
}
|
||||
|
||||
#endregion // struct RedrawLockScope
|
||||
|
||||
/// <summary>Initializes a new multi page view window for the windows forms designer</summary>
|
||||
public MultiPageViewForm() {
|
||||
this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView));
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new multi page view window</summary>
|
||||
/// <param name="windowManager">
|
||||
/// Window manager that is used to set up the child views
|
||||
/// </param>
|
||||
/// <param name="cachePageViews">Whether page views should be kept alive and reused</param>
|
||||
public MultiPageViewForm(IWindowManager windowManager, bool cachePageViews = false) {
|
||||
this.windowManager = windowManager;
|
||||
this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView));
|
||||
|
||||
if(cachePageViews) {
|
||||
this.cachedViews = new Dictionary<Type, Control>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when the control is being disposed</summary>
|
||||
/// <param name="calledExplicitly">
|
||||
/// Whether the call was made by user code (vs. the garbage collector)
|
||||
/// </param>
|
||||
protected override void Dispose(bool calledExplicitly) {
|
||||
if(calledExplicitly) {
|
||||
|
||||
// Disable the active view, if any
|
||||
if(this.activePageView != null) {
|
||||
if(this.childViewContainer != null) {
|
||||
this.childViewContainer.Controls.Remove(this.activePageView);
|
||||
}
|
||||
}
|
||||
|
||||
// If caching is disabled, dispose of the active child view, if any
|
||||
if(this.cachedViews == null) {
|
||||
if(this.activePageView != null) {
|
||||
disposeIfSupported(this.activePageView);
|
||||
this.activePageView = null;
|
||||
}
|
||||
} else { // Caching is enabled, dispose of any cached child views
|
||||
foreach(Control childView in this.cachedViews.Values) {
|
||||
disposeIfSupported(childView);
|
||||
}
|
||||
this.cachedViews.Clear();
|
||||
this.cachedViews = null;
|
||||
this.activePageView = null;
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(calledExplicitly);
|
||||
}
|
||||
|
||||
/// <summary>Discovers the container control used to host the child views</summary>
|
||||
/// <returns>The container control is which the child views will be hosted</returns>
|
||||
/// <remarks>
|
||||
/// This is supposed to be overriden by the user, simply returning the container
|
||||
/// control that should host the page views. If it isn't, however, we use some
|
||||
/// heuristics to figure out the most likely candidate: it should be a container,
|
||||
/// and it should cover most of the window's client area.
|
||||
/// </remarks>
|
||||
protected virtual Control IdentifyPageContainer() {
|
||||
Size halfWindowSize = Size;
|
||||
halfWindowSize.Width /= 2;
|
||||
halfWindowSize.Height /= 2;
|
||||
|
||||
// First container control we found -- if we find no likely candidate,
|
||||
// we simply use the first
|
||||
Control firstContainer = null;
|
||||
|
||||
// Check all top-level controls in the window. If there's a container that
|
||||
// covers most of the window, it's our best bet
|
||||
int controlCount = Controls.Count;
|
||||
for(int index = 0; index < controlCount; ++index) {
|
||||
Control control = Controls[index];
|
||||
|
||||
// Only check container controls
|
||||
if((control is ContainerControl) || (control is Panel)) {
|
||||
if(firstContainer == null) {
|
||||
firstContainer = control;
|
||||
}
|
||||
|
||||
// If this control covers most of the view, it's our candidate!
|
||||
Size controlSize = control.Size;
|
||||
bool goodCandidate = (
|
||||
(controlSize.Width > halfWindowSize.Width) &&
|
||||
(controlSize.Height > halfWindowSize.Height)
|
||||
);
|
||||
if(goodCandidate) {
|
||||
return control;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no candidate was found, return the first container control we encountered
|
||||
// or create a new UserControl as the container if nothing was found at all.
|
||||
if(firstContainer == null) {
|
||||
firstContainer = new Panel();
|
||||
Controls.Add(firstContainer);
|
||||
firstContainer.Dock = DockStyle.Fill;
|
||||
}
|
||||
|
||||
return firstContainer;
|
||||
}
|
||||
|
||||
/// <summary>Called when the window's data context is changed</summary>
|
||||
/// <param name="sender">Window whose data context was changed</param>
|
||||
/// <param name="oldDataContext">Data context that was previously used</param>
|
||||
/// <param name="newDataContext">Data context that will be used from now on</param>
|
||||
protected override void OnDataContextChanged(
|
||||
object sender, object oldDataContext, object newDataContext
|
||||
) {
|
||||
|
||||
// Kill the currently active view if there was an old view model.
|
||||
if(oldDataContext != null) {
|
||||
disableActivePageView();
|
||||
}
|
||||
|
||||
base.OnDataContextChanged(sender, oldDataContext, newDataContext);
|
||||
|
||||
// If a valid view model was assigned, create a new view its active page view model
|
||||
if(newDataContext != null) {
|
||||
var dataContextAsMultiPageViewModel = newDataContext as IMultiPageViewModel;
|
||||
if(dataContextAsMultiPageViewModel != null) {
|
||||
activatePageView(dataContextAsMultiPageViewModel.GetActivePageViewModel());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>Called when a property of the view model is changed</summary>
|
||||
/// <param name="sender">View model in which a property was changed</param>
|
||||
/// <param name="arguments">Contains the name of the property that has changed</param>
|
||||
protected override void OnViewModelPropertyChanged(
|
||||
object sender, PropertyChangedEventArgs arguments
|
||||
) {
|
||||
base.OnViewModelPropertyChanged(sender, arguments);
|
||||
|
||||
if(arguments.AreAffecting(nameof(MultiPageViewModel<object>.ActivePage))) {
|
||||
var viewModelAsMultiPageviewModel = DataContext as IMultiPageViewModel;
|
||||
if(viewModelAsMultiPageviewModel != null) {
|
||||
if(InvokeRequired) {
|
||||
Invoke(
|
||||
new Action<object>(activatePageView),
|
||||
viewModelAsMultiPageviewModel.GetActivePageViewModel()
|
||||
);
|
||||
} else {
|
||||
activatePageView(viewModelAsMultiPageviewModel.GetActivePageViewModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Currently active page view control</summary>
|
||||
protected Control ActivePageView {
|
||||
get { return this.activePageView; }
|
||||
}
|
||||
|
||||
/// <summary>The view model running the currently active page</summary>
|
||||
protected object ActivePageViewModel {
|
||||
get {
|
||||
var activePageViewAsView = this.activePageView as IView;
|
||||
if(activePageViewAsView == null) {
|
||||
return null;
|
||||
} else {
|
||||
return activePageViewAsView.DataContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Activates the page view for the specified page view model</summary>
|
||||
/// <param name="pageViewModel">
|
||||
/// Page view model for which the page view will be activated
|
||||
/// </param>
|
||||
private void activatePageView(object pageViewModel) {
|
||||
object activePageViewModel = null;
|
||||
{
|
||||
var activePageViewAsView = this.activePageView as IView;
|
||||
if(activePageViewAsView != null) {
|
||||
activePageViewModel = activePageViewAsView.DataContext;
|
||||
}
|
||||
}
|
||||
|
||||
// Try from the cheapest to the most expensive way to get to our goal,
|
||||
// an activated view suiting the specified view model.
|
||||
|
||||
// If we already have the target view model selected, do nothing
|
||||
if(activePageViewModel == pageViewModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the page view model for the old and the new page are of the same
|
||||
// type, we can reuse the currently active page view
|
||||
if((activePageViewModel != null) && (pageViewModel != null)) {
|
||||
if(pageViewModel.GetType() == this.activePageView.GetType()) {
|
||||
var activePageViewAsView = this.activePageView as IView;
|
||||
if(activePageViewAsView != null) {
|
||||
activePageViewAsView.DataContext = pageViewModel;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Worst, but usual, case: the new page view model might require
|
||||
// a different view. Create or look up the new view and put it in the container
|
||||
{
|
||||
if(pageViewModel == null) {
|
||||
disableActivePageView();
|
||||
} else {
|
||||
Control pageViewContainer = getPageViewContainer();
|
||||
using(new RedrawLockScope(pageViewContainer)) {
|
||||
disableActivePageView();
|
||||
|
||||
this.activePageView = getOrCreatePageView(pageViewModel);
|
||||
pageViewContainer.Controls.Add(this.activePageView);
|
||||
this.activePageView.Dock = DockStyle.Fill;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the cached child view or creates a new one if not cached</summary>
|
||||
/// <param name="viewModel">View model for which a child view will be returned</param>
|
||||
/// <returns>A child view suitable for the specified view model</returns>
|
||||
private Control getOrCreatePageView(object viewModel) {
|
||||
Type viewModelType = viewModel.GetType();
|
||||
|
||||
Control view;
|
||||
|
||||
// If caching is enabled, check if we have a cached view
|
||||
if(this.cachedViews != null) {
|
||||
if(this.cachedViews.TryGetValue(viewModelType, out view)) {
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, call the window manager's CreateView() method
|
||||
MethodInfo specializedCreateViewMethod = (
|
||||
this.createViewMethod.MakeGenericMethod(viewModelType)
|
||||
);
|
||||
view = (Control)specializedCreateViewMethod.Invoke(
|
||||
this.windowManager, new object[1] { viewModel }
|
||||
);
|
||||
|
||||
// If caching is enabled, register the view in the cache
|
||||
if(this.cachedViews != null) {
|
||||
this.cachedViews.Add(viewModelType, view);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/// <summary>Disables the currently active page view control</summary>
|
||||
private void disableActivePageView() {
|
||||
if(this.activePageView != null) {
|
||||
Control container = getPageViewContainer();
|
||||
container.Controls.Remove(this.activePageView);
|
||||
|
||||
// If we don't reuse views, kill it now
|
||||
if(this.cachedViews == null) {
|
||||
disposeIfSupported(this.activePageView);
|
||||
this.activePageView = null;
|
||||
} else {
|
||||
var activePageViewAsView = this.activePageView as IView;
|
||||
if(activePageViewAsView != null) {
|
||||
activePageViewAsView.DataContext = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fetches the container that holds the child views</summary>
|
||||
/// <returns>The container for the child views</returns>
|
||||
private Control getPageViewContainer() {
|
||||
if(this.childViewContainer == null) {
|
||||
this.childViewContainer = IdentifyPageContainer();
|
||||
}
|
||||
|
||||
return this.childViewContainer;
|
||||
}
|
||||
|
||||
/// <summary>Disposes the specified object if it is disposable</summary>
|
||||
/// <param name="potentiallyDisposable">Object that will be disposed if supported</param>
|
||||
private static void disposeIfSupported(object potentiallyDisposable) {
|
||||
var disposable = potentiallyDisposable as IDisposable;
|
||||
if(disposable != null) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Window manager through which the child views are created</summary>
|
||||
private IWindowManager windowManager;
|
||||
/// <summary>Reflection info for the createView() method of the window manager</summary>
|
||||
private MethodInfo createViewMethod;
|
||||
|
||||
/// <summary>Container in which the child views will be hosted</summary>
|
||||
private Control childViewContainer;
|
||||
/// <summary>Cached views that will be reused when the view model activates them</summary>
|
||||
private Dictionary<Type, Control> cachedViews;
|
||||
/// <summary>The currently active child view</summary>
|
||||
private Control activePageView;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Views
|
||||
|
|
|
@ -1,89 +1,88 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Views {
|
||||
|
||||
/// <summary>
|
||||
/// Base class for MVVM user controls that act as views connected to a view model
|
||||
/// </summary>
|
||||
public class ViewControl : UserControl, IView {
|
||||
|
||||
/// <summary>Initializes a new view control</summary>
|
||||
public ViewControl() {
|
||||
this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>Called when the control's data context is changed</summary>
|
||||
/// <param name="sender">Control whose data context was changed</param>
|
||||
/// <param name="oldDataContext">Data context that was previously used</param>
|
||||
/// <param name="newDataContext">Data context that will be used from now on</param>
|
||||
protected virtual void OnDataContextChanged(
|
||||
object sender, object oldDataContext, object newDataContext
|
||||
) {
|
||||
var oldViewModel = oldDataContext as INotifyPropertyChanged;
|
||||
if(oldViewModel != null) {
|
||||
oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate;
|
||||
}
|
||||
|
||||
var newViewModel = newDataContext as INotifyPropertyChanged;
|
||||
if(newViewModel != null) {
|
||||
newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate;
|
||||
InvalidateAllViewModelProperties();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Refreshes all properties from the view model</summary>
|
||||
protected void InvalidateAllViewModelProperties() {
|
||||
OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard);
|
||||
}
|
||||
|
||||
/// <summary>Called when a property of the view model is changed</summary>
|
||||
/// <param name="sender">View model in which a property was changed</param>
|
||||
/// <param name="arguments">Contains the name of the property that has changed</param>
|
||||
protected virtual void OnViewModelPropertyChanged(
|
||||
object sender, PropertyChangedEventArgs arguments
|
||||
) { }
|
||||
|
||||
/// <summary>Provides the data binding target for the view</summary>
|
||||
public object DataContext {
|
||||
get { return this.dataContext; }
|
||||
set {
|
||||
if(value != this.dataContext) {
|
||||
object oldDataContext = this.dataContext;
|
||||
this.dataContext = value;
|
||||
OnDataContextChanged(this, oldDataContext, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Active data binding target, can be null</summary>
|
||||
private object dataContext;
|
||||
/// <summary>Delegate for the OnViewModelPropertyChanged() method</summary>
|
||||
private PropertyChangedEventHandler onViewModelPropertyChangedDelegate;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Views
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Views {
|
||||
|
||||
/// <summary>
|
||||
/// Base class for MVVM user controls that act as views connected to a view model
|
||||
/// </summary>
|
||||
public class ViewControl : UserControl, IView {
|
||||
|
||||
/// <summary>Initializes a new view control</summary>
|
||||
public ViewControl() {
|
||||
this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>Called when the control's data context is changed</summary>
|
||||
/// <param name="sender">Control whose data context was changed</param>
|
||||
/// <param name="oldDataContext">Data context that was previously used</param>
|
||||
/// <param name="newDataContext">Data context that will be used from now on</param>
|
||||
protected virtual void OnDataContextChanged(
|
||||
object sender, object oldDataContext, object newDataContext
|
||||
) {
|
||||
var oldViewModel = oldDataContext as INotifyPropertyChanged;
|
||||
if(oldViewModel != null) {
|
||||
oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate;
|
||||
}
|
||||
|
||||
var newViewModel = newDataContext as INotifyPropertyChanged;
|
||||
if(newViewModel != null) {
|
||||
newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate;
|
||||
InvalidateAllViewModelProperties();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Refreshes all properties from the view model</summary>
|
||||
protected void InvalidateAllViewModelProperties() {
|
||||
OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard);
|
||||
}
|
||||
|
||||
/// <summary>Called when a property of the view model is changed</summary>
|
||||
/// <param name="sender">View model in which a property was changed</param>
|
||||
/// <param name="arguments">Contains the name of the property that has changed</param>
|
||||
protected virtual void OnViewModelPropertyChanged(
|
||||
object sender, PropertyChangedEventArgs arguments
|
||||
) { }
|
||||
|
||||
/// <summary>Provides the data binding target for the view</summary>
|
||||
public object DataContext {
|
||||
get { return this.dataContext; }
|
||||
set {
|
||||
if(value != this.dataContext) {
|
||||
object oldDataContext = this.dataContext;
|
||||
this.dataContext = value;
|
||||
OnDataContextChanged(this, oldDataContext, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Active data binding target, can be null</summary>
|
||||
private object dataContext;
|
||||
/// <summary>Delegate for the OnViewModelPropertyChanged() method</summary>
|
||||
private PropertyChangedEventHandler onViewModelPropertyChangedDelegate;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Views
|
||||
|
|
|
@ -1,89 +1,88 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Views {
|
||||
|
||||
/// <summary>
|
||||
/// Base class for MVVM windows that act as views connected to a view model
|
||||
/// </summary>
|
||||
public class ViewForm : Form, IView {
|
||||
|
||||
/// <summary>Initializes a new view control</summary>
|
||||
public ViewForm() {
|
||||
this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>Provides the data binding target for the view</summary>
|
||||
public object DataContext {
|
||||
get { return this.dataContext; }
|
||||
set {
|
||||
if(value != this.dataContext) {
|
||||
object oldDataContext = this.dataContext;
|
||||
this.dataContext = value;
|
||||
OnDataContextChanged(this, oldDataContext, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when the window's data context is changed</summary>
|
||||
/// <param name="sender">Window whose data context was changed</param>
|
||||
/// <param name="oldDataContext">Data context that was previously used</param>
|
||||
/// <param name="newDataContext">Data context that will be used from now on</param>
|
||||
protected virtual void OnDataContextChanged(
|
||||
object sender, object oldDataContext, object newDataContext
|
||||
) {
|
||||
var oldViewModel = oldDataContext as INotifyPropertyChanged;
|
||||
if(oldViewModel != null) {
|
||||
oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate;
|
||||
}
|
||||
|
||||
var newViewModel = newDataContext as INotifyPropertyChanged;
|
||||
if(newViewModel != null) {
|
||||
newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate;
|
||||
InvalidateAllViewModelProperties();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Refreshes all properties from the view model</summary>
|
||||
protected void InvalidateAllViewModelProperties() {
|
||||
OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard);
|
||||
}
|
||||
|
||||
/// <summary>Called when a property of the view model is changed</summary>
|
||||
/// <param name="sender">View model in which a property was changed</param>
|
||||
/// <param name="arguments">Contains the name of the property that has changed</param>
|
||||
protected virtual void OnViewModelPropertyChanged(
|
||||
object sender, PropertyChangedEventArgs arguments
|
||||
) { }
|
||||
|
||||
/// <summary>Active data binding target, can be null</summary>
|
||||
private object dataContext;
|
||||
/// <summary>Delegate for the OnViewModelPropertyChanged() method</summary>
|
||||
private PropertyChangedEventHandler onViewModelPropertyChangedDelegate;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Views
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
|
||||
namespace Nuclex.Windows.Forms.Views {
|
||||
|
||||
/// <summary>
|
||||
/// Base class for MVVM windows that act as views connected to a view model
|
||||
/// </summary>
|
||||
public class ViewForm : Form, IView {
|
||||
|
||||
/// <summary>Initializes a new view control</summary>
|
||||
public ViewForm() {
|
||||
this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>Provides the data binding target for the view</summary>
|
||||
public object DataContext {
|
||||
get { return this.dataContext; }
|
||||
set {
|
||||
if(value != this.dataContext) {
|
||||
object oldDataContext = this.dataContext;
|
||||
this.dataContext = value;
|
||||
OnDataContextChanged(this, oldDataContext, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when the window's data context is changed</summary>
|
||||
/// <param name="sender">Window whose data context was changed</param>
|
||||
/// <param name="oldDataContext">Data context that was previously used</param>
|
||||
/// <param name="newDataContext">Data context that will be used from now on</param>
|
||||
protected virtual void OnDataContextChanged(
|
||||
object sender, object oldDataContext, object newDataContext
|
||||
) {
|
||||
var oldViewModel = oldDataContext as INotifyPropertyChanged;
|
||||
if(oldViewModel != null) {
|
||||
oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate;
|
||||
}
|
||||
|
||||
var newViewModel = newDataContext as INotifyPropertyChanged;
|
||||
if(newViewModel != null) {
|
||||
newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate;
|
||||
InvalidateAllViewModelProperties();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Refreshes all properties from the view model</summary>
|
||||
protected void InvalidateAllViewModelProperties() {
|
||||
OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard);
|
||||
}
|
||||
|
||||
/// <summary>Called when a property of the view model is changed</summary>
|
||||
/// <param name="sender">View model in which a property was changed</param>
|
||||
/// <param name="arguments">Contains the name of the property that has changed</param>
|
||||
protected virtual void OnViewModelPropertyChanged(
|
||||
object sender, PropertyChangedEventArgs arguments
|
||||
) { }
|
||||
|
||||
/// <summary>Active data binding target, can be null</summary>
|
||||
private object dataContext;
|
||||
/// <summary>Delegate for the OnViewModelPropertyChanged() method</summary>
|
||||
private PropertyChangedEventHandler onViewModelPropertyChangedDelegate;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms.Views
|
||||
|
|
|
@ -1,41 +1,40 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Unit test for the window manager</summary>
|
||||
[TestFixture]
|
||||
public class WindowManagerTest {
|
||||
|
||||
/// <summary>Verifies that the window manager provides a default constructor</summary>
|
||||
[Test]
|
||||
public void HasDefaultConstructor() {
|
||||
Assert.DoesNotThrow(
|
||||
() => new WindowManager()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Unit test for the window manager</summary>
|
||||
[TestFixture]
|
||||
public class WindowManagerTest {
|
||||
|
||||
/// <summary>Verifies that the window manager provides a default constructor</summary>
|
||||
[Test]
|
||||
public void HasDefaultConstructor() {
|
||||
Assert.DoesNotThrow(
|
||||
() => new WindowManager()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
|
@ -1,442 +1,441 @@
|
|||
#region CPL License
|
||||
/*
|
||||
Nuclex Framework
|
||||
Copyright (C) 2002-2019 Nuclex Development Labs
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the IBM Common Public License as
|
||||
published by the IBM Corporation; either version 1.0 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
IBM Common Public License for more details.
|
||||
|
||||
You should have received a copy of the IBM Common Public
|
||||
License along with this library
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
using Nuclex.Windows.Forms.AutoBinding;
|
||||
using Nuclex.Windows.Forms.Views;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Manages an application's windows and views</summary>
|
||||
public class WindowManager : Observable, IWindowManager {
|
||||
|
||||
#region class CancellableDisposer
|
||||
|
||||
/// <summary>Disposes an object that potentially implements IDisposable</summary>
|
||||
private struct CancellableDisposer : IDisposable {
|
||||
|
||||
/// <summary>Initializes a new cancellable disposer</summary>
|
||||
/// <param name="potentiallyDisposable">
|
||||
/// Object that potentially implements IDisposable
|
||||
/// </param>
|
||||
public CancellableDisposer(object potentiallyDisposable = null) {
|
||||
this.potentiallyDisposable = potentiallyDisposable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the assigned object if the disposer has not been cancelled
|
||||
/// </summary>
|
||||
public void Dispose() {
|
||||
var disposable = this.potentiallyDisposable as IDisposable;
|
||||
if(disposable != null) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cancels the call to Dispose(), keeping the object alive</summary>
|
||||
public void Dismiss() {
|
||||
this.potentiallyDisposable = null;
|
||||
}
|
||||
|
||||
/// <summary>Assigns a new potentially disposable object</summary>
|
||||
/// <param name="potentiallyDisposable">
|
||||
/// Potentially disposable object that the disposer will dispose
|
||||
/// </param>
|
||||
public void Set(object potentiallyDisposable) {
|
||||
this.potentiallyDisposable = potentiallyDisposable;
|
||||
}
|
||||
|
||||
/// <summary>Object that will be disposed unless the disposer is cancelled</summary>
|
||||
private object potentiallyDisposable;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class CancellableDisposer
|
||||
|
||||
/// <summary>Initializes a new window manager</summary>
|
||||
/// <param name="autoBinder">
|
||||
/// View model binder that will be used to bind all created views to their models
|
||||
/// </param>
|
||||
public WindowManager(IAutoBinder autoBinder = null) {
|
||||
this.autoBinder = autoBinder;
|
||||
|
||||
this.rootWindowActivatedDelegate = rootWindowActivated;
|
||||
this.rootWindowClosedDelegate = rootWindowClosed;
|
||||
this.viewTypesForViewModels = new ConcurrentDictionary<Type, Type>();
|
||||
}
|
||||
|
||||
/// <summary>The currently active top-level or modal window</summary>
|
||||
public Form ActiveWindow {
|
||||
get { return this.activeWindow; }
|
||||
private set {
|
||||
if(value != this.activeWindow) {
|
||||
this.activeWindow = value;
|
||||
OnPropertyChanged(nameof(ActiveWindow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Opens a view as a new root window of the application</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model a root window will be opened for
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a window will be opened for. If null, the view model will be
|
||||
/// created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <param name="disposeOnClose">
|
||||
/// Whether the view model should be disposed when the view is closed
|
||||
/// </param>
|
||||
/// <returns>The window that has been opened by the window manager</returns>
|
||||
public Form OpenRoot<TViewModel>(
|
||||
TViewModel viewModel = null, bool disposeOnClose = true
|
||||
) where TViewModel : class {
|
||||
|
||||
Form window = (Form)CreateView(viewModel);
|
||||
window.Activated += this.rootWindowActivatedDelegate;
|
||||
window.Closed += this.rootWindowClosedDelegate;
|
||||
|
||||
// If we either created the view model or the user explicitly asked us to
|
||||
// dispose his view model, tag the window so that we know to dispose it
|
||||
// when we're done (but still allow the user to change his mind)
|
||||
if((viewModel == null) || disposeOnClose) {
|
||||
window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead?
|
||||
//window.SetValue(DisposeViewModelOnCloseProperty, true);
|
||||
}
|
||||
|
||||
window.Show();
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>Displays a view as a modal window</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of the view model for which a view will be displayed
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a modal window will be displayed for. If null, the view model will
|
||||
/// be created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <param name="disposeOnClose">
|
||||
/// Whether the view model should be disposed when the view is closed
|
||||
/// </param>
|
||||
/// <returns>The return value of the modal window</returns>
|
||||
public bool? ShowModal<TViewModel>(
|
||||
TViewModel viewModel = null, bool disposeOnClose = true
|
||||
) where TViewModel : class {
|
||||
Form window = (Form)CreateView(viewModel);
|
||||
window.Owner = this.activeWindow;
|
||||
window.Activated += this.rootWindowActivatedDelegate;
|
||||
|
||||
try {
|
||||
// If we either created the view model or the user explicitly asked us to
|
||||
// dispose his view model, tag the window so that we know to dispose it
|
||||
// when we're done (but still allow the user to change his mind)
|
||||
if((viewModel == null) || disposeOnClose) {
|
||||
window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead?
|
||||
//window.SetValue(DisposeViewModelOnCloseProperty, true);
|
||||
}
|
||||
|
||||
DialogResult result = window.ShowDialog(this.activeWindow);
|
||||
if((result == DialogResult.OK) || (result == DialogResult.Yes)) {
|
||||
return true;
|
||||
} else if((result == DialogResult.Cancel) || (result == DialogResult.No)) {
|
||||
return false;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
window.Activated -= this.rootWindowActivatedDelegate;
|
||||
ActiveWindow = window.Owner;
|
||||
|
||||
if(shouldDisposeViewModelOnClose(window)) {
|
||||
IView windowAsView = window as IView;
|
||||
if(windowAsView != null) {
|
||||
object viewModelAsObject = windowAsView.DataContext;
|
||||
windowAsView.DataContext = null;
|
||||
disposeIfDisposable(viewModelAsObject);
|
||||
}
|
||||
}
|
||||
disposeIfDisposable(window);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates the view for the specified view model</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model for which a view will be created
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a view will be created for. If null, the view model will be
|
||||
/// created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <returns>The view for the specified view model</returns>
|
||||
public virtual Control CreateView<TViewModel>(
|
||||
TViewModel viewModel = null
|
||||
) where TViewModel : class {
|
||||
Type viewType = LocateViewForViewModel(typeof(TViewModel));
|
||||
Control viewControl = (Control)CreateInstance(viewType);
|
||||
using(var viewDisposer = new CancellableDisposer(viewControl)) {
|
||||
|
||||
// Create a view model if none was provided, and in either case assign
|
||||
// the view model to the view (provided it implements IView).
|
||||
using(var viewModelDisposer = new CancellableDisposer()) {
|
||||
IView viewControlAsView = viewControl as IView;
|
||||
if(viewModel == null) { // No view model provided, create one
|
||||
if(viewControlAsView == null) { // View doesn't implement IView
|
||||
viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
|
||||
viewModelDisposer.Set(viewModel);
|
||||
} else if(viewControlAsView.DataContext == null) { // View has no view model
|
||||
viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
|
||||
viewModelDisposer.Set(viewModel);
|
||||
viewControlAsView.DataContext = viewModel;
|
||||
} else { // There's an existing view model
|
||||
viewModel = viewControlAsView.DataContext as TViewModel;
|
||||
if(viewModel == null) { // The existing view model is another type
|
||||
viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
|
||||
viewModelDisposer.Set(viewModel);
|
||||
viewControlAsView.DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
} else if(viewControlAsView != null) { // Caller has provided a view model
|
||||
viewControlAsView.DataContext = viewModel;
|
||||
}
|
||||
|
||||
// If an auto binder was provided, automatically bind the view to the view model
|
||||
if(this.autoBinder != null) {
|
||||
this.autoBinder.Bind(viewControl, viewModel);
|
||||
}
|
||||
|
||||
viewModelDisposer.Dismiss(); // Everything went well, we keep the view model
|
||||
}
|
||||
|
||||
viewDisposer.Dismiss(); // Everything went well, we keep the view
|
||||
|
||||
}
|
||||
|
||||
return viewControl;
|
||||
}
|
||||
|
||||
/// <summary>Creates a view model without a matching view</summary>
|
||||
/// <typeparam name="TViewModel">Type of view model that will be created</typeparam>
|
||||
/// <returns>The new view model</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is useful if a view model needs to create child view models (i.e. paged container
|
||||
/// and wants to ensure the same dependency injector (if any) if used as the window
|
||||
/// manager uses for other view models it creates.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This way, view models can set up their child view models without having to immediately
|
||||
/// bind a view to them. Later on, views can use the window manager to create a matching
|
||||
/// child view and store it in a container.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public TViewModel CreateViewModel<TViewModel>() where TViewModel : class {
|
||||
return (TViewModel)CreateInstance(typeof(TViewModel));
|
||||
}
|
||||
|
||||
/// <summary>Locates the view that will be used to a view model</summary>
|
||||
/// <param name="viewModelType">
|
||||
/// Type of view model for which the view will be located
|
||||
/// </param>
|
||||
/// <returns>The type of view that should be used for the specified view model</returns>
|
||||
protected virtual Type LocateViewForViewModel(Type viewModelType) {
|
||||
Type viewType;
|
||||
if(!this.viewTypesForViewModels.TryGetValue(viewModelType, out viewType)) {
|
||||
string viewName = viewModelType.Name;
|
||||
if(viewName.EndsWith("ViewModel")) {
|
||||
viewName = viewName.Substring(0, viewName.Length - 9);
|
||||
}
|
||||
|
||||
Type[] exportedTypes = viewModelType.Assembly.GetExportedTypes();
|
||||
Type[] namespaceTypes = filterTypesByNamespace(exportedTypes, viewModelType.Namespace);
|
||||
|
||||
// First, search the own namespace (because if two identical view models exist in
|
||||
// different namespaces, the one in the same namespace is most likely the desired one)
|
||||
viewType = findBestMatch(
|
||||
namespaceTypes,
|
||||
viewName + "View",
|
||||
viewName + "Page",
|
||||
viewName + "Form",
|
||||
viewName + "Window",
|
||||
viewName + "Dialog",
|
||||
viewName + "Control"
|
||||
);
|
||||
|
||||
// If the view model doesn't exist in the same namespace, expand the search to
|
||||
// the entire assembly the view is in.
|
||||
if(viewType == null) {
|
||||
viewType = findBestMatch(
|
||||
exportedTypes,
|
||||
viewName + "View",
|
||||
viewName + "Page",
|
||||
viewName + "Form",
|
||||
viewName + "Window",
|
||||
viewName + "Dialog",
|
||||
viewName + "Control"
|
||||
);
|
||||
}
|
||||
|
||||
// Still no view found? We give up!
|
||||
if(viewType == null) {
|
||||
throw new InvalidOperationException(
|
||||
string.Format("Could not locate view for view model '{0}'", viewModelType.Name)
|
||||
);
|
||||
}
|
||||
|
||||
this.viewTypesForViewModels.TryAdd(viewModelType, viewType);
|
||||
}
|
||||
|
||||
return viewType;
|
||||
}
|
||||
|
||||
/// <summary>Creates an instance of the specified type</summary>
|
||||
/// <param name="type">Type an instance will be created of</param>
|
||||
/// <returns>The created instance</returns>
|
||||
/// <remarks>
|
||||
/// Use this to wire up your dependency injection container. By default,
|
||||
/// the Activator class will be used to create instances which only works
|
||||
/// if all of your view models are concrete classes.
|
||||
/// </remarks>
|
||||
protected virtual object CreateInstance(Type type) {
|
||||
return Activator.CreateInstance(type);
|
||||
}
|
||||
|
||||
/// <summary>Called when one of the application's root windows is closed</summary>
|
||||
/// <param name="sender">Window that has been closed</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void rootWindowClosed(object sender, EventArgs arguments) {
|
||||
Form closedWindow = (Form)sender;
|
||||
closedWindow.Closed -= this.rootWindowClosedDelegate;
|
||||
closedWindow.Activated -= this.rootWindowActivatedDelegate;
|
||||
|
||||
// If the view model was created just for this view or if the user asked us
|
||||
// to dispose of his view model, do so now.
|
||||
if(shouldDisposeViewModelOnClose(closedWindow)) {
|
||||
IView windowAsView = closedWindow as IView;
|
||||
if(windowAsView != null) {
|
||||
object viewModelAsObject = windowAsView.DataContext;
|
||||
windowAsView.DataContext = null;
|
||||
disposeIfDisposable(viewModelAsObject);
|
||||
}
|
||||
}
|
||||
|
||||
lock(this) {
|
||||
ActiveWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when one of the application's root windows is activated</summary>
|
||||
/// <param name="sender">Window that has been put in the foreground</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void rootWindowActivated(object sender, EventArgs arguments) {
|
||||
lock(this) {
|
||||
ActiveWindow = (Form)sender;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tries to find the best match for a named type in a list of types</summary>
|
||||
/// <param name="types">List of types the search will take place in</param>
|
||||
/// <param name="typeNames">
|
||||
/// The candidates the method will look for, starting with the best match
|
||||
/// </param>
|
||||
/// <returns>The best match in the list of types, if any match was found</returns>
|
||||
private static Type findBestMatch(Type[] types, params string[] typeNames) {
|
||||
int bestMatchFound = typeNames.Length;
|
||||
|
||||
Type type = null;
|
||||
for(int index = 0; index < types.Length; ++index) {
|
||||
for(int nameIndex = 0; nameIndex < bestMatchFound; ++nameIndex) {
|
||||
if(types[index].Name == typeNames[nameIndex]) {
|
||||
bestMatchFound = nameIndex;
|
||||
type = types[index];
|
||||
|
||||
if(bestMatchFound == 0) { // There can be no better match
|
||||
return type;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/// <summary>Disposes the specified object if it implements IDisposable</summary>
|
||||
/// <typeparam name="T">Type of object that will disposed if possible</typeparam>
|
||||
/// <param name="instance">Object that the method will attempt to dispose</param>
|
||||
private static void disposeIfDisposable<T>(T instance) where T : class {
|
||||
var disposable = instance as IDisposable;
|
||||
if(disposable != null) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Determines if the view owns the view model</summary>
|
||||
/// <param name="view">View that will be checked for ownership</param>
|
||||
/// <returns>True if the view owns the view model</returns>
|
||||
private static bool shouldDisposeViewModelOnClose(Control view) {
|
||||
string tagAsString = view.Tag as string;
|
||||
if(tagAsString != null) {
|
||||
return tagAsString.Contains("DisposeViewModelOnClose");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Filters a list of types to contain only those in a specific namespace</summary>
|
||||
/// <param name="exportedTypes">List of exported types that will be filtered</param>
|
||||
/// <param name="filteredNamespace">
|
||||
/// Namespace the types in the filtered list will be in
|
||||
/// </param>
|
||||
/// <returns>A subset of the specified types that are in the provided namespace</returns>
|
||||
private static Type[] filterTypesByNamespace(Type[] exportedTypes, string filteredNamespace) {
|
||||
var filteredTypes = new List<Type>(exportedTypes.Length / 2);
|
||||
for(int index = 0; index < exportedTypes.Length; ++index) {
|
||||
Type exportedType = exportedTypes[index];
|
||||
if(exportedType.Namespace == filteredNamespace) {
|
||||
filteredTypes.Add(exportedType);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTypes.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>The application's currently active root window</summary>
|
||||
private Form activeWindow;
|
||||
/// <summary>Invoked when a root window is put in the foreground</summary>
|
||||
private EventHandler rootWindowActivatedDelegate;
|
||||
/// <summary>Invoked when a root window has been closed</summary>
|
||||
private EventHandler rootWindowClosedDelegate;
|
||||
/// <summary>View model binder that will be used on all created views</summary>
|
||||
private IAutoBinder autoBinder;
|
||||
/// <summary>Caches the view types to use for a view model</summary>
|
||||
private ConcurrentDictionary<Type, Type> viewTypesForViewModels;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
|
||||
using Nuclex.Support;
|
||||
using Nuclex.Windows.Forms.AutoBinding;
|
||||
using Nuclex.Windows.Forms.Views;
|
||||
|
||||
namespace Nuclex.Windows.Forms {
|
||||
|
||||
/// <summary>Manages an application's windows and views</summary>
|
||||
public class WindowManager : Observable, IWindowManager {
|
||||
|
||||
#region class CancellableDisposer
|
||||
|
||||
/// <summary>Disposes an object that potentially implements IDisposable</summary>
|
||||
private struct CancellableDisposer : IDisposable {
|
||||
|
||||
/// <summary>Initializes a new cancellable disposer</summary>
|
||||
/// <param name="potentiallyDisposable">
|
||||
/// Object that potentially implements IDisposable
|
||||
/// </param>
|
||||
public CancellableDisposer(object potentiallyDisposable = null) {
|
||||
this.potentiallyDisposable = potentiallyDisposable;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the assigned object if the disposer has not been cancelled
|
||||
/// </summary>
|
||||
public void Dispose() {
|
||||
var disposable = this.potentiallyDisposable as IDisposable;
|
||||
if(disposable != null) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cancels the call to Dispose(), keeping the object alive</summary>
|
||||
public void Dismiss() {
|
||||
this.potentiallyDisposable = null;
|
||||
}
|
||||
|
||||
/// <summary>Assigns a new potentially disposable object</summary>
|
||||
/// <param name="potentiallyDisposable">
|
||||
/// Potentially disposable object that the disposer will dispose
|
||||
/// </param>
|
||||
public void Set(object potentiallyDisposable) {
|
||||
this.potentiallyDisposable = potentiallyDisposable;
|
||||
}
|
||||
|
||||
/// <summary>Object that will be disposed unless the disposer is cancelled</summary>
|
||||
private object potentiallyDisposable;
|
||||
|
||||
}
|
||||
|
||||
#endregion // class CancellableDisposer
|
||||
|
||||
/// <summary>Initializes a new window manager</summary>
|
||||
/// <param name="autoBinder">
|
||||
/// View model binder that will be used to bind all created views to their models
|
||||
/// </param>
|
||||
public WindowManager(IAutoBinder autoBinder = null) {
|
||||
this.autoBinder = autoBinder;
|
||||
|
||||
this.rootWindowActivatedDelegate = rootWindowActivated;
|
||||
this.rootWindowClosedDelegate = rootWindowClosed;
|
||||
this.viewTypesForViewModels = new ConcurrentDictionary<Type, Type>();
|
||||
}
|
||||
|
||||
/// <summary>The currently active top-level or modal window</summary>
|
||||
public Form ActiveWindow {
|
||||
get { return this.activeWindow; }
|
||||
private set {
|
||||
if(value != this.activeWindow) {
|
||||
this.activeWindow = value;
|
||||
OnPropertyChanged(nameof(ActiveWindow));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Opens a view as a new root window of the application</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model a root window will be opened for
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a window will be opened for. If null, the view model will be
|
||||
/// created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <param name="disposeOnClose">
|
||||
/// Whether the view model should be disposed when the view is closed
|
||||
/// </param>
|
||||
/// <returns>The window that has been opened by the window manager</returns>
|
||||
public Form OpenRoot<TViewModel>(
|
||||
TViewModel viewModel = null, bool disposeOnClose = true
|
||||
) where TViewModel : class {
|
||||
|
||||
Form window = (Form)CreateView(viewModel);
|
||||
window.Activated += this.rootWindowActivatedDelegate;
|
||||
window.Closed += this.rootWindowClosedDelegate;
|
||||
|
||||
// If we either created the view model or the user explicitly asked us to
|
||||
// dispose his view model, tag the window so that we know to dispose it
|
||||
// when we're done (but still allow the user to change his mind)
|
||||
if((viewModel == null) || disposeOnClose) {
|
||||
window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead?
|
||||
//window.SetValue(DisposeViewModelOnCloseProperty, true);
|
||||
}
|
||||
|
||||
window.Show();
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/// <summary>Displays a view as a modal window</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of the view model for which a view will be displayed
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a modal window will be displayed for. If null, the view model will
|
||||
/// be created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <param name="disposeOnClose">
|
||||
/// Whether the view model should be disposed when the view is closed
|
||||
/// </param>
|
||||
/// <returns>The return value of the modal window</returns>
|
||||
public bool? ShowModal<TViewModel>(
|
||||
TViewModel viewModel = null, bool disposeOnClose = true
|
||||
) where TViewModel : class {
|
||||
Form window = (Form)CreateView(viewModel);
|
||||
window.Owner = this.activeWindow;
|
||||
window.Activated += this.rootWindowActivatedDelegate;
|
||||
|
||||
try {
|
||||
// If we either created the view model or the user explicitly asked us to
|
||||
// dispose his view model, tag the window so that we know to dispose it
|
||||
// when we're done (but still allow the user to change his mind)
|
||||
if((viewModel == null) || disposeOnClose) {
|
||||
window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead?
|
||||
//window.SetValue(DisposeViewModelOnCloseProperty, true);
|
||||
}
|
||||
|
||||
DialogResult result = window.ShowDialog(this.activeWindow);
|
||||
if((result == DialogResult.OK) || (result == DialogResult.Yes)) {
|
||||
return true;
|
||||
} else if((result == DialogResult.Cancel) || (result == DialogResult.No)) {
|
||||
return false;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
window.Activated -= this.rootWindowActivatedDelegate;
|
||||
ActiveWindow = window.Owner;
|
||||
|
||||
if(shouldDisposeViewModelOnClose(window)) {
|
||||
IView windowAsView = window as IView;
|
||||
if(windowAsView != null) {
|
||||
object viewModelAsObject = windowAsView.DataContext;
|
||||
windowAsView.DataContext = null;
|
||||
disposeIfDisposable(viewModelAsObject);
|
||||
}
|
||||
}
|
||||
disposeIfDisposable(window);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates the view for the specified view model</summary>
|
||||
/// <typeparam name="TViewModel">
|
||||
/// Type of view model for which a view will be created
|
||||
/// </typeparam>
|
||||
/// <param name="viewModel">
|
||||
/// View model a view will be created for. If null, the view model will be
|
||||
/// created as well (unless the dialog already specifies one as a resource)
|
||||
/// </param>
|
||||
/// <returns>The view for the specified view model</returns>
|
||||
public virtual Control CreateView<TViewModel>(
|
||||
TViewModel viewModel = null
|
||||
) where TViewModel : class {
|
||||
Type viewType = LocateViewForViewModel(typeof(TViewModel));
|
||||
Control viewControl = (Control)CreateInstance(viewType);
|
||||
using(var viewDisposer = new CancellableDisposer(viewControl)) {
|
||||
|
||||
// Create a view model if none was provided, and in either case assign
|
||||
// the view model to the view (provided it implements IView).
|
||||
using(var viewModelDisposer = new CancellableDisposer()) {
|
||||
IView viewControlAsView = viewControl as IView;
|
||||
if(viewModel == null) { // No view model provided, create one
|
||||
if(viewControlAsView == null) { // View doesn't implement IView
|
||||
viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
|
||||
viewModelDisposer.Set(viewModel);
|
||||
} else if(viewControlAsView.DataContext == null) { // View has no view model
|
||||
viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
|
||||
viewModelDisposer.Set(viewModel);
|
||||
viewControlAsView.DataContext = viewModel;
|
||||
} else { // There's an existing view model
|
||||
viewModel = viewControlAsView.DataContext as TViewModel;
|
||||
if(viewModel == null) { // The existing view model is another type
|
||||
viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
|
||||
viewModelDisposer.Set(viewModel);
|
||||
viewControlAsView.DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
} else if(viewControlAsView != null) { // Caller has provided a view model
|
||||
viewControlAsView.DataContext = viewModel;
|
||||
}
|
||||
|
||||
// If an auto binder was provided, automatically bind the view to the view model
|
||||
if(this.autoBinder != null) {
|
||||
this.autoBinder.Bind(viewControl, viewModel);
|
||||
}
|
||||
|
||||
viewModelDisposer.Dismiss(); // Everything went well, we keep the view model
|
||||
}
|
||||
|
||||
viewDisposer.Dismiss(); // Everything went well, we keep the view
|
||||
|
||||
}
|
||||
|
||||
return viewControl;
|
||||
}
|
||||
|
||||
/// <summary>Creates a view model without a matching view</summary>
|
||||
/// <typeparam name="TViewModel">Type of view model that will be created</typeparam>
|
||||
/// <returns>The new view model</returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is useful if a view model needs to create child view models (i.e. paged container
|
||||
/// and wants to ensure the same dependency injector (if any) if used as the window
|
||||
/// manager uses for other view models it creates.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This way, view models can set up their child view models without having to immediately
|
||||
/// bind a view to them. Later on, views can use the window manager to create a matching
|
||||
/// child view and store it in a container.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public TViewModel CreateViewModel<TViewModel>() where TViewModel : class {
|
||||
return (TViewModel)CreateInstance(typeof(TViewModel));
|
||||
}
|
||||
|
||||
/// <summary>Locates the view that will be used to a view model</summary>
|
||||
/// <param name="viewModelType">
|
||||
/// Type of view model for which the view will be located
|
||||
/// </param>
|
||||
/// <returns>The type of view that should be used for the specified view model</returns>
|
||||
protected virtual Type LocateViewForViewModel(Type viewModelType) {
|
||||
Type viewType;
|
||||
if(!this.viewTypesForViewModels.TryGetValue(viewModelType, out viewType)) {
|
||||
string viewName = viewModelType.Name;
|
||||
if(viewName.EndsWith("ViewModel")) {
|
||||
viewName = viewName.Substring(0, viewName.Length - 9);
|
||||
}
|
||||
|
||||
Type[] exportedTypes = viewModelType.Assembly.GetExportedTypes();
|
||||
Type[] namespaceTypes = filterTypesByNamespace(exportedTypes, viewModelType.Namespace);
|
||||
|
||||
// First, search the own namespace (because if two identical view models exist in
|
||||
// different namespaces, the one in the same namespace is most likely the desired one)
|
||||
viewType = findBestMatch(
|
||||
namespaceTypes,
|
||||
viewName + "View",
|
||||
viewName + "Page",
|
||||
viewName + "Form",
|
||||
viewName + "Window",
|
||||
viewName + "Dialog",
|
||||
viewName + "Control"
|
||||
);
|
||||
|
||||
// If the view model doesn't exist in the same namespace, expand the search to
|
||||
// the entire assembly the view is in.
|
||||
if(viewType == null) {
|
||||
viewType = findBestMatch(
|
||||
exportedTypes,
|
||||
viewName + "View",
|
||||
viewName + "Page",
|
||||
viewName + "Form",
|
||||
viewName + "Window",
|
||||
viewName + "Dialog",
|
||||
viewName + "Control"
|
||||
);
|
||||
}
|
||||
|
||||
// Still no view found? We give up!
|
||||
if(viewType == null) {
|
||||
throw new InvalidOperationException(
|
||||
string.Format("Could not locate view for view model '{0}'", viewModelType.Name)
|
||||
);
|
||||
}
|
||||
|
||||
this.viewTypesForViewModels.TryAdd(viewModelType, viewType);
|
||||
}
|
||||
|
||||
return viewType;
|
||||
}
|
||||
|
||||
/// <summary>Creates an instance of the specified type</summary>
|
||||
/// <param name="type">Type an instance will be created of</param>
|
||||
/// <returns>The created instance</returns>
|
||||
/// <remarks>
|
||||
/// Use this to wire up your dependency injection container. By default,
|
||||
/// the Activator class will be used to create instances which only works
|
||||
/// if all of your view models are concrete classes.
|
||||
/// </remarks>
|
||||
protected virtual object CreateInstance(Type type) {
|
||||
return Activator.CreateInstance(type);
|
||||
}
|
||||
|
||||
/// <summary>Called when one of the application's root windows is closed</summary>
|
||||
/// <param name="sender">Window that has been closed</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void rootWindowClosed(object sender, EventArgs arguments) {
|
||||
Form closedWindow = (Form)sender;
|
||||
closedWindow.Closed -= this.rootWindowClosedDelegate;
|
||||
closedWindow.Activated -= this.rootWindowActivatedDelegate;
|
||||
|
||||
// If the view model was created just for this view or if the user asked us
|
||||
// to dispose of his view model, do so now.
|
||||
if(shouldDisposeViewModelOnClose(closedWindow)) {
|
||||
IView windowAsView = closedWindow as IView;
|
||||
if(windowAsView != null) {
|
||||
object viewModelAsObject = windowAsView.DataContext;
|
||||
windowAsView.DataContext = null;
|
||||
disposeIfDisposable(viewModelAsObject);
|
||||
}
|
||||
}
|
||||
|
||||
lock(this) {
|
||||
ActiveWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called when one of the application's root windows is activated</summary>
|
||||
/// <param name="sender">Window that has been put in the foreground</param>
|
||||
/// <param name="arguments">Not used</param>
|
||||
private void rootWindowActivated(object sender, EventArgs arguments) {
|
||||
lock(this) {
|
||||
ActiveWindow = (Form)sender;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tries to find the best match for a named type in a list of types</summary>
|
||||
/// <param name="types">List of types the search will take place in</param>
|
||||
/// <param name="typeNames">
|
||||
/// The candidates the method will look for, starting with the best match
|
||||
/// </param>
|
||||
/// <returns>The best match in the list of types, if any match was found</returns>
|
||||
private static Type findBestMatch(Type[] types, params string[] typeNames) {
|
||||
int bestMatchFound = typeNames.Length;
|
||||
|
||||
Type type = null;
|
||||
for(int index = 0; index < types.Length; ++index) {
|
||||
for(int nameIndex = 0; nameIndex < bestMatchFound; ++nameIndex) {
|
||||
if(types[index].Name == typeNames[nameIndex]) {
|
||||
bestMatchFound = nameIndex;
|
||||
type = types[index];
|
||||
|
||||
if(bestMatchFound == 0) { // There can be no better match
|
||||
return type;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/// <summary>Disposes the specified object if it implements IDisposable</summary>
|
||||
/// <typeparam name="T">Type of object that will disposed if possible</typeparam>
|
||||
/// <param name="instance">Object that the method will attempt to dispose</param>
|
||||
private static void disposeIfDisposable<T>(T instance) where T : class {
|
||||
var disposable = instance as IDisposable;
|
||||
if(disposable != null) {
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Determines if the view owns the view model</summary>
|
||||
/// <param name="view">View that will be checked for ownership</param>
|
||||
/// <returns>True if the view owns the view model</returns>
|
||||
private static bool shouldDisposeViewModelOnClose(Control view) {
|
||||
string tagAsString = view.Tag as string;
|
||||
if(tagAsString != null) {
|
||||
return tagAsString.Contains("DisposeViewModelOnClose");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Filters a list of types to contain only those in a specific namespace</summary>
|
||||
/// <param name="exportedTypes">List of exported types that will be filtered</param>
|
||||
/// <param name="filteredNamespace">
|
||||
/// Namespace the types in the filtered list will be in
|
||||
/// </param>
|
||||
/// <returns>A subset of the specified types that are in the provided namespace</returns>
|
||||
private static Type[] filterTypesByNamespace(Type[] exportedTypes, string filteredNamespace) {
|
||||
var filteredTypes = new List<Type>(exportedTypes.Length / 2);
|
||||
for(int index = 0; index < exportedTypes.Length; ++index) {
|
||||
Type exportedType = exportedTypes[index];
|
||||
if(exportedType.Namespace == filteredNamespace) {
|
||||
filteredTypes.Add(exportedType);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTypes.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>The application's currently active root window</summary>
|
||||
private Form activeWindow;
|
||||
/// <summary>Invoked when a root window is put in the foreground</summary>
|
||||
private EventHandler rootWindowActivatedDelegate;
|
||||
/// <summary>Invoked when a root window has been closed</summary>
|
||||
private EventHandler rootWindowClosedDelegate;
|
||||
/// <summary>View model binder that will be used on all created views</summary>
|
||||
private IAutoBinder autoBinder;
|
||||
/// <summary>Caches the view types to use for a view model</summary>
|
||||
private ConcurrentDictionary<Type, Type> viewTypesForViewModels;
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Windows.Forms
|
||||
|
|
Loading…
Reference in New Issue
Block a user