Changed license to Apache License 2.0

This commit is contained in:
cygon 2024-06-14 16:42:33 +02:00
parent 857917aad5
commit 1bb2363a07
47 changed files with 5942 additions and 5849 deletions

View File

@ -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")]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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 &lt; 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 &lt; 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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