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; #region Apache License 2.0
using System.Runtime.CompilerServices; /*
using System.Runtime.InteropServices; Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information Licensed under the Apache License, Version 2.0 (the "License");
// associated with an assembly. you may not use this file except in compliance with the License.
[assembly: AssemblyTitle("Nuclex.Windows.Forms")] You may obtain a copy of the License at
[assembly: AssemblyProduct("Nuclex.Windows.Forms")]
[assembly: AssemblyDescription("Lean and elegant MVVM library with extras for WinForms")] http://www.apache.org/licenses/LICENSE-2.0
[assembly: AssemblyCompany("Nuclex Development Labs")]
[assembly: AssemblyCopyright("Copyright © Nuclex Development Labs 2019")] Unless required by applicable law or agreed to in writing, software
[assembly: AssemblyTrademark("")] distributed under the License is distributed on an "AS IS" BASIS,
[assembly: AssemblyCulture("")] WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
// Setting ComVisible to false makes the types in this assembly not visible limitations under the License.
// to COM components. If you need to access a type in this assembly from */
// COM, set the ComVisible attribute to true on that type. #endregion // Apache License 2.0
[assembly: ComVisible(false)]
using System.Reflection;
// The following GUID is for the ID of the typelib if this project is exposed to COM using System.Runtime.CompilerServices;
[assembly: Guid("08254ad9-394e-4638-8412-098c8c4a4c39")] using System.Runtime.InteropServices;
// Version information for an assembly consists of the following four values: // General Information about an assembly is controlled through the following
// // set of attributes. Change these attribute values to modify the information
// Major Version // associated with an assembly.
// Minor Version [assembly: AssemblyTitle("Nuclex.Windows.Forms")]
// Build Number [assembly: AssemblyProduct("Nuclex.Windows.Forms")]
// Revision [assembly: AssemblyDescription("Lean and elegant MVVM library with extras for WinForms")]
// [assembly: AssemblyCompany("Nuclex Development Labs")]
[assembly: AssemblyVersion("1.0.0.0")] [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 #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
partial class AsyncProgressBar {
partial class AsyncProgressBar {
/// <summary>Required designer variable.</summary>
/// <summary>Required designer variable.</summary> private System.ComponentModel.IContainer components = null;
private System.ComponentModel.IContainer components = null;
/// <summary>Clean up any resources being used.</summary>
/// <summary>Clean up any resources being used.</summary> /// <param name="disposing">
/// <param name="disposing"> /// true if managed resources should be disposed; otherwise, false.
/// true if managed resources should be disposed; otherwise, false. /// </param>
/// </param> protected override void Dispose(bool disposing) {
protected override void Dispose(bool disposing) { if(disposing && (components != null)) {
if(disposing && (components != null)) { components.Dispose();
components.Dispose(); }
}
base.Dispose(disposing);
base.Dispose(disposing); }
}
#region Component Designer generated code
#region Component Designer generated code
/// <summary>
/// <summary> /// Required method for Designer support - do not modify
/// Required method for Designer support - do not modify /// the contents of this method with the code editor.
/// the contents of this method with the code editor. /// </summary>
/// </summary> private void InitializeComponent() {
private void InitializeComponent() { components = new System.ComponentModel.Container();
components = new System.ComponentModel.Container(); }
}
#endregion
#endregion }
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,71 +1,70 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
#if UNITTEST
#if UNITTEST
using System;
using System; using System.IO;
using System.IO; using System.Windows.Forms;
using System.Windows.Forms;
using NUnit.Framework;
using NUnit.Framework;
using Nuclex.Support;
using Nuclex.Support;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Unit Test for the asynchronously updating progress bar</summary>
/// <summary>Unit Test for the asynchronously updating progress bar</summary> [TestFixture, Explicit]
[TestFixture, Explicit] public class AsyncProgressBarTest {
public class AsyncProgressBarTest {
/// <summary>
/// <summary> /// Verifies that asynchronous progress assignment is working
/// Verifies that asynchronous progress assignment is working /// </summary>
/// </summary> [Test]
[Test] public void TestProgressAssignment() {
public void TestProgressAssignment() { using(AsyncProgressBar progressBar = new AsyncProgressBar()) {
using(AsyncProgressBar progressBar = new AsyncProgressBar()) {
// Let the control create its window handle
// Let the control create its window handle progressBar.CreateControl();
progressBar.CreateControl(); progressBar.Minimum = 0;
progressBar.Minimum = 0; progressBar.Maximum = 100;
progressBar.Maximum = 100;
Assert.AreEqual(0, progressBar.Value);
Assert.AreEqual(0, progressBar.Value);
// Assign the new value. This will be done asynchronously, so we call
// Assign the new value. This will be done asynchronously, so we call // Application.DoEvents() to execute the message pump once, guaranteeing
// Application.DoEvents() to execute the message pump once, guaranteeing // that the call will have been executed after Application.DoEvents() returns.
// that the call will have been executed after Application.DoEvents() returns. progressBar.AsyncSetValue(0.33f);
progressBar.AsyncSetValue(0.33f); Application.DoEvents();
Application.DoEvents();
Assert.AreEqual(33, progressBar.Value);
Assert.AreEqual(33, progressBar.Value);
progressBar.AsyncSetValue(0.66f);
progressBar.AsyncSetValue(0.66f); Application.DoEvents();
Application.DoEvents();
Assert.AreEqual(66, progressBar.Value);
Assert.AreEqual(66, progressBar.Value);
}
} }
}
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms
#endif // UNITTEST
#endif // UNITTEST

View File

@ -1,140 +1,139 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Threading;
using System.Threading; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Progress bar with optimized multi-threading behavior</summary>
/// <summary>Progress bar with optimized multi-threading behavior</summary> /// <remarks>
/// <remarks> /// <para>
/// <para> /// If a background thread is generating lots of progress updates, using synchronized
/// If a background thread is generating lots of progress updates, using synchronized /// calls can drastically reduce performance. This progress bar optimizes that case
/// calls can drastically reduce performance. This progress bar optimizes that case /// by performing the update asynchronously and keeping only the most recent update
/// by performing the update asynchronously and keeping only the most recent update /// when multiple updates arrive while the asynchronous update call is still running.
/// when multiple updates arrive while the asynchronous update call is still running. /// </para>
/// </para> /// <para>
/// <para> /// This design eliminates useless queueing of progress updates, thereby reducing
/// 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
/// CPU load occuring in the UI thread and at the same time avoids blocking the /// worker thread, increasing its performance.
/// worker thread, increasing its performance. /// </para>
/// </para> /// </remarks>
/// </remarks> public partial class AsyncProgressBar : ProgressBar {
public partial class AsyncProgressBar : ProgressBar {
/// <summary>Initializes a new asynchronous progress bar</summary>
/// <summary>Initializes a new asynchronous progress bar</summary> public AsyncProgressBar() {
public AsyncProgressBar() { InitializeComponent();
InitializeComponent();
this.Disposed += new EventHandler(progressBarDisposed);
this.Disposed += new EventHandler(progressBarDisposed); this.updateProgressDelegate = new MethodInvoker(updateProgress);
this.updateProgressDelegate = new MethodInvoker(updateProgress);
// Could probably use VolatileWrite() as well, but for consistency reasons
// Could probably use VolatileWrite() as well, but for consistency reasons // this is an Interlocked call, too. Mixing different synchronization measures
// this is an Interlocked call, too. Mixing different synchronization measures // for a variable raises a red flag whenever I see it :)
// for a variable raises a red flag whenever I see it :) Interlocked.Exchange(ref this.newProgress, -1.0f);
Interlocked.Exchange(ref this.newProgress, -1.0f); }
}
/// <summary>Called when the progress bar is being disposed</summary>
/// <summary>Called when the progress bar is being disposed</summary> /// <param name="sender">Progress bar that is being disposed</param>
/// <param name="sender">Progress bar that is being disposed</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void progressBarDisposed(object sender, EventArgs arguments) {
private void progressBarDisposed(object sender, EventArgs arguments) {
// CHECK: This method is only called on an explicit Dispose() of the control.
// CHECK: This method is only called on an explicit Dispose() of the control. // It is legal to call Control.BeginInvoke() without calling Control.EndInvoke(),
// 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?
// 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
// 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()
// 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
// 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
// have EndInvoke() called on it yet, so we do this here before the control // is finally disposed.
// is finally disposed. if(this.progressUpdateAsyncResult != null) {
if(this.progressUpdateAsyncResult != null) { EndInvoke(this.progressUpdateAsyncResult);
EndInvoke(this.progressUpdateAsyncResult); this.progressUpdateAsyncResult = null;
this.progressUpdateAsyncResult = null; }
}
}
}
/// <summary>Asynchronously updates the value to be shown in the progress bar</summary>
/// <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>
/// <param name="value">New value to set the progress bar to</param> /// <remarks>
/// <remarks> /// This will schedule an asynchronous update of the progress bar in the UI thread.
/// 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
/// 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
/// 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
/// 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
/// 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.
/// the last most progress value set will be shown and never skipped. /// </remarks>
/// </remarks> public void AsyncSetValue(float value) {
public void AsyncSetValue(float value) {
// Update the value to be shown on the progress bar. If this happens multiple
// 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
// 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.
// and always tries to show the most recent value assigned. float oldValue = Interlocked.Exchange(ref this.newProgress, value);
float oldValue = Interlocked.Exchange(ref this.newProgress, value);
// If the previous value was -1, the UI thread has already taken out the most recent
// 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.
// 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
// 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
// begin the next update - since we know that the value the UI thread has extracted // is no longer the most recent one.
// is no longer the most recent one. if(oldValue == -1.0f) {
if(oldValue == -1.0f) { if(this.progressUpdateAsyncResult != null) {
if(this.progressUpdateAsyncResult != null) { EndInvoke(this.progressUpdateAsyncResult);
EndInvoke(this.progressUpdateAsyncResult); }
}
this.progressUpdateAsyncResult = BeginInvoke(this.updateProgressDelegate);
this.progressUpdateAsyncResult = BeginInvoke(this.updateProgressDelegate); }
}
}
}
/// <summary>Synchronously updates the value visualized in the progress bar</summary>
/// <summary>Synchronously updates the value visualized in the progress bar</summary> private void updateProgress() {
private void updateProgress() {
// Cache these to shorten the code that follows :)
// Cache these to shorten the code that follows :) int minimum = base.Minimum;
int minimum = base.Minimum; int maximum = base.Maximum;
int maximum = base.Maximum;
// Take out the most recent value that has been given to the asynchronous progress
// 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
// 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
// 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.
// invocation to ensure the most recent value will remain at the end. float progress = Interlocked.Exchange(ref this.newProgress, -1.0f);
float progress = Interlocked.Exchange(ref this.newProgress, -1.0f);
// Restrain the value to the progress bar's configured range and assign it.
// 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
// 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
// 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())
// this method, so we cannot validate the value in AsyncSetValue()) int value = (int)(progress * (maximum - minimum)) + minimum;
int value = (int)(progress * (maximum - minimum)) + minimum; base.Value = Math.Min(Math.Max(value, minimum), maximum);
base.Value = Math.Min(Math.Max(value, minimum), maximum);
}
}
/// <summary>New progress being assigned to the progress bar</summary>
/// <summary>New progress being assigned to the progress bar</summary> private float newProgress;
private float newProgress; /// <summary>Delegate for the progress update method</summary>
/// <summary>Delegate for the progress update method</summary> private MethodInvoker updateProgressDelegate;
private MethodInvoker updateProgressDelegate; /// <summary>Async result for the invoked control state update method</summary>
/// <summary>Async result for the invoked control state update method</summary> private volatile IAsyncResult progressUpdateAsyncResult;
private volatile IAsyncResult progressUpdateAsyncResult;
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,47 +1,66 @@
using Nuclex.Windows.Forms.Views; #region Apache License 2.0
using System; /*
using System.Windows.Forms; Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
namespace Nuclex.Windows.Forms.AutoBinding {
Licensed under the Apache License, Version 2.0 (the "License");
/// <summary> you may not use this file except in compliance with the License.
/// Binds a view to its model using a convention-over-configuration approach You may obtain a copy of the License at
/// </summary>
public class ConventionBinder : IAutoBinder { http://www.apache.org/licenses/LICENSE-2.0
/// <summary>Binds the specified view to an explicitly selected view model</summary> Unless required by applicable law or agreed to in writing, software
/// <typeparam name="TViewModel"> distributed under the License is distributed on an "AS IS" BASIS,
/// Type of view model the view will be bound to WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// </typeparam> See the License for the specific language governing permissions and
/// <param name="view">View that will be bound to a view model</param> limitations under the License.
/// <param name="viewModel">View model the view will be bound to</param> */
public void Bind<TViewModel>(Control view, TViewModel viewModel) #endregion // Apache License 2.0
where TViewModel : class {
bind(view, viewModel); using Nuclex.Windows.Forms.Views;
} using System;
using System.Windows.Forms;
/// <summary>
/// Binds the specified view to the view model specified in its DataContext namespace Nuclex.Windows.Forms.AutoBinding {
/// </summary>
/// <param name="viewControl">View that will be bound</param> /// <summary>
public void Bind(Control viewControl) { /// Binds a view to its model using a convention-over-configuration approach
IView viewControlAsView = viewControl as IView; /// </summary>
if(viewControlAsView == null) { public class ConventionBinder : IAutoBinder {
throw new InvalidOperationException(
"The specified view has no view model associated. Either assign your " + /// <summary>Binds the specified view to an explicitly selected view model</summary>
"view model to the view's data context beforehand or use the overload " + /// <typeparam name="TViewModel">
"of Bind() that allows you to explicitly specify the view model." /// 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>
bind(viewControl, viewControlAsView.DataContext); public void Bind<TViewModel>(Control view, TViewModel viewModel)
} where TViewModel : class {
bind(view, viewModel);
/// <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> /// <summary>
private void bind(Control view, object viewModel) { /// 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) {
} // namespace Nuclex.Windows.Forms.AutoBinding 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; #region Apache License 2.0
/*
using System.Windows.Forms; Nuclex .NET Framework
using Nuclex.Windows.Forms.Views; Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
namespace Nuclex.Windows.Forms.AutoBinding { Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
/// <summary>Binds views to their view models</summary> You may obtain a copy of the License at
public interface IAutoBinder {
http://www.apache.org/licenses/LICENSE-2.0
/// <summary>Binds the specified view to an explicitly selected view model</summary>
/// <typeparam name="TViewModel"> Unless required by applicable law or agreed to in writing, software
/// Type of view model the view will be bound to distributed under the License is distributed on an "AS IS" BASIS,
/// </typeparam> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// <param name="view">View that will be bound to a view model</param> See the License for the specific language governing permissions and
/// <param name="viewModel">View model the view will be bound to</param> limitations under the License.
void Bind<TViewModel>(Control view, TViewModel viewModel) */
where TViewModel : class; #endregion // Apache License 2.0
/// <summary> using System;
/// Binds the specified view to the view model specified in its DataContext
/// </summary> using System.Windows.Forms;
/// <param name="view">View that will be bound</param> using Nuclex.Windows.Forms.Views;
void Bind(Control view);
namespace Nuclex.Windows.Forms.AutoBinding {
}
/// <summary>Binds views to their view models</summary>
} // namespace Nuclex.Windows.Forms.AutoBinding 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 #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Text;
using System.Text; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms.CommonDialogs {
namespace Nuclex.Windows.Forms.CommonDialogs {
/// <summary>Displays common dialogs for selecting files and directories</summary>
/// <summary>Displays common dialogs for selecting files and directories</summary> public class CommonDialogManager : ICommonDialogService {
public class CommonDialogManager : ICommonDialogService {
/// <summary>Initializes a new task dialog message service</summary>
/// <summary>Initializes a new task dialog message service</summary> public CommonDialogManager() : this(NullActiveWindowTracker.Default) { }
public CommonDialogManager() : this(NullActiveWindowTracker.Default) { }
/// <summary>Initializes a new task dialog message service</summary>
/// <summary>Initializes a new task dialog message service</summary> /// <param name="tracker">
/// <param name="tracker"> /// Active window tracker used to obtain the parent window for message boxes
/// Active window tracker used to obtain the parent window for message boxes /// </param>
/// </param> public CommonDialogManager(IActiveWindowTracker tracker) {
public CommonDialogManager(IActiveWindowTracker tracker) { this.tracker = tracker;
this.tracker = tracker; }
}
/// <summary>Asks the user for a location to save a file under</summary>
/// <summary>Asks the user for a location to save a file under</summary> /// <param name="caption">Caption of the dialog</param>
/// <param name="caption">Caption of the dialog</param> /// <param name="masks">
/// <param name="masks"> /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" /// </param>
/// </param> /// <returns>The full path of the file the user wishes to save as</returns>
/// <returns>The full path of the file the user wishes to save as</returns> public string AskForSaveLocation(string caption, params string[] masks) {
public string AskForSaveLocation(string caption, params string[] masks) { var saveDialog = new SaveFileDialog() {
var saveDialog = new SaveFileDialog() { Title = caption,
Title = caption, Filter = combineMasks(masks)
Filter = combineMasks(masks) };
};
DialogResult result;
DialogResult result; {
{ Form activeWindow = this.tracker.ActiveWindow;
Form activeWindow = this.tracker.ActiveWindow; if(activeWindow == null) {
if(activeWindow == null) { result = saveDialog.ShowDialog();
result = saveDialog.ShowDialog(); } else {
} else { result = saveDialog.ShowDialog(activeWindow);
result = saveDialog.ShowDialog(activeWindow); }
} }
}
if(result == DialogResult.OK) {
if(result == DialogResult.OK) { return saveDialog.FileName;
return saveDialog.FileName; } else {
} else { return null;
return null; }
} }
}
/// <summary>Asks the user to select a file to open</summary>
/// <summary>Asks the user to select a file to open</summary> /// <param name="caption">Caption of the dialog</param>
/// <param name="caption">Caption of the dialog</param> /// <param name="masks">
/// <param name="masks"> /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" /// </param>
/// </param> /// <returns>The full path of the file the user selected</returns>
/// <returns>The full path of the file the user selected</returns> public string AskForFileToOpen(string caption, params string[] masks) {
public string AskForFileToOpen(string caption, params string[] masks) { var openDialog = new OpenFileDialog() {
var openDialog = new OpenFileDialog() { Title = caption,
Title = caption, Filter = combineMasks(masks),
Filter = combineMasks(masks), CheckFileExists = true,
CheckFileExists = true, CheckPathExists = true,
CheckPathExists = true, Multiselect = false
Multiselect = false };
};
DialogResult result;
DialogResult result; {
{ Form activeWindow = this.tracker.ActiveWindow;
Form activeWindow = this.tracker.ActiveWindow; if(activeWindow == null) {
if(activeWindow == null) { result = openDialog.ShowDialog();
result = openDialog.ShowDialog(); } else {
} else { result = openDialog.ShowDialog(activeWindow);
result = openDialog.ShowDialog(activeWindow); }
} }
}
if(result == DialogResult.OK) {
if(result == DialogResult.OK) { return openDialog.FileName;
return openDialog.FileName; } else {
} else { return null;
return null; }
} }
}
/// <summary>Asks the user to select one or more files to open</summary>
/// <summary>Asks the user to select one or more files to open</summary> /// <param name="caption">Caption of the dialog</param>
/// <param name="caption">Caption of the dialog</param> /// <param name="masks">
/// <param name="masks"> /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" /// </param>
/// </param> /// <returns>The full path of all files the user selected</returns>
/// <returns>The full path of all files the user selected</returns> public string[] AskForFilesToOpen(string caption, params string[] masks) {
public string[] AskForFilesToOpen(string caption, params string[] masks) { var openDialog = new OpenFileDialog() {
var openDialog = new OpenFileDialog() { Title = caption,
Title = caption, Filter = combineMasks(masks),
Filter = combineMasks(masks), CheckFileExists = true,
CheckFileExists = true, CheckPathExists = true,
CheckPathExists = true, Multiselect = true
Multiselect = true };
};
DialogResult result;
DialogResult result; {
{ Form activeWindow = this.tracker.ActiveWindow;
Form activeWindow = this.tracker.ActiveWindow; if(activeWindow == null) {
if(activeWindow == null) { result = openDialog.ShowDialog();
result = openDialog.ShowDialog(); } else {
} else { result = openDialog.ShowDialog(activeWindow);
result = openDialog.ShowDialog(activeWindow); }
} }
}
if(result == DialogResult.OK) {
if(result == DialogResult.OK) { return openDialog.FileNames;
return openDialog.FileNames; } else {
} else { return null;
return null; }
} }
}
/// <summary>Asks the user to select a directory</summary>
/// <summary>Asks the user to select a directory</summary> /// <returns>The directory the user has selected</returns>
/// <returns>The directory the user has selected</returns> public string AskForDirectory(string caption) {
public string AskForDirectory(string caption) { var folderDialog = new System.Windows.Forms.FolderBrowserDialog() {
var folderDialog = new System.Windows.Forms.FolderBrowserDialog() { Description = caption
Description = caption };
};
DialogResult result;
DialogResult result; {
{ Form activeWindow = this.tracker.ActiveWindow;
Form activeWindow = this.tracker.ActiveWindow; if(activeWindow == null) {
if(activeWindow == null) { result = folderDialog.ShowDialog();
result = folderDialog.ShowDialog(); } else {
} else { result = folderDialog.ShowDialog(activeWindow);
result = folderDialog.ShowDialog(activeWindow); }
} }
}
if(result == DialogResult.OK) {
if(result == DialogResult.OK) { return folderDialog.SelectedPath;
return folderDialog.SelectedPath; } else {
} else { return null;
return null; }
} }
}
/// <summary>Combines an array of file masks into a single mask</summary>
/// <summary>Combines an array of file masks into a single mask</summary> /// <param name="masks">Masks that will be combined</param>
/// <param name="masks">Masks that will be combined</param> /// <returns>That combined masks</returns>
/// <returns>That combined masks</returns> private static string combineMasks(string[] masks) {
private static string combineMasks(string[] masks) { if((masks == null) || (masks.Length == 0)) {
if((masks == null) || (masks.Length == 0)) { return null;
return null; }
}
int requiredCapacity = 0;
int requiredCapacity = 0; for(int index = 0; index < masks.Length; ++index) {
for(int index = 0; index < masks.Length; ++index) { requiredCapacity += masks[index].Length;
requiredCapacity += masks[index].Length; requiredCapacity += 1;
requiredCapacity += 1; }
}
var maskBuilder = new StringBuilder(requiredCapacity);
var maskBuilder = new StringBuilder(requiredCapacity); maskBuilder.Append(masks[0]);
maskBuilder.Append(masks[0]); for(int index = 1; index < masks.Length; ++index) {
for(int index = 1; index < masks.Length; ++index) { maskBuilder.Append('|');
maskBuilder.Append('|'); maskBuilder.Append(masks[index]);
maskBuilder.Append(masks[index]); }
}
return maskBuilder.ToString();
return maskBuilder.ToString(); }
}
/// <summary>Provides the currently active top-level window</summary>
/// <summary>Provides the currently active top-level window</summary> private IActiveWindowTracker tracker;
private IActiveWindowTracker tracker;
}
}
} // namespace Nuclex.Windows.Forms.CommonDialogs
} // namespace Nuclex.Windows.Forms.CommonDialogs

View File

@ -1,58 +1,57 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System;
namespace Nuclex.Windows.Forms.CommonDialogs {
namespace Nuclex.Windows.Forms.CommonDialogs {
/// <summary>Displays common dialogs for selecting files and directories</summary>
/// <summary>Displays common dialogs for selecting files and directories</summary> public interface ICommonDialogService {
public interface ICommonDialogService {
/// <summary>Asks the user for a location to save a file under</summary>
/// <summary>Asks the user for a location to save a file under</summary> /// <param name="caption">Caption of the dialog</param>
/// <param name="caption">Caption of the dialog</param> /// <param name="masks">
/// <param name="masks"> /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" /// </param>
/// </param> /// <returns>The full path of the file the user wishes to save as</returns>
/// <returns>The full path of the file the user wishes to save as</returns> string AskForSaveLocation(string caption, params string[] masks);
string AskForSaveLocation(string caption, params string[] masks);
/// <summary>Asks the user to select a file to open</summary>
/// <summary>Asks the user to select a file to open</summary> /// <param name="caption">Caption of the dialog</param>
/// <param name="caption">Caption of the dialog</param> /// <param name="masks">
/// <param name="masks"> /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" /// </param>
/// </param> /// <returns>The full path of the file the user selected</returns>
/// <returns>The full path of the file the user selected</returns> string AskForFileToOpen(string caption, params string[] masks);
string AskForFileToOpen(string caption, params string[] masks);
/// <summary>Asks the user to select one or more files to open</summary>
/// <summary>Asks the user to select one or more files to open</summary> /// <param name="caption">Caption of the dialog</param>
/// <param name="caption">Caption of the dialog</param> /// <param name="masks">
/// <param name="masks"> /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3"
/// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" /// </param>
/// </param> /// <returns>The full path of all files the user selected</returns>
/// <returns>The full path of all files the user selected</returns> string[] AskForFilesToOpen(string caption, params string[] masks);
string[] AskForFilesToOpen(string caption, params string[] masks);
/// <summary>Asks the user to select a directory</summary>
/// <summary>Asks the user to select a directory</summary> /// <returns>The directory the user has selected</returns>
/// <returns>The directory the user has selected</returns> string AskForDirectory(string caption);
string AskForDirectory(string caption);
}
}
} // namespace Nuclex.Windows.Forms.CommonDialogs
} // namespace Nuclex.Windows.Forms.CommonDialogs

View File

@ -1,53 +1,52 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
partial class ContainerListView {
partial class ContainerListView {
/// <summary>Required designer variable.</summary>
/// <summary>Required designer variable.</summary> private System.ComponentModel.IContainer components = null;
private System.ComponentModel.IContainer components = null;
/// <summary>Clean up any resources being used.</summary>
/// <summary>Clean up any resources being used.</summary> /// <param name="disposing">
/// <param name="disposing"> /// true if managed resources should be disposed; otherwise, false.
/// true if managed resources should be disposed; otherwise, false. /// </param>
/// </param> protected override void Dispose(bool disposing) {
protected override void Dispose(bool disposing) { if(disposing && (components != null)) {
if(disposing && (components != null)) { components.Dispose();
components.Dispose(); }
} base.Dispose(disposing);
base.Dispose(disposing); }
}
#region Component Designer generated code
#region Component Designer generated code
/// <summary>
/// <summary> /// Required method for Designer support - do not modify
/// Required method for Designer support - do not modify /// the contents of this method with the code editor.
/// the contents of this method with the code editor. /// </summary>
/// </summary> private void InitializeComponent() {
private void InitializeComponent() { components = new System.ComponentModel.Container();
components = new System.ComponentModel.Container(); }
}
#endregion
#endregion
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,83 +1,82 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
#if UNITTEST
#if UNITTEST
using System;
using System; using System.IO;
using System.IO; using System.Windows.Forms;
using System.Windows.Forms;
using NUnit.Framework;
using NUnit.Framework;
using Nuclex.Support;
using Nuclex.Support;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Unit Test for the control container list view</summary>
/// <summary>Unit Test for the control container list view</summary> [TestFixture, Explicit]
[TestFixture, Explicit] public class ContainerListViewTest {
public class ContainerListViewTest {
/// <summary>
/// <summary> /// Verifies that the asynchronous progress bar's constructor is working
/// Verifies that the asynchronous progress bar's constructor is working /// </summary>
/// </summary> [Test]
[Test] public void TestConstructor() {
public void TestConstructor() { using(ContainerListView listView = new ContainerListView()) {
using(ContainerListView listView = new ContainerListView()) {
// Let the control create its window handle
// Let the control create its window handle listView.CreateControl();
listView.CreateControl(); listView.Columns.Add("Numeric");
listView.Columns.Add("Numeric"); listView.Columns.Add("Spelled");
listView.Columns.Add("Spelled"); listView.Columns.Add("Nonsense");
listView.Columns.Add("Nonsense");
addRow(listView, "1", "One");
addRow(listView, "1", "One"); addRow(listView, "2", "Two");
addRow(listView, "2", "Two"); addRow(listView, "3", "Three");
addRow(listView, "3", "Three");
using(CheckBox checkBox = new CheckBox()) {
using(CheckBox checkBox = new CheckBox()) { listView.EmbeddedControls.Add(new ListViewEmbeddedControl(checkBox, 2, 0));
listView.EmbeddedControls.Add(new ListViewEmbeddedControl(checkBox, 2, 0)); listView.EmbeddedControls.Clear();
listView.EmbeddedControls.Clear();
listView.Refresh();
listView.Refresh();
ListViewEmbeddedControl embeddedControl = new ListViewEmbeddedControl(
ListViewEmbeddedControl embeddedControl = new ListViewEmbeddedControl( checkBox, 2, 0
checkBox, 2, 0 );
); listView.EmbeddedControls.Add(embeddedControl);
listView.EmbeddedControls.Add(embeddedControl); listView.EmbeddedControls.Remove(embeddedControl);
listView.EmbeddedControls.Remove(embeddedControl);
listView.Refresh();
listView.Refresh(); }
}
}
} }
}
/// <summary>Adds a row to a control container list view</summary>
/// <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="listView">List view control the row will be added to</param> /// <param name="columns">Values that will appear in the individual columns</param>
/// <param name="columns">Values that will appear in the individual columns</param> private void addRow(ContainerListView listView, params string[] columns) {
private void addRow(ContainerListView listView, params string[] columns) { listView.Items.Add(new ListViewItem(columns));
listView.Items.Add(new ListViewItem(columns)); }
}
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms
#endif // UNITTEST
#endif // UNITTEST

View File

@ -1,247 +1,246 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Collections.Generic;
using System.Collections.Generic; using System.ComponentModel;
using System.ComponentModel; using System.Drawing;
using System.Drawing; using System.Data;
using System.Data; using System.Text;
using System.Text; using System.Windows.Forms;
using System.Windows.Forms; using System.Runtime.InteropServices;
using System.Runtime.InteropServices;
using Nuclex.Support.Collections;
using Nuclex.Support.Collections;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>ListView allowing for other controls to be embedded in its cells</summary>
/// <summary>ListView allowing for other controls to be embedded in its cells</summary> /// <remarks>
/// <remarks> /// <para>
/// <para> /// There basically were two possible design choices: Provide a specialized
/// There basically were two possible design choices: Provide a specialized /// ListViewSubItem that carries a Control instead of a string or manage the
/// ListViewSubItem that carries a Control instead of a string or manage the /// embedded controls seperate of the ListView's items.
/// embedded controls seperate of the ListView's items. /// </para>
/// </para> /// <para>
/// <para> /// The first option requires a complete rewrite of the ListViewItem class
/// The first option requires a complete rewrite of the ListViewItem class /// and its related support classes, all of which are surprisingly large and
/// and its related support classes, all of which are surprisingly large and /// complex. Thus, I chose the less clean but more doable latter option.
/// complex. Thus, I chose the less clean but more doable latter option. /// </para>
/// </para> /// <para>
/// <para> /// This control is useful for simple item lists where you want to provide
/// 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.
/// 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
/// 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
/// requires a control to be created per row and management of the embedded /// controls is designed for limited usage.
/// controls is designed for limited usage. /// </para>
/// </para> /// </remarks>
/// </remarks> public partial class ContainerListView : System.Windows.Forms.ListView {
public partial class ContainerListView : System.Windows.Forms.ListView {
/// <summary>Message sent to a control to let it paint itself</summary>
/// <summary>Message sent to a control to let it paint itself</summary> private const int WM_PAINT = 0x000F;
private const int WM_PAINT = 0x000F;
/// <summary>Initializes a new ContainerListView</summary>
/// <summary>Initializes a new ContainerListView</summary> public ContainerListView() {
public ContainerListView() { this.embeddedControlClickedDelegate = new EventHandler(embeddedControlClicked);
this.embeddedControlClickedDelegate = new EventHandler(embeddedControlClicked);
this.embeddedControls = new ObservableList<ListViewEmbeddedControl>();
this.embeddedControls = new ObservableList<ListViewEmbeddedControl>(); this.embeddedControls.ItemAdded +=
this.embeddedControls.ItemAdded += new EventHandler<ItemEventArgs<ListViewEmbeddedControl>>(embeddedControlAdded);
new EventHandler<ItemEventArgs<ListViewEmbeddedControl>>(embeddedControlAdded); this.embeddedControls.ItemRemoved +=
this.embeddedControls.ItemRemoved += new EventHandler<ItemEventArgs<ListViewEmbeddedControl>>(embeddedControlRemoved);
new EventHandler<ItemEventArgs<ListViewEmbeddedControl>>(embeddedControlRemoved); this.embeddedControls.Clearing += new EventHandler(embeddedControlsClearing);
this.embeddedControls.Clearing += new EventHandler(embeddedControlsClearing);
InitializeComponent();
InitializeComponent();
// Eliminate flickering
// Eliminate flickering SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
base.View = View.Details;
base.View = View.Details;
this.columnHeaderHeight = Font.Height;
this.columnHeaderHeight = Font.Height; }
}
/// <summary>Controls being embedded in the ListView</summary>
/// <summary>Controls being embedded in the ListView</summary> public ICollection<ListViewEmbeddedControl> EmbeddedControls {
public ICollection<ListViewEmbeddedControl> EmbeddedControls { get { return this.embeddedControls; }
get { return this.embeddedControls; } }
}
/// <summary>Updates the controls embeded into the list view</summary>
/// <summary>Updates the controls embeded into the list view</summary> public void UpdateEmbeddedControls() {
public void UpdateEmbeddedControls() { if(View != View.Details) {
if(View != View.Details) { for(int index = 0; index < this.embeddedControls.Count; ++index) {
for(int index = 0; index < this.embeddedControls.Count; ++index) { this.embeddedControls[index].Control.Visible = false;
this.embeddedControls[index].Control.Visible = false; }
} } else {
} else { for(int index = 0; index < this.embeddedControls.Count; ++index) {
for(int index = 0; index < this.embeddedControls.Count; ++index) { ListViewEmbeddedControl embeddedControl = this.embeddedControls[index];
ListViewEmbeddedControl embeddedControl = this.embeddedControls[index];
Rectangle cellBounds = this.GetSubItemBounds(
Rectangle cellBounds = this.GetSubItemBounds( Items[embeddedControl.Row], embeddedControl.Column
Items[embeddedControl.Row], embeddedControl.Column );
);
bool intersectsColumnHeader =
bool intersectsColumnHeader = (base.HeaderStyle != ColumnHeaderStyle.None) &&
(base.HeaderStyle != ColumnHeaderStyle.None) && (cellBounds.Top < base.Font.Height);
(cellBounds.Top < base.Font.Height);
embeddedControl.Control.Visible = !intersectsColumnHeader;
embeddedControl.Control.Visible = !intersectsColumnHeader; embeddedControl.Control.Bounds = cellBounds;
embeddedControl.Control.Bounds = cellBounds; }
} }
} }
}
/// <summary>Calculates the boundaries of a cell in the list view</summary>
/// <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="item">Item in the list view from which to calculate the cell</param> /// <param name="subItem">Index der cell whose boundaries to calculate</param>
/// <param name="subItem">Index der cell whose boundaries to calculate</param> /// <returns>The boundaries of the specified list view cell</returns>
/// <returns>The boundaries of the specified list view cell</returns> /// <exception cref="IndexOutOfRangeException">
/// <exception cref="IndexOutOfRangeException"> /// When the specified sub item index is not in the range of valid sub items
/// When the specified sub item index is not in the range of valid sub items /// </exception>
/// </exception> protected Rectangle GetSubItemBounds(ListViewItem item, int subItem) {
protected Rectangle GetSubItemBounds(ListViewItem item, int subItem) { int[] order = GetColumnOrder();
int[] order = GetColumnOrder(); if(order == null) { // No Columns
if(order == null) { // No Columns return Rectangle.Empty;
return Rectangle.Empty; }
}
if(subItem >= order.Length) {
if(subItem >= order.Length) { throw new IndexOutOfRangeException("SubItem " + subItem + " out of range");
throw new IndexOutOfRangeException("SubItem " + subItem + " out of range"); }
}
// Determine the border of the entire ListViewItem, including all sub items
// Determine the border of the entire ListViewItem, including all sub items Rectangle itemBounds = item.GetBounds(ItemBoundsPortion.Entire);
Rectangle itemBounds = item.GetBounds(ItemBoundsPortion.Entire); int subItemX = itemBounds.Left;
int subItemX = itemBounds.Left;
// Find the horizontal position of the sub item. Because the column order can vary,
// 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!
// we need to use Columns[order[i]] instead of simply doing Columns[i] here! ColumnHeader columnHeader;
ColumnHeader columnHeader; int i;
int i; for(i = 0; i < order.Length; ++i) {
for(i = 0; i < order.Length; ++i) { columnHeader = this.Columns[order[i]];
columnHeader = this.Columns[order[i]]; if(columnHeader.Index == subItem) {
if(columnHeader.Index == subItem) { break;
break; }
}
subItemX += columnHeader.Width;
subItemX += columnHeader.Width; }
}
return new Rectangle(
return new Rectangle( subItemX, itemBounds.Top, this.Columns[order[i]].Width, itemBounds.Height
subItemX, itemBounds.Top, this.Columns[order[i]].Width, itemBounds.Height );
); }
}
/// <summary>Responds to window messages sent by the operating system</summary>
/// <summary>Responds to window messages sent by the operating system</summary> /// <param name="message">Window message that will be processed</param>
/// <param name="message">Window message that will be processed</param> protected override void WndProc(ref Message message) {
protected override void WndProc(ref Message message) { switch(message.Msg) {
switch(message.Msg) { case WM_PAINT: {
case WM_PAINT: { UpdateEmbeddedControls();
UpdateEmbeddedControls(); break;
break; }
} }
}
base.WndProc(ref message);
base.WndProc(ref message); }
}
/// <summary>Called when the list of embedded controls has been cleared</summary>
/// <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="sender">Collection that has been cleared of its controls</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void embeddedControlsClearing(object sender, EventArgs arguments) {
private void embeddedControlsClearing(object sender, EventArgs arguments) { this.BeginUpdate();
this.BeginUpdate(); try {
try { foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) {
foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) { embeddedControl.Control.Click -= this.embeddedControlClickedDelegate;
embeddedControl.Control.Click -= this.embeddedControlClickedDelegate; this.Controls.Remove(embeddedControl.Control);
this.Controls.Remove(embeddedControl.Control); }
} }
} finally {
finally { this.EndUpdate();
this.EndUpdate(); }
} }
}
/// <summary>Called when a control gets removed from the embedded controls list</summary>
/// <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="sender">List from which the control has been removed</param> /// <param name="arguments">
/// <param name="arguments"> /// Event arguments providing a reference to the removed control
/// Event arguments providing a reference to the removed control /// </param>
/// </param> private void embeddedControlAdded(
private void embeddedControlAdded( object sender, ItemEventArgs<ListViewEmbeddedControl> arguments
object sender, ItemEventArgs<ListViewEmbeddedControl> arguments ) {
) { arguments.Item.Control.Click += this.embeddedControlClickedDelegate;
arguments.Item.Control.Click += this.embeddedControlClickedDelegate; this.Controls.Add(arguments.Item.Control);
this.Controls.Add(arguments.Item.Control); }
}
/// <summary>Called when a control gets added to the embedded controls list</summary>
/// <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="sender">List to which the control has been added</param> /// <param name="arguments">
/// <param name="arguments"> /// Event arguments providing a reference to the added control
/// Event arguments providing a reference to the added control /// </param>
/// </param> private void embeddedControlRemoved(
private void embeddedControlRemoved( object sender, ItemEventArgs<ListViewEmbeddedControl> arguments
object sender, ItemEventArgs<ListViewEmbeddedControl> arguments ) {
) { if(this.Controls.Contains(arguments.Item.Control)) {
if(this.Controls.Contains(arguments.Item.Control)) { arguments.Item.Control.Click -= this.embeddedControlClickedDelegate;
arguments.Item.Control.Click -= this.embeddedControlClickedDelegate; this.Controls.Remove(arguments.Item.Control);
this.Controls.Remove(arguments.Item.Control); }
} }
}
/// <summary>Called when an embedded control has been clicked on</summary>
/// <summary>Called when an embedded control has been clicked on</summary> /// <param name="sender">Embedded control that has been clicked</param>
/// <param name="sender">Embedded control that has been clicked</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void embeddedControlClicked(object sender, EventArgs arguments) {
private void embeddedControlClicked(object sender, EventArgs arguments) { this.BeginUpdate();
this.BeginUpdate();
try {
try { SelectedItems.Clear();
SelectedItems.Clear();
foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) {
foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) { if(ReferenceEquals(embeddedControl.Control, sender)) {
if(ReferenceEquals(embeddedControl.Control, sender)) { if((embeddedControl.Row > 0) && (embeddedControl.Row < Items.Count)) {
if((embeddedControl.Row > 0) && (embeddedControl.Row < Items.Count)) { Items[embeddedControl.Row].Selected = true;
Items[embeddedControl.Row].Selected = true; }
} }
} }
} }
} finally {
finally { this.EndUpdate();
this.EndUpdate(); }
} }
}
/// <summary>Obtains the current column order of the list</summary>
/// <summary>Obtains the current column order of the list</summary> /// <returns>An array indicating the order of the list's columns</returns>
/// <returns>An array indicating the order of the list's columns</returns> private int[] GetColumnOrder() {
private int[] GetColumnOrder() { int[] order = new int[this.Columns.Count];
int[] order = new int[this.Columns.Count];
for(int index = 0; index < this.Columns.Count; ++index) {
for(int index = 0; index < this.Columns.Count; ++index) { order[this.Columns[index].DisplayIndex] = index;
order[this.Columns[index].DisplayIndex] = index; }
}
return order;
return order; }
}
/// <summary>Height of the list view's column header</summary>
/// <summary>Height of the list view's column header</summary> private int columnHeaderHeight;
private int columnHeaderHeight; /// <summary>Event handler for when embedded controls are clicked on</summary>
/// <summary>Event handler for when embedded controls are clicked on</summary> private EventHandler embeddedControlClickedDelegate;
private EventHandler embeddedControlClickedDelegate; /// <summary>Controls being embedded in this ListView</summary>
/// <summary>Controls being embedded in this ListView</summary> private ObservableList<ListViewEmbeddedControl> embeddedControls;
private ObservableList<ListViewEmbeddedControl> embeddedControls;
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,64 +1,63 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Collections.Generic;
using System.Collections.Generic; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Stores informations about an embedded control</summary>
/// <summary>Stores informations about an embedded control</summary> public class ListViewEmbeddedControl {
public class ListViewEmbeddedControl {
/// <summary>Initializes a new embedded control holder</summary>
/// <summary>Initializes a new embedded control holder</summary> /// <param name="control">Control being embedded in a list view</param>
/// <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="row">List row at which the control will be embedded</param> /// <param name="column">List column 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) {
public ListViewEmbeddedControl(Control control, int row, int column) { this.control = control;
this.control = control; this.row = row;
this.row = row; this.column = column;
this.column = column; }
}
/// <summary>Control that is being embedded in the ListView</summary>
/// <summary>Control that is being embedded in the ListView</summary> public Control Control {
public Control Control { get { return this.control; }
get { return this.control; } }
}
/// <summary>Row the control has been embedded in</summary>
/// <summary>Row the control has been embedded in</summary> public int Row {
public int Row { get { return this.row; }
get { return this.row; } }
}
/// <summary>Column the control has been embedded in</summary>
/// <summary>Column the control has been embedded in</summary> public int Column {
public int Column { get { return this.column; }
get { return this.column; } }
}
/// <summary>Embedded control</summary>
/// <summary>Embedded control</summary> private Control control;
private Control control; /// <summary>Row where the control is embedded</summary>
/// <summary>Row where the control is embedded</summary> private int row;
private int row; /// <summary>Column where the control is embedded</summary>
/// <summary>Column where the control is embedded</summary> private int column;
private int column;
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,49 +1,68 @@
namespace Nuclex.Windows.Forms.Controls { #region Apache License 2.0
/*
partial class ProgressSpinner { Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
/// <summary> Required designer variable.</summary>
private System.ComponentModel.IContainer components = null; Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
/// <summary> Clean up any resources being used.</summary> You may obtain a copy of the License at
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) { http://www.apache.org/licenses/LICENSE-2.0
if(disposing && (components != null)) {
components.Dispose(); Unless required by applicable law or agreed to in writing, software
} distributed under the License is distributed on an "AS IS" BASIS,
base.Dispose(disposing); 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.
#region Component Designer generated code */
#endregion // Apache License 2.0
/// <summary>
/// Required method for Designer support - do not modify namespace Nuclex.Windows.Forms.Controls {
/// the contents of this method with the code editor.
/// </summary> partial class ProgressSpinner {
private void InitializeComponent() {
this.animationUpdateTimer = new System.Windows.Forms.Timer(); /// <summary> Required designer variable.</summary>
this.SuspendLayout(); private System.ComponentModel.IContainer components = null;
//
// animationUpdateTimer /// <summary> Clean up any resources being used.</summary>
// /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
this.animationUpdateTimer.Tick += new System.EventHandler(this.animationTimerTicked); protected override void Dispose(bool disposing) {
// if(disposing && (components != null)) {
// ProgressSpinner components.Dispose();
// }
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); base.Dispose(disposing);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; }
this.BackColor = System.Drawing.Color.Transparent;
this.DoubleBuffered = true; #region Component Designer generated code
this.Name = "ProgressSpinner";
this.ResumeLayout(false); /// <summary>
/// Required method for Designer support - do not modify
} /// the contents of this method with the code editor.
/// </summary>
#endregion private void InitializeComponent() {
this.animationUpdateTimer = new System.Windows.Forms.Timer();
/// <summary>Timer used to update the progress animation</summary> this.SuspendLayout();
private System.Windows.Forms.Timer animationUpdateTimer; //
// animationUpdateTimer
} //
this.animationUpdateTimer.Tick += new System.EventHandler(this.animationTimerTicked);
} // namespace Nuclex.Windows.Forms.Controls //
// 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; #region Apache License 2.0
using System.Data; /*
using System.Drawing; Nuclex .NET Framework
using System.Drawing.Drawing2D; Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
using System.Linq;
using System.Windows.Forms; Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
namespace Nuclex.Windows.Forms.Controls { You may obtain a copy of the License at
/// <summary>Displays a progress spinner to entertain the user while waiting</summary> http://www.apache.org/licenses/LICENSE-2.0
public partial class ProgressSpinner : UserControl {
Unless required by applicable law or agreed to in writing, software
/// <summary>Number of dots the progress spinner will display</summary> distributed under the License is distributed on an "AS IS" BASIS,
private const int DotCount = 8; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// <summary>Size of a normal dot (only ever assumed by the trailing dot)</summary> See the License for the specific language governing permissions and
private const int DotRadius = 4; limitations under the License.
/// <summary> */
/// The leading dot will be DotCount times this larger than a normal dot #endregion // Apache License 2.0
/// </summary>
private const int ScaleFactor = 1; using System;
using System.Data;
/// <summary>Initializes a new progress spinner</summary> using System.Drawing;
public ProgressSpinner() { using System.Drawing.Drawing2D;
SetStyle( using System.Linq;
( using System.Windows.Forms;
ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer | namespace Nuclex.Windows.Forms.Controls {
ControlStyles.ResizeRedraw | ControlStyles.UserPaint |
ControlStyles.SupportsTransparentBackColor /// <summary>Displays a progress spinner to entertain the user while waiting</summary>
), public partial class ProgressSpinner : UserControl {
true
); /// <summary>Number of dots the progress spinner will display</summary>
private const int DotCount = 8;
InitializeComponent(); /// <summary>Size of a normal dot (only ever assumed by the trailing dot)</summary>
private const int DotRadius = 4;
Disposed += new EventHandler(OnDisposed); /// <summary>
/// The leading dot will be DotCount times this larger than a normal dot
if(!DesignMode) { /// </summary>
StartSpinner(); private const int ScaleFactor = 1;
}
} /// <summary>Initializes a new progress spinner</summary>
public ProgressSpinner() {
/// <summary>Releases all resources owned by the control when it is destroyed</summary> SetStyle(
/// <param name="sender">Control that is being destroyed</param> (
/// <param name="arguments">Not used</param> ControlStyles.AllPaintingInWmPaint |
private void OnDisposed(object sender, EventArgs arguments) { ControlStyles.OptimizedDoubleBuffer |
if(this.dotOutlinePen != null) { ControlStyles.ResizeRedraw | ControlStyles.UserPaint |
this.dotOutlinePen.Dispose(); ControlStyles.SupportsTransparentBackColor
this.dotOutlinePen = null; ),
} true
if(this.dotFillBrush != null) { );
this.dotFillBrush.Dispose();
this.dotFillBrush = null; InitializeComponent();
}
} Disposed += new EventHandler(OnDisposed);
/// <summary>Starts the spinner's animation</summary> if(!DesignMode) {
public void StartSpinner() { StartSpinner();
this.spinnerRunning = true; }
this.animationUpdateTimer.Enabled = true; }
}
/// <summary>Releases all resources owned by the control when it is destroyed</summary>
/// <summary>Stops the spinner's animation</summary> /// <param name="sender">Control that is being destroyed</param>
public void StopSpinner() { /// <param name="arguments">Not used</param>
this.animationUpdateTimer.Enabled = false; private void OnDisposed(object sender, EventArgs arguments) {
this.spinnerRunning = false; if(this.dotOutlinePen != null) {
} this.dotOutlinePen.Dispose();
this.dotOutlinePen = null;
/// <summary>Color used to fill the dots</summary> }
public Color DotFillColor { if(this.dotFillBrush != null) {
get { return this.dotFillColor; } this.dotFillBrush.Dispose();
set { this.dotFillBrush = null;
if(value != this.dotFillColor) { }
this.dotFillColor = value; }
if(this.dotFillBrush != null) {
this.dotFillBrush.Dispose(); /// <summary>Starts the spinner's animation</summary>
this.dotFillBrush = null; public void StartSpinner() {
} this.spinnerRunning = true;
} this.animationUpdateTimer.Enabled = true;
} }
}
/// <summary>Stops the spinner's animation</summary>
/// <summary>Color used for the dots' outline</summary> public void StopSpinner() {
public Color DotOutlineColor { this.animationUpdateTimer.Enabled = false;
get { return this.dotOutlineColor; } this.spinnerRunning = false;
set { }
if(value != this.dotOutlineColor) {
this.dotOutlineColor = value; /// <summary>Color used to fill the dots</summary>
if(this.dotOutlinePen != null) { public Color DotFillColor {
this.dotOutlinePen.Dispose(); get { return this.dotFillColor; }
this.dotOutlinePen = null; set {
} if(value != this.dotFillColor) {
} this.dotFillColor = value;
} if(this.dotFillBrush != null) {
} this.dotFillBrush.Dispose();
this.dotFillBrush = 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. /// <summary>Color used for the dots' outline</summary>
/// </remarks> public Color DotOutlineColor {
public Size GetOptimalSize() { get { return this.dotOutlineColor; }
SizeF textRectangle; set {
using(var dummyImage = new Bitmap(1, 1)) { if(value != this.dotOutlineColor) {
using(Graphics graphics = Graphics.FromImage(dummyImage)) { this.dotOutlineColor = value;
textRectangle = graphics.MeasureString( if(this.dotOutlinePen != null) {
this.statusText, this.statusFont this.dotOutlinePen.Dispose();
); this.dotOutlinePen = null;
} }
} }
}
return new Size( }
Math.Max(128, (int)(textRectangle.Width + 2.0f)),
this.statusFont.Height + 128 /// <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
/// <summary>Font that is used to display the status text</summary> /// redraw every control behind it each time it updates. Thus it's wise to keep it
public Font StatusFont { /// as small as possible, but wide enough to fit the status text, if any.
get { return this.statusFont; } /// </remarks>
set { this.statusFont = value; } public Size GetOptimalSize() {
} SizeF textRectangle;
using(var dummyImage = new Bitmap(1, 1)) {
/// <summary>Text that will be displayed as the control's status</summary> using(Graphics graphics = Graphics.FromImage(dummyImage)) {
public string StatusText { textRectangle = graphics.MeasureString(
get { return this.statusText; } this.statusText, this.statusFont
set { this.statusText = value; } );
} }
}
/// <summary>Called when the control is hidden or shown</summary>
/// <param name="arguments">Not used</param> return new Size(
protected override void OnVisibleChanged(EventArgs arguments) { Math.Max(128, (int)(textRectangle.Width + 2.0f)),
base.OnVisibleChanged(arguments); this.statusFont.Height + 128
this.animationUpdateTimer.Enabled = this.spinnerRunning && Visible; );
} }
/// <summary>Called when the control should redraw itself</summary> /// <summary>Font that is used to display the status text</summary>
/// <param name="arguments">Provides access to the drawing surface and tools</param> public Font StatusFont {
protected override void OnPaint(PaintEventArgs arguments) { get { return this.statusFont; }
paintControlsBehindMe(arguments); set { this.statusFont = value; }
paintAnimatedDots(arguments); }
paintStatusMessage(arguments);
} /// <summary>Text that will be displayed as the control's status</summary>
public string StatusText {
/// <summary>Forcefully redraws the controls below this one</summary> get { return this.statusText; }
/// <param name="arguments">Provides access to the drawing surface and tools</param> set { this.statusText = value; }
/// <remarks> }
/// <para>
/// WinForms has very poor transparency support. A transparent control will only /// <summary>Called when the control is hidden or shown</summary>
/// be transparent to its immediate parent (so the parent needs to be a container /// <param name="arguments">Not used</param>
/// control and hold the transparent control as its preferrably only child). protected override void OnVisibleChanged(EventArgs arguments) {
/// </para> base.OnVisibleChanged(arguments);
/// <para> this.animationUpdateTimer.Enabled = this.spinnerRunning && Visible;
/// 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 /// <summary>Called when the control should redraw itself</summary>
/// that are behind this control and whose bounding box intersect this control. /// <param name="arguments">Provides access to the drawing surface and tools</param>
/// </para> protected override void OnPaint(PaintEventArgs arguments) {
/// </remarks> paintControlsBehindMe(arguments);
private void paintControlsBehindMe(PaintEventArgs arguments) { paintAnimatedDots(arguments);
if(Parent != null && this.BackColor == Color.Transparent) { paintStatusMessage(arguments);
using(var bmp = new Bitmap(Parent.Width, Parent.Height)) { }
Parent.Controls.Cast<Control>()
.Where(c => Parent.Controls.GetChildIndex(c) > Parent.Controls.GetChildIndex(this)) /// <summary>Forcefully redraws the controls below this one</summary>
.Where(c => c.Bounds.IntersectsWith(this.Bounds)) /// <param name="arguments">Provides access to the drawing surface and tools</param>
.OrderByDescending(c => Parent.Controls.GetChildIndex(c)) /// <remarks>
.ToList() /// <para>
.ForEach(c => c.DrawToBitmap(bmp, c.Bounds)); /// 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
arguments.Graphics.DrawImage(bmp, -Left, -Top); /// 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
/// <summary>Draws a simple animated dots animation</summary> /// edit something. This method fixes those issues by repainting all controls
/// <param name="arguments">Provides access to the drawing surface and tools</param> /// that are behind this control and whose bounding box intersect this control.
private void paintAnimatedDots(PaintEventArgs arguments) { /// </para>
if(this.dotOutlinePen == null) { /// </remarks>
this.dotOutlinePen = new Pen(this.dotOutlineColor); private void paintControlsBehindMe(PaintEventArgs arguments) {
} if(Parent != null && this.BackColor == Color.Transparent) {
if(this.dotFillBrush == null) { using(var bmp = new Bitmap(Parent.Width, Parent.Height)) {
this.dotFillBrush = new SolidBrush(this.dotFillColor); Parent.Controls.Cast<Control>()
} .Where(c => Parent.Controls.GetChildIndex(c) > Parent.Controls.GetChildIndex(this))
.Where(c => c.Bounds.IntersectsWith(this.Bounds))
SmoothingMode prevousSmoothingMode = arguments.Graphics.SmoothingMode; .OrderByDescending(c => Parent.Controls.GetChildIndex(c))
arguments.Graphics.SmoothingMode = SmoothingMode.HighQuality; .ToList()
try { .ForEach(c => c.DrawToBitmap(bmp, c.Bounds));
PointF center = new PointF(Width / 2.0f, (Height - this.statusFont.Height - 2) / 2.0f);
arguments.Graphics.DrawImage(bmp, -Left, -Top);
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; /// <summary>Draws a simple animated dots animation</summary>
for(int index = 0; index < DotCount; ++index) { /// <param name="arguments">Provides access to the drawing surface and tools</param>
int dotIndex = (index + leadingDotIndex) % DotCount; private void paintAnimatedDots(PaintEventArgs arguments) {
if(this.dotOutlinePen == null) {
var dotPosition = new PointF( this.dotOutlinePen = new Pen(this.dotOutlineColor);
center.X + (float)(bigRadius * Math.Cos(unitAngle * dotIndex * Math.PI / 180.0f)), }
center.Y + (float)(bigRadius * Math.Sin(unitAngle * dotIndex * Math.PI / 180.0f)) if(this.dotFillBrush == null) {
); this.dotFillBrush = new SolidBrush(this.dotFillColor);
}
int currentDotRadius = DotRadius + index * ScaleFactor;
SmoothingMode prevousSmoothingMode = arguments.Graphics.SmoothingMode;
var corner = new PointF( arguments.Graphics.SmoothingMode = SmoothingMode.HighQuality;
dotPosition.X - currentDotRadius, dotPosition.Y - currentDotRadius try {
); PointF center = new PointF(Width / 2.0f, (Height - this.statusFont.Height - 2) / 2.0f);
arguments.Graphics.FillEllipse(
this.dotFillBrush, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius int diameter = Math.Min(Width, Height - this.statusFont.Height - 2);
); int bigRadius = diameter / 2 - DotRadius - (DotCount - 1) * ScaleFactor;
arguments.Graphics.DrawEllipse(
this.dotOutlinePen, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius // Draw the dots
); float unitAngle = 360.0f / DotCount;
} for(int index = 0; index < DotCount; ++index) {
} int dotIndex = (index + leadingDotIndex) % DotCount;
finally {
arguments.Graphics.SmoothingMode = prevousSmoothingMode; 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))
);
/// <summary>Draws the status message under the animated dots</summary>
/// <param name="arguments">Provides access to the drawing surface and tools</param> int currentDotRadius = DotRadius + index * ScaleFactor;
private void paintStatusMessage(PaintEventArgs arguments) {
if(!string.IsNullOrEmpty(this.statusText)) { var corner = new PointF(
SizeF textRectangle = arguments.Graphics.MeasureString( dotPosition.X - currentDotRadius, dotPosition.Y - currentDotRadius
this.statusText, this.statusFont );
); arguments.Graphics.FillEllipse(
this.dotFillBrush, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius
var messageArea = new RectangleF( );
(Width - textRectangle.Width) / 2.0f, arguments.Graphics.DrawEllipse(
Height - this.statusFont.Height - 1.0f, this.dotOutlinePen, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius
textRectangle.Width, );
this.statusFont.Height }
); }
finally {
// Draw text with a white halo. This is a little bit ugly... arguments.Graphics.SmoothingMode = prevousSmoothingMode;
{ }
messageArea.Offset(-1.0f, 0.0f); }
arguments.Graphics.DrawString(
this.statusText, this.statusFont, Brushes.White, messageArea /// <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) {
messageArea.Offset(2.0f, 0.0f); if(!string.IsNullOrEmpty(this.statusText)) {
arguments.Graphics.DrawString( SizeF textRectangle = arguments.Graphics.MeasureString(
this.statusText, this.statusFont, Brushes.White, messageArea this.statusText, this.statusFont
); );
messageArea.Offset(-1.0f, -1.0f); var messageArea = new RectangleF(
arguments.Graphics.DrawString( (Width - textRectangle.Width) / 2.0f,
this.statusText, this.statusFont, Brushes.White, messageArea Height - this.statusFont.Height - 1.0f,
); textRectangle.Width,
this.statusFont.Height
messageArea.Offset(0.0f, 2.0f); );
arguments.Graphics.DrawString(
this.statusText, this.statusFont, Brushes.White, messageArea // Draw text with a white halo. This is a little bit ugly...
); {
messageArea.Offset(-1.0f, 0.0f);
messageArea.Offset(0.0f, -1.0f); arguments.Graphics.DrawString(
arguments.Graphics.DrawString( this.statusText, this.statusFont, Brushes.White, messageArea
this.statusText, this.statusFont, this.dotFillBrush, messageArea );
);
} messageArea.Offset(2.0f, 0.0f);
} arguments.Graphics.DrawString(
} this.statusText, this.statusFont, Brushes.White, messageArea
);
/// <summary>Called when the animation timer ticks to update the animation state</summary> messageArea.Offset(-1.0f, -1.0f);
/// <param name="sender">Animation timer that has ticked</param> arguments.Graphics.DrawString(
/// <param name="arguments">Not used</param> this.statusText, this.statusFont, Brushes.White, messageArea
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 messageArea.Offset(0.0f, 2.0f);
} arguments.Graphics.DrawString(
this.statusText, this.statusFont, Brushes.White, messageArea
/// <summary>Whether the spinner has been started</summary> );
private bool spinnerRunning;
/// <summary>Index of the currently leading dot</summary> messageArea.Offset(0.0f, -1.0f);
private int leadingDotIndex = 0; arguments.Graphics.DrawString(
/// <summary>Text that will be displayed under the control as the current status</summary> this.statusText, this.statusFont, this.dotFillBrush, messageArea
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> /// <summary>Called when the animation timer ticks to update the animation state</summary>
private Brush dotFillBrush; /// <param name="sender">Animation timer that has ticked</param>
/// <summary>Brush used for the dots' outline</summary> /// <param name="arguments">Not used</param>
private Pen dotOutlinePen; private void animationTimerTicked(object sender, EventArgs arguments) {
/// <summary>Font that is used to display the status text</summary> this.leadingDotIndex = (this.leadingDotIndex + 1) % DotCount; // Advance the animation
private Font statusFont = SystemFonts.SmallCaptionFont; Invalidate(); // Request a redraw at the earliest opportune time
}
}
/// <summary>Whether the spinner has been started</summary>
} // namespace Nuclex.Windows.Forms.Controls 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 #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Enables consumer to look up the currently active window</summary>
/// <summary>Enables consumer to look up the currently active window</summary> public interface IActiveWindowTracker {
public interface IActiveWindowTracker {
/// <summary>The currently active top-level or modal window</summary>
/// <summary>The currently active top-level or modal window</summary> /// <remarks>
/// <remarks> /// If windows live in multiple threads, the property change notification for
/// If windows live in multiple threads, the property change notification for /// this property, if supported, might be fired from a different thread.
/// this property, if supported, might be fired from a different thread. /// </remarks>
/// </remarks> Form ActiveWindow { get; }
Form ActiveWindow { get; }
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,93 +1,92 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Interface for a window manager used in an MVVM environment</summary>
/// <summary>Interface for a window manager used in an MVVM environment</summary> public interface IWindowManager : IActiveWindowTracker {
public interface IWindowManager : IActiveWindowTracker {
/// <summary>Opens a view as a new root window of the application</summary>
/// <summary>Opens a view as a new root window of the application</summary> /// <typeparam name="TViewModel">
/// <typeparam name="TViewModel"> /// Type of view model a root window will be opened for
/// Type of view model a root window will be opened for /// </typeparam>
/// </typeparam> /// <param name="viewModel">
/// <param name="viewModel"> /// View model a window will be opened for. If null, the view model will be
/// 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)
/// created as well (unless the dialog already specifies one as a resource) /// </param>
/// </param> /// <param name="disposeOnClose">
/// <param name="disposeOnClose"> /// Whether the view model should be disposed when the view is closed
/// Whether the view model should be disposed when the view is closed /// </param>
/// </param> /// <returns>The window that has been opened by the window manager</returns>
/// <returns>The window that has been opened by the window manager</returns> Form OpenRoot<TViewModel>(
Form OpenRoot<TViewModel>( TViewModel viewModel = null, bool disposeOnClose = true
TViewModel viewModel = null, bool disposeOnClose = true ) where TViewModel : class;
) where TViewModel : class;
/// <summary>Displays a view as a modal window</summary>
/// <summary>Displays a view as a modal window</summary> /// <typeparam name="TViewModel">
/// <typeparam name="TViewModel"> /// Type of the view model for which a view will be displayed
/// Type of the view model for which a view will be displayed /// </typeparam>
/// </typeparam> /// <param name="viewModel">
/// <param name="viewModel"> /// View model a modal window will be displayed for. If null, the view model will
/// 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)
/// be created as well (unless the dialog already specifies one as a resource) /// </param>
/// </param> /// <param name="disposeOnClose">
/// <param name="disposeOnClose"> /// Whether the view model should be disposed when the view is closed
/// Whether the view model should be disposed when the view is closed /// </param>
/// </param> /// <returns>The return value of the modal window</returns>
/// <returns>The return value of the modal window</returns> bool? ShowModal<TViewModel>(
bool? ShowModal<TViewModel>( TViewModel viewModel = null, bool disposeOnClose = true
TViewModel viewModel = null, bool disposeOnClose = true ) where TViewModel : class;
) where TViewModel : class;
/// <summary>Creates the view for the specified view model</summary>
/// <summary>Creates the view for the specified view model</summary> /// <typeparam name="TViewModel">
/// <typeparam name="TViewModel"> /// Type of view model for which a view will be created
/// Type of view model for which a view will be created /// </typeparam>
/// </typeparam> /// <param name="viewModel">
/// <param name="viewModel"> /// View model a view will be created for. If null, the view model will be
/// 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)
/// created as well (unless the dialog already specifies one as a resource) /// </param>
/// </param> /// <returns>The view for the specified view model</returns>
/// <returns>The view for the specified view model</returns> Control CreateView<TViewModel>(TViewModel viewModel = null)
Control CreateView<TViewModel>(TViewModel viewModel = null) where TViewModel : class;
where TViewModel : class;
/// <summary>Creates a view model without a matching view</summary>
/// <summary>Creates a view model without a matching view</summary> /// <typeparam name="TViewModel">Type of view model that will be created</typeparam>
/// <typeparam name="TViewModel">Type of view model that will be created</typeparam> /// <returns>The new view model</returns>
/// <returns>The new view model</returns> /// <remarks>
/// <remarks> /// <para>
/// <para> /// This is useful if a view model needs to create child view models (i.e. paged container
/// 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
/// and wants to ensure the same dependency injector (if any) if used as the window /// manager uses for other view models it creates.
/// manager uses for other view models it creates. /// </para>
/// </para> /// <para>
/// <para> /// This way, view models can set up their child view models without having to immediately
/// 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
/// 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.
/// child view and store it in a container. /// </para>
/// </para> /// </remarks>
/// </remarks> TViewModel CreateViewModel<TViewModel>()
TViewModel CreateViewModel<TViewModel>() where TViewModel : class;
where TViewModel : class;
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,157 +1,156 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>
/// <summary> /// Proxy stand-in to delay checking for the main window until it has been created
/// Proxy stand-in to delay checking for the main window until it has been created /// </summary>
/// </summary> /// <remarks>
/// <remarks> /// <para>
/// <para> /// The issue: when the view model for the main window is created, the main window
/// 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
/// may only exist as a .NET object, without the underlying operating system window /// (done in <see cref="System.Windows.Forms.Control.CreateControl" /> checkable via
/// (done in <see cref="System.Windows.Forms.Control.CreateControl" /> checkable via /// <see cref="System.Windows.Forms.Control.IsHandleCreated" />). Not only will things
/// <see cref="System.Windows.Forms.Control.IsHandleCreated" />). Not only will things /// like <see cref="System.Windows.Forms.Control.Invoke(Delegate)" /> fail, we can't
/// like <see cref="System.Windows.Forms.Control.Invoke(Delegate)" /> fail, we can't /// even locate the main window at that stage.
/// even locate the main window at that stage. /// </para>
/// </para> /// <para>
/// <para> /// Thus, if the main window cannot be found at the time a view model is created,
/// 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
/// 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.
/// the main window only when something needs to be executed in the UI thread. /// </para>
/// </para> /// </remarks>
/// </remarks> class LateCheckedSynchronizer : ISynchronizeInvoke {
class LateCheckedSynchronizer : ISynchronizeInvoke {
/// <summary>Initializes a new late-checked main window synchronizer</summary>
/// <summary>Initializes a new late-checked main window synchronizer</summary> /// <param name="uiContextFoundCallback"></param>
/// <param name="uiContextFoundCallback"></param> public LateCheckedSynchronizer(Action<ISynchronizeInvoke> uiContextFoundCallback) {
public LateCheckedSynchronizer(Action<ISynchronizeInvoke> uiContextFoundCallback) { this.uiContextFoundCallback = uiContextFoundCallback;
this.uiContextFoundCallback = uiContextFoundCallback; }
}
/// <summary>Finds the application's main window</summary>
/// <summary>Finds the application's main window</summary> /// <returns>Main window of the application or null if none has been created</returns>
/// <returns>Main window of the application or null if none has been created</returns> /// <remarks>
/// <remarks> /// The application's main window, if it has been created yet
/// The application's main window, if it has been created yet /// </remarks>
/// </remarks> public static Form GetMainWindow() {
public static Form GetMainWindow() { IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
// We can get two things: a list of all open windows and the handle of
// 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
// the window that the process has registered as main window. Use the latter // to pick the correct window from the former.
// to pick the correct window from the former. FormCollection openForms = Application.OpenForms;
FormCollection openForms = Application.OpenForms; int openFormCount = openForms.Count;
int openFormCount = openForms.Count; for(int index = 0; index < openFormCount; ++index) {
for(int index = 0; index < openFormCount; ++index) { Form form = openForms[index];
Form form = openForms[index];
IntPtr handle;
IntPtr handle; if(form.InvokeRequired) {
if(form.InvokeRequired) { handle = (IntPtr)form.Invoke(new Func<Form, IntPtr>(getWindowHandle), form);
handle = (IntPtr)form.Invoke(new Func<Form, IntPtr>(getWindowHandle), form); } else {
} else { handle = getWindowHandle(form);
handle = getWindowHandle(form); }
} if(handle != IntPtr.Zero) {
if(handle != IntPtr.Zero) { if(handle == mainWindowHandle) {
if(handle == mainWindowHandle) { return form;
return form; }
} }
} }
}
// No matching main window found: use the first one in good faith or fail.
// No matching main window found: use the first one in good faith or fail. if(openFormCount > 0) {
if(openFormCount > 0) { return openForms[0];
return openForms[0]; } else {
} else { return null;
return null; }
} }
}
/// <summary>Checks whether the calling thread needs to use Invoke()</summary>
/// <summary>Checks whether the calling thread needs to use Invoke()</summary> public bool InvokeRequired {
public bool InvokeRequired { get { return getMainWindowOrFail().InvokeRequired; }
get { return getMainWindowOrFail().InvokeRequired; } }
}
/// <summary>Schedules a method to be run by the main UI thread</summary>
/// <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="method">Method that will be scheduled to run</param> /// <param name="args">Arguments that will be passed to the method</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>
/// <returns>An asynchronous result handle that can be used to track the call</returns> public IAsyncResult BeginInvoke(Delegate method, object[] args) {
public IAsyncResult BeginInvoke(Delegate method, object[] args) { return getMainWindowOrFail().BeginInvoke(method, args);
return getMainWindowOrFail().BeginInvoke(method, args); }
}
/// <summary>Waits for a call scheduled on the main UI thread to complete</summary>
/// <summary>Waits for a call scheduled on the main UI thread to complete</summary> /// <param name="result">Asynchronous result handle returned by BeginInvoke()</param>
/// <param name="result">Asynchronous result handle returned by BeginInvoke()</param> /// <returns>The value returned by the method ran in the main UI thread</returns>
/// <returns>The value returned by the method ran in the main UI thread</returns> public object EndInvoke(IAsyncResult result) {
public object EndInvoke(IAsyncResult result) { return getMainWindowOrFail().EndInvoke(result);
return getMainWindowOrFail().EndInvoke(result); }
}
/// <summary>Executes a method on the main UI thread and waits for it to complete</summary>
/// <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="method">Method that will be run by the main UI thread</param> /// <param name="arguments">Arguments that will be passed to the method</param>
/// <param name="arguments">Arguments that will be passed to the method</param> /// <returns>The value returned by the method</returns>
/// <returns>The value returned by the method</returns> public object Invoke(Delegate method, object[] arguments) {
public object Invoke(Delegate method, object[] arguments) { return getMainWindowOrFail().Invoke(method, arguments);
return getMainWindowOrFail().Invoke(method, arguments); }
}
/// <summary>Retrieves the application's current main window</summary>
/// <summary>Retrieves the application's current main window</summary> /// <returns>The application's current main window</returns>
/// <returns>The application's current main window</returns> /// <remarks>
/// <remarks> /// If there is no main window, an exception will be thrown
/// If there is no main window, an exception will be thrown /// </remarks>
/// </remarks> private Form getMainWindowOrFail() {
private Form getMainWindowOrFail() { Form mainWindow = GetMainWindow();
Form mainWindow = GetMainWindow(); if(mainWindow == null) {
if(mainWindow == null) { throw new InvalidOperationException(
throw new InvalidOperationException( "Could not schedule work for the UI thread because no WinForms UI main window " +
"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 " +
"was found. Create a main window first or specify the UI synchronization context " + "explicitly to the view model."
"explicitly to the view model." );
); }
}
if(this.uiContextFoundCallback != null) {
if(this.uiContextFoundCallback != null) { this.uiContextFoundCallback(mainWindow);
this.uiContextFoundCallback(mainWindow); this.uiContextFoundCallback = null;
this.uiContextFoundCallback = null; }
}
return mainWindow;
return mainWindow; }
}
/// <summary>Returns a Form's window handle without forcing its creation</summary>
/// <summary>Returns a Form's window handle without forcing its creation</summary> /// <param name="form">Form whose window handle will be returned</param>
/// <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>
/// <returns>The form's window handle of IntPtr.Zero if it has none</returns> private static IntPtr getWindowHandle(Form form) {
private static IntPtr getWindowHandle(Form form) { if(form.IsHandleCreated) {
if(form.IsHandleCreated) { return form.Handle;
return form.Handle; } else {
} else { return IntPtr.Zero;
return IntPtr.Zero; }
} }
}
/// <summary>Called when the late-checked synchronizer finds the main window</summary>
/// <summary>Called when the late-checked synchronizer finds the main window</summary> private Action<ISynchronizeInvoke> uiContextFoundCallback;
private Action<ISynchronizeInvoke> uiContextFoundCallback;
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,55 +1,54 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms.Messages {
namespace Nuclex.Windows.Forms.Messages {
/// <summary>Performs simple user interaction</summary>
/// <summary>Performs simple user interaction</summary> /// <remarks>
/// <remarks> /// Methods provided by this service can be covered using plain old message boxes
/// 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.
/// and do not require special dialogs or calls to the task dialog API. /// </remarks>
/// </remarks> public interface IMessageService {
public interface IMessageService {
/// <summary>Triggered when a message is about to be displayed to the user</summary>
/// <summary>Triggered when a message is about to be displayed to the user</summary> event EventHandler<MessageEventArgs> MessageDisplaying;
event EventHandler<MessageEventArgs> MessageDisplaying;
/// <summary>Triggered when the user has acknowledged the current message</summary>
/// <summary>Triggered when the user has acknowledged the current message</summary> event EventHandler MessageAcknowledged;
event EventHandler MessageAcknowledged;
/// <summary>Asks the user a question that can be answered via several buttons</summary>
/// <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="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="text">Text that will be shown to the user</param> /// <param name="buttons">Buttons available for the user to click on</param>
/// <param name="buttons">Buttons available for the user to click on</param> /// <returns>The button the user has clicked on</returns>
/// <returns>The button the user has clicked on</returns> DialogResult ShowQuestion(
DialogResult ShowQuestion( MessageBoxIcon image, MessageText text, MessageBoxButtons buttons
MessageBoxIcon image, MessageText text, MessageBoxButtons buttons );
);
/// <summary>Displays a notification to the user</summary>
/// <summary>Displays a notification to the user</summary> /// <param name="image">Image that will be shown on the message bx</param>
/// <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>
/// <param name="text">Text that will be shown to the user</param> void ShowNotification(MessageBoxIcon image, MessageText text);
void ShowNotification(MessageBoxIcon image, MessageText text);
}
}
} // namespace Nuclex.Windows.Forms.Messages
} // namespace Nuclex.Windows.Forms.Messages

View File

@ -1,49 +1,48 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
using NUnit.Framework;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.Messages {
namespace Nuclex.Windows.Forms.Messages {
/// <summary>Unit tests for the message box event argument container</summary>
/// <summary>Unit tests for the message box event argument container</summary> [TestFixture]
[TestFixture] internal class MessageEventArgsTest {
internal class MessageEventArgsTest {
/// <summary>Verifies that the image associated with the message gets stored</summary>
/// <summary>Verifies that the image associated with the message gets stored</summary> [Test]
[Test] public void ImageIsStored() {
public void ImageIsStored() { var arguments = new MessageEventArgs(MessageBoxIcon.Exclamation, null);
var arguments = new MessageEventArgs(MessageBoxIcon.Exclamation, null); Assert.AreEqual(MessageBoxIcon.Exclamation, arguments.Image);
Assert.AreEqual(MessageBoxIcon.Exclamation, arguments.Image); }
}
/// <summary>Verifies that the text associated with the message gets stored</summary>
/// <summary>Verifies that the text associated with the message gets stored</summary> [Test]
[Test] public void TextIsStored() {
public void TextIsStored() { var text = new MessageText();
var text = new MessageText(); var arguments = new MessageEventArgs(MessageBoxIcon.None, text);
var arguments = new MessageEventArgs(MessageBoxIcon.None, text); Assert.AreSame(text, arguments.Text);
Assert.AreSame(text, arguments.Text); }
}
}
}
} // namespace Nuclex.Windows.Forms.Messages
} // namespace Nuclex.Windows.Forms.Messages

View File

@ -1,54 +1,53 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms.Messages {
namespace Nuclex.Windows.Forms.Messages {
/// <summary>Provides a displayed message and its severity to event subscribers</summary>
/// <summary>Provides a displayed message and its severity to event subscribers</summary> public class MessageEventArgs : EventArgs {
public class MessageEventArgs : EventArgs {
/// <summary>Initializes a new message box event argument container</summary>
/// <summary>Initializes a new message box event argument container</summary> /// <param name="image">Image the message box will be displaying</param>
/// <param name="image">Image the message box will be displaying</param> /// <param name="text">Text that will be displayed in the message box</param>
/// <param name="text">Text that will be displayed in the message box</param> public MessageEventArgs(MessageBoxIcon image, MessageText text) {
public MessageEventArgs(MessageBoxIcon image, MessageText text) { this.image = image;
this.image = image; this.text = text;
this.text = text; }
}
/// <summary>Image that indicates the severity of the message being displayed</summary>
/// <summary>Image that indicates the severity of the message being displayed</summary> public MessageBoxIcon Image {
public MessageBoxIcon Image { get { return this.image; }
get { return this.image; } }
}
/// <summary>Text that is being displayed in the message box</summary>
/// <summary>Text that is being displayed in the message box</summary> public MessageText Text {
public MessageText Text { get { return this.text; }
get { return this.text; } }
}
/// <summary>Image that indicates the severity of the message being displayed</summary>
/// <summary>Image that indicates the severity of the message being displayed</summary> private MessageBoxIcon image;
private MessageBoxIcon image; /// <summary>Text that is being displayed in the message box</summary>
/// <summary>Text that is being displayed in the message box</summary> private MessageText text;
private MessageText text;
}
}
} // namespace Nuclex.Windows.Forms.Messages
} // namespace Nuclex.Windows.Forms.Messages

View File

@ -1,108 +1,107 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms.Messages {
namespace Nuclex.Windows.Forms.Messages {
/// <summary>Contains helper methods for the message service</summary>
/// <summary>Contains helper methods for the message service</summary> public static class MessageServiceHelper {
public static class MessageServiceHelper {
/// <summary>Asks the user a question that can be answered with yes or no</summary>
/// <summary>Asks the user a question that can be answered with yes or no</summary> /// <param name="messageService">
/// <param name="messageService"> /// Message service that will be used to display the question
/// Message service that will be used to display the question /// </param>
/// </param> /// <param name="text">Text that will be shown on the message box</param>
/// <param name="text">Text that will be shown on the message box</param> /// <returns>The button the user has clicked on</returns>
/// <returns>The button the user has clicked on</returns> public static DialogResult AskYesNo(
public static DialogResult AskYesNo( this IMessageService messageService, MessageText text
this IMessageService messageService, MessageText text ) {
) { return messageService.ShowQuestion(
return messageService.ShowQuestion( MessageBoxIcon.Question, text, MessageBoxButtons.YesNo
MessageBoxIcon.Question, text, MessageBoxButtons.YesNo );
); }
}
/// <summary>Asks the user a question that can be answered with ok or cancel</summary>
/// <summary>Asks the user a question that can be answered with ok or cancel</summary> /// <param name="messageService">
/// <param name="messageService"> /// Message service that will be used to display the question
/// Message service that will be used to display the question /// </param>
/// </param> /// <param name="text">Text that will be shown on the message box</param>
/// <param name="text">Text that will be shown on the message box</param> /// <returns>The button the user has clicked on</returns>
/// <returns>The button the user has clicked on</returns> public static DialogResult AskOkCancel(
public static DialogResult AskOkCancel( this IMessageService messageService, MessageText text
this IMessageService messageService, MessageText text ) {
) { return messageService.ShowQuestion(
return messageService.ShowQuestion( MessageBoxIcon.Question, text, MessageBoxButtons.OKCancel
MessageBoxIcon.Question, text, MessageBoxButtons.OKCancel );
); }
}
/// <summary>
/// <summary> /// Asks the user a question that can be answered with yes, no or cancel
/// Asks the user a question that can be answered with yes, no or cancel /// </summary>
/// </summary> /// <param name="messageService">
/// <param name="messageService"> /// Message service that will be used to display the question
/// Message service that will be used to display the question /// </param>
/// </param> /// <param name="text">Text that will be shown on the message box</param>
/// <param name="text">Text that will be shown on the message box</param> /// <returns>The button the user has clicked on</returns>
/// <returns>The button the user has clicked on</returns> public static DialogResult AskYesNoCancel(
public static DialogResult AskYesNoCancel( this IMessageService messageService, MessageText text
this IMessageService messageService, MessageText text ) {
) { return messageService.ShowQuestion(
return messageService.ShowQuestion( MessageBoxIcon.Question, text, MessageBoxButtons.YesNoCancel
MessageBoxIcon.Question, text, MessageBoxButtons.YesNoCancel );
); }
}
/// <summary>Displays an informative message</summary>
/// <summary>Displays an informative message</summary> /// <param name="messageService">
/// <param name="messageService"> /// Message service that will be used to display the warning
/// Message service that will be used to display the warning /// </param>
/// </param> /// <param name="text">Text to be displayed on the warning message</param>
/// <param name="text">Text to be displayed on the warning message</param> public static void Inform(
public static void Inform( this IMessageService messageService, MessageText text
this IMessageService messageService, MessageText text ) {
) { messageService.ShowNotification(MessageBoxIcon.Information, text);
messageService.ShowNotification(MessageBoxIcon.Information, text); }
}
/// <summary>Displays a warning</summary>
/// <summary>Displays a warning</summary> /// <param name="messageService">
/// <param name="messageService"> /// Message service that will be used to display the warning
/// Message service that will be used to display the warning /// </param>
/// </param> /// <param name="text">Text to be displayed on the warning message</param>
/// <param name="text">Text to be displayed on the warning message</param> public static void Warn(
public static void Warn( this IMessageService messageService, MessageText text
this IMessageService messageService, MessageText text ) {
) { messageService.ShowNotification(MessageBoxIcon.Warning, text);
messageService.ShowNotification(MessageBoxIcon.Warning, text); }
}
/// <summary>Reports an error</summary>
/// <summary>Reports an error</summary> /// <param name="messageService">
/// <param name="messageService"> /// Message service that will be used to display the warning
/// Message service that will be used to display the warning /// </param>
/// </param> /// <param name="text">Text to be displayed on the warning message</param>
/// <param name="text">Text to be displayed on the warning message</param> public static void ReportError(
public static void ReportError( this IMessageService messageService, MessageText text
this IMessageService messageService, MessageText text ) {
) { messageService.ShowNotification(MessageBoxIcon.Error, text);
messageService.ShowNotification(MessageBoxIcon.Error, text); }
}
}
}
} // namespace Nuclex.Windows.Forms.Messages
} // namespace Nuclex.Windows.Forms.Messages

View File

@ -1,51 +1,50 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows;
using System.Windows;
using NUnit.Framework;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.Messages {
namespace Nuclex.Windows.Forms.Messages {
/// <summary>Unit tests for the message text container</summary>
/// <summary>Unit tests for the message text container</summary> [TestFixture]
[TestFixture] internal class MessageTextTest {
internal class MessageTextTest {
/// <summary>Ensures that the message text class provides a copy constructor</summary>
/// <summary>Ensures that the message text class provides a copy constructor</summary> [Test]
[Test] public void HasCopyConstructor() {
public void HasCopyConstructor() { var text = new MessageText() {
var text = new MessageText() { Caption = "Caption",
Caption = "Caption", Message = "Message",
Message = "Message", Details = "Details",
Details = "Details", ExpandedDetails = "ExpandedDetails"
ExpandedDetails = "ExpandedDetails" };
}; var copy = new MessageText(text);
var copy = new MessageText(text);
Assert.AreEqual(text.Caption, copy.Caption);
Assert.AreEqual(text.Caption, copy.Caption); Assert.AreEqual(text.Message, copy.Message);
Assert.AreEqual(text.Message, copy.Message); Assert.AreEqual(text.Details, copy.Details);
Assert.AreEqual(text.Details, copy.Details); Assert.AreEqual(text.ExpandedDetails, copy.ExpandedDetails);
Assert.AreEqual(text.ExpandedDetails, copy.ExpandedDetails); }
}
}
}
} // namespace Nuclex.Windows.Forms.Messages
} // namespace Nuclex.Windows.Forms.Messages

View File

@ -1,55 +1,54 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System;
namespace Nuclex.Windows.Forms.Messages {
namespace Nuclex.Windows.Forms.Messages {
/// <summary>Text that will be displayed in a message box</summary>
/// <summary>Text that will be displayed in a message box</summary> public class MessageText {
public class MessageText {
/// <summary>Initializs a new message text</summary>
/// <summary>Initializs a new message text</summary> public MessageText() { }
public MessageText() { }
/// <summary>Initializes a new message text by copying another instance</summary>
/// <summary>Initializes a new message text by copying another instance</summary> /// <param name="other">Instance that will be copied</param>
/// <param name="other">Instance that will be copied</param> public MessageText(MessageText other) {
public MessageText(MessageText other) { Caption = other.Caption;
Caption = other.Caption; Message = other.Message;
Message = other.Message; Details = other.Details;
Details = other.Details; ExpandedDetails = other.ExpandedDetails;
ExpandedDetails = other.ExpandedDetails; }
}
/// <summary>The caption used when the is displayed in a message box</summary>
/// <summary>The caption used when the is displayed in a message box</summary> public string Caption { get; set; }
public string Caption { get; set; } /// <summary>Main message being displayed to the user</summary>
/// <summary>Main message being displayed to the user</summary> public string Message { get; set; }
public string Message { get; set; } /// <summary>Message details shown below the main message</summary>
/// <summary>Message details shown below the main message</summary> public string Details { get; set; }
public string Details { get; set; } /// <summary>
/// <summary> /// Additional informations the user can display by expanding
/// Additional informations the user can display by expanding /// the message dialog. Can be null, in which case the message dialog
/// the message dialog. Can be null, in which case the message dialog /// will not be expandable.
/// will not be expandable. /// </summary>
/// </summary> public string ExpandedDetails { get; set; }
public string ExpandedDetails { get; set; }
}
}
} // namespace Nuclex.Windows.Forms.Messages
} // namespace Nuclex.Windows.Forms.Messages

View File

@ -1,162 +1,161 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms.Messages {
namespace Nuclex.Windows.Forms.Messages {
/// <summary>Uses task dialogs to display message boxes</summary>
/// <summary>Uses task dialogs to display message boxes</summary> public class StandardMessageBoxManager : IMessageService {
public class StandardMessageBoxManager : IMessageService {
#region class MessageScope
#region class MessageScope
/// <summary>Triggers the message displayed and acknowledged events</summary>
/// <summary>Triggers the message displayed and acknowledged events</summary> private class MessageScope : IDisposable {
private class MessageScope : IDisposable {
/// <summary>
/// <summary> /// Initializes a new message scope, triggering the message displayed event
/// Initializes a new message scope, triggering the message displayed event /// </summary>
/// </summary> /// <param name="self">Message service the scope belongs to</param>
/// <param name="self">Message service the scope belongs to</param> /// <param name="image">Image of the message being displayed</param>
/// <param name="image">Image of the message being displayed</param> /// <param name="text">Text contained in the message being displayed</param>
/// <param name="text">Text contained in the message being displayed</param> public MessageScope(
public MessageScope( StandardMessageBoxManager self, MessageBoxIcon image, MessageText text
StandardMessageBoxManager self, MessageBoxIcon image, MessageText text ) {
) { EventHandler<MessageEventArgs> messageDisplayed = self.MessageDisplaying;
EventHandler<MessageEventArgs> messageDisplayed = self.MessageDisplaying; if(messageDisplayed != null) {
if(messageDisplayed != null) { messageDisplayed(this, new MessageEventArgs(image, text));
messageDisplayed(this, new MessageEventArgs(image, text)); }
}
this.self = self;
this.self = self; }
}
/// <summary>Triggers the message acknowledged event</summary>
/// <summary>Triggers the message acknowledged event</summary> public void Dispose() {
public void Dispose() { EventHandler messageAcknowledged = self.MessageAcknowledged;
EventHandler messageAcknowledged = self.MessageAcknowledged; if(messageAcknowledged != null) {
if(messageAcknowledged != null) { messageAcknowledged(this, EventArgs.Empty);
messageAcknowledged(this, EventArgs.Empty); }
} }
}
/// <summary>Message service the scope belongs to</summary>
/// <summary>Message service the scope belongs to</summary> private StandardMessageBoxManager self;
private StandardMessageBoxManager self;
}
}
#endregion // class MessageScope
#endregion // class MessageScope
/// <summary>Delegate for the standard message box show function</summary>
/// <summary>Delegate for the standard message box show function</summary> /// <param name="owner">Window that will modally display the message box</param>
/// <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="text">Text that will be presented to the user</param> /// <param name="caption">Contents of the message box' title bar</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="buttons">Buttons available for the user to choose from</param> /// <param name="icon">Icon that will be displayed next to the text</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>
/// <returns>The choice made by the user if multiple buttons were provided</returns> private delegate DialogResult ShowMessageBoxDelegate(
private delegate DialogResult ShowMessageBoxDelegate( IWin32Window owner,
IWin32Window owner, string text,
string text, string caption,
string caption, MessageBoxButtons buttons,
MessageBoxButtons buttons, MessageBoxIcon icon
MessageBoxIcon icon );
);
/// <summary>Triggered when a message is displayed to the user</summary>
/// <summary>Triggered when a message is displayed to the user</summary> public event EventHandler<MessageEventArgs> MessageDisplaying;
public event EventHandler<MessageEventArgs> MessageDisplaying;
/// <summary>Triggered when the user has acknowledged the current message</summary>
/// <summary>Triggered when the user has acknowledged the current message</summary> public event EventHandler MessageAcknowledged;
public event EventHandler MessageAcknowledged;
/// <summary>Initializes a new task dialog message service</summary>
/// <summary>Initializes a new task dialog message service</summary> public StandardMessageBoxManager() : this(NullActiveWindowTracker.Default) { }
public StandardMessageBoxManager() : this(NullActiveWindowTracker.Default) { }
/// <summary>Initializes a new task dialog message service</summary>
/// <summary>Initializes a new task dialog message service</summary> /// <param name="tracker">
/// <param name="tracker"> /// Active window tracker used to obtain the parent window for message boxes
/// Active window tracker used to obtain the parent window for message boxes /// </param>
/// </param> public StandardMessageBoxManager(IActiveWindowTracker tracker) {
public StandardMessageBoxManager(IActiveWindowTracker tracker) { this.tracker = tracker;
this.tracker = tracker; this.showMessageDelegate = new ShowMessageBoxDelegate(MessageBox.Show);
this.showMessageDelegate = new ShowMessageBoxDelegate(MessageBox.Show); }
}
/// <summary>Asks the user a question that can be answered via several buttons</summary>
/// <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="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="text">Text that will be shown to the user</param> /// <param name="buttons">Buttons available for the user to click on</param>
/// <param name="buttons">Buttons available for the user to click on</param> /// <returns>The button the user has clicked on</returns>
/// <returns>The button the user has clicked on</returns> public DialogResult ShowQuestion(
public DialogResult ShowQuestion( MessageBoxIcon image, MessageText text, MessageBoxButtons buttons
MessageBoxIcon image, MessageText text, MessageBoxButtons buttons ) {
) { using(var scope = new MessageScope(this, image, text)) {
using(var scope = new MessageScope(this, image, text)) { return showMessageBoxInActiveUiThread(
return showMessageBoxInActiveUiThread( text.Message,
text.Message, text.Caption,
text.Caption, buttons,
buttons, image
image );
); }
} }
}
/// <summary>Displays a notification to the user</summary>
/// <summary>Displays a notification to the user</summary> /// <param name="image">Image that will be shown on the message bx</param>
/// <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>
/// <param name="text">Text that will be shown to the user</param> public void ShowNotification(MessageBoxIcon image, MessageText text) {
public void ShowNotification(MessageBoxIcon image, MessageText text) { using(var scope = new MessageScope(this, image, text)) {
using(var scope = new MessageScope(this, image, text)) { showMessageBoxInActiveUiThread(
showMessageBoxInActiveUiThread( text.Message,
text.Message, text.Caption,
text.Caption, MessageBoxButtons.OK,
MessageBoxButtons.OK, image
image );
); }
} }
}
/// <summary>Displays the message box in the active view's thread</summary>
/// <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="message">Text that will be presented to the user</param> /// <param name="caption">Contents of the message box' title bar</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="buttons">Buttons available for the user to choose from</param> /// <param name="image">Image that will be displayed next to the text</param>
/// <param name="image">Image that will be displayed next to the text</param> /// <returns></returns>
/// <returns></returns> private DialogResult showMessageBoxInActiveUiThread(
private DialogResult showMessageBoxInActiveUiThread( string message,
string message, string caption,
string caption, MessageBoxButtons buttons,
MessageBoxButtons buttons, MessageBoxIcon image
MessageBoxIcon image ) {
) { Form mainWindow = this.tracker.ActiveWindow;
Form mainWindow = this.tracker.ActiveWindow; if(mainWindow != null) {
if(mainWindow != null) { return (DialogResult)mainWindow.Invoke(
return (DialogResult)mainWindow.Invoke( this.showMessageDelegate,
this.showMessageDelegate, (IWin32Window)mainWindow, message, caption, buttons, image
(IWin32Window)mainWindow, message, caption, buttons, image );
); }
}
// No window tracker or unknown main window -- just show the message box
// No window tracker or unknown main window -- just show the message box return MessageBox.Show(message, caption, buttons, image);
return MessageBox.Show(message, caption, buttons, image); }
}
/// <summary>Provides the currently active top-level window</summary>
/// <summary>Provides the currently active top-level window</summary> private IActiveWindowTracker tracker;
private IActiveWindowTracker tracker; /// <summary>Delegate for the MessageBox.Show() method</summary>
/// <summary>Delegate for the MessageBox.Show() method</summary> private ShowMessageBoxDelegate showMessageDelegate;
private ShowMessageBoxDelegate showMessageDelegate;
}
}
} // namespace Nuclex.Windows.Forms.Messages
} // namespace Nuclex.Windows.Forms.Messages

View File

@ -1,37 +1,36 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Dummy implementation of the active window tracker service</summary>
/// <summary>Dummy implementation of the active window tracker service</summary> internal class NullActiveWindowTracker : IActiveWindowTracker {
internal class NullActiveWindowTracker : IActiveWindowTracker {
/// <summary>The default instance of the dummy window tracker</summary>
/// <summary>The default instance of the dummy window tracker</summary> public static readonly NullActiveWindowTracker Default = new NullActiveWindowTracker();
public static readonly NullActiveWindowTracker Default = new NullActiveWindowTracker();
/// <summary>The currently active top-level or modal window</summary>
/// <summary>The currently active top-level or modal window</summary> public Form ActiveWindow { get { return null; } }
public Form ActiveWindow { get { return null; } }
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,120 +1,119 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms { partial class ProgressReporterForm {
partial class ProgressReporterForm { /// <summary>
/// <summary> /// Required designer variable.
/// Required designer variable. /// </summary>
/// </summary> private System.ComponentModel.IContainer components = null;
private System.ComponentModel.IContainer components = null;
/// <summary>
/// <summary> /// Clean up any resources being used.
/// Clean up any resources being used. /// </summary>
/// </summary> /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> protected override void Dispose(bool disposing) {
protected override void Dispose(bool disposing) { if(disposing && (components != null)) {
if(disposing && (components != null)) { components.Dispose();
components.Dispose(); }
} base.Dispose(disposing);
base.Dispose(disposing); }
}
#region Windows Form Designer generated code
#region Windows Form Designer generated code
/// <summary>
/// <summary> /// Required method for Designer support - do not modify
/// Required method for Designer support - do not modify /// the contents of this method with the code editor.
/// the contents of this method with the code editor. /// </summary>
/// </summary> private void InitializeComponent() {
private void InitializeComponent() { this.components = new System.ComponentModel.Container();
this.components = new System.ComponentModel.Container(); this.cancelButton = new System.Windows.Forms.Button();
this.cancelButton = new System.Windows.Forms.Button(); this.progressBar = new Nuclex.Windows.Forms.AsyncProgressBar();
this.progressBar = new Nuclex.Windows.Forms.AsyncProgressBar(); this.statusLabel = new System.Windows.Forms.Label();
this.statusLabel = new System.Windows.Forms.Label(); this.controlCreationTimer = new System.Windows.Forms.Timer(this.components);
this.controlCreationTimer = new System.Windows.Forms.Timer(this.components); this.SuspendLayout();
this.SuspendLayout(); //
// // cancelButton
// cancelButton //
// this.cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Top;
this.cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Top; this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; this.cancelButton.Location = new System.Drawing.Point(151, 55);
this.cancelButton.Location = new System.Drawing.Point(151, 55); this.cancelButton.Name = "cancelButton";
this.cancelButton.Name = "cancelButton"; this.cancelButton.Size = new System.Drawing.Size(75, 23);
this.cancelButton.Size = new System.Drawing.Size(75, 23); this.cancelButton.TabIndex = 0;
this.cancelButton.TabIndex = 0; this.cancelButton.Text = "&Cancel";
this.cancelButton.Text = "&Cancel"; this.cancelButton.UseVisualStyleBackColor = true;
this.cancelButton.UseVisualStyleBackColor = true; this.cancelButton.Click += new System.EventHandler(this.cancelClicked);
this.cancelButton.Click += new System.EventHandler(this.cancelClicked); //
// // progressBar
// progressBar //
// this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
| System.Windows.Forms.AnchorStyles.Right))); this.progressBar.Location = new System.Drawing.Point(12, 26);
this.progressBar.Location = new System.Drawing.Point(12, 26); this.progressBar.Name = "progressBar";
this.progressBar.Name = "progressBar"; this.progressBar.Size = new System.Drawing.Size(352, 23);
this.progressBar.Size = new System.Drawing.Size(352, 23); this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee;
this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; this.progressBar.TabIndex = 1;
this.progressBar.TabIndex = 1; //
// // statusLabel
// statusLabel //
// this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
| System.Windows.Forms.AnchorStyles.Right))); this.statusLabel.Location = new System.Drawing.Point(12, 9);
this.statusLabel.Location = new System.Drawing.Point(12, 9); this.statusLabel.Name = "statusLabel";
this.statusLabel.Name = "statusLabel"; this.statusLabel.Size = new System.Drawing.Size(352, 14);
this.statusLabel.Size = new System.Drawing.Size(352, 14); this.statusLabel.TabIndex = 2;
this.statusLabel.TabIndex = 2; this.statusLabel.Text = "Please Wait...";
this.statusLabel.Text = "Please Wait..."; this.statusLabel.TextAlign = System.Drawing.ContentAlignment.TopCenter;
this.statusLabel.TextAlign = System.Drawing.ContentAlignment.TopCenter; //
// // controlCreationTimer
// controlCreationTimer //
// this.controlCreationTimer.Enabled = true;
this.controlCreationTimer.Enabled = true; this.controlCreationTimer.Interval = 1;
this.controlCreationTimer.Interval = 1; this.controlCreationTimer.Tick += new System.EventHandler(this.controlCreationTimerTicked);
this.controlCreationTimer.Tick += new System.EventHandler(this.controlCreationTimerTicked); //
// // ProgressReporterForm
// ProgressReporterForm //
// this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.cancelButton;
this.CancelButton = this.cancelButton; this.ClientSize = new System.Drawing.Size(376, 90);
this.ClientSize = new System.Drawing.Size(376, 90); this.ControlBox = false;
this.ControlBox = false; this.Controls.Add(this.statusLabel);
this.Controls.Add(this.statusLabel); this.Controls.Add(this.progressBar);
this.Controls.Add(this.progressBar); this.Controls.Add(this.cancelButton);
this.Controls.Add(this.cancelButton); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; this.MaximizeBox = false;
this.MaximizeBox = false; this.MinimizeBox = false;
this.MinimizeBox = false; this.Name = "ProgressReporterForm";
this.Name = "ProgressReporterForm"; this.ShowIcon = false;
this.ShowIcon = false; this.ShowInTaskbar = false;
this.ShowInTaskbar = false; this.Text = "Progress";
this.Text = "Progress"; this.ResumeLayout(false);
this.ResumeLayout(false);
}
}
#endregion
#endregion
private System.Windows.Forms.Button cancelButton;
private System.Windows.Forms.Button cancelButton; private Nuclex.Windows.Forms.AsyncProgressBar progressBar;
private Nuclex.Windows.Forms.AsyncProgressBar progressBar; private System.Windows.Forms.Label statusLabel;
private System.Windows.Forms.Label statusLabel; private System.Windows.Forms.Timer controlCreationTimer;
private System.Windows.Forms.Timer controlCreationTimer; }
}
} }

View File

@ -1,273 +1,272 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Threading;
using System.Threading; using System.Windows.Forms;
using System.Windows.Forms;
using Nuclex.Support.Scheduling;
using Nuclex.Support.Scheduling; using Nuclex.Support.Tracking;
using Nuclex.Support.Tracking;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>
/// <summary> /// Blocking progress dialog that prevents the user from accessing the application
/// Blocking progress dialog that prevents the user from accessing the application /// window during a modal asynchronous processes.
/// window during a modal asynchronous processes. /// </summary>
/// </summary> /// <example>
/// <example> /// <code>
/// <code> /// class Test : Nuclex.Support.Scheduling.ThreadOperation {
/// class Test : Nuclex.Support.Scheduling.ThreadOperation { ///
/// /// static void Main() {
/// static void Main() { /// Test myTest = new Test();
/// Test myTest = new Test(); /// myTest.Begin();
/// myTest.Begin(); /// Nuclex.Windows.Forms.ProgressReporterForm.Track(myTest);
/// Nuclex.Windows.Forms.ProgressReporterForm.Track(myTest); /// myTest.End();
/// myTest.End(); /// }
/// } ///
/// /// protected override void Execute() {
/// protected override void Execute() { /// for(int i = 0; i &lt; 10000000; ++i) {
/// for(int i = 0; i &lt; 10000000; ++i) { /// OnAsyncProgressUpdated((float)i / 10000000.0f);
/// OnAsyncProgressUpdated((float)i / 10000000.0f); /// }
/// } /// }
/// } ///
/// /// }
/// } /// </code>
/// </code> /// </example>
/// </example> public partial class ProgressReporterForm : Form {
public partial class ProgressReporterForm : Form {
/// <summary>Initializes a new progress reporter</summary>
/// <summary>Initializes a new progress reporter</summary> internal ProgressReporterForm() {
internal ProgressReporterForm() { InitializeComponent();
InitializeComponent();
this.asyncEndedDelegate = new EventHandler(asyncEnded);
this.asyncEndedDelegate = new EventHandler(asyncEnded); this.asyncProgressChangedDelegate = new EventHandler<ProgressReportEventArgs>(
this.asyncProgressChangedDelegate = new EventHandler<ProgressReportEventArgs>( asyncProgressChanged
asyncProgressChanged );
); }
}
/// <summary>
/// <summary> /// Shows the progress reporter until the specified transaction has ended.
/// Shows the progress reporter until the specified transaction has ended. /// </summary>
/// </summary> /// <param name="transaction">
/// <param name="transaction"> /// Transaction for whose duration to show the progress reporter
/// Transaction for whose duration to show the progress reporter /// </param>
/// </param> public static void Track(Transaction transaction) {
public static void Track(Transaction transaction) { Track(null, transaction);
Track(null, transaction); }
}
/// <summary>
/// <summary> /// Shows the progress reporter until the specified transaction has ended.
/// Shows the progress reporter until the specified transaction has ended. /// </summary>
/// </summary> /// <param name="windowTitle">
/// <param name="windowTitle"> /// Text to be shown in the progress reporter's title bar
/// Text to be shown in the progress reporter's title bar /// </param>
/// </param> /// <param name="transaction">
/// <param name="transaction"> /// Process for whose duration to show the progress reporter
/// Process for whose duration to show the progress reporter /// </param>
/// </param> public static void Track(string windowTitle, Transaction transaction) {
public static void Track(string windowTitle, Transaction transaction) {
// Small optimization to avoid the lengthy control creation when the background
// Small optimization to avoid the lengthy control creation when the background // process has already ended. This is an accepted race condition: If the process
// 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
// finishes right after this line, it doesn't change the outcome, it just // causes the progress dialog to be constructed needlessly.
// causes the progress dialog to be constructed needlessly. if(transaction.Ended) {
if(transaction.Ended) { return;
return; }
}
// Open the form and let it monitor the transaction's state
// Open the form and let it monitor the transaction's state using(ProgressReporterForm theForm = new ProgressReporterForm()) {
using(ProgressReporterForm theForm = new ProgressReporterForm()) { theForm.track(windowTitle, transaction);
theForm.track(windowTitle, transaction); }
}
}
}
/// <summary>Called when the user tries to close the form manually</summary>
/// <summary>Called when the user tries to close the form manually</summary> /// <param name="arguments">
/// <param name="arguments"> /// Contains a flag that can be used to abort the close attempt
/// Contains a flag that can be used to abort the close attempt /// </param>
/// </param> protected override void OnClosing(CancelEventArgs arguments) {
protected override void OnClosing(CancelEventArgs arguments) { base.OnClosing(arguments);
base.OnClosing(arguments);
// Only allow the form to close when the form is ready to close and the
// Only allow the form to close when the form is ready to close and the // transaction being tracked has also finished.
// transaction being tracked has also finished. arguments.Cancel = (Thread.VolatileRead(ref this.state) < 2);
arguments.Cancel = (Thread.VolatileRead(ref this.state) < 2); }
}
/// <summary>
/// <summary> /// Shows the progress reporter until the specified transaction has ended.
/// Shows the progress reporter until the specified transaction has ended. /// </summary>
/// </summary> /// <param name="windowTitle">
/// <param name="windowTitle"> /// Text to be shown in the progress reporter's title bar
/// Text to be shown in the progress reporter's title bar /// </param>
/// </param> /// <param name="transaction">
/// <param name="transaction"> /// Transaction for whose duration to show the progress reporter
/// Transaction for whose duration to show the progress reporter /// </param>
/// </param> private void track(string windowTitle, Transaction transaction) {
private void track(string windowTitle, Transaction transaction) {
// Set the window title if the user wants to use a custom one
// Set the window title if the user wants to use a custom one if(windowTitle != null) {
if(windowTitle != null) { Text = windowTitle;
Text = windowTitle; }
}
// Only enable the cancel button if the transaction can be aborted
// Only enable the cancel button if the transaction can be aborted this.abortReceiver = (transaction as IAbortable);
this.abortReceiver = (transaction as IAbortable); this.cancelButton.Enabled = (this.abortReceiver != null);
this.cancelButton.Enabled = (this.abortReceiver != null);
// Make sure the progress bar control has been created (otherwise, we've got
// Make sure the progress bar control has been created (otherwise, we've got // a chance that BeginInvoke() would fail if the first progress notification
// a chance that BeginInvoke() would fail if the first progress notification // arrived before we called ShowDialog()!)
// arrived before we called ShowDialog()!) { IntPtr tempDummy = this.progressBar.Handle; }
{ IntPtr tempDummy = this.progressBar.Handle; }
// Subscribe the form to the transaction it is supposed to monitor.
// Subscribe the form to the transaction it is supposed to monitor. // Careful: With the new design, this can cause the asyncEndedDelegate()
// Careful: With the new design, this can cause the asyncEndedDelegate() // callback to be called immediately and synchronously!
// callback to be called immediately and synchronously! transaction.AsyncEnded += this.asyncEndedDelegate;
transaction.AsyncEnded += this.asyncEndedDelegate; IProgressReporter progressReporter = transaction as IProgressReporter;
IProgressReporter progressReporter = transaction as IProgressReporter; if(progressReporter != null) {
if(progressReporter != null) { progressReporter.AsyncProgressChanged += this.asyncProgressChangedDelegate;
progressReporter.AsyncProgressChanged += this.asyncProgressChangedDelegate; }
}
// The transaction might have ended before this line was reached, if that's
// The transaction might have ended before this line was reached, if that's // the case, we don't show the dialog at all.
// the case, we don't show the dialog at all. if(!transaction.Ended)
if(!transaction.Ended) ShowDialog();
ShowDialog();
// We're done, unsubscribe from the transaction's events again
// We're done, unsubscribe from the transaction's events again progressReporter = transaction as IProgressReporter;
progressReporter = transaction as IProgressReporter; if(progressReporter != null) {
if(progressReporter != null) { progressReporter.AsyncProgressChanged -= this.asyncProgressChangedDelegate;
progressReporter.AsyncProgressChanged -= this.asyncProgressChangedDelegate; }
} transaction.AsyncEnded -= this.asyncEndedDelegate;
transaction.AsyncEnded -= this.asyncEndedDelegate;
}
}
/// <summary>Called when the transaction has ended</summary>
/// <summary>Called when the transaction has ended</summary> /// <param name="sender">Transaction that has ended</param>
/// <param name="sender">Transaction that has ended</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void asyncEnded(object sender, EventArgs arguments) {
private void asyncEnded(object sender, EventArgs arguments) {
// If the new state is 2, the form was ready to close (since the state
// 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)
// is incremented once when the form becomes ready to be closed) if(Interlocked.Increment(ref this.state) == 2) {
if(Interlocked.Increment(ref this.state) == 2) {
// Close the dialog. Ensure the Close() method is invoked from the
// Close the dialog. Ensure the Close() method is invoked from the // same thread the dialog was created in.
// same thread the dialog was created in. if(InvokeRequired) {
if(InvokeRequired) { Invoke(new MethodInvoker(Close));
Invoke(new MethodInvoker(Close)); } else {
} else { Close();
Close(); }
}
}
}
}
}
/// <summary>Called when the tracked transaction's progress updates</summary>
/// <summary>Called when the tracked transaction's progress updates</summary> /// <param name="sender">Transaction whose progress has been updated</param>
/// <param name="sender">Transaction whose progress has been updated</param> /// <param name="arguments">
/// <param name="arguments"> /// Contains the new progress achieved by the transaction
/// Contains the new progress achieved by the transaction /// </param>
/// </param> private void asyncProgressChanged(object sender, ProgressReportEventArgs arguments) {
private void asyncProgressChanged(object sender, ProgressReportEventArgs arguments) {
// See if this is the first progress update we're receiving. If yes, we need to
// 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!
// switch the progress bar from marquee into its normal mode! int haveProgress = Interlocked.Exchange(ref this.areProgressUpdatesIncoming, 1);
int haveProgress = Interlocked.Exchange(ref this.areProgressUpdatesIncoming, 1); if(haveProgress == 0) {
if(haveProgress == 0) { this.progressBar.BeginInvoke(
this.progressBar.BeginInvoke( (MethodInvoker)delegate() { this.progressBar.Style = ProgressBarStyle.Blocks; }
(MethodInvoker)delegate() { this.progressBar.Style = ProgressBarStyle.Blocks; } );
); }
}
// Send the new progress to the progress bar
// Send the new progress to the progress bar this.progressBar.AsyncSetValue(arguments.Progress);
this.progressBar.AsyncSetValue(arguments.Progress);
}
}
/// <summary>
/// <summary> /// One-time timer callback that ensurs the form doesn't stay open when the
/// One-time timer callback that ensurs the form doesn't stay open when the /// close request arrives at an inappropriate time.
/// close request arrives at an inappropriate time. /// </summary>
/// </summary> /// <param name="sender">Timer that has ticked</param>
/// <param name="sender">Timer that has ticked</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void controlCreationTimerTicked(object sender, EventArgs arguments) {
private void controlCreationTimerTicked(object sender, EventArgs arguments) {
// This timer is intended to run only once to find out when the dialog has
// 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
// been fully constructed and is running its message pump. So we'll disable // it as soon as it has been triggered once.
// it as soon as it has been triggered once. this.controlCreationTimer.Enabled = false;
this.controlCreationTimer.Enabled = false;
// If the new state is 2, then the form was requested to close before it had
// 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!
// been fully constructed, so we should close it now! if(Interlocked.Increment(ref this.state) == 2) {
if(Interlocked.Increment(ref this.state) == 2) { Close();
Close(); }
}
}
}
/// <summary>
/// <summary> /// Aborts the background operation when the user clicks the cancel button
/// Aborts the background operation when the user clicks the cancel button /// </summary>
/// </summary> /// <param name="sender">Button that has been clicked</param>
/// <param name="sender">Button that has been clicked</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void cancelClicked(object sender, EventArgs arguments) {
private void cancelClicked(object sender, EventArgs arguments) {
if(this.abortReceiver != null) {
if(this.abortReceiver != null) {
// Do this first because the abort receiver might trigger the AsyncEnded()
// Do this first because the abort receiver might trigger the AsyncEnded() // event in the calling thread (us!) and thus destroy our window even in
// event in the calling thread (us!) and thus destroy our window even in // the safe and synchronous UI thread :)
// the safe and synchronous UI thread :) this.cancelButton.Enabled = false;
this.cancelButton.Enabled = false;
// Now we're ready to abort!
// Now we're ready to abort! this.abortReceiver.AsyncAbort();
this.abortReceiver.AsyncAbort(); this.abortReceiver = null;
this.abortReceiver = null;
}
}
}
}
/// <summary>Delegate for the asyncEnded() method</summary>
/// <summary>Delegate for the asyncEnded() method</summary> private EventHandler asyncEndedDelegate;
private EventHandler asyncEndedDelegate; /// <summary>Delegate for the asyncProgressUpdated() method</summary>
/// <summary>Delegate for the asyncProgressUpdated() method</summary> private EventHandler<ProgressReportEventArgs> asyncProgressChangedDelegate;
private EventHandler<ProgressReportEventArgs> asyncProgressChangedDelegate; /// <summary>Whether the form can be closed and should be closed</summary>
/// <summary>Whether the form can be closed and should be closed</summary> /// <remarks>
/// <remarks> /// 0: Nothing happened yet
/// 0: Nothing happened yet /// 1: Ready to close or close requested
/// 1: Ready to close or close requested /// 2: Ready to close and close requested, triggers close
/// 2: Ready to close and close requested, triggers close /// </remarks>
/// </remarks> private int state;
private int state; /// <summary>Whether we're receiving progress updates from the transaction</summary>
/// <summary>Whether we're receiving progress updates from the transaction</summary> /// <remarks>
/// <remarks> /// 0: No progress updates have arrived so far
/// 0: No progress updates have arrived so far /// 1: We have received at least one progress update from the transaction
/// 1: We have received at least one progress update from the transaction /// </remarks>
/// </remarks> private int areProgressUpdatesIncoming;
private int areProgressUpdatesIncoming; /// <summary>
/// <summary> /// If set, reference to an object implementing IAbortable by which the
/// If set, reference to an object implementing IAbortable by which the /// ongoing background process can be aborted.
/// ongoing background process can be aborted. /// </summary>
/// </summary> private IAbortable abortReceiver;
private IAbortable abortReceiver;
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,114 +1,113 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Drawing;
using System.Drawing; using System.Windows.Forms;
using System.Windows.Forms;
using Nuclex.Support.Tracking;
using Nuclex.Support.Tracking;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Tracking bar that can be hosted in a tool strip container</summary>
/// <summary>Tracking bar that can be hosted in a tool strip container</summary> public class ToolStripTrackingBar : ToolStripControlHost {
public class ToolStripTrackingBar : ToolStripControlHost {
/// <summary>Initializes a new tool strip tracking bar</summary>
/// <summary>Initializes a new tool strip tracking bar</summary> public ToolStripTrackingBar() : base(createTrackingBar()) {
public ToolStripTrackingBar() : base(createTrackingBar()) { hideControlAtRuntime();
hideControlAtRuntime(); }
}
/// <summary>Initializes a new tool strip tracking bar with a name</summary>
/// <summary>Initializes a new tool strip tracking bar with a name</summary> /// <param name="name">Name of the tracking bar control</param>
/// <param name="name">Name of the tracking bar control</param> public ToolStripTrackingBar(string name) : base(createTrackingBar(), name) {
public ToolStripTrackingBar(string name) : base(createTrackingBar(), name) { hideControlAtRuntime();
hideControlAtRuntime(); }
}
/// <summary>The tracking bar control being hosted by the tool strip host</summary>
/// <summary>The tracking bar control being hosted by the tool strip host</summary> public TrackingBar TrackingBarControl {
public TrackingBar TrackingBarControl { get { return base.Control as TrackingBar; }
get { return base.Control as TrackingBar; } }
}
/// <summary>Tracks the specified transaction in the tracking bar</summary>
/// <summary>Tracks the specified transaction in the tracking bar</summary> /// <param name="transaction">Transaction to be tracked</param>
/// <param name="transaction">Transaction to be tracked</param> public void Track(Transaction transaction) {
public void Track(Transaction transaction) { TrackingBarControl.Track(transaction);
TrackingBarControl.Track(transaction); }
}
/// <summary>Tracks the specified transaction in the tracking bar</summary>
/// <summary>Tracks the specified transaction in the tracking bar</summary> /// <param name="transaction">Transaction to be tracked</param>
/// <param name="transaction">Transaction to be tracked</param> /// <param name="weight">Weight of this transaction in the total progress</param>
/// <param name="weight">Weight of this transaction in the total progress</param> public void Track(Transaction transaction, float weight) {
public void Track(Transaction transaction, float weight) { TrackingBarControl.Track(transaction, weight);
TrackingBarControl.Track(transaction, weight); }
}
/// <summary>Stops tracking the specified transaction</summary>
/// <summary>Stops tracking the specified transaction</summary> /// <param name="transaction">Transaction to stop tracking</param>
/// <param name="transaction">Transaction to stop tracking</param> public void Untrack(Transaction transaction) {
public void Untrack(Transaction transaction) { TrackingBarControl.Untrack(transaction);
TrackingBarControl.Untrack(transaction); }
}
/// <summary>Default size of the hosted control</summary>
/// <summary>Default size of the hosted control</summary> protected override Size DefaultSize {
protected override Size DefaultSize { get { return new Size(100, 15); }
get { return new Size(100, 15); } }
}
/// <summary>Default margin to leave around the control in the tool strip</summary>
/// <summary>Default margin to leave around the control in the tool strip</summary> protected override Padding DefaultMargin {
protected override Padding DefaultMargin { get {
get { if((base.Owner != null) && (base.Owner is StatusStrip)) {
if((base.Owner != null) && (base.Owner is StatusStrip)) { return new Padding(1, 3, 1, 3);
return new Padding(1, 3, 1, 3); }
}
return new Padding(1, 2, 1, 1);
return new Padding(1, 2, 1, 1); }
} }
}
/// <summary>Creates a new tracking bar</summary>
/// <summary>Creates a new tracking bar</summary> /// <returns>A new tracking bar</returns>
/// <returns>A new tracking bar</returns> private static TrackingBar createTrackingBar() {
private static TrackingBar createTrackingBar() { TrackingBar trackingBar = new TrackingBar();
TrackingBar trackingBar = new TrackingBar(); trackingBar.Size = new Size(100, 15);
trackingBar.Size = new Size(100, 15); return trackingBar;
return trackingBar; }
}
/// <summary>Hides the control during runtime usage</summary>
/// <summary>Hides the control during runtime usage</summary> private void hideControlAtRuntime() {
private void hideControlAtRuntime() { TrackingBarControl.VisibleChanged += new EventHandler(trackingBarVisibleChanged);
TrackingBarControl.VisibleChanged += new EventHandler(trackingBarVisibleChanged);
LicenseUsageMode usageMode = System.ComponentModel.LicenseManager.UsageMode;
LicenseUsageMode usageMode = System.ComponentModel.LicenseManager.UsageMode; if(usageMode == LicenseUsageMode.Runtime) {
if(usageMode == LicenseUsageMode.Runtime) { base.Visible = false;
base.Visible = false; }
} }
}
/// <summary>
/// <summary> /// Toggles the visibility of the tool strip host when the tracking bar control's
/// Toggles the visibility of the tool strip host when the tracking bar control's /// visibility changes.
/// visibility changes. /// </summary>
/// </summary> /// <param name="sender">Tracking bar control whose visiblity has changed</param>
/// <param name="sender">Tracking bar control whose visiblity has changed</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void trackingBarVisibleChanged(object sender, EventArgs arguments) {
private void trackingBarVisibleChanged(object sender, EventArgs arguments) { base.Visible = TrackingBarControl.Visible;
base.Visible = TrackingBarControl.Visible; }
}
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,51 +1,50 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
partial class TrackingBar {
partial class TrackingBar { /// <summary>Required designer variable.</summary>
/// <summary>Required designer variable.</summary> private System.ComponentModel.IContainer components = null;
private System.ComponentModel.IContainer components = null;
/// <summary> Clean up any resources being used.</summary>
/// <summary> Clean up any resources being used.</summary> /// <param name="disposing">
/// <param name="disposing"> /// true if managed resources should be disposed; otherwise, false.
/// true if managed resources should be disposed; otherwise, false. /// </param>
/// </param> protected override void Dispose(bool disposing) {
protected override void Dispose(bool disposing) { if(disposing && (components != null)) {
if(disposing && (components != null)) { components.Dispose();
components.Dispose(); }
} base.Dispose(disposing);
base.Dispose(disposing); }
}
#region Component Designer generated code
#region Component Designer generated code
/// <summary>
/// <summary> /// Required method for Designer support - do not modify
/// Required method for Designer support - do not modify /// the contents of this method with the code editor.
/// the contents of this method with the code editor. /// </summary>
/// </summary> private void InitializeComponent() {
private void InitializeComponent() { components = new System.ComponentModel.Container();
components = new System.ComponentModel.Container(); }
}
#endregion
#endregion }
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,134 +1,133 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Collections.Generic;
using System.Collections.Generic; using System.ComponentModel;
using System.ComponentModel; using System.Drawing;
using System.Drawing; using System.Data;
using System.Data; using System.Text;
using System.Text; using System.Windows.Forms;
using System.Windows.Forms; using System.Threading;
using System.Threading;
using Nuclex.Support.Tracking;
using Nuclex.Support.Tracking;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Progress bar for tracking the progress of background operations</summary>
/// <summary>Progress bar for tracking the progress of background operations</summary> public partial class TrackingBar : AsyncProgressBar {
public partial class TrackingBar : AsyncProgressBar {
/// <summary>Initializes a new tracking bar</summary>
/// <summary>Initializes a new tracking bar</summary> public TrackingBar() {
public TrackingBar() { InitializeComponent();
InitializeComponent();
// We start off being in the idle state (and thus, being invisible)
// We start off being in the idle state (and thus, being invisible) this.isIdle = true;
this.isIdle = true; this.Visible = false;
this.Visible = false;
// Initialize the delegates we use to update the control's state and those
// Initialize the delegates we use to update the control's state and those // we use to register ourselfes to the tracker's events
// we use to register ourselfes to the tracker's events this.updateIdleStateDelegate = new MethodInvoker(updateIdleState);
this.updateIdleStateDelegate = new MethodInvoker(updateIdleState); this.asyncIdleStateChangedDelegate = new EventHandler<IdleStateEventArgs>(
this.asyncIdleStateChangedDelegate = new EventHandler<IdleStateEventArgs>( asyncIdleStateChanged
asyncIdleStateChanged );
); this.asyncProgressUpdateDelegate = new EventHandler<ProgressReportEventArgs>(
this.asyncProgressUpdateDelegate = new EventHandler<ProgressReportEventArgs>( asyncProgressUpdated
asyncProgressUpdated );
);
// Create the tracker and attach ourselfes to its events
// Create the tracker and attach ourselfes to its events this.tracker = new ProgressTracker();
this.tracker = new ProgressTracker(); this.tracker.AsyncIdleStateChanged += this.asyncIdleStateChangedDelegate;
this.tracker.AsyncIdleStateChanged += this.asyncIdleStateChangedDelegate; this.tracker.AsyncProgressChanged += this.asyncProgressUpdateDelegate;
this.tracker.AsyncProgressChanged += this.asyncProgressUpdateDelegate; }
}
/// <summary>Tracks the specified transaction in the tracking bar</summary>
/// <summary>Tracks the specified transaction in the tracking bar</summary> /// <param name="transaction">Transaction to be tracked</param>
/// <param name="transaction">Transaction to be tracked</param> public void Track(Transaction transaction) {
public void Track(Transaction transaction) { this.tracker.Track(transaction);
this.tracker.Track(transaction); }
}
/// <summary>Tracks the specified transaction in the tracking bar</summary>
/// <summary>Tracks the specified transaction in the tracking bar</summary> /// <param name="transaction">Transaction to be tracked</param>
/// <param name="transaction">Transaction to be tracked</param> /// <param name="weight">Weight of this transaction in the total progress</param>
/// <param name="weight">Weight of this transaction in the total progress</param> public void Track(Transaction transaction, float weight) {
public void Track(Transaction transaction, float weight) { this.tracker.Track(transaction, weight);
this.tracker.Track(transaction, weight); }
}
/// <summary>Stops tracking the specified transaction</summary>
/// <summary>Stops tracking the specified transaction</summary> /// <param name="transaction">Transaction to stop tracking</param>
/// <param name="transaction">Transaction to stop tracking</param> public void Untrack(Transaction transaction) {
public void Untrack(Transaction transaction) { this.tracker.Untrack(transaction);
this.tracker.Untrack(transaction); }
}
/// <summary>
/// <summary> /// Called when the summed progressed of the tracked transaction has changed
/// Called when the summed progressed of the tracked transaction has changed /// </summary>
/// </summary> /// <param name="sender">Transaction whose progress has changed</param>
/// <param name="sender">Transaction whose progress has changed</param> /// <param name="arguments">Contains the progress achieved by the transaction</param>
/// <param name="arguments">Contains the progress achieved by the transaction</param> private void asyncProgressUpdated(
private void asyncProgressUpdated( object sender, ProgressReportEventArgs arguments
object sender, ProgressReportEventArgs arguments ) {
) { AsyncSetValue(arguments.Progress);
AsyncSetValue(arguments.Progress); }
}
/// <summary>Called when the tracker becomes enters of leaves the idle state</summary>
/// <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="sender">Tracker that has entered or left the idle state</param> /// <param name="arguments">Contains the new idle state</param>
/// <param name="arguments">Contains the new idle state</param> private void asyncIdleStateChanged(object sender, IdleStateEventArgs arguments) {
private void asyncIdleStateChanged(object sender, IdleStateEventArgs arguments) {
// Do a fully synchronous update of the idle state. This update must not be
// 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,
// lost because otherwise, the progress bar might stay on-screen when in fact, // the background operation has already finished and nothing is happening anymore.
// the background operation has already finished and nothing is happening anymore. this.isIdle = arguments.Idle;
this.isIdle = arguments.Idle;
// Update the bar's idle state
// Update the bar's idle state if(InvokeRequired) {
if(InvokeRequired) { Invoke(this.updateIdleStateDelegate);
Invoke(this.updateIdleStateDelegate); } else {
} else { updateIdleState();
updateIdleState(); }
}
}
}
/// <summary>
/// <summary> /// Updates the idle state of the progress bar
/// Updates the idle state of the progress bar /// (controls whether the progress bar is shown or invisible)
/// (controls whether the progress bar is shown or invisible) /// </summary>
/// </summary> private void updateIdleState() {
private void updateIdleState() {
// Only show the progress bar when something is happening
// Only show the progress bar when something is happening base.Visible = !this.isIdle;
base.Visible = !this.isIdle;
}
}
/// <summary>Whether the progress bar is in the idle state</summary>
/// <summary>Whether the progress bar is in the idle state</summary> private volatile bool isIdle;
private volatile bool isIdle; /// <summary>Tracker used to sum and update the total progress</summary>
/// <summary>Tracker used to sum and update the total progress</summary> private ProgressTracker tracker;
private ProgressTracker tracker; /// <summary>Delegate for the idle state update method</summary>
/// <summary>Delegate for the idle state update method</summary> private MethodInvoker updateIdleStateDelegate;
private MethodInvoker updateIdleStateDelegate; /// <summary>Delegate for the asyncIdleStateChanged() method</summary>
/// <summary>Delegate for the asyncIdleStateChanged() method</summary> private EventHandler<IdleStateEventArgs> asyncIdleStateChangedDelegate;
private EventHandler<IdleStateEventArgs> asyncIdleStateChangedDelegate; /// <summary>Delegate for the asyncProgressUpdate() method</summary>
/// <summary>Delegate for the asyncProgressUpdate() method</summary> private EventHandler<ProgressReportEventArgs> asyncProgressUpdateDelegate;
private EventHandler<ProgressReportEventArgs> asyncProgressUpdateDelegate;
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,156 +1,155 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
#if UNITTEST
#if UNITTEST
using System;
using System;
using NUnit.Framework;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the dialog view model</summary>
/// <summary>Unit test for the dialog view model</summary> [TestFixture]
[TestFixture] public class DialogViewModelTest {
public class DialogViewModelTest {
#region class DialogViewModelSubscriber
#region class DialogViewModelSubscriber
/// <summary>Subscriber for the events offered by a dialog view model</summary>
/// <summary>Subscriber for the events offered by a dialog view model</summary> private class DialogViewModelSubscriber {
private class DialogViewModelSubscriber {
/// <summary>Indicates that the user has accepted the dialog</summary>
/// <summary>Indicates that the user has accepted the dialog</summary> public void Confirmed(object sender, EventArgs arguments) {
public void Confirmed(object sender, EventArgs arguments) { ++this.confirmCallCount;
++this.confirmCallCount; }
}
/// <summary>Indicates that the user has cancelled the dialog</summary>
/// <summary>Indicates that the user has cancelled the dialog</summary> public void Cancelled(object sender, EventArgs arguments) {
public void Cancelled(object sender, EventArgs arguments) { ++this.cancelCallCount;
++this.cancelCallCount; }
}
/// <summary>Indicates that the dialog was simply closed</summary>
/// <summary>Indicates that the dialog was simply closed</summary> public void Submitted(object sender, EventArgs arguments) {
public void Submitted(object sender, EventArgs arguments) { ++this.submitCallCount;
++this.submitCallCount; }
}
/// <summary>How many times the Confirmed() method was called</summary>
/// <summary>How many times the Confirmed() method was called</summary> public int ConfirmCallCount {
public int ConfirmCallCount { get { return this.confirmCallCount; }
get { return this.confirmCallCount; } }
}
/// <summary>How many times the Cancelled() method was called</summary>
/// <summary>How many times the Cancelled() method was called</summary> public int CancelCallCount {
public int CancelCallCount { get { return this.cancelCallCount; }
get { return this.cancelCallCount; } }
}
/// <summary>How many times the Submitted() method was called</summary>
/// <summary>How many times the Submitted() method was called</summary> public int SubmitCallCount {
public int SubmitCallCount { get { return this.submitCallCount; }
get { return this.submitCallCount; } }
}
/// <summary>How many times the Confirmed() method was called</summary>
/// <summary>How many times the Confirmed() method was called</summary> private int confirmCallCount;
private int confirmCallCount; /// <summary>How many times the Cancelled() method was called</summary>
/// <summary>How many times the Cancelled() method was called</summary> private int cancelCallCount;
private int cancelCallCount; /// <summary>How many times the Submitted() method was called</summary>
/// <summary>How many times the Submitted() method was called</summary> private int submitCallCount;
private int submitCallCount;
}
}
#endregion // class DialogViewModelSubscriber
#endregion // class DialogViewModelSubscriber
/// <summary>Verifies that the dialog view model has a default constructor</summary>
/// <summary>Verifies that the dialog view model has a default constructor</summary> [Test]
[Test] public void HasDefaultConstructor() {
public void HasDefaultConstructor() { Assert.DoesNotThrow(
Assert.DoesNotThrow( delegate() { new DialogViewModel(); }
delegate() { new DialogViewModel(); } );
); }
}
/// <summary>
/// <summary> /// Verifies that calling Confirm() on the dialog view model triggers
/// Verifies that calling Confirm() on the dialog view model triggers /// the 'Confirmed' event
/// the 'Confirmed' event /// </summary>
/// </summary> [Test]
[Test] public void ConfirmTriggersConfirmedEvent() {
public void ConfirmTriggersConfirmedEvent() { var viewModel = new DialogViewModel();
var viewModel = new DialogViewModel(); var subscriber = createSubscriber(viewModel);
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); viewModel.Confirm();
viewModel.Confirm(); Assert.AreEqual(1, subscriber.ConfirmCallCount);
Assert.AreEqual(1, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); }
}
/// <summary>
/// <summary> /// Verifies that calling Cancel() on the dialog view model triggers
/// Verifies that calling Cancel() on the dialog view model triggers /// the 'Cancelled' event
/// the 'Cancelled' event /// </summary>
/// </summary> [Test]
[Test] public void CancelTriggersCancelledEvent() {
public void CancelTriggersCancelledEvent() { var viewModel = new DialogViewModel();
var viewModel = new DialogViewModel(); var subscriber = createSubscriber(viewModel);
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); viewModel.Cancel();
viewModel.Cancel(); Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(1, subscriber.CancelCallCount);
Assert.AreEqual(1, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); }
}
/// <summary>
/// <summary> /// Verifies that calling Submitm() on the dialog view model triggers
/// Verifies that calling Submitm() on the dialog view model triggers /// the 'Submitted' event
/// the 'Submitted' event /// </summary>
/// </summary> [Test]
[Test] public void SubmitTriggersSubmittedEvent() {
public void SubmitTriggersSubmittedEvent() { var viewModel = new DialogViewModel();
var viewModel = new DialogViewModel(); var subscriber = createSubscriber(viewModel);
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); viewModel.Submit();
viewModel.Submit(); Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(1, subscriber.SubmitCallCount);
Assert.AreEqual(1, subscriber.SubmitCallCount); }
}
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
/// <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>
/// <param name="viewModel">View model a subscriber will be created for</param> /// <returns>A subscriber for the events of the specified view model</returns>
/// <returns>A subscriber for the events of the specified view model</returns> private DialogViewModelSubscriber createSubscriber(DialogViewModel viewModel) {
private DialogViewModelSubscriber createSubscriber(DialogViewModel viewModel) { var subscriber = new DialogViewModelSubscriber();
var subscriber = new DialogViewModelSubscriber(); viewModel.Confirmed += subscriber.Confirmed;
viewModel.Confirmed += subscriber.Confirmed; viewModel.Canceled += subscriber.Cancelled;
viewModel.Canceled += subscriber.Cancelled; viewModel.Submitted += subscriber.Submitted;
viewModel.Submitted += subscriber.Submitted; return subscriber;
return subscriber; }
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST
#endif // UNITTEST

View File

@ -1,76 +1,75 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System;
using Nuclex.Support;
using Nuclex.Support;
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Base class for the view model of dialogs (typically modal ones)</summary>
/// <summary>Base class for the view model of dialogs (typically modal ones)</summary> public class DialogViewModel : Observable {
public class DialogViewModel : Observable {
/// <summary>Indicates that the view should close with a positive result</summary>
/// <summary>Indicates that the view should close with a positive result</summary> /// <remarks>
/// <remarks> /// This event typically corresponds to the 'Ok' button in a dialog.
/// This event typically corresponds to the 'Ok' button in a dialog. /// </remarks>
/// </remarks> public event EventHandler Confirmed;
public event EventHandler Confirmed;
/// <summary>Indicates that the view should close with a negative result</summary>
/// <summary>Indicates that the view should close with a negative result</summary> /// <remarks>
/// <remarks> /// This event typically corresponds to the 'Cancel' button in a dialog.
/// This event typically corresponds to the 'Cancel' button in a dialog. /// </remarks>
/// </remarks> public event EventHandler Canceled;
public event EventHandler Canceled;
/// <summary>Indicates that the view should close</summary>
/// <summary>Indicates that the view should close</summary> /// <remarks>
/// <remarks> /// This closes the view with a neutral result, used when the view doesn't follow
/// 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.
/// an ok/cancel scheme or the result is transmitted in some other way. /// </remarks>
/// </remarks> public event EventHandler Submitted;
public event EventHandler Submitted;
/// <summary>
/// <summary> /// Indicates that the dialog should be closed with a positive outcome
/// Indicates that the dialog should be closed with a positive outcome /// </summary>
/// </summary> public virtual void Confirm() {
public virtual void Confirm() { if(Confirmed != null) {
if(Confirmed != null) { Confirmed(this, EventArgs.Empty);
Confirmed(this, EventArgs.Empty); }
} }
}
/// <summary>
/// <summary> /// Indicates that the dialog should be closed with a negative outcome
/// Indicates that the dialog should be closed with a negative outcome /// </summary>
/// </summary> public virtual void Cancel() {
public virtual void Cancel() { if(Canceled != null) {
if(Canceled != null) { Canceled(this, EventArgs.Empty);
Canceled(this, EventArgs.Empty); }
} }
}
/// <summary>Indicates that the dialog should be closed</summary>
/// <summary>Indicates that the dialog should be closed</summary> public virtual void Submit() {
public virtual void Submit() { if(Submitted != null) {
if(Submitted != null) { Submitted(this, EventArgs.Empty);
Submitted(this, EventArgs.Empty); }
} }
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels

View File

@ -1,14 +1,33 @@
using System; #region Apache License 2.0
/*
namespace Nuclex.Windows.Forms.ViewModels { Nuclex .NET Framework
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
/// <summary>Interface for vew models that can switch between different pages</summary>
public interface IMultiPageViewModel { Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary> You may obtain a copy of the License at
/// <returns>A view model for the active page on the multi-page view model</returns>
object GetActivePageViewModel(); 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,
} // namespace Nuclex.Windows.Forms.ViewModels 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 #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Collections.Concurrent;
using System.Collections.Concurrent;
using Nuclex.Support;
using Nuclex.Support;
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Base class for view models that have multiple child view models</summary>
/// <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>
/// <typeparam name="TPageEnumeration">Enum type by which pages can be indicated</typeparam> public abstract class MultiPageViewModel<TPageEnumeration> :
public abstract class MultiPageViewModel<TPageEnumeration> : Observable, IMultiPageViewModel, IDisposable {
Observable, IMultiPageViewModel, IDisposable {
/// <summary>Initializes a new multi-page view model</summary>
/// <summary>Initializes a new multi-page view model</summary> /// <param name="windowManager">
/// <param name="windowManager"> /// Window manager the view model uses to create child views
/// Window manager the view model uses to create child views /// </param>
/// </param> /// <param name="cachePageViewModels">
/// <param name="cachePageViewModels"> /// Whether child view models will be kept alive and reused
/// Whether child view models will be kept alive and reused /// </param>
/// </param> public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) {
public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) { this.windowManager = windowManager;
this.windowManager = windowManager; if(cachePageViewModels) {
if(cachePageViewModels) { this.cachedViewModels = new ConcurrentDictionary<TPageEnumeration, object>();
this.cachedViewModels = new ConcurrentDictionary<TPageEnumeration, object>(); }
} }
}
/// <summary>Immediately releases all resources owned by the instance</summary>
/// <summary>Immediately releases all resources owned by the instance</summary> public virtual void Dispose() {
public virtual void Dispose() { if(this.cachedViewModels != null) {
if(this.cachedViewModels != null) { foreach(object cacheViewModel in this.cachedViewModels.Values) {
foreach(object cacheViewModel in this.cachedViewModels.Values) { disposeIfSupported(cacheViewModel);
disposeIfSupported(cacheViewModel); }
} this.activePageViewModel = null;
this.activePageViewModel = null;
this.cachedViewModels.Clear();
this.cachedViewModels.Clear(); this.cachedViewModels = null;
this.cachedViewModels = null; } else if(this.activePageViewModel != null) {
} else if(this.activePageViewModel != null) { disposeIfSupported(this.activePageViewModel);
disposeIfSupported(this.activePageViewModel); this.activePageViewModel = null;
this.activePageViewModel = null; }
} }
}
/// <summary>Child page that is currently being displayed by the view model</summary>
/// <summary>Child page that is currently being displayed by the view model</summary> public TPageEnumeration ActivePage {
public TPageEnumeration ActivePage { get { return this.activePage; }
get { return this.activePage; } set {
set { if(!this.activePage.Equals(value)) {
if(!this.activePage.Equals(value)) { this.activePage = value;
this.activePage = value; if(this.activePageViewModel != null) {
if(this.activePageViewModel != null) { if(this.cachedViewModels == null) {
if(this.cachedViewModels == null) { disposeIfSupported(this.activePageViewModel);
disposeIfSupported(this.activePageViewModel); }
} this.activePageViewModel = null;
this.activePageViewModel = null; }
} OnPropertyChanged(nameof(ActivePage));
OnPropertyChanged(nameof(ActivePage)); }
} }
} }
}
/// <summary>Retrieves (and, if needed, creates) the view model for the active page</summary>
/// <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>
/// <returns>A view model for the active page on the multi-page view model</returns> public object GetActivePageViewModel() {
public object GetActivePageViewModel() { if(this.cachedViewModels == null) {
if(this.cachedViewModels == null) { if(this.activePageViewModel == null) {
if(this.activePageViewModel == null) { this.activePageViewModel = CreateViewModelForPage(this.activePage);
this.activePageViewModel = CreateViewModelForPage(this.activePage); }
} } else if(this.activePageViewModel == null) {
} else if(this.activePageViewModel == null) { this.activePageViewModel = this.cachedViewModels.GetOrAdd(
this.activePageViewModel = this.cachedViewModels.GetOrAdd( this.activePage,
this.activePage, delegate(TPageEnumeration activePage) {
delegate(TPageEnumeration activePage) { return CreateViewModelForPage(this.activePage);
return CreateViewModelForPage(this.activePage); }
} );
); }
}
return this.activePageViewModel;
return this.activePageViewModel; }
}
/// <summary>Windowmanager that can create view models and display other views</summary>
/// <summary>Windowmanager that can create view models and display other views</summary> protected IWindowManager WindowManager {
protected IWindowManager WindowManager { get { return this.windowManager; }
get { return this.windowManager; } }
}
/// <summary>Creates a view model for the specified page</summary>
/// <summary>Creates a view model for the specified page</summary> /// <param name="page">Page for which a view model will be created</param>
/// <param name="page">Page for which a view model will be created</param> /// <returns>The view model for the specified page</returns>
/// <returns>The view model for the specified page</returns> protected abstract object CreateViewModelForPage(TPageEnumeration page);
protected abstract object CreateViewModelForPage(TPageEnumeration page);
/// <summary>Disposes the specified object if it is disposable</summary>
/// <summary>Disposes the specified object if it is disposable</summary> /// <param name="potentiallyDisposable">Object that will be disposed if supported</param>
/// <param name="potentiallyDisposable">Object that will be disposed if supported</param> private static void disposeIfSupported(object potentiallyDisposable) {
private static void disposeIfSupported(object potentiallyDisposable) { var disposable = potentiallyDisposable as IDisposable;
var disposable = potentiallyDisposable as IDisposable; if(disposable != null) {
if(disposable != null) { disposable.Dispose();
disposable.Dispose(); }
} }
}
/// <summary>Page that is currently active in the multi-page view model</summary>
/// <summary>Page that is currently active in the multi-page view model</summary> private TPageEnumeration activePage;
private TPageEnumeration activePage; /// <summary>Window manager that can be used to display other views</summary>
/// <summary>Window manager that can be used to display other views</summary> private IWindowManager windowManager;
private IWindowManager windowManager;
/// <summary>View model for the active page</summary>
/// <summary>View model for the active page</summary> private object activePageViewModel;
private object activePageViewModel; /// <summary>Cached page view models, if caching is enabled</summary>
/// <summary>Cached page view models, if caching is enabled</summary> private ConcurrentDictionary<TPageEnumeration, object> cachedViewModels;
private ConcurrentDictionary<TPageEnumeration, object> cachedViewModels;
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels

View File

@ -1,271 +1,270 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
#if UNITTEST
#if UNITTEST
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Threading;
using System.Threading;
using NUnit.Framework;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded action class</summary>
/// <summary>Unit test for the threaded action class</summary> [TestFixture]
[TestFixture] public class ThreadedActionTest {
public class ThreadedActionTest {
#region class DummyContext
#region class DummyContext
/// <summary>Synchronization context that does absolutely nothing</summary>
/// <summary>Synchronization context that does absolutely nothing</summary> private class DummyContext : ISynchronizeInvoke {
private class DummyContext : ISynchronizeInvoke {
#region class SimpleAsyncResult
#region class SimpleAsyncResult
/// <summary>Barebones implementation of an asynchronous result</summary>
/// <summary>Barebones implementation of an asynchronous result</summary> private class SimpleAsyncResult : IAsyncResult {
private class SimpleAsyncResult : IAsyncResult {
/// <summary>Ehether the asynchronous operation is complete</summary>
/// <summary>Ehether the asynchronous operation is complete</summary> /// <remarks>
/// <remarks> /// Always true because it completes synchronously
/// Always true because it completes synchronously /// </remarks>
/// </remarks> public bool IsCompleted { get { return true; } }
public bool IsCompleted { get { return true; } }
/// <summary>
/// <summary> /// Wait handle that can be used to wait for the asynchronous operation
/// Wait handle that can be used to wait for the asynchronous operation /// </summary>
/// </summary> public WaitHandle AsyncWaitHandle {
public WaitHandle AsyncWaitHandle { get { throw new NotImplementedException("Not implemented"); }
get { throw new NotImplementedException("Not implemented"); } }
}
/// <summary>Custom state that can be used to pass information around</summary>
/// <summary>Custom state that can be used to pass information around</summary> public object AsyncState {
public object AsyncState { get { throw new NotImplementedException("Not implemented"); }
get { throw new NotImplementedException("Not implemented"); } }
}
/// <summary>Whether the asynchronous operation completed synchronously</summary>
/// <summary>Whether the asynchronous operation completed synchronously</summary> public bool CompletedSynchronously { get { return true; } }
public bool CompletedSynchronously { get { return true; } }
/// <summary>The value returned from the asynchronous operation</summary>
/// <summary>The value returned from the asynchronous operation</summary> public object ReturnedValue;
public object ReturnedValue;
}
}
#endregion // class SimpleAsyncResult
#endregion // class SimpleAsyncResult
/// <summary>Whether the calling thread needs to use Invoke()</summary>
/// <summary>Whether the calling thread needs to use Invoke()</summary> public bool InvokeRequired {
public bool InvokeRequired { get { return true; }
get { return true; } }
}
/// <summary>Schedules the specified method for execution in the target thread</summary>
/// <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="method">Method the target thread will execute when it is idle</param> /// <param name="arguments">Arguments that will be passed to the method</param>
/// <param name="arguments">Arguments that will be passed to the method</param> /// <returns>
/// <returns> /// An asynchronous result handle that can be used to check on the status of
/// An asynchronous result handle that can be used to check on the status of /// the call and wait for its completion
/// the call and wait for its completion /// </returns>
/// </returns> public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) { var asyncResult = new SimpleAsyncResult();
var asyncResult = new SimpleAsyncResult(); asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments); return asyncResult;
return asyncResult; }
}
/// <summary>Waits for the asychronous call to complete</summary>
/// <summary>Waits for the asychronous call to complete</summary> /// <param name="result">
/// <param name="result"> /// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method /// </param>
/// </param> /// <returns>The original result returned by the asychronously called method</returns>
/// <returns>The original result returned by the asychronously called method</returns> public object EndInvoke(IAsyncResult result) {
public object EndInvoke(IAsyncResult result) { return ((SimpleAsyncResult)result).ReturnedValue;
return ((SimpleAsyncResult)result).ReturnedValue; }
}
/// <summary>
/// <summary> /// Schedules the specified method for execution in the target thread and waits
/// Schedules the specified method for execution in the target thread and waits /// for it to complete
/// for it to complete /// </summary>
/// </summary> /// <param name="method">Method that will be executed by the target thread</param>
/// <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>
/// <param name="arguments">Arguments that will be passed to the method</param> /// <returns>The result returned by the specified method</returns>
/// <returns>The result returned by the specified method</returns> public object Invoke(Delegate method, object[] arguments) {
public object Invoke(Delegate method, object[] arguments) { return method.Method.Invoke(method.Target, arguments);
return method.Method.Invoke(method.Target, arguments); }
}
}
}
#endregion // class DummyContext
#endregion // class DummyContext
#region class DummyThreadedAction
#region class DummyThreadedAction
/// <summary>Implementation of a threaded action for the unit test</summary>
/// <summary>Implementation of a threaded action for the unit test</summary> private class DummyThreadedAction : ThreadedAction {
private class DummyThreadedAction : ThreadedAction {
/// <summary>
/// <summary> /// Initializes a new threaded action, letting the base class figure out the UI thread
/// Initializes a new threaded action, letting the base class figure out the UI thread /// </summary>
/// </summary> public DummyThreadedAction() : base() {
public DummyThreadedAction() : base() { this.finishedGate = new ManualResetEvent(initialState: false);
this.finishedGate = new ManualResetEvent(initialState: false); }
}
/// <summary>
/// <summary> /// Initializes a new view model using the specified UI context explicitly
/// Initializes a new view model using the specified UI context explicitly /// </summary>
/// </summary> public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) {
public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) { this.finishedGate = new ManualResetEvent(initialState: false);
this.finishedGate = new ManualResetEvent(initialState: false); }
}
/// <summary>Immediately releases all resources owned by the instance</summary>
/// <summary>Immediately releases all resources owned by the instance</summary> public override void Dispose() {
public override void Dispose() { base.Dispose();
base.Dispose();
if(this.finishedGate != null) {
if(this.finishedGate != null) { this.finishedGate.Dispose();
this.finishedGate.Dispose(); this.finishedGate = null;
this.finishedGate = null; }
} }
}
/// <summary>Waits until the first background operation is finished</summary>
/// <summary>Waits until the first background operation is finished</summary> /// <returns>
/// <returns> /// True if the background operation is finished, false if it is ongoing
/// True if the background operation is finished, false if it is ongoing /// </returns>
/// </returns> public bool WaitUntilFinished() {
public bool WaitUntilFinished() { return this.finishedGate.WaitOne(100);
return this.finishedGate.WaitOne(100); }
}
/// <summary>Selects the value that will be assigned when the action runs</summary>
/// <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>
/// <param name="valueToAssign">Value the action will assigned when it runs</param> public void SetValueToAssign(int valueToAssign) {
public void SetValueToAssign(int valueToAssign) { this.valueToAssign = valueToAssign;
this.valueToAssign = valueToAssign; }
}
/// <summary>Sets up an error the action will fail with when run</summary>
/// <summary>Sets up an error the action will fail with when run</summary> /// <param name="errorToFailWith">Error the action will fail with</param>
/// <param name="errorToFailWith">Error the action will fail with</param> public void SetErrorToFailWith(Exception errorToFailWith) {
public void SetErrorToFailWith(Exception errorToFailWith) { this.errorToFailWith = errorToFailWith;
this.errorToFailWith = errorToFailWith; }
}
/// <summary>Last error that was reported by the threaded view model</summary>
/// <summary>Last error that was reported by the threaded view model</summary> public Exception ReportedError {
public Exception ReportedError { get { return this.reportedError; }
get { return this.reportedError; } }
}
/// <summary>Value that has been assigned from the background thread</summary>
/// <summary>Value that has been assigned from the background thread</summary> public int AssignedValue {
public int AssignedValue { get { return this.assignedValue; }
get { return this.assignedValue; } }
}
/// <summary>Executes the threaded action from the background thread</summary>
/// <summary>Executes the threaded action from the background thread</summary> /// <param name="cancellationToken">Token by which execution can be canceled</param>
/// <param name="cancellationToken">Token by which execution can be canceled</param> protected override void Run(CancellationToken cancellationToken) {
protected override void Run(CancellationToken cancellationToken) { if(this.errorToFailWith != null) {
if(this.errorToFailWith != null) { throw this.errorToFailWith;
throw this.errorToFailWith; }
}
this.assignedValue = this.valueToAssign;
this.assignedValue = this.valueToAssign; this.finishedGate.Set();
this.finishedGate.Set(); }
}
/// <summary>Called when an error occurs in the background thread</summary>
/// <summary>Called when an error occurs in the background thread</summary> /// <param name="exception">Exception that was thrown in the background thread</param>
/// <param name="exception">Exception that was thrown in the background thread</param> protected override void ReportError(Exception exception) {
protected override void ReportError(Exception exception) { this.reportedError = exception;
this.reportedError = exception; this.finishedGate.Set();
this.finishedGate.Set(); }
}
/// <summary>Error the action will fail with, if set</summary>
/// <summary>Error the action will fail with, if set</summary> private Exception errorToFailWith;
private Exception errorToFailWith; /// <summary>Value the action will assign to its same-named field</summary>
/// <summary>Value the action will assign to its same-named field</summary> private int valueToAssign;
private int valueToAssign;
/// <summary>Last error that was reported by the threaded view model</summary>
/// <summary>Last error that was reported by the threaded view model</summary> private volatile Exception reportedError;
private volatile Exception reportedError; /// <summary>Triggered when the </summary>
/// <summary>Triggered when the </summary> private ManualResetEvent finishedGate;
private ManualResetEvent finishedGate; /// <summary>Value that is assigned through the background thread</summary>
/// <summary>Value that is assigned through the background thread</summary> private volatile int assignedValue;
private volatile int assignedValue;
}
}
#endregion // class DummyThreadedAction
#endregion // class DummyThreadedAction
/// <summary>Verifies that the threaded action has a default constructor</summary>
/// <summary>Verifies that the threaded action has a default constructor</summary> [Test, Explicit]
[Test, Explicit] public void HasDefaultConstructor() {
public void HasDefaultConstructor() { using(var mainForm = new System.Windows.Forms.Form()) {
using(var mainForm = new System.Windows.Forms.Form()) { mainForm.Show();
mainForm.Show(); try {
try { mainForm.Visible = false;
mainForm.Visible = false; using(new DummyThreadedAction()) { }
using(new DummyThreadedAction()) { } }
} finally {
finally { mainForm.Close();
mainForm.Close(); }
} }
} }
}
/// <summary>
/// <summary> /// Verifies that the threaded action can be constructed with a custom UI context
/// Verifies that the threaded action can be constructed with a custom UI context /// </summary>
/// </summary> [Test]
[Test] public void HasCustomSychronizationContextConstructor() {
public void HasCustomSychronizationContextConstructor() { using(new DummyThreadedAction(new DummyContext())) { }
using(new DummyThreadedAction(new DummyContext())) { } }
}
/// <summary>Checks that a new threadd action starts out idle and not busy</summary>
/// <summary>Checks that a new threadd action starts out idle and not busy</summary> [Test]
[Test] public void NewInstanceIsNotBusy() {
public void NewInstanceIsNotBusy() { using(var action = new DummyThreadedAction(new DummyContext())) {
using(var action = new DummyThreadedAction(new DummyContext())) { Assert.IsFalse(action.IsBusy);
Assert.IsFalse(action.IsBusy); }
} }
}
/// <summary>
/// <summary> /// Verifies that errors happening in the background processing threads are
/// Verifies that errors happening in the background processing threads are /// reported to the main thread
/// reported to the main thread /// </summary>
/// </summary> [Test]
[Test] public void ErrorsInBackgroundThreadAreReported() {
public void ErrorsInBackgroundThreadAreReported() { using(var action = new DummyThreadedAction(new DummyContext())) {
using(var action = new DummyThreadedAction(new DummyContext())) { var testError = new ArgumentException("Mooh");
var testError = new ArgumentException("Mooh"); action.SetErrorToFailWith(testError);
action.SetErrorToFailWith(testError); action.Start();
action.Start(); action.WaitUntilFinished();
action.WaitUntilFinished(); Assert.AreSame(testError, action.ReportedError);
Assert.AreSame(testError, action.ReportedError); }
} }
}
/// <summary>
/// <summary> /// Verifies that the background thread actually executes and can do work
/// Verifies that the background thread actually executes and can do work /// </summary>
/// </summary> [Test]
[Test] public void BackgroundThreadExecutesTasks() {
public void BackgroundThreadExecutesTasks() { using(var action = new DummyThreadedAction(new DummyContext())) {
using(var action = new DummyThreadedAction(new DummyContext())) { action.SetValueToAssign(42001);
action.SetValueToAssign(42001); action.Start();
action.Start(); action.WaitUntilFinished();
action.WaitUntilFinished(); Assert.AreEqual(42001, action.AssignedValue);
Assert.AreEqual(42001, action.AssignedValue); }
} }
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST #endif // UNITTEST

View File

@ -1,398 +1,397 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Threading;
using System.Threading; using System.Windows.Forms;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support; using Nuclex.Support.Threading;
using Nuclex.Support.Threading;
// Possible problem:
// Possible problem: //
// // After Run() is called, the action may not actually run if
// After Run() is called, the action may not actually run if // it is using another thread runner and that one is cancelled.
// it is using another thread runner and that one is cancelled. //
// // Thus, a second call to Run() has to schedule the action again,
// Thus, a second call to Run() has to schedule the action again, // even if it might already be scheduled, but should also not execute
// even if it might already be scheduled, but should also not execute // the action a second time if is was indeed still scheduled.
// the action a second time if is was indeed still scheduled.
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Encapsulates an action that can run in a thread</summary>
/// <summary>Encapsulates an action that can run in a thread</summary> /// <remarks>
/// <remarks> /// <para>
/// <para> /// Sometimes a view model wants to allow multiple actions to take place
/// Sometimes a view model wants to allow multiple actions to take place /// at the same time. Think multiple panels on the view require updating
/// 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
/// from a web service - you can make both requests at the same time /// instead of sequentially.
/// instead of sequentially. /// </para>
/// </para> /// <para>
/// <para> /// This class is also of use for things that need to be done sequentially
/// 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,
/// by sharing the thread runner of the threaded view model. That way, /// you still have cancellable actions you can run at will and they
/// you still have cancellable actions you can run at will and they /// automatically queue themselves to be executed one after another.
/// automatically queue themselves to be executed one after another. /// </para>
/// </para> /// </remarks>
/// </remarks> public abstract class ThreadedAction : Observable, IDisposable {
public abstract class ThreadedAction : Observable, IDisposable {
#region class ThreadedActionThreadRunner
#region class ThreadedActionThreadRunner
/// <summary>Thread runner for the threaded action</summary>
/// <summary>Thread runner for the threaded action</summary> private class ThreadedActionThreadRunner : ThreadRunner {
private class ThreadedActionThreadRunner : ThreadRunner {
/// <summary>Initializes a new thread runner for the threaded view model</summary>
/// <summary>Initializes a new thread runner for the threaded view model</summary> public ThreadedActionThreadRunner(ThreadedAction viewModel) {
public ThreadedActionThreadRunner(ThreadedAction viewModel) { this.threadedAction = viewModel;
this.threadedAction = viewModel; }
}
/// <summary>Reports an error</summary>
/// <summary>Reports an error</summary> /// <param name="exception">Error that will be reported</param>
/// <param name="exception">Error that will be reported</param> protected override void ReportError(Exception exception) {
protected override void ReportError(Exception exception) { this.threadedAction.reportErrorFromThread(exception);
this.threadedAction.reportErrorFromThread(exception); }
}
/// <summary>Called when the status of the busy flag changes</summary>
/// <summary>Called when the status of the busy flag changes</summary> protected override void BusyChanged() {
protected override void BusyChanged() { // Narf. Can't use this.
// Narf. Can't use this. }
}
/// <summary>View model the thread runner belongs to</summary>
/// <summary>View model the thread runner belongs to</summary> private ThreadedAction threadedAction;
private ThreadedAction threadedAction;
}
}
#endregion // class ThreadedActionThreadRunner
#endregion // class ThreadedActionThreadRunner
/// <summary>Initializes all common fields of the instance</summary>
/// <summary>Initializes all common fields of the instance</summary> private ThreadedAction() {
private ThreadedAction() { this.callRunIfNotCancelledDelegate = new Action<CancellationTokenSource>(
this.callRunIfNotCancelledDelegate = new Action<CancellationTokenSource>( callThreadedExecuteIfNotCancelled
callThreadedExecuteIfNotCancelled );
); this.reportErrorDelegate = new Action<Exception>(ReportError);
this.reportErrorDelegate = new Action<Exception>(ReportError); }
}
/// <summary>Initializes a threaded action that uses its own thread runner</summary>
/// <summary>Initializes a threaded action that uses its own thread runner</summary> public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() {
public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() { if(uiContext == null) {
if(uiContext == null) { this.uiContext = LateCheckedSynchronizer.GetMainWindow();
this.uiContext = LateCheckedSynchronizer.GetMainWindow(); if(this.uiContext == null) {
if(this.uiContext == null) { this.uiContext = new LateCheckedSynchronizer(updateUiContext);
this.uiContext = new LateCheckedSynchronizer(updateUiContext); }
} } else {
} else { this.uiContext = uiContext;
this.uiContext = uiContext; }
}
this.ownThreadRunner = new ThreadedActionThreadRunner(this);
this.ownThreadRunner = new ThreadedActionThreadRunner(this); }
}
/// <summary>
/// <summary> /// Initializes a threaded action that uses the view model's thread runner
/// Initializes a threaded action that uses the view model's thread runner /// </summary>
/// </summary> /// <param name="viewModel">View model whose thread runner will be used</param>
/// <param name="viewModel">View model whose thread runner will be used</param> /// <param name="uiContext">
/// <param name="uiContext"> /// UI dispatcher that can be used to run callbacks in the UI thread
/// UI dispatcher that can be used to run callbacks in the UI thread /// </param>
/// </param> public ThreadedAction(
public ThreadedAction( ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null
ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null ) : this() {
) : this() { if(uiContext == null) {
if(uiContext == null) { this.uiContext = LateCheckedSynchronizer.GetMainWindow();
this.uiContext = LateCheckedSynchronizer.GetMainWindow(); if(this.uiContext == null) {
if(this.uiContext == null) { this.uiContext = new LateCheckedSynchronizer(updateUiContext);
this.uiContext = new LateCheckedSynchronizer(updateUiContext); }
} } else {
} else { this.uiContext = uiContext;
this.uiContext = uiContext; }
}
this.externalThreadRunner = viewModel.ThreadRunner;
this.externalThreadRunner = viewModel.ThreadRunner; }
}
/// <summary>Immediately releases all resources owned by the instance</summary>
/// <summary>Immediately releases all resources owned by the instance</summary> public virtual void Dispose() {
public virtual void Dispose() { if(this.isBusy) {
if(this.isBusy) { Cancel();
Cancel(); }
} if(this.ownThreadRunner != null) {
if(this.ownThreadRunner != null) { this.ownThreadRunner.Dispose();
this.ownThreadRunner.Dispose(); this.ownThreadRunner = null;
this.ownThreadRunner = null; }
} if(this.currentCancellationTokenSource != null) {
if(this.currentCancellationTokenSource != null) { this.currentCancellationTokenSource.Dispose();
this.currentCancellationTokenSource.Dispose(); this.currentCancellationTokenSource = null;
this.currentCancellationTokenSource = null; }
} }
}
/// <summary>Whether the view model is currently busy executing a task</summary>
/// <summary>Whether the view model is currently busy executing a task</summary> public bool IsBusy {
public bool IsBusy { get { return this.isBusy; }
get { return this.isBusy; } private set {
private set { if(value != this.isBusy) {
if(value != this.isBusy) { this.isBusy = value;
this.isBusy = value; OnPropertyChanged(nameof(IsBusy));
OnPropertyChanged(nameof(IsBusy)); }
} }
} }
}
/// <summary>Cancels the running background task, if any</summary>
/// <summary>Cancels the running background task, if any</summary> public void Cancel() {
public void Cancel() { lock(this.runningTaskSyncRoot) {
lock(this.runningTaskSyncRoot) {
// If the background task is not running, do nothing. This also allows
// If the background task is not running, do nothing. This also allows // us to avoid needless recreation of the same cancellation token source.
// us to avoid needless recreation of the same cancellation token source. if(!this.isBusy) {
if(!this.isBusy) { return;
return; }
}
// If a task is currently running, cancel it
// If a task is currently running, cancel it if(this.isRunning) {
if(this.isRunning) { if(this.currentCancellationTokenSource != null) {
if(this.currentCancellationTokenSource != null) { this.currentCancellationTokenSource.Cancel();
this.currentCancellationTokenSource.Cancel(); this.currentCancellationTokenSource = null;
this.currentCancellationTokenSource = null; }
} }
}
// If the task was scheduled to be repeated, we also have to mark
// If the task was scheduled to be repeated, we also have to mark // the upcoming cancellation token source as canceled because the scheduled
// the upcoming cancellation token source as canceled because the scheduled // run will still be happening (it will just cancel out immediately).
// run will still be happening (it will just cancel out immediately). if(this.nextCancellationTokenSource != null) {
if(this.nextCancellationTokenSource != null) { this.nextCancellationTokenSource.Cancel();
this.nextCancellationTokenSource.Cancel(); this.nextCancellationTokenSource = null;
this.nextCancellationTokenSource = null; }
} this.isScheduledAgain = false;
this.isScheduledAgain = false;
// If the task was not running, we can clear the busy state because it
// If the task was not running, we can clear the busy state because it // is not going to reach the running state.
// is not going to reach the running state. if(!this.isRunning) {
if(!this.isRunning) { this.isBusy = false;
this.isBusy = false; }
}
}
} }
}
/// <summary>
/// <summary> /// Starts the task, cancelling the running task before doing so
/// Starts the task, cancelling the running task before doing so /// </summary>
/// </summary> public void Restart() {
public void Restart() { bool reportBusyChange = false;
bool reportBusyChange = false;
lock(this.runningTaskSyncRoot) {
lock(this.runningTaskSyncRoot) {
// If we're already in the execution phase, schedule another execution right
// If we're already in the execution phase, schedule another execution right // after this one is finished (because now, data might have changed after
// after this one is finished (because now, data might have changed after // execution has finished).
// execution has finished). if(this.isRunning) {
if(this.isRunning) { //System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution");
//System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution"); if(this.currentCancellationTokenSource != null) {
if(this.currentCancellationTokenSource != null) { this.currentCancellationTokenSource.Cancel();
this.currentCancellationTokenSource.Cancel(); }
}
this.currentCancellationTokenSource = this.nextCancellationTokenSource;
this.currentCancellationTokenSource = this.nextCancellationTokenSource; this.nextCancellationTokenSource = null;
this.nextCancellationTokenSource = null; this.isScheduledAgain = false;
this.isScheduledAgain = false; }
}
// If there's no cancellation token source, create one. If an execution
// If there's no cancellation token source, create one. If an execution // was already scheduled and the cancellation token source is still valid,
// was already scheduled and the cancellation token source is still valid, // then reuse that in order to be able to cancel all scheduled executions.
// then reuse that in order to be able to cancel all scheduled executions. if(this.currentCancellationTokenSource == null) {
if(this.currentCancellationTokenSource == null) { //System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token");
//System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token"); this.currentCancellationTokenSource = new CancellationTokenSource();
this.currentCancellationTokenSource = new CancellationTokenSource(); }
}
// Schedule another execution of the action
// Schedule another execution of the action scheduleExecution();
scheduleExecution();
reportBusyChange = (this.isBusy == false);
reportBusyChange = (this.isBusy == false); this.isBusy = true;
this.isBusy = true; }
}
if(reportBusyChange) {
if(reportBusyChange) { OnPropertyChanged(nameof(IsBusy));
OnPropertyChanged(nameof(IsBusy)); }
} }
}
/// <summary>Starts the task</summary>
/// <summary>Starts the task</summary> public void Start() {
public void Start() { bool reportBusyChange = false;
bool reportBusyChange = false;
lock(this.runningTaskSyncRoot) {
lock(this.runningTaskSyncRoot) {
// If we're already in the execution phase, schedule another execution right
// If we're already in the execution phase, schedule another execution right // after this one is finished (because now, data might have changed after
// after this one is finished (because now, data might have changed after // execution has finished).
// execution has finished). if(this.isRunning) {
if(this.isRunning) {
// If we already created a new cancellation token source, keep it,
// If we already created a new cancellation token source, keep it, // otherwise create a new one for the next execution
// otherwise create a new one for the next execution if(!this.isScheduledAgain) {
if(!this.isScheduledAgain) { this.nextCancellationTokenSource = new CancellationTokenSource();
this.nextCancellationTokenSource = new CancellationTokenSource(); this.isScheduledAgain = true;
this.isScheduledAgain = true; }
}
} else {
} else {
// If there's no cancellation token source, create one. If an execution
// If there's no cancellation token source, create one. If an execution // was already scheduled and the cancellation token source is still valid,
// was already scheduled and the cancellation token source is still valid, // then reuse that in order to be able to cancel all scheduled executions.
// then reuse that in order to be able to cancel all scheduled executions. if(this.currentCancellationTokenSource == null) {
if(this.currentCancellationTokenSource == null) { this.currentCancellationTokenSource = new CancellationTokenSource();
this.currentCancellationTokenSource = new CancellationTokenSource(); }
}
// Schedule another execution of the action
// Schedule another execution of the action scheduleExecution();
scheduleExecution();
}
}
reportBusyChange = (this.isBusy == false);
reportBusyChange = (this.isBusy == false); this.isBusy = true;
this.isBusy = true; }
}
if(reportBusyChange) {
if(reportBusyChange) { OnPropertyChanged(nameof(IsBusy));
OnPropertyChanged(nameof(IsBusy)); }
} }
}
/// <summary>Reports an error</summary>
/// <summary>Reports an error</summary> /// <param name="exception">Error that will be reported</param>
/// <param name="exception">Error that will be reported</param> protected abstract void ReportError(Exception exception);
protected abstract void ReportError(Exception exception);
/// <summary>Executes the threaded action from the background thread</summary>
/// <summary>Executes the threaded action from the background thread</summary> /// <param name="cancellationToken">Token by which execution can be canceled</param>
/// <param name="cancellationToken">Token by which execution can be canceled</param> protected abstract void Run(CancellationToken cancellationToken);
protected abstract void Run(CancellationToken cancellationToken);
/// <summary>
/// <summary> /// Calls the Run() method from the background thread and manages the flags
/// Calls the Run() method from the background thread and manages the flags /// </summary>
/// </summary> /// <param name="cancellationTokenSource"></param>
/// <param name="cancellationTokenSource"></param> private void callThreadedExecuteIfNotCancelled(
private void callThreadedExecuteIfNotCancelled( CancellationTokenSource cancellationTokenSource
CancellationTokenSource cancellationTokenSource ) {
) { lock(this) {
lock(this) { if(cancellationTokenSource.Token.IsCancellationRequested) {
if(cancellationTokenSource.Token.IsCancellationRequested) { return;
return; }
}
this.isRunning = true;
this.isRunning = true; }
}
try {
try { Run(cancellationTokenSource.Token);
Run(cancellationTokenSource.Token); }
} finally {
finally { bool reportBusyChange = false;
bool reportBusyChange = false;
lock(this) {
lock(this) { this.isRunning = false;
this.isRunning = false;
// Cancel the current cancellation token because this execution may have
// Cancel the current cancellation token because this execution may have // been scheduled multiple times (there's no way for the Run() method to
// 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
// know if the currently scheduled execution was cancelled, so it is forced // to reschedule on each call - accepting redundant schedules).
// to reschedule on each call - accepting redundant schedules). cancellationTokenSource.Cancel();
cancellationTokenSource.Cancel();
// Pick the next cancellation token source. Normally it is null, but this
// 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 :)
// is more elegant because we can avoid an while if statement this way :) this.currentCancellationTokenSource = nextCancellationTokenSource;
this.currentCancellationTokenSource = nextCancellationTokenSource; this.nextCancellationTokenSource = null;
this.nextCancellationTokenSource = null;
// If Start() was called while we were executing, another execution is required
// If Start() was called while we were executing, another execution is required // (because the data may have changed during the call to Start()).
// (because the data may have changed during the call to Start()). if(this.isScheduledAgain) {
if(this.isScheduledAgain) { this.isScheduledAgain = false;
this.isScheduledAgain = false; scheduleExecution();
scheduleExecution(); } else { // We're idle now
} else { // We're idle now reportBusyChange = (this.isBusy == true);
reportBusyChange = (this.isBusy == true); this.isBusy = false;
this.isBusy = false; }
} }
}
if(reportBusyChange) {
if(reportBusyChange) { OnPropertyChanged(nameof(IsBusy));
OnPropertyChanged(nameof(IsBusy)); }
} }
} }
}
/// <summary>Schedules one execution of the action</summary>
/// <summary>Schedules one execution of the action</summary> private void scheduleExecution() {
private void scheduleExecution() { //System.Diagnostics.Debug.WriteLine("Scheduling execution");
//System.Diagnostics.Debug.WriteLine("Scheduling execution");
ThreadRunner runner = this.externalThreadRunner;
ThreadRunner runner = this.externalThreadRunner; if(runner != null) {
if(runner != null) { runner.RunInBackground(
runner.RunInBackground( this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource );
); }
}
runner = this.ownThreadRunner;
runner = this.ownThreadRunner; if(runner != null) {
if(runner != null) { runner.RunInBackground(
runner.RunInBackground( this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource
this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource );
); }
} }
}
/// <summary>Reports an error that occurred in the runner's background thread</summary>
/// <summary>Reports an error that occurred in the runner's background thread</summary> /// <param name="exception">Exception that the thread has encountered</param>
/// <param name="exception">Exception that the thread has encountered</param> private void reportErrorFromThread(Exception exception) {
private void reportErrorFromThread(Exception exception) { this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception }); }
}
/// <summary>Sets the UI context that will be used by the threaded action</summary>
/// <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>
/// <param name="uiContext">The UI context the threaded action will use</param> private void updateUiContext(ISynchronizeInvoke uiContext) {
private void updateUiContext(ISynchronizeInvoke uiContext) { this.uiContext = uiContext;
this.uiContext = uiContext; }
}
/// <summary>Synchronization context of the thread in which the view runs</summary>
/// <summary>Synchronization context of the thread in which the view runs</summary> private ISynchronizeInvoke uiContext;
private ISynchronizeInvoke uiContext; /// <summary>Delegate for the ReportError() method</summary>
/// <summary>Delegate for the ReportError() method</summary> private Action<Exception> reportErrorDelegate;
private Action<Exception> reportErrorDelegate; /// <summary>Delegate for the callThreadedExecuteIfNotCancelled() method</summary>
/// <summary>Delegate for the callThreadedExecuteIfNotCancelled() method</summary> private Action<CancellationTokenSource> callRunIfNotCancelledDelegate;
private Action<CancellationTokenSource> callRunIfNotCancelledDelegate;
/// <summary>Thread runner on which the action can run its background task</summary>
/// <summary>Thread runner on which the action can run its background task</summary> private ThreadedActionThreadRunner ownThreadRunner;
private ThreadedActionThreadRunner ownThreadRunner; /// <summary>
/// <summary> /// External thread runner on which the action runs its background task if assigned
/// External thread runner on which the action runs its background task if assigned /// </summary>
/// </summary> private ThreadRunner externalThreadRunner;
private ThreadRunner externalThreadRunner;
/// <summary>Synchronization root for the threaded execute method</summary>
/// <summary>Synchronization root for the threaded execute method</summary> private object runningTaskSyncRoot = new object();
private object runningTaskSyncRoot = new object(); /// <summary>Used to cancel the currently running task</summary>
/// <summary>Used to cancel the currently running task</summary> private CancellationTokenSource currentCancellationTokenSource;
private CancellationTokenSource currentCancellationTokenSource; /// <summary>Used to cancel the upcoming task if a re-run was scheduled</summary>
/// <summary>Used to cancel the upcoming task if a re-run was scheduled</summary> private CancellationTokenSource nextCancellationTokenSource;
private CancellationTokenSource nextCancellationTokenSource; /// <summary>Whether the background task is running or waiting to run</summary>
/// <summary>Whether the background task is running or waiting to run</summary> private volatile bool isBusy;
private volatile bool isBusy; /// <summary>Whether execution is taking place right now</summary>
/// <summary>Whether execution is taking place right now</summary> /// <remarks>
/// <remarks> /// If this flag is set and the Start() method is called, another run needs to
/// If this flag is set and the Start() method is called, another run needs to /// be scheduled.
/// be scheduled. /// </remarks>
/// </remarks> private bool isRunning;
private bool isRunning; /// <summary>Whether run was called while the action was already running</summary>
/// <summary>Whether run was called while the action was already running</summary> private bool isScheduledAgain;
private bool isScheduledAgain;
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels

View File

@ -1,174 +1,173 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
#if UNITTEST
#if UNITTEST
using System;
using System;
using NUnit.Framework;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded dialog view model</summary>
/// <summary>Unit test for the threaded dialog view model</summary> [TestFixture]
[TestFixture] public class ThreadedDialogViewModelTest {
public class ThreadedDialogViewModelTest {
#region class DialogViewModelSubscriber
#region class DialogViewModelSubscriber
/// <summary>Subscriber for the events offered by a dialog view model</summary>
/// <summary>Subscriber for the events offered by a dialog view model</summary> private class DialogViewModelSubscriber {
private class DialogViewModelSubscriber {
/// <summary>Indicates that the user has accepted the dialog</summary>
/// <summary>Indicates that the user has accepted the dialog</summary> public void Confirmed(object sender, EventArgs arguments) {
public void Confirmed(object sender, EventArgs arguments) { ++this.confirmCallCount;
++this.confirmCallCount; }
}
/// <summary>Indicates that the user has cancelled the dialog</summary>
/// <summary>Indicates that the user has cancelled the dialog</summary> public void Cancelled(object sender, EventArgs arguments) {
public void Cancelled(object sender, EventArgs arguments) { ++this.cancelCallCount;
++this.cancelCallCount; }
}
/// <summary>Indicates that the dialog was simply closed</summary>
/// <summary>Indicates that the dialog was simply closed</summary> public void Submitted(object sender, EventArgs arguments) {
public void Submitted(object sender, EventArgs arguments) { ++this.submitCallCount;
++this.submitCallCount; }
}
/// <summary>How many times the Confirmed() method was called</summary>
/// <summary>How many times the Confirmed() method was called</summary> public int ConfirmCallCount {
public int ConfirmCallCount { get { return this.confirmCallCount; }
get { return this.confirmCallCount; } }
}
/// <summary>How many times the Cancelled() method was called</summary>
/// <summary>How many times the Cancelled() method was called</summary> public int CancelCallCount {
public int CancelCallCount { get { return this.cancelCallCount; }
get { return this.cancelCallCount; } }
}
/// <summary>How many times the Submitted() method was called</summary>
/// <summary>How many times the Submitted() method was called</summary> public int SubmitCallCount {
public int SubmitCallCount { get { return this.submitCallCount; }
get { return this.submitCallCount; } }
}
/// <summary>How many times the Confirmed() method was called</summary>
/// <summary>How many times the Confirmed() method was called</summary> private int confirmCallCount;
private int confirmCallCount; /// <summary>How many times the Cancelled() method was called</summary>
/// <summary>How many times the Cancelled() method was called</summary> private int cancelCallCount;
private int cancelCallCount; /// <summary>How many times the Submitted() method was called</summary>
/// <summary>How many times the Submitted() method was called</summary> private int submitCallCount;
private int submitCallCount;
}
}
#endregion // class DialogViewModelSubscriber
#endregion // class DialogViewModelSubscriber
#region class TestViewModel
#region class TestViewModel
private class TestViewModel : ThreadedDialogViewModel {
private class TestViewModel : ThreadedDialogViewModel {
public Exception ReportedError {
public Exception ReportedError { get { return this.reportedError; }
get { return this.reportedError; } }
}
protected override void ReportError(Exception exception) {
protected override void ReportError(Exception exception) { this.reportedError = exception;
this.reportedError = exception; }
}
private Exception reportedError;
private Exception reportedError;
}
}
#endregion // class TestViewModel
#endregion // class TestViewModel
/// <summary>Verifies that the dialog view model has a default constructor</summary>
/// <summary>Verifies that the dialog view model has a default constructor</summary> [Test]
[Test] public void HasDefaultConstructor() {
public void HasDefaultConstructor() { Assert.DoesNotThrow(
Assert.DoesNotThrow( delegate() { new TestViewModel(); }
delegate() { new TestViewModel(); } );
); }
}
/// <summary>
/// <summary> /// Verifies that calling Confirm() on the dialog view model triggers
/// Verifies that calling Confirm() on the dialog view model triggers /// the 'Confirmed' event
/// the 'Confirmed' event /// </summary>
/// </summary> [Test]
[Test] public void ConfirmTriggersConfirmedEvent() {
public void ConfirmTriggersConfirmedEvent() { var viewModel = new TestViewModel();
var viewModel = new TestViewModel(); var subscriber = createSubscriber(viewModel);
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); viewModel.Confirm();
viewModel.Confirm(); Assert.AreEqual(1, subscriber.ConfirmCallCount);
Assert.AreEqual(1, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); }
}
/// <summary>
/// <summary> /// Verifies that calling Cancel() on the dialog view model triggers
/// Verifies that calling Cancel() on the dialog view model triggers /// the 'Cancelled' event
/// the 'Cancelled' event /// </summary>
/// </summary> [Test]
[Test] public void CancelTriggersCancelledEvent() {
public void CancelTriggersCancelledEvent() { var viewModel = new TestViewModel();
var viewModel = new TestViewModel(); var subscriber = createSubscriber(viewModel);
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); viewModel.Cancel();
viewModel.Cancel(); Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(1, subscriber.CancelCallCount);
Assert.AreEqual(1, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); }
}
/// <summary>
/// <summary> /// Verifies that calling Submitm() on the dialog view model triggers
/// Verifies that calling Submitm() on the dialog view model triggers /// the 'Submitted' event
/// the 'Submitted' event /// </summary>
/// </summary> [Test]
[Test] public void SubmitTriggersSubmittedEvent() {
public void SubmitTriggersSubmittedEvent() { var viewModel = new TestViewModel();
var viewModel = new TestViewModel(); var subscriber = createSubscriber(viewModel);
var subscriber = createSubscriber(viewModel);
Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(0, subscriber.SubmitCallCount);
Assert.AreEqual(0, subscriber.SubmitCallCount); viewModel.Submit();
viewModel.Submit(); Assert.AreEqual(0, subscriber.ConfirmCallCount);
Assert.AreEqual(0, subscriber.ConfirmCallCount); Assert.AreEqual(0, subscriber.CancelCallCount);
Assert.AreEqual(0, subscriber.CancelCallCount); Assert.AreEqual(1, subscriber.SubmitCallCount);
Assert.AreEqual(1, subscriber.SubmitCallCount); }
}
/// <summary>Constructs a new subscriber for the dialog view model's events</summary>
/// <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>
/// <param name="viewModel">View model a subscriber will be created for</param> /// <returns>A subscriber for the events of the specified view model</returns>
/// <returns>A subscriber for the events of the specified view model</returns> private DialogViewModelSubscriber createSubscriber(ThreadedDialogViewModel viewModel) {
private DialogViewModelSubscriber createSubscriber(ThreadedDialogViewModel viewModel) { var subscriber = new DialogViewModelSubscriber();
var subscriber = new DialogViewModelSubscriber(); viewModel.Confirmed += subscriber.Confirmed;
viewModel.Confirmed += subscriber.Confirmed; viewModel.Canceled += subscriber.Cancelled;
viewModel.Canceled += subscriber.Cancelled; viewModel.Submitted += subscriber.Submitted;
viewModel.Submitted += subscriber.Submitted; return subscriber;
return subscriber; }
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST
#endif // UNITTEST

View File

@ -1,74 +1,73 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System;
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>View model for a dialog that can execute tasks in a background thread</summary>
/// <summary>View model for a dialog that can execute tasks in a background thread</summary> public abstract class ThreadedDialogViewModel : ThreadedViewModel {
public abstract class ThreadedDialogViewModel : ThreadedViewModel {
/// <summary>Indicates that the view should close with a positive result</summary>
/// <summary>Indicates that the view should close with a positive result</summary> /// <remarks>
/// <remarks> /// This event typically corresponds to the 'Ok' button in a dialog.
/// This event typically corresponds to the 'Ok' button in a dialog. /// </remarks>
/// </remarks> public event EventHandler Confirmed;
public event EventHandler Confirmed;
/// <summary>Indicates that the view should close with a negative result</summary>
/// <summary>Indicates that the view should close with a negative result</summary> /// <remarks>
/// <remarks> /// This event typically corresponds to the 'Cancel' button in a dialog.
/// This event typically corresponds to the 'Cancel' button in a dialog. /// </remarks>
/// </remarks> public event EventHandler Canceled;
public event EventHandler Canceled;
/// <summary>Indicates that the view should close</summary>
/// <summary>Indicates that the view should close</summary> /// <remarks>
/// <remarks> /// This closes the view with a neutral result, used when the view doesn't follow
/// 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.
/// an ok/cancel scheme or the result is transmitted in some other way. /// </remarks>
/// </remarks> public event EventHandler Submitted;
public event EventHandler Submitted;
/// <summary>
/// <summary> /// Indicates that the dialog should be closed with a positive outcome
/// Indicates that the dialog should be closed with a positive outcome /// </summary>
/// </summary> public virtual void Confirm() {
public virtual void Confirm() { if(Confirmed != null) {
if(Confirmed != null) { Confirmed(this, EventArgs.Empty);
Confirmed(this, EventArgs.Empty); }
} }
}
/// <summary>
/// <summary> /// Indicates that the dialog should be closed with a negative outcome
/// Indicates that the dialog should be closed with a negative outcome /// </summary>
/// </summary> public virtual void Cancel() {
public virtual void Cancel() { if(Canceled != null) {
if(Canceled != null) { Canceled(this, EventArgs.Empty);
Canceled(this, EventArgs.Empty); }
} }
}
/// <summary>Indicates that the dialog should be closed</summary>
/// <summary>Indicates that the dialog should be closed</summary> public virtual void Submit() {
public virtual void Submit() { if(Submitted != null) {
if(Submitted != null) { Submitted(this, EventArgs.Empty);
Submitted(this, EventArgs.Empty); }
} }
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels

View File

@ -1,262 +1,261 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
#if UNITTEST
#if UNITTEST
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Threading;
using System.Threading; using NUnit.Framework;
using NUnit.Framework;
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Unit test for the threaded view model base class</summary>
/// <summary>Unit test for the threaded view model base class</summary> [TestFixture]
[TestFixture] public class ThreadedViewModelTest {
public class ThreadedViewModelTest {
#region class DummyContext
#region class DummyContext
/// <summary>Synchronization context that does absolutely nothing</summary>
/// <summary>Synchronization context that does absolutely nothing</summary> private class DummyContext : ISynchronizeInvoke {
private class DummyContext : ISynchronizeInvoke {
#region class SimpleAsyncResult
#region class SimpleAsyncResult
/// <summary>Barebones implementation of an asynchronous result</summary>
/// <summary>Barebones implementation of an asynchronous result</summary> private class SimpleAsyncResult : IAsyncResult {
private class SimpleAsyncResult : IAsyncResult {
/// <summary>Ehether the asynchronous operation is complete</summary>
/// <summary>Ehether the asynchronous operation is complete</summary> /// <remarks>
/// <remarks> /// Always true because it completes synchronously
/// Always true because it completes synchronously /// </remarks>
/// </remarks> public bool IsCompleted { get { return true; } }
public bool IsCompleted { get { return true; } }
/// <summary>
/// <summary> /// Wait handle that can be used to wait for the asynchronous operation
/// Wait handle that can be used to wait for the asynchronous operation /// </summary>
/// </summary> public WaitHandle AsyncWaitHandle {
public WaitHandle AsyncWaitHandle { get { throw new NotImplementedException("Not implemented"); }
get { throw new NotImplementedException("Not implemented"); } }
}
/// <summary>Custom state that can be used to pass information around</summary>
/// <summary>Custom state that can be used to pass information around</summary> public object AsyncState {
public object AsyncState { get { throw new NotImplementedException("Not implemented"); }
get { throw new NotImplementedException("Not implemented"); } }
}
/// <summary>Whether the asynchronous operation completed synchronously</summary>
/// <summary>Whether the asynchronous operation completed synchronously</summary> public bool CompletedSynchronously { get { return true; } }
public bool CompletedSynchronously { get { return true; } }
/// <summary>The value returned from the asynchronous operation</summary>
/// <summary>The value returned from the asynchronous operation</summary> public object ReturnedValue;
public object ReturnedValue;
}
}
#endregion // class SimpleAsyncResult
#endregion // class SimpleAsyncResult
/// <summary>Whether the calling thread needs to use Invoke()</summary>
/// <summary>Whether the calling thread needs to use Invoke()</summary> public bool InvokeRequired {
public bool InvokeRequired { get { return true; }
get { return true; } }
}
/// <summary>Schedules the specified method for execution in the target thread</summary>
/// <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="method">Method the target thread will execute when it is idle</param> /// <param name="arguments">Arguments that will be passed to the method</param>
/// <param name="arguments">Arguments that will be passed to the method</param> /// <returns>
/// <returns> /// An asynchronous result handle that can be used to check on the status of
/// An asynchronous result handle that can be used to check on the status of /// the call and wait for its completion
/// the call and wait for its completion /// </returns>
/// </returns> public IAsyncResult BeginInvoke(Delegate method, object[] arguments) {
public IAsyncResult BeginInvoke(Delegate method, object[] arguments) { var asyncResult = new SimpleAsyncResult();
var asyncResult = new SimpleAsyncResult(); asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments);
asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments); return asyncResult;
return asyncResult; }
}
/// <summary>Waits for the asychronous call to complete</summary>
/// <summary>Waits for the asychronous call to complete</summary> /// <param name="result">
/// <param name="result"> /// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method
/// Asynchronous result handle returned by the <see cref="BeginInvoke" /> method /// </param>
/// </param> /// <returns>The original result returned by the asychronously called method</returns>
/// <returns>The original result returned by the asychronously called method</returns> public object EndInvoke(IAsyncResult result) {
public object EndInvoke(IAsyncResult result) { return ((SimpleAsyncResult)result).ReturnedValue;
return ((SimpleAsyncResult)result).ReturnedValue; }
}
/// <summary>
/// <summary> /// Schedules the specified method for execution in the target thread and waits
/// Schedules the specified method for execution in the target thread and waits /// for it to complete
/// for it to complete /// </summary>
/// </summary> /// <param name="method">Method that will be executed by the target thread</param>
/// <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>
/// <param name="arguments">Arguments that will be passed to the method</param> /// <returns>The result returned by the specified method</returns>
/// <returns>The result returned by the specified method</returns> public object Invoke(Delegate method, object[] arguments) {
public object Invoke(Delegate method, object[] arguments) { return method.Method.Invoke(method.Target, arguments);
return method.Method.Invoke(method.Target, arguments); }
}
}
}
#endregion // class DummyContext
#endregion // class DummyContext
#region class TestViewModel
#region class TestViewModel
/// <summary>View model used to unit test the threaded view model base class</summary>
/// <summary>View model used to unit test the threaded view model base class</summary> private class TestViewModel : ThreadedViewModel {
private class TestViewModel : ThreadedViewModel {
/// <summary>
/// <summary> /// Initializes a new view model, letting the base class figure out the UI thread
/// Initializes a new view model, letting the base class figure out the UI thread /// </summary>
/// </summary> public TestViewModel() : base() {
public TestViewModel() : base() { this.finishedGate = new ManualResetEvent(initialState: false);
this.finishedGate = new ManualResetEvent(initialState: false); }
}
/// <summary>
/// <summary> /// Initializes a new view model, using the specified context for the UI thread
/// Initializes a new view model, using the specified context for the UI thread /// </summary>
/// </summary> /// <param name="uiContext">Synchronization context of the UI thread</param>
/// <param name="uiContext">Synchronization context of the UI thread</param> public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) {
public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) { this.finishedGate = new ManualResetEvent(initialState: false);
this.finishedGate = new ManualResetEvent(initialState: false); }
}
/// <summary>Immediately releases all resources owned by the instance</summary>
/// <summary>Immediately releases all resources owned by the instance</summary> public override void Dispose() {
public override void Dispose() { base.Dispose();
base.Dispose();
if(this.finishedGate != null) {
if(this.finishedGate != null) { this.finishedGate.Dispose();
this.finishedGate.Dispose(); this.finishedGate = null;
this.finishedGate = null; }
} }
}
/// <summary>Waits until the first background operation is finished</summary>
/// <summary>Waits until the first background operation is finished</summary> /// <returns>
/// <returns> /// True if the background operation is finished, false if it is ongoing
/// True if the background operation is finished, false if it is ongoing /// </returns>
/// </returns> public bool WaitUntilFinished() {
public bool WaitUntilFinished() { return this.finishedGate.WaitOne(100);
return this.finishedGate.WaitOne(100); }
}
/// <summary>Runs a background process that causes the specified error</summary>
/// <summary>Runs a background process that causes the specified error</summary> /// <param name="error">Error that will be caused in the background process</param>
/// <param name="error">Error that will be caused in the background process</param> public void CauseErrorInBackgroundThread(Exception error) {
public void CauseErrorInBackgroundThread(Exception error) { RunInBackground(
RunInBackground( delegate() { throw error; }
delegate() { throw error; } );
); }
}
/// <summary>
/// <summary> /// Assigns the specified value to the same-named property from a background thread
/// Assigns the specified value to the same-named property from a background thread /// </summary>
/// </summary> /// <param name="value">Value that will be assigned to the same-named property</param>
/// <param name="value">Value that will be assigned to the same-named property</param> public void AssignValueInBackgroundThread(int value) {
public void AssignValueInBackgroundThread(int value) { RunInBackground(
RunInBackground( delegate () {
delegate () { this.assignedValue = value;
this.assignedValue = value; this.finishedGate.Set();
this.finishedGate.Set(); }
} );
); }
}
/// <summary>Last error that was reported by the threaded view model</summary>
/// <summary>Last error that was reported by the threaded view model</summary> public Exception ReportedError {
public Exception ReportedError { get { return this.reportedError; }
get { return this.reportedError; } }
}
/// <summary>Value that has been assigned from the background thread</summary>
/// <summary>Value that has been assigned from the background thread</summary> public int AssignedValue {
public int AssignedValue { get { return this.assignedValue; }
get { return this.assignedValue; } }
}
/// <summary>Called when an error occurs in the background thread</summary>
/// <summary>Called when an error occurs in the background thread</summary> /// <param name="exception">Exception that was thrown in the background thread</param>
/// <param name="exception">Exception that was thrown in the background thread</param> protected override void ReportError(Exception exception) {
protected override void ReportError(Exception exception) { this.reportedError = exception;
this.reportedError = exception; this.finishedGate.Set();
this.finishedGate.Set(); }
}
/// <summary>Last error that was reported by the threaded view model</summary>
/// <summary>Last error that was reported by the threaded view model</summary> private volatile Exception reportedError;
private volatile Exception reportedError; /// <summary>Triggered when the </summary>
/// <summary>Triggered when the </summary> private ManualResetEvent finishedGate;
private ManualResetEvent finishedGate; /// <summary>Value that is assigned through the background thread</summary>
/// <summary>Value that is assigned through the background thread</summary> private volatile int assignedValue;
private volatile int assignedValue;
}
}
#endregion // class TestViewModel
#endregion // class TestViewModel
/// <summary>Verifies that the threaded view model has a default constructor</summary>
/// <summary>Verifies that the threaded view model has a default constructor</summary> [Test, Explicit]
[Test, Explicit] public void HasDefaultConstructor() {
public void HasDefaultConstructor() { using(var mainForm = new System.Windows.Forms.Form()) {
using(var mainForm = new System.Windows.Forms.Form()) { mainForm.Show();
mainForm.Show(); try {
try { mainForm.Visible = false;
mainForm.Visible = false; using(new TestViewModel()) { }
using(new TestViewModel()) { } }
} finally {
finally { mainForm.Close();
mainForm.Close(); }
} }
} }
}
/// <summary>
/// <summary> /// Verifies that the threaded view model can be constructed with a custom UI context
/// Verifies that the threaded view model can be constructed with a custom UI context /// </summary>
/// </summary> [Test]
[Test] public void HasCustomSychronizationContextConstructor() {
public void HasCustomSychronizationContextConstructor() { using(new TestViewModel(new DummyContext())) { }
using(new TestViewModel(new DummyContext())) { } }
}
/// <summary>Checks that a new view model starts out idle and not busy</summary>
/// <summary>Checks that a new view model starts out idle and not busy</summary> [Test]
[Test] public void NewInstanceIsNotBusy() {
public void NewInstanceIsNotBusy() { using(var viewModel = new TestViewModel(new DummyContext())) {
using(var viewModel = new TestViewModel(new DummyContext())) { Assert.IsFalse(viewModel.IsBusy);
Assert.IsFalse(viewModel.IsBusy); }
} }
}
/// <summary>
/// <summary> /// Verifies that errors happening in the background processing threads are
/// Verifies that errors happening in the background processing threads are /// reported to the main thread
/// reported to the main thread /// </summary>
/// </summary> [Test]
[Test] public void ErrorsInBackgroundThreadAreReported() {
public void ErrorsInBackgroundThreadAreReported() { using(var viewModel = new TestViewModel(new DummyContext())) {
using(var viewModel = new TestViewModel(new DummyContext())) { var testError = new ArgumentException("Mooh");
var testError = new ArgumentException("Mooh"); viewModel.CauseErrorInBackgroundThread(testError);
viewModel.CauseErrorInBackgroundThread(testError); viewModel.WaitUntilFinished();
viewModel.WaitUntilFinished(); Assert.AreSame(testError, viewModel.ReportedError);
Assert.AreSame(testError, viewModel.ReportedError); }
} }
}
/// <summary>
/// <summary> /// Verifies that the background thread actually executes and can do work
/// Verifies that the background thread actually executes and can do work /// </summary>
/// </summary> [Test]
[Test] public void BackgroundThreadExecutesTasks() {
public void BackgroundThreadExecutesTasks() { using(var viewModel = new TestViewModel(new DummyContext())) {
using(var viewModel = new TestViewModel(new DummyContext())) { viewModel.AssignValueInBackgroundThread(10042);
viewModel.AssignValueInBackgroundThread(10042); viewModel.WaitUntilFinished();
viewModel.WaitUntilFinished(); Assert.AreEqual(10042, viewModel.AssignedValue);
Assert.AreEqual(10042, viewModel.AssignedValue); }
} }
}
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels
#endif // UNITTEST #endif // UNITTEST

View File

@ -1,232 +1,231 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Windows.Forms;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support; using Nuclex.Support.Threading;
using Nuclex.Support.Threading;
namespace Nuclex.Windows.Forms.ViewModels {
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>View model that can execute tasks in a background thread</summary>
/// <summary>View model that can execute tasks in a background thread</summary> public abstract class ThreadedViewModel : Observable, IDisposable {
public abstract class ThreadedViewModel : Observable, IDisposable {
#region class ViewModelThreadRunner
#region class ViewModelThreadRunner
/// <summary>Thread runner for the threaded view model</summary>
/// <summary>Thread runner for the threaded view model</summary> private class ViewModelThreadRunner : ThreadRunner {
private class ViewModelThreadRunner : ThreadRunner {
/// <summary>Initializes a new thread runner for the threaded view model</summary>
/// <summary>Initializes a new thread runner for the threaded view model</summary> public ViewModelThreadRunner(ThreadedViewModel viewModel) {
public ViewModelThreadRunner(ThreadedViewModel viewModel) { this.viewModel = viewModel;
this.viewModel = viewModel; }
}
/// <summary>Reports an error</summary>
/// <summary>Reports an error</summary> /// <param name="exception">Error that will be reported</param>
/// <param name="exception">Error that will be reported</param> protected override void ReportError(Exception exception) {
protected override void ReportError(Exception exception) { this.viewModel.reportErrorFromThread(exception);
this.viewModel.reportErrorFromThread(exception); }
}
/// <summary>Called when the status of the busy flag changes</summary>
/// <summary>Called when the status of the busy flag changes</summary> protected override void BusyChanged() {
protected override void BusyChanged() { this.viewModel.OnIsBusyChanged();
this.viewModel.OnIsBusyChanged(); }
}
/// <summary>View model the thread runner belongs to</summary>
/// <summary>View model the thread runner belongs to</summary> private ThreadedViewModel viewModel;
private ThreadedViewModel viewModel;
}
}
#endregion // class ViewModelThreadRunner
#endregion // class ViewModelThreadRunner
/// <summary>Initializes a new view model for background processing</summary>
/// <summary>Initializes a new view model for background processing</summary> /// <param name="uiContext">
/// <param name="uiContext"> /// UI dispatcher that can be used to run callbacks in the UI thread
/// UI dispatcher that can be used to run callbacks in the UI thread /// </param>
/// </param> protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) {
protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) { if(uiContext == null) {
if(uiContext == null) { this.uiContext = LateCheckedSynchronizer.GetMainWindow();
this.uiContext = LateCheckedSynchronizer.GetMainWindow(); if(this.uiContext == null) {
if(this.uiContext == null) { this.uiContext = new LateCheckedSynchronizer(updateUiContext);
this.uiContext = new LateCheckedSynchronizer(updateUiContext); }
} } else {
} else { this.uiContext = uiContext;
this.uiContext = uiContext; }
}
this.reportErrorDelegate = new Action<Exception>(ReportError);
this.reportErrorDelegate = new Action<Exception>(ReportError);
this.threadRunner = new ViewModelThreadRunner(this);
this.threadRunner = new ViewModelThreadRunner(this); }
}
/// <summary>Immediately releases all resources owned by the instance</summary>
/// <summary>Immediately releases all resources owned by the instance</summary> public virtual void Dispose() {
public virtual void Dispose() { if(this.threadRunner != null) {
if(this.threadRunner != null) { this.threadRunner.Dispose();
this.threadRunner.Dispose(); this.threadRunner = null;
this.threadRunner = null; }
} }
}
/// <summary>Whether the view model is currently busy executing a task</summary>
/// <summary>Whether the view model is currently busy executing a task</summary> public bool IsBusy {
public bool IsBusy { get { return this.threadRunner.IsBusy; }
get { return this.threadRunner.IsBusy; } }
}
/// <summary>Reports an error to the user</summary>
/// <summary>Reports an error to the user</summary> /// <param name="exception">Error that will be reported</param>
/// <param name="exception">Error that will be reported</param> /// <remarks>
/// <remarks> /// <para>
/// <para> /// You can use this method as a default handling method for your own error reporting
/// 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).
/// (displaying the error to the user, logging it or whatever else is appropriate). /// </para>
/// </para> /// <para>
/// <para> /// When <see cref="RunInBackground(System.Action)" /> is used, this method will also
/// 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.
/// 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
/// 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
/// operations that might fail in a try..catch pair anyway in order to inform /// the user instead of aborting the entire application.
/// the user instead of aborting the entire application. /// </para>
/// </para> /// </remarks>
/// </remarks> protected abstract void ReportError(Exception exception);
protected abstract void ReportError(Exception exception);
/// <summary>Executes the specified operation in the background</summary>
/// <summary>Executes the specified operation in the background</summary> /// <param name="action">Action that will be executed in the background</param>
/// <param name="action">Action that will be executed in the background</param> protected void RunInBackground(Action action) {
protected void RunInBackground(Action action) { this.threadRunner.RunInBackground(action);
this.threadRunner.RunInBackground(action); }
}
/// <summary>Executes the specified operation in the background</summary>
/// <summary>Executes the specified operation in the background</summary> /// <param name="action">Action that will be executed in the background</param>
/// <param name="action">Action that will be executed in the background</param> protected void RunInBackground(CancellableAction action) {
protected void RunInBackground(CancellableAction action) { this.threadRunner.RunInBackground(action);
this.threadRunner.RunInBackground(action); }
}
/// <summary>Executes the specified operation in the background</summary>
/// <summary>Executes the specified operation in the background</summary> /// <param name="action">Action that will be executed in the background</param>
/// <param name="action">Action that will be executed in the background</param> /// <param name="parameter1">Parameter that will be passed to the action</param>
/// <param name="parameter1">Parameter that will be passed to the action</param> protected void RunInBackground<P1>(Action<P1> action, P1 parameter1) {
protected void RunInBackground<P1>(Action<P1> action, P1 parameter1) { this.threadRunner.RunInBackground(action, parameter1);
this.threadRunner.RunInBackground(action, parameter1); }
}
/// <summary>Executes the specified operation in the background</summary>
/// <summary>Executes the specified operation in the background</summary> /// <param name="action">Action that will be executed in the background</param>
/// <param name="action">Action that will be executed in the background</param> /// <param name="parameter1">Parameter that will be passed to the action</param>
/// <param name="parameter1">Parameter that will be passed to the action</param> protected void RunInBackground<P1>(CancellableAction<P1> action, P1 parameter1) {
protected void RunInBackground<P1>(CancellableAction<P1> action, P1 parameter1) { this.threadRunner.RunInBackground(action, parameter1);
this.threadRunner.RunInBackground(action, parameter1); }
}
/// <summary>Executes the specified operation in the background</summary>
/// <summary>Executes the specified operation in the background</summary> /// <param name="action">Action that will be executed in the background</param>
/// <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="parameter1">First parameter that will be passed to the action</param> /// <param name="parameter2">Second 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) {
protected void RunInBackground<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) { this.threadRunner.RunInBackground(action, parameter1, parameter2);
this.threadRunner.RunInBackground(action, parameter1, parameter2); }
}
/// <summary>Executes the specified operation in the background</summary>
/// <summary>Executes the specified operation in the background</summary> /// <param name="action">Action that will be executed in the background</param>
/// <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="parameter1">First parameter that will be passed to the action</param> /// <param name="parameter2">Second 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>(
protected void RunInBackground<P1, P2>( CancellableAction<P1, P2> action, P1 parameter1, P2 parameter2
CancellableAction<P1, P2> action, P1 parameter1, P2 parameter2 ) {
) { this.threadRunner.RunInBackground(action, parameter1, parameter2);
this.threadRunner.RunInBackground(action, parameter1, parameter2); }
}
/// <summary>Cancels the currently running background operation</summary>
/// <summary>Cancels the currently running background operation</summary> protected void CancelBackgroundOperation() {
protected void CancelBackgroundOperation() { this.threadRunner.CancelBackgroundOperation();
this.threadRunner.CancelBackgroundOperation(); }
}
/// <summary>Cancels all queued and the currently running background operation</summary>
/// <summary>Cancels all queued and the currently running background operation</summary> protected void CancelAllBackgroundOperations() {
protected void CancelAllBackgroundOperations() { this.threadRunner.CancelAllBackgroundOperations();
this.threadRunner.CancelAllBackgroundOperations(); }
}
/// <summary>Whether the background operation has been cancelled</summary>
/// <summary>Whether the background operation has been cancelled</summary> //[Obsolete("Please use a method accepting a cancellation token instead of using this")]
//[Obsolete("Please use a method accepting a cancellation token instead of using this")] protected bool IsBackgroundOperationCancelled {
protected bool IsBackgroundOperationCancelled { get { return this.threadRunner.IsBackgroundOperationCancelled; }
get { return this.threadRunner.IsBackgroundOperationCancelled; } }
}
/// <summary>Throws an exception if the background operation was cancelled</summary>
/// <summary>Throws an exception if the background operation was cancelled</summary> //[Obsolete("Please use a method accepting a cancellation token instead of using this")]
//[Obsolete("Please use a method accepting a cancellation token instead of using this")] protected void ThrowIfBackgroundOperationCancelled() {
protected void ThrowIfBackgroundOperationCancelled() { this.threadRunner.ThrowIfBackgroundOperationCancelled();
this.threadRunner.ThrowIfBackgroundOperationCancelled(); }
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <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="action">Action that will be executed in the UI thread</param> protected void RunInUIThread(Action action) {
protected void RunInUIThread(Action action) { this.uiContext.Invoke(action, EmptyObjectArray);
this.uiContext.Invoke(action, EmptyObjectArray); }
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <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="action">Action that will be executed in the UI thread</param> /// <param name="parameter1">Parameter that will be passed to the action</param>
/// <param name="parameter1">Parameter that will be passed to the action</param> protected void RunInUIThread<P1>(Action<P1> action, P1 parameter1) {
protected void RunInUIThread<P1>(Action<P1> action, P1 parameter1) { this.uiContext.Invoke(action, new object[1] { parameter1 });
this.uiContext.Invoke(action, new object[1] { parameter1 }); }
}
/// <summary>Executes the specified action in the UI thread</summary>
/// <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="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="parameter1">First parameter that will be passed to the action</param> /// <param name="parameter2">Second 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) {
protected void RunInUIThread<P1, P2>(Action<P1, P2> action, P1 parameter1, P2 parameter2) { this.uiContext.Invoke(action, new object[2] { parameter1, parameter2 });
this.uiContext.Invoke(action, new object[2] { parameter1, parameter2 }); }
}
/// <summary>Called when the thread runner's busy flag changes</summary>
/// <summary>Called when the thread runner's busy flag changes</summary> protected virtual void OnIsBusyChanged() {
protected virtual void OnIsBusyChanged() { OnPropertyChanged(nameof(IsBusy));
OnPropertyChanged(nameof(IsBusy)); }
}
// For the ThreadedAction class - there should be a better way!
// For the ThreadedAction class - there should be a better way! /// <summary>Thread runner that manages the view model's thread</summary>
/// <summary>Thread runner that manages the view model's thread</summary> internal ThreadRunner ThreadRunner {
internal ThreadRunner ThreadRunner { get { return this.threadRunner; }
get { return this.threadRunner; } }
}
/// <summary>Reports an error that occurred in the runner's background thread</summary>
/// <summary>Reports an error that occurred in the runner's background thread</summary> /// <param name="exception">Exception that the thread has encountered</param>
/// <param name="exception">Exception that the thread has encountered</param> private void reportErrorFromThread(Exception exception) {
private void reportErrorFromThread(Exception exception) { this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception });
this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception }); }
}
/// <summary>Sets the UI context that will be used by the threaded action</summary>
/// <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>
/// <param name="uiContext">The UI context the threaded action will use</param> private void updateUiContext(ISynchronizeInvoke uiContext) {
private void updateUiContext(ISynchronizeInvoke uiContext) { this.uiContext = uiContext;
this.uiContext = uiContext; }
}
/// <summary>An array of zero objects</summary>
/// <summary>An array of zero objects</summary> private static readonly object[] EmptyObjectArray = new object[0];
private static readonly object[] EmptyObjectArray = new object[0];
/// <summary>UI dispatcher of the thread in which the view runs</summary>
/// <summary>UI dispatcher of the thread in which the view runs</summary> private ISynchronizeInvoke uiContext;
private ISynchronizeInvoke uiContext; /// <summary>Delegate for the ReportError() method</summary>
/// <summary>Delegate for the ReportError() method</summary> private Action<Exception> reportErrorDelegate;
private Action<Exception> reportErrorDelegate; /// <summary>Thread runner that manages the view model's thread</summary>
/// <summary>Thread runner that manages the view model's thread</summary> private ViewModelThreadRunner threadRunner;
private ViewModelThreadRunner threadRunner;
}
}
} // namespace Nuclex.Windows.Forms.ViewModels
} // namespace Nuclex.Windows.Forms.ViewModels

View File

@ -1,38 +1,37 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Windows.Forms;
using System.Windows.Forms;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Enables consumer to look up the currently active window</summary>
/// <summary>Enables consumer to look up the currently active window</summary> public interface IActiveWindowTracker {
public interface IActiveWindowTracker {
/// <summary>The currently active top-level or modal window</summary>
/// <summary>The currently active top-level or modal window</summary> /// <remarks>
/// <remarks> /// If windows live in multiple threads, the property change notification for
/// If windows live in multiple threads, the property change notification for /// this property, if supported, might be fired from a different thread.
/// this property, if supported, might be fired from a different thread. /// </remarks>
/// </remarks> Form ActiveWindow { get; }
Form ActiveWindow { get; }
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,41 +1,40 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System;
namespace Nuclex.Windows.Forms.Views {
namespace Nuclex.Windows.Forms.Views {
/// <summary>View with support for data binding</summary>
/// <summary>View with support for data binding</summary> public interface IView {
public interface IView {
/// <summary>Provides the data binding target for the view</summary>
/// <summary>Provides the data binding target for the view</summary> /// <remarks>
/// <remarks> /// This property is identical to the same-named one in WPF, it provides
/// This property is identical to the same-named one in WPF, it provides /// the view model to which the view should bind its controls.
/// the view model to which the view should bind its controls. /// </remarks>
/// </remarks> object DataContext { get; set; }
object DataContext { get; set; }
// Whether the view owns its view model and it needs to be disposed after
// Whether the view owns its view model and it needs to be disposed after // the view ceases to exist
// the view ceases to exist //bool IsOwnedByView { get; set; }
//bool IsOwnedByView { get; set; }
}
}
} // namespace Nuclex.Windows.Forms.Views
} // namespace Nuclex.Windows.Forms.Views

View File

@ -1,366 +1,385 @@
using System; #region Apache License 2.0
using System.Collections.Generic; /*
using System.ComponentModel; Nuclex .NET Framework
using System.Drawing; Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
using System.Reflection;
using System.Runtime.InteropServices; Licensed under the Apache License, Version 2.0 (the "License");
using System.Windows.Forms; you may not use this file except in compliance with the License.
You may obtain a copy of the License at
using Nuclex.Support;
using Nuclex.Windows.Forms.ViewModels; http://www.apache.org/licenses/LICENSE-2.0
namespace Nuclex.Windows.Forms.Views { Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
/// <summary>Special view form that can display different child views</summary> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
public class MultiPageViewForm : ViewForm { See the License for the specific language governing permissions and
limitations under the License.
#region struct RedrawLockScope */
#endregion // Apache License 2.0
/// <summary>Prevents controls from redrawing themselves for a while</summary>
private struct RedrawLockScope : IDisposable { using System;
using System.Collections.Generic;
/// <summary>Window message that enables or disables control redraw</summary> using System.ComponentModel;
private const int WM_SETREDRAW = 11; using System.Drawing;
using System.Reflection;
/// <summary>Sends a window message to the specified window</summary> using System.Runtime.InteropServices;
/// <param name="windowHandle">Window a message will be sent to</param> using System.Windows.Forms;
/// <param name="messageId">ID of the message that will be sent</param>
/// <param name="firstArgument">First argument to the window procedure</param> using Nuclex.Support;
/// <param name="secondArgument">Second argument to the window procedure</param> using Nuclex.Windows.Forms.ViewModels;
/// <returns>The return value of the window procedure</returns>
[DllImport("user32")] namespace Nuclex.Windows.Forms.Views {
public static extern int SendMessage(
IntPtr windowHandle, int messageId, bool firstArgument, int secondArgument /// <summary>Special view form that can display different child views</summary>
); public class MultiPageViewForm : ViewForm {
/// <summary>Stops redrawing the specified control</summary> #region struct RedrawLockScope
/// <param name="control">Control to stop redrawing</param>
public RedrawLockScope(Control control) { /// <summary>Prevents controls from redrawing themselves for a while</summary>
if(Environment.OSVersion.Platform == PlatformID.Win32NT) { private struct RedrawLockScope : IDisposable {
SendMessage(control.Handle, WM_SETREDRAW, false, 0);
this.control = control; /// <summary>Window message that enables or disables control redraw</summary>
} else { private const int WM_SETREDRAW = 11;
this.control = null;
} /// <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>
/// <summary>Enables redrawing again when the lock scope is disposed</summary> /// <param name="firstArgument">First argument to the window procedure</param>
public void Dispose() { /// <param name="secondArgument">Second argument to the window procedure</param>
if(this.control != null) { /// <returns>The return value of the window procedure</returns>
SendMessage(this.control.Handle, WM_SETREDRAW, true, 0); [DllImport("user32")]
this.control.Invalidate(true); public static extern int SendMessage(
} IntPtr windowHandle, int messageId, bool firstArgument, int secondArgument
} );
/// <summary>Control that has been stopped from redrawing itself</summary> /// <summary>Stops redrawing the specified control</summary>
private Control control; /// <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);
#endregion // struct RedrawLockScope this.control = control;
} else {
/// <summary>Initializes a new multi page view window for the windows forms designer</summary> this.control = null;
public MultiPageViewForm() { }
this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView)); }
}
/// <summary>Enables redrawing again when the lock scope is disposed</summary>
/// <summary>Initializes a new multi page view window</summary> public void Dispose() {
/// <param name="windowManager"> if(this.control != null) {
/// Window manager that is used to set up the child views SendMessage(this.control.Handle, WM_SETREDRAW, true, 0);
/// </param> this.control.Invalidate(true);
/// <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)); /// <summary>Control that has been stopped from redrawing itself</summary>
private Control control;
if(cachePageViews) {
this.cachedViews = new Dictionary<Type, Control>(); }
}
} #endregion // struct RedrawLockScope
/// <summary>Called when the control is being disposed</summary> /// <summary>Initializes a new multi page view window for the windows forms designer</summary>
/// <param name="calledExplicitly"> public MultiPageViewForm() {
/// Whether the call was made by user code (vs. the garbage collector) this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView));
/// </param> }
protected override void Dispose(bool calledExplicitly) {
if(calledExplicitly) { /// <summary>Initializes a new multi page view window</summary>
/// <param name="windowManager">
// Disable the active view, if any /// Window manager that is used to set up the child views
if(this.activePageView != null) { /// </param>
if(this.childViewContainer != null) { /// <param name="cachePageViews">Whether page views should be kept alive and reused</param>
this.childViewContainer.Controls.Remove(this.activePageView); public MultiPageViewForm(IWindowManager windowManager, bool cachePageViews = false) {
} this.windowManager = windowManager;
} this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView));
// If caching is disabled, dispose of the active child view, if any if(cachePageViews) {
if(this.cachedViews == null) { this.cachedViews = new Dictionary<Type, Control>();
if(this.activePageView != null) { }
disposeIfSupported(this.activePageView); }
this.activePageView = null;
} /// <summary>Called when the control is being disposed</summary>
} else { // Caching is enabled, dispose of any cached child views /// <param name="calledExplicitly">
foreach(Control childView in this.cachedViews.Values) { /// Whether the call was made by user code (vs. the garbage collector)
disposeIfSupported(childView); /// </param>
} protected override void Dispose(bool calledExplicitly) {
this.cachedViews.Clear(); if(calledExplicitly) {
this.cachedViews = null;
this.activePageView = null; // Disable the active view, if any
} if(this.activePageView != null) {
} if(this.childViewContainer != null) {
this.childViewContainer.Controls.Remove(this.activePageView);
base.Dispose(calledExplicitly); }
} }
/// <summary>Discovers the container control used to host the child views</summary> // If caching is disabled, dispose of the active child view, if any
/// <returns>The container control is which the child views will be hosted</returns> if(this.cachedViews == null) {
/// <remarks> if(this.activePageView != null) {
/// This is supposed to be overriden by the user, simply returning the container disposeIfSupported(this.activePageView);
/// control that should host the page views. If it isn't, however, we use some this.activePageView = null;
/// heuristics to figure out the most likely candidate: it should be a container, }
/// and it should cover most of the window's client area. } else { // Caching is enabled, dispose of any cached child views
/// </remarks> foreach(Control childView in this.cachedViews.Values) {
protected virtual Control IdentifyPageContainer() { disposeIfSupported(childView);
Size halfWindowSize = Size; }
halfWindowSize.Width /= 2; this.cachedViews.Clear();
halfWindowSize.Height /= 2; this.cachedViews = null;
this.activePageView = null;
// First container control we found -- if we find no likely candidate, }
// we simply use the first }
Control firstContainer = null;
base.Dispose(calledExplicitly);
// 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; /// <summary>Discovers the container control used to host the child views</summary>
for(int index = 0; index < controlCount; ++index) { /// <returns>The container control is which the child views will be hosted</returns>
Control control = Controls[index]; /// <remarks>
/// This is supposed to be overriden by the user, simply returning the container
// Only check container controls /// control that should host the page views. If it isn't, however, we use some
if((control is ContainerControl) || (control is Panel)) { /// heuristics to figure out the most likely candidate: it should be a container,
if(firstContainer == null) { /// and it should cover most of the window's client area.
firstContainer = control; /// </remarks>
} protected virtual Control IdentifyPageContainer() {
Size halfWindowSize = Size;
// If this control covers most of the view, it's our candidate! halfWindowSize.Width /= 2;
Size controlSize = control.Size; halfWindowSize.Height /= 2;
bool goodCandidate = (
(controlSize.Width > halfWindowSize.Width) && // First container control we found -- if we find no likely candidate,
(controlSize.Height > halfWindowSize.Height) // we simply use the first
); Control firstContainer = null;
if(goodCandidate) {
return control; // 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];
// 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. // Only check container controls
if(firstContainer == null) { if((control is ContainerControl) || (control is Panel)) {
firstContainer = new Panel(); if(firstContainer == null) {
Controls.Add(firstContainer); firstContainer = control;
firstContainer.Dock = DockStyle.Fill; }
}
// If this control covers most of the view, it's our candidate!
return firstContainer; Size controlSize = control.Size;
} bool goodCandidate = (
(controlSize.Width > halfWindowSize.Width) &&
/// <summary>Called when the window's data context is changed</summary> (controlSize.Height > halfWindowSize.Height)
/// <param name="sender">Window whose data context was changed</param> );
/// <param name="oldDataContext">Data context that was previously used</param> if(goodCandidate) {
/// <param name="newDataContext">Data context that will be used from now on</param> return control;
protected override void OnDataContextChanged( }
object sender, object oldDataContext, object newDataContext }
) { }
// Kill the currently active view if there was an old view model. // If no candidate was found, return the first container control we encountered
if(oldDataContext != null) { // or create a new UserControl as the container if nothing was found at all.
disableActivePageView(); if(firstContainer == null) {
} firstContainer = new Panel();
Controls.Add(firstContainer);
base.OnDataContextChanged(sender, oldDataContext, newDataContext); firstContainer.Dock = DockStyle.Fill;
}
// If a valid view model was assigned, create a new view its active page view model
if(newDataContext != null) { return firstContainer;
var dataContextAsMultiPageViewModel = newDataContext as IMultiPageViewModel; }
if(dataContextAsMultiPageViewModel != null) {
activatePageView(dataContextAsMultiPageViewModel.GetActivePageViewModel()); /// <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
/// <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> // Kill the currently active view if there was an old view model.
protected override void OnViewModelPropertyChanged( if(oldDataContext != null) {
object sender, PropertyChangedEventArgs arguments disableActivePageView();
) { }
base.OnViewModelPropertyChanged(sender, arguments);
base.OnDataContextChanged(sender, oldDataContext, newDataContext);
if(arguments.AreAffecting(nameof(MultiPageViewModel<object>.ActivePage))) {
var viewModelAsMultiPageviewModel = DataContext as IMultiPageViewModel; // If a valid view model was assigned, create a new view its active page view model
if(viewModelAsMultiPageviewModel != null) { if(newDataContext != null) {
if(InvokeRequired) { var dataContextAsMultiPageViewModel = newDataContext as IMultiPageViewModel;
Invoke( if(dataContextAsMultiPageViewModel != null) {
new Action<object>(activatePageView), activatePageView(dataContextAsMultiPageViewModel.GetActivePageViewModel());
viewModelAsMultiPageviewModel.GetActivePageViewModel() }
); }
} else {
activatePageView(viewModelAsMultiPageviewModel.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(
/// <summary>Currently active page view control</summary> object sender, PropertyChangedEventArgs arguments
protected Control ActivePageView { ) {
get { return this.activePageView; } base.OnViewModelPropertyChanged(sender, arguments);
}
if(arguments.AreAffecting(nameof(MultiPageViewModel<object>.ActivePage))) {
/// <summary>The view model running the currently active page</summary> var viewModelAsMultiPageviewModel = DataContext as IMultiPageViewModel;
protected object ActivePageViewModel { if(viewModelAsMultiPageviewModel != null) {
get { if(InvokeRequired) {
var activePageViewAsView = this.activePageView as IView; Invoke(
if(activePageViewAsView == null) { new Action<object>(activatePageView),
return null; viewModelAsMultiPageviewModel.GetActivePageViewModel()
} else { );
return activePageViewAsView.DataContext; } else {
} activatePageView(viewModelAsMultiPageviewModel.GetActivePageViewModel());
} }
} }
}
/// <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 /// <summary>Currently active page view control</summary>
/// </param> protected Control ActivePageView {
private void activatePageView(object pageViewModel) { get { return this.activePageView; }
object activePageViewModel = null; }
{
var activePageViewAsView = this.activePageView as IView; /// <summary>The view model running the currently active page</summary>
if(activePageViewAsView != null) { protected object ActivePageViewModel {
activePageViewModel = activePageViewAsView.DataContext; get {
} var activePageViewAsView = this.activePageView as IView;
} if(activePageViewAsView == null) {
return null;
// Try from the cheapest to the most expensive way to get to our goal, } else {
// an activated view suiting the specified view model. return activePageViewAsView.DataContext;
}
// If we already have the target view model selected, do nothing }
if(activePageViewModel == pageViewModel) { }
return;
} /// <summary>Activates the page view for the specified page view model</summary>
/// <param name="pageViewModel">
// If the page view model for the old and the new page are of the same /// Page view model for which the page view will be activated
// type, we can reuse the currently active page view /// </param>
if((activePageViewModel != null) && (pageViewModel != null)) { private void activatePageView(object pageViewModel) {
if(pageViewModel.GetType() == this.activePageView.GetType()) { object activePageViewModel = null;
var activePageViewAsView = this.activePageView as IView; {
if(activePageViewAsView != null) { var activePageViewAsView = this.activePageView as IView;
activePageViewAsView.DataContext = pageViewModel; if(activePageViewAsView != null) {
} activePageViewModel = activePageViewAsView.DataContext;
}
return; }
}
} // Try from the cheapest to the most expensive way to get to our goal,
// an activated view suiting the specified view model.
// 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 we already have the target view model selected, do nothing
{ if(activePageViewModel == pageViewModel) {
if(pageViewModel == null) { return;
disableActivePageView(); }
} else {
Control pageViewContainer = getPageViewContainer(); // If the page view model for the old and the new page are of the same
using(new RedrawLockScope(pageViewContainer)) { // type, we can reuse the currently active page view
disableActivePageView(); if((activePageViewModel != null) && (pageViewModel != null)) {
if(pageViewModel.GetType() == this.activePageView.GetType()) {
this.activePageView = getOrCreatePageView(pageViewModel); var activePageViewAsView = this.activePageView as IView;
pageViewContainer.Controls.Add(this.activePageView); if(activePageViewAsView != null) {
this.activePageView.Dock = DockStyle.Fill; activePageViewAsView.DataContext = pageViewModel;
} }
}
} return;
} }
}
/// <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> // Worst, but usual, case: the new page view model might require
/// <returns>A child view suitable for the specified view model</returns> // a different view. Create or look up the new view and put it in the container
private Control getOrCreatePageView(object viewModel) { {
Type viewModelType = viewModel.GetType(); if(pageViewModel == null) {
disableActivePageView();
Control view; } else {
Control pageViewContainer = getPageViewContainer();
// If caching is enabled, check if we have a cached view using(new RedrawLockScope(pageViewContainer)) {
if(this.cachedViews != null) { disableActivePageView();
if(this.cachedViews.TryGetValue(viewModelType, out view)) {
return view; this.activePageView = getOrCreatePageView(pageViewModel);
} pageViewContainer.Controls.Add(this.activePageView);
} this.activePageView.Dock = DockStyle.Fill;
}
// Otherwise, call the window manager's CreateView() method }
MethodInfo specializedCreateViewMethod = ( }
this.createViewMethod.MakeGenericMethod(viewModelType) }
);
view = (Control)specializedCreateViewMethod.Invoke( /// <summary>Gets the cached child view or creates a new one if not cached</summary>
this.windowManager, new object[1] { viewModel } /// <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) {
// If caching is enabled, register the view in the cache Type viewModelType = viewModel.GetType();
if(this.cachedViews != null) {
this.cachedViews.Add(viewModelType, view); Control view;
}
// If caching is enabled, check if we have a cached view
return view; if(this.cachedViews != null) {
} if(this.cachedViews.TryGetValue(viewModelType, out view)) {
return view;
/// <summary>Disables the currently active page view control</summary> }
private void disableActivePageView() { }
if(this.activePageView != null) {
Control container = getPageViewContainer(); // Otherwise, call the window manager's CreateView() method
container.Controls.Remove(this.activePageView); MethodInfo specializedCreateViewMethod = (
this.createViewMethod.MakeGenericMethod(viewModelType)
// If we don't reuse views, kill it now );
if(this.cachedViews == null) { view = (Control)specializedCreateViewMethod.Invoke(
disposeIfSupported(this.activePageView); this.windowManager, new object[1] { viewModel }
this.activePageView = null; );
} else {
var activePageViewAsView = this.activePageView as IView; // If caching is enabled, register the view in the cache
if(activePageViewAsView != null) { if(this.cachedViews != null) {
activePageViewAsView.DataContext = null; this.cachedViews.Add(viewModelType, view);
} }
}
} return view;
} }
/// <summary>Fetches the container that holds the child views</summary> /// <summary>Disables the currently active page view control</summary>
/// <returns>The container for the child views</returns> private void disableActivePageView() {
private Control getPageViewContainer() { if(this.activePageView != null) {
if(this.childViewContainer == null) { Control container = getPageViewContainer();
this.childViewContainer = IdentifyPageContainer(); container.Controls.Remove(this.activePageView);
}
// If we don't reuse views, kill it now
return this.childViewContainer; if(this.cachedViews == null) {
} disposeIfSupported(this.activePageView);
this.activePageView = null;
/// <summary>Disposes the specified object if it is disposable</summary> } else {
/// <param name="potentiallyDisposable">Object that will be disposed if supported</param> var activePageViewAsView = this.activePageView as IView;
private static void disposeIfSupported(object potentiallyDisposable) { if(activePageViewAsView != null) {
var disposable = potentiallyDisposable as IDisposable; activePageViewAsView.DataContext = null;
if(disposable != null) { }
disposable.Dispose(); }
} }
} }
/// <summary>Window manager through which the child views are created</summary> /// <summary>Fetches the container that holds the child views</summary>
private IWindowManager windowManager; /// <returns>The container for the child views</returns>
/// <summary>Reflection info for the createView() method of the window manager</summary> private Control getPageViewContainer() {
private MethodInfo createViewMethod; if(this.childViewContainer == null) {
this.childViewContainer = IdentifyPageContainer();
/// <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> return this.childViewContainer;
private Dictionary<Type, Control> cachedViews; }
/// <summary>The currently active child view</summary>
private Control activePageView; /// <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;
} // namespace Nuclex.Windows.Forms.Views 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 #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Windows.Forms;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support;
namespace Nuclex.Windows.Forms.Views {
namespace Nuclex.Windows.Forms.Views {
/// <summary>
/// <summary> /// Base class for MVVM user controls that act as views connected to a view model
/// Base class for MVVM user controls that act as views connected to a view model /// </summary>
/// </summary> public class ViewControl : UserControl, IView {
public class ViewControl : UserControl, IView {
/// <summary>Initializes a new view control</summary>
/// <summary>Initializes a new view control</summary> public ViewControl() {
public ViewControl() { this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged;
this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged; }
}
/// <summary>Called when the control's data context is changed</summary>
/// <summary>Called when the control's data context is changed</summary> /// <param name="sender">Control whose data context was changed</param>
/// <param name="sender">Control whose data context was changed</param> /// <param name="oldDataContext">Data context that was previously used</param>
/// <param name="oldDataContext">Data context that was previously used</param> /// <param name="newDataContext">Data context that will be used from now on</param>
/// <param name="newDataContext">Data context that will be used from now on</param> protected virtual void OnDataContextChanged(
protected virtual void OnDataContextChanged( object sender, object oldDataContext, object newDataContext
object sender, object oldDataContext, object newDataContext ) {
) { var oldViewModel = oldDataContext as INotifyPropertyChanged;
var oldViewModel = oldDataContext as INotifyPropertyChanged; if(oldViewModel != null) {
if(oldViewModel != null) { oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate;
oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate; }
}
var newViewModel = newDataContext as INotifyPropertyChanged;
var newViewModel = newDataContext as INotifyPropertyChanged; if(newViewModel != null) {
if(newViewModel != null) { newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate;
newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate; InvalidateAllViewModelProperties();
InvalidateAllViewModelProperties(); }
} }
}
/// <summary>Refreshes all properties from the view model</summary>
/// <summary>Refreshes all properties from the view model</summary> protected void InvalidateAllViewModelProperties() {
protected void InvalidateAllViewModelProperties() { OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard);
OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard); }
}
/// <summary>Called when a property of the view model is changed</summary>
/// <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="sender">View model in which a property was changed</param> /// <param name="arguments">Contains the name of the property that has changed</param>
/// <param name="arguments">Contains the name of the property that has changed</param> protected virtual void OnViewModelPropertyChanged(
protected virtual void OnViewModelPropertyChanged( object sender, PropertyChangedEventArgs arguments
object sender, PropertyChangedEventArgs arguments ) { }
) { }
/// <summary>Provides the data binding target for the view</summary>
/// <summary>Provides the data binding target for the view</summary> public object DataContext {
public object DataContext { get { return this.dataContext; }
get { return this.dataContext; } set {
set { if(value != this.dataContext) {
if(value != this.dataContext) { object oldDataContext = this.dataContext;
object oldDataContext = this.dataContext; this.dataContext = value;
this.dataContext = value; OnDataContextChanged(this, oldDataContext, value);
OnDataContextChanged(this, oldDataContext, value); }
} }
} }
}
/// <summary>Active data binding target, can be null</summary>
/// <summary>Active data binding target, can be null</summary> private object dataContext;
private object dataContext; /// <summary>Delegate for the OnViewModelPropertyChanged() method</summary>
/// <summary>Delegate for the OnViewModelPropertyChanged() method</summary> private PropertyChangedEventHandler onViewModelPropertyChangedDelegate;
private PropertyChangedEventHandler onViewModelPropertyChangedDelegate;
}
}
} // namespace Nuclex.Windows.Forms.Views
} // namespace Nuclex.Windows.Forms.Views

View File

@ -1,89 +1,88 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.ComponentModel;
using System.ComponentModel; using System.Windows.Forms;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support;
namespace Nuclex.Windows.Forms.Views {
namespace Nuclex.Windows.Forms.Views {
/// <summary>
/// <summary> /// Base class for MVVM windows that act as views connected to a view model
/// Base class for MVVM windows that act as views connected to a view model /// </summary>
/// </summary> public class ViewForm : Form, IView {
public class ViewForm : Form, IView {
/// <summary>Initializes a new view control</summary>
/// <summary>Initializes a new view control</summary> public ViewForm() {
public ViewForm() { this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged;
this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged; }
}
/// <summary>Provides the data binding target for the view</summary>
/// <summary>Provides the data binding target for the view</summary> public object DataContext {
public object DataContext { get { return this.dataContext; }
get { return this.dataContext; } set {
set { if(value != this.dataContext) {
if(value != this.dataContext) { object oldDataContext = this.dataContext;
object oldDataContext = this.dataContext; this.dataContext = value;
this.dataContext = value; OnDataContextChanged(this, oldDataContext, value);
OnDataContextChanged(this, oldDataContext, value); }
} }
} }
}
/// <summary>Called when the window's data context is changed</summary>
/// <summary>Called when the window's data context is changed</summary> /// <param name="sender">Window whose data context was changed</param>
/// <param name="sender">Window whose data context was changed</param> /// <param name="oldDataContext">Data context that was previously used</param>
/// <param name="oldDataContext">Data context that was previously used</param> /// <param name="newDataContext">Data context that will be used from now on</param>
/// <param name="newDataContext">Data context that will be used from now on</param> protected virtual void OnDataContextChanged(
protected virtual void OnDataContextChanged( object sender, object oldDataContext, object newDataContext
object sender, object oldDataContext, object newDataContext ) {
) { var oldViewModel = oldDataContext as INotifyPropertyChanged;
var oldViewModel = oldDataContext as INotifyPropertyChanged; if(oldViewModel != null) {
if(oldViewModel != null) { oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate;
oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate; }
}
var newViewModel = newDataContext as INotifyPropertyChanged;
var newViewModel = newDataContext as INotifyPropertyChanged; if(newViewModel != null) {
if(newViewModel != null) { newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate;
newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate; InvalidateAllViewModelProperties();
InvalidateAllViewModelProperties(); }
} }
}
/// <summary>Refreshes all properties from the view model</summary>
/// <summary>Refreshes all properties from the view model</summary> protected void InvalidateAllViewModelProperties() {
protected void InvalidateAllViewModelProperties() { OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard);
OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard); }
}
/// <summary>Called when a property of the view model is changed</summary>
/// <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="sender">View model in which a property was changed</param> /// <param name="arguments">Contains the name of the property that has changed</param>
/// <param name="arguments">Contains the name of the property that has changed</param> protected virtual void OnViewModelPropertyChanged(
protected virtual void OnViewModelPropertyChanged( object sender, PropertyChangedEventArgs arguments
object sender, PropertyChangedEventArgs arguments ) { }
) { }
/// <summary>Active data binding target, can be null</summary>
/// <summary>Active data binding target, can be null</summary> private object dataContext;
private object dataContext; /// <summary>Delegate for the OnViewModelPropertyChanged() method</summary>
/// <summary>Delegate for the OnViewModelPropertyChanged() method</summary> private PropertyChangedEventHandler onViewModelPropertyChangedDelegate;
private PropertyChangedEventHandler onViewModelPropertyChangedDelegate;
}
}
} // namespace Nuclex.Windows.Forms.Views
} // namespace Nuclex.Windows.Forms.Views

View File

@ -1,41 +1,40 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System;
using NUnit.Framework;
using NUnit.Framework;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Unit test for the window manager</summary>
/// <summary>Unit test for the window manager</summary> [TestFixture]
[TestFixture] public class WindowManagerTest {
public class WindowManagerTest {
/// <summary>Verifies that the window manager provides a default constructor</summary>
/// <summary>Verifies that the window manager provides a default constructor</summary> [Test]
[Test] public void HasDefaultConstructor() {
public void HasDefaultConstructor() { Assert.DoesNotThrow(
Assert.DoesNotThrow( () => new WindowManager()
() => new WindowManager() );
); }
}
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms

View File

@ -1,442 +1,441 @@
#region CPL License #region Apache License 2.0
/* /*
Nuclex Framework Nuclex .NET Framework
Copyright (C) 2002-2019 Nuclex Development Labs Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
This library is free software; you can redistribute it and/or Licensed under the Apache License, Version 2.0 (the "License");
modify it under the terms of the IBM Common Public License as you may not use this file except in compliance with the License.
published by the IBM Corporation; either version 1.0 of the You may obtain a copy of the License at
License, or (at your option) any later version.
http://www.apache.org/licenses/LICENSE-2.0
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of Unless required by applicable law or agreed to in writing, software
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the distributed under the License is distributed on an "AS IS" BASIS,
IBM Common Public License for more details. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
You should have received a copy of the IBM Common Public limitations under the License.
License along with this library */
*/ #endregion // Apache License 2.0
#endregion
using System;
using System; using System.Collections.Concurrent;
using System.Collections.Concurrent; using System.Collections.Generic;
using System.Collections.Generic; using System.Windows.Forms;
using System.Windows.Forms;
using Nuclex.Support;
using Nuclex.Support; using Nuclex.Windows.Forms.AutoBinding;
using Nuclex.Windows.Forms.AutoBinding; using Nuclex.Windows.Forms.Views;
using Nuclex.Windows.Forms.Views;
namespace Nuclex.Windows.Forms {
namespace Nuclex.Windows.Forms {
/// <summary>Manages an application's windows and views</summary>
/// <summary>Manages an application's windows and views</summary> public class WindowManager : Observable, IWindowManager {
public class WindowManager : Observable, IWindowManager {
#region class CancellableDisposer
#region class CancellableDisposer
/// <summary>Disposes an object that potentially implements IDisposable</summary>
/// <summary>Disposes an object that potentially implements IDisposable</summary> private struct CancellableDisposer : IDisposable {
private struct CancellableDisposer : IDisposable {
/// <summary>Initializes a new cancellable disposer</summary>
/// <summary>Initializes a new cancellable disposer</summary> /// <param name="potentiallyDisposable">
/// <param name="potentiallyDisposable"> /// Object that potentially implements IDisposable
/// Object that potentially implements IDisposable /// </param>
/// </param> public CancellableDisposer(object potentiallyDisposable = null) {
public CancellableDisposer(object potentiallyDisposable = null) { this.potentiallyDisposable = potentiallyDisposable;
this.potentiallyDisposable = potentiallyDisposable; }
}
/// <summary>
/// <summary> /// Disposes the assigned object if the disposer has not been cancelled
/// Disposes the assigned object if the disposer has not been cancelled /// </summary>
/// </summary> public void Dispose() {
public void Dispose() { var disposable = this.potentiallyDisposable as IDisposable;
var disposable = this.potentiallyDisposable as IDisposable; if(disposable != null) {
if(disposable != null) { disposable.Dispose();
disposable.Dispose(); }
} }
}
/// <summary>Cancels the call to Dispose(), keeping the object alive</summary>
/// <summary>Cancels the call to Dispose(), keeping the object alive</summary> public void Dismiss() {
public void Dismiss() { this.potentiallyDisposable = null;
this.potentiallyDisposable = null; }
}
/// <summary>Assigns a new potentially disposable object</summary>
/// <summary>Assigns a new potentially disposable object</summary> /// <param name="potentiallyDisposable">
/// <param name="potentiallyDisposable"> /// Potentially disposable object that the disposer will dispose
/// Potentially disposable object that the disposer will dispose /// </param>
/// </param> public void Set(object potentiallyDisposable) {
public void Set(object potentiallyDisposable) { this.potentiallyDisposable = potentiallyDisposable;
this.potentiallyDisposable = potentiallyDisposable; }
}
/// <summary>Object that will be disposed unless the disposer is cancelled</summary>
/// <summary>Object that will be disposed unless the disposer is cancelled</summary> private object potentiallyDisposable;
private object potentiallyDisposable;
}
}
#endregion // class CancellableDisposer
#endregion // class CancellableDisposer
/// <summary>Initializes a new window manager</summary>
/// <summary>Initializes a new window manager</summary> /// <param name="autoBinder">
/// <param name="autoBinder"> /// View model binder that will be used to bind all created views to their models
/// View model binder that will be used to bind all created views to their models /// </param>
/// </param> public WindowManager(IAutoBinder autoBinder = null) {
public WindowManager(IAutoBinder autoBinder = null) { this.autoBinder = autoBinder;
this.autoBinder = autoBinder;
this.rootWindowActivatedDelegate = rootWindowActivated;
this.rootWindowActivatedDelegate = rootWindowActivated; this.rootWindowClosedDelegate = rootWindowClosed;
this.rootWindowClosedDelegate = rootWindowClosed; this.viewTypesForViewModels = new ConcurrentDictionary<Type, Type>();
this.viewTypesForViewModels = new ConcurrentDictionary<Type, Type>(); }
}
/// <summary>The currently active top-level or modal window</summary>
/// <summary>The currently active top-level or modal window</summary> public Form ActiveWindow {
public Form ActiveWindow { get { return this.activeWindow; }
get { return this.activeWindow; } private set {
private set { if(value != this.activeWindow) {
if(value != this.activeWindow) { this.activeWindow = value;
this.activeWindow = value; OnPropertyChanged(nameof(ActiveWindow));
OnPropertyChanged(nameof(ActiveWindow)); }
} }
} }
}
/// <summary>Opens a view as a new root window of the application</summary>
/// <summary>Opens a view as a new root window of the application</summary> /// <typeparam name="TViewModel">
/// <typeparam name="TViewModel"> /// Type of view model a root window will be opened for
/// Type of view model a root window will be opened for /// </typeparam>
/// </typeparam> /// <param name="viewModel">
/// <param name="viewModel"> /// View model a window will be opened for. If null, the view model will be
/// 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)
/// created as well (unless the dialog already specifies one as a resource) /// </param>
/// </param> /// <param name="disposeOnClose">
/// <param name="disposeOnClose"> /// Whether the view model should be disposed when the view is closed
/// Whether the view model should be disposed when the view is closed /// </param>
/// </param> /// <returns>The window that has been opened by the window manager</returns>
/// <returns>The window that has been opened by the window manager</returns> public Form OpenRoot<TViewModel>(
public Form OpenRoot<TViewModel>( TViewModel viewModel = null, bool disposeOnClose = true
TViewModel viewModel = null, bool disposeOnClose = true ) where TViewModel : class {
) where TViewModel : class {
Form window = (Form)CreateView(viewModel);
Form window = (Form)CreateView(viewModel); window.Activated += this.rootWindowActivatedDelegate;
window.Activated += this.rootWindowActivatedDelegate; window.Closed += this.rootWindowClosedDelegate;
window.Closed += this.rootWindowClosedDelegate;
// If we either created the view model or the user explicitly asked us to
// 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
// 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)
// when we're done (but still allow the user to change his mind) if((viewModel == null) || disposeOnClose) {
if((viewModel == null) || disposeOnClose) { window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead?
window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead? //window.SetValue(DisposeViewModelOnCloseProperty, true);
//window.SetValue(DisposeViewModelOnCloseProperty, true); }
}
window.Show();
window.Show();
return window;
return window; }
}
/// <summary>Displays a view as a modal window</summary>
/// <summary>Displays a view as a modal window</summary> /// <typeparam name="TViewModel">
/// <typeparam name="TViewModel"> /// Type of the view model for which a view will be displayed
/// Type of the view model for which a view will be displayed /// </typeparam>
/// </typeparam> /// <param name="viewModel">
/// <param name="viewModel"> /// View model a modal window will be displayed for. If null, the view model will
/// 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)
/// be created as well (unless the dialog already specifies one as a resource) /// </param>
/// </param> /// <param name="disposeOnClose">
/// <param name="disposeOnClose"> /// Whether the view model should be disposed when the view is closed
/// Whether the view model should be disposed when the view is closed /// </param>
/// </param> /// <returns>The return value of the modal window</returns>
/// <returns>The return value of the modal window</returns> public bool? ShowModal<TViewModel>(
public bool? ShowModal<TViewModel>( TViewModel viewModel = null, bool disposeOnClose = true
TViewModel viewModel = null, bool disposeOnClose = true ) where TViewModel : class {
) where TViewModel : class { Form window = (Form)CreateView(viewModel);
Form window = (Form)CreateView(viewModel); window.Owner = this.activeWindow;
window.Owner = this.activeWindow; window.Activated += this.rootWindowActivatedDelegate;
window.Activated += this.rootWindowActivatedDelegate;
try {
try { // If we either created the view model or the user explicitly asked us to
// 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
// 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)
// when we're done (but still allow the user to change his mind) if((viewModel == null) || disposeOnClose) {
if((viewModel == null) || disposeOnClose) { window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead?
window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead? //window.SetValue(DisposeViewModelOnCloseProperty, true);
//window.SetValue(DisposeViewModelOnCloseProperty, true); }
}
DialogResult result = window.ShowDialog(this.activeWindow);
DialogResult result = window.ShowDialog(this.activeWindow); if((result == DialogResult.OK) || (result == DialogResult.Yes)) {
if((result == DialogResult.OK) || (result == DialogResult.Yes)) { return true;
return true; } else if((result == DialogResult.Cancel) || (result == DialogResult.No)) {
} else if((result == DialogResult.Cancel) || (result == DialogResult.No)) { return false;
return false; } else {
} else { return null;
return null; }
} }
} finally {
finally { window.Activated -= this.rootWindowActivatedDelegate;
window.Activated -= this.rootWindowActivatedDelegate; ActiveWindow = window.Owner;
ActiveWindow = window.Owner;
if(shouldDisposeViewModelOnClose(window)) {
if(shouldDisposeViewModelOnClose(window)) { IView windowAsView = window as IView;
IView windowAsView = window as IView; if(windowAsView != null) {
if(windowAsView != null) { object viewModelAsObject = windowAsView.DataContext;
object viewModelAsObject = windowAsView.DataContext; windowAsView.DataContext = null;
windowAsView.DataContext = null; disposeIfDisposable(viewModelAsObject);
disposeIfDisposable(viewModelAsObject); }
} }
} disposeIfDisposable(window);
disposeIfDisposable(window); }
} }
}
/// <summary>Creates the view for the specified view model</summary>
/// <summary>Creates the view for the specified view model</summary> /// <typeparam name="TViewModel">
/// <typeparam name="TViewModel"> /// Type of view model for which a view will be created
/// Type of view model for which a view will be created /// </typeparam>
/// </typeparam> /// <param name="viewModel">
/// <param name="viewModel"> /// View model a view will be created for. If null, the view model will be
/// 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)
/// created as well (unless the dialog already specifies one as a resource) /// </param>
/// </param> /// <returns>The view for the specified view model</returns>
/// <returns>The view for the specified view model</returns> public virtual Control CreateView<TViewModel>(
public virtual Control CreateView<TViewModel>( TViewModel viewModel = null
TViewModel viewModel = null ) where TViewModel : class {
) where TViewModel : class { Type viewType = LocateViewForViewModel(typeof(TViewModel));
Type viewType = LocateViewForViewModel(typeof(TViewModel)); Control viewControl = (Control)CreateInstance(viewType);
Control viewControl = (Control)CreateInstance(viewType); using(var viewDisposer = new CancellableDisposer(viewControl)) {
using(var viewDisposer = new CancellableDisposer(viewControl)) {
// Create a view model if none was provided, and in either case assign
// Create a view model if none was provided, and in either case assign // the view model to the view (provided it implements IView).
// the view model to the view (provided it implements IView). using(var viewModelDisposer = new CancellableDisposer()) {
using(var viewModelDisposer = new CancellableDisposer()) { IView viewControlAsView = viewControl as IView;
IView viewControlAsView = viewControl as IView; if(viewModel == null) { // No view model provided, create one
if(viewModel == null) { // No view model provided, create one if(viewControlAsView == null) { // View doesn't implement IView
if(viewControlAsView == null) { // View doesn't implement IView viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); viewModelDisposer.Set(viewModel);
viewModelDisposer.Set(viewModel); } else if(viewControlAsView.DataContext == null) { // View has no view model
} else if(viewControlAsView.DataContext == null) { // View has no view model viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); viewModelDisposer.Set(viewModel);
viewModelDisposer.Set(viewModel); viewControlAsView.DataContext = viewModel;
viewControlAsView.DataContext = viewModel; } else { // There's an existing view model
} else { // There's an existing view model viewModel = viewControlAsView.DataContext as TViewModel;
viewModel = viewControlAsView.DataContext as TViewModel; if(viewModel == null) { // The existing view model is another type
if(viewModel == null) { // The existing view model is another type viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); viewModelDisposer.Set(viewModel);
viewModelDisposer.Set(viewModel); viewControlAsView.DataContext = viewModel;
viewControlAsView.DataContext = viewModel; }
} }
} } else if(viewControlAsView != null) { // Caller has provided a view model
} else if(viewControlAsView != null) { // Caller has provided a view model viewControlAsView.DataContext = viewModel;
viewControlAsView.DataContext = viewModel; }
}
// If an auto binder was provided, automatically bind the view to the view model
// If an auto binder was provided, automatically bind the view to the view model if(this.autoBinder != null) {
if(this.autoBinder != null) { this.autoBinder.Bind(viewControl, viewModel);
this.autoBinder.Bind(viewControl, viewModel); }
}
viewModelDisposer.Dismiss(); // Everything went well, we keep the view model
viewModelDisposer.Dismiss(); // Everything went well, we keep the view model }
}
viewDisposer.Dismiss(); // Everything went well, we keep the view
viewDisposer.Dismiss(); // Everything went well, we keep the view
}
}
return viewControl;
return viewControl; }
}
/// <summary>Creates a view model without a matching view</summary>
/// <summary>Creates a view model without a matching view</summary> /// <typeparam name="TViewModel">Type of view model that will be created</typeparam>
/// <typeparam name="TViewModel">Type of view model that will be created</typeparam> /// <returns>The new view model</returns>
/// <returns>The new view model</returns> /// <remarks>
/// <remarks> /// <para>
/// <para> /// This is useful if a view model needs to create child view models (i.e. paged container
/// 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
/// and wants to ensure the same dependency injector (if any) if used as the window /// manager uses for other view models it creates.
/// manager uses for other view models it creates. /// </para>
/// </para> /// <para>
/// <para> /// This way, view models can set up their child view models without having to immediately
/// 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
/// 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.
/// child view and store it in a container. /// </para>
/// </para> /// </remarks>
/// </remarks> public TViewModel CreateViewModel<TViewModel>() where TViewModel : class {
public TViewModel CreateViewModel<TViewModel>() where TViewModel : class { return (TViewModel)CreateInstance(typeof(TViewModel));
return (TViewModel)CreateInstance(typeof(TViewModel)); }
}
/// <summary>Locates the view that will be used to a view model</summary>
/// <summary>Locates the view that will be used to a view model</summary> /// <param name="viewModelType">
/// <param name="viewModelType"> /// Type of view model for which the view will be located
/// Type of view model for which the view will be located /// </param>
/// </param> /// <returns>The type of view that should be used for the specified view model</returns>
/// <returns>The type of view that should be used for the specified view model</returns> protected virtual Type LocateViewForViewModel(Type viewModelType) {
protected virtual Type LocateViewForViewModel(Type viewModelType) { Type viewType;
Type viewType; if(!this.viewTypesForViewModels.TryGetValue(viewModelType, out viewType)) {
if(!this.viewTypesForViewModels.TryGetValue(viewModelType, out viewType)) { string viewName = viewModelType.Name;
string viewName = viewModelType.Name; if(viewName.EndsWith("ViewModel")) {
if(viewName.EndsWith("ViewModel")) { viewName = viewName.Substring(0, viewName.Length - 9);
viewName = viewName.Substring(0, viewName.Length - 9); }
}
Type[] exportedTypes = viewModelType.Assembly.GetExportedTypes();
Type[] exportedTypes = viewModelType.Assembly.GetExportedTypes(); Type[] namespaceTypes = filterTypesByNamespace(exportedTypes, viewModelType.Namespace);
Type[] namespaceTypes = filterTypesByNamespace(exportedTypes, viewModelType.Namespace);
// First, search the own namespace (because if two identical view models exist in
// 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)
// different namespaces, the one in the same namespace is most likely the desired one) viewType = findBestMatch(
viewType = findBestMatch( namespaceTypes,
namespaceTypes, viewName + "View",
viewName + "View", viewName + "Page",
viewName + "Page", viewName + "Form",
viewName + "Form", viewName + "Window",
viewName + "Window", viewName + "Dialog",
viewName + "Dialog", viewName + "Control"
viewName + "Control" );
);
// If the view model doesn't exist in the same namespace, expand the search to
// If the view model doesn't exist in the same namespace, expand the search to // the entire assembly the view is in.
// the entire assembly the view is in. if(viewType == null) {
if(viewType == null) { viewType = findBestMatch(
viewType = findBestMatch( exportedTypes,
exportedTypes, viewName + "View",
viewName + "View", viewName + "Page",
viewName + "Page", viewName + "Form",
viewName + "Form", viewName + "Window",
viewName + "Window", viewName + "Dialog",
viewName + "Dialog", viewName + "Control"
viewName + "Control" );
); }
}
// Still no view found? We give up!
// Still no view found? We give up! if(viewType == null) {
if(viewType == null) { throw new InvalidOperationException(
throw new InvalidOperationException( string.Format("Could not locate view for view model '{0}'", viewModelType.Name)
string.Format("Could not locate view for view model '{0}'", viewModelType.Name) );
); }
}
this.viewTypesForViewModels.TryAdd(viewModelType, viewType);
this.viewTypesForViewModels.TryAdd(viewModelType, viewType); }
}
return viewType;
return viewType; }
}
/// <summary>Creates an instance of the specified type</summary>
/// <summary>Creates an instance of the specified type</summary> /// <param name="type">Type an instance will be created of</param>
/// <param name="type">Type an instance will be created of</param> /// <returns>The created instance</returns>
/// <returns>The created instance</returns> /// <remarks>
/// <remarks> /// Use this to wire up your dependency injection container. By default,
/// Use this to wire up your dependency injection container. By default, /// the Activator class will be used to create instances which only works
/// the Activator class will be used to create instances which only works /// if all of your view models are concrete classes.
/// if all of your view models are concrete classes. /// </remarks>
/// </remarks> protected virtual object CreateInstance(Type type) {
protected virtual object CreateInstance(Type type) { return Activator.CreateInstance(type);
return Activator.CreateInstance(type); }
}
/// <summary>Called when one of the application's root windows is closed</summary>
/// <summary>Called when one of the application's root windows is closed</summary> /// <param name="sender">Window that has been closed</param>
/// <param name="sender">Window that has been closed</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void rootWindowClosed(object sender, EventArgs arguments) {
private void rootWindowClosed(object sender, EventArgs arguments) { Form closedWindow = (Form)sender;
Form closedWindow = (Form)sender; closedWindow.Closed -= this.rootWindowClosedDelegate;
closedWindow.Closed -= this.rootWindowClosedDelegate; closedWindow.Activated -= this.rootWindowActivatedDelegate;
closedWindow.Activated -= this.rootWindowActivatedDelegate;
// If the view model was created just for this view or if the user asked us
// 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.
// to dispose of his view model, do so now. if(shouldDisposeViewModelOnClose(closedWindow)) {
if(shouldDisposeViewModelOnClose(closedWindow)) { IView windowAsView = closedWindow as IView;
IView windowAsView = closedWindow as IView; if(windowAsView != null) {
if(windowAsView != null) { object viewModelAsObject = windowAsView.DataContext;
object viewModelAsObject = windowAsView.DataContext; windowAsView.DataContext = null;
windowAsView.DataContext = null; disposeIfDisposable(viewModelAsObject);
disposeIfDisposable(viewModelAsObject); }
} }
}
lock(this) {
lock(this) { ActiveWindow = null;
ActiveWindow = null; }
} }
}
/// <summary>Called when one of the application's root windows is activated</summary>
/// <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="sender">Window that has been put in the foreground</param> /// <param name="arguments">Not used</param>
/// <param name="arguments">Not used</param> private void rootWindowActivated(object sender, EventArgs arguments) {
private void rootWindowActivated(object sender, EventArgs arguments) { lock(this) {
lock(this) { ActiveWindow = (Form)sender;
ActiveWindow = (Form)sender; }
} }
}
/// <summary>Tries to find the best match for a named type in a list of types</summary>
/// <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="types">List of types the search will take place in</param> /// <param name="typeNames">
/// <param name="typeNames"> /// The candidates the method will look for, starting with the best match
/// The candidates the method will look for, starting with the best match /// </param>
/// </param> /// <returns>The best match in the list of types, if any match was found</returns>
/// <returns>The best match in the list of types, if any match was found</returns> private static Type findBestMatch(Type[] types, params string[] typeNames) {
private static Type findBestMatch(Type[] types, params string[] typeNames) { int bestMatchFound = typeNames.Length;
int bestMatchFound = typeNames.Length;
Type type = null;
Type type = null; for(int index = 0; index < types.Length; ++index) {
for(int index = 0; index < types.Length; ++index) { for(int nameIndex = 0; nameIndex < bestMatchFound; ++nameIndex) {
for(int nameIndex = 0; nameIndex < bestMatchFound; ++nameIndex) { if(types[index].Name == typeNames[nameIndex]) {
if(types[index].Name == typeNames[nameIndex]) { bestMatchFound = nameIndex;
bestMatchFound = nameIndex; type = types[index];
type = types[index];
if(bestMatchFound == 0) { // There can be no better match
if(bestMatchFound == 0) { // There can be no better match return type;
return type; }
}
break;
break; }
} }
} }
}
return type;
return type; }
}
/// <summary>Disposes the specified object if it implements IDisposable</summary>
/// <summary>Disposes the specified object if it implements IDisposable</summary> /// <typeparam name="T">Type of object that will disposed if possible</typeparam>
/// <typeparam name="T">Type of object that will disposed if possible</typeparam> /// <param name="instance">Object that the method will attempt to dispose</param>
/// <param name="instance">Object that the method will attempt to dispose</param> private static void disposeIfDisposable<T>(T instance) where T : class {
private static void disposeIfDisposable<T>(T instance) where T : class { var disposable = instance as IDisposable;
var disposable = instance as IDisposable; if(disposable != null) {
if(disposable != null) { disposable.Dispose();
disposable.Dispose(); }
} }
}
/// <summary>Determines if the view owns the view model</summary>
/// <summary>Determines if the view owns the view model</summary> /// <param name="view">View that will be checked for ownership</param>
/// <param name="view">View that will be checked for ownership</param> /// <returns>True if the view owns the view model</returns>
/// <returns>True if the view owns the view model</returns> private static bool shouldDisposeViewModelOnClose(Control view) {
private static bool shouldDisposeViewModelOnClose(Control view) { string tagAsString = view.Tag as string;
string tagAsString = view.Tag as string; if(tagAsString != null) {
if(tagAsString != null) { return tagAsString.Contains("DisposeViewModelOnClose");
return tagAsString.Contains("DisposeViewModelOnClose"); } else {
} else { return false;
return false; }
} }
}
/// <summary>Filters a list of types to contain only those in a specific namespace</summary>
/// <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="exportedTypes">List of exported types that will be filtered</param> /// <param name="filteredNamespace">
/// <param name="filteredNamespace"> /// Namespace the types in the filtered list will be in
/// Namespace the types in the filtered list will be in /// </param>
/// </param> /// <returns>A subset of the specified types that are in the provided namespace</returns>
/// <returns>A subset of the specified types that are in the provided namespace</returns> private static Type[] filterTypesByNamespace(Type[] exportedTypes, string filteredNamespace) {
private static Type[] filterTypesByNamespace(Type[] exportedTypes, string filteredNamespace) { var filteredTypes = new List<Type>(exportedTypes.Length / 2);
var filteredTypes = new List<Type>(exportedTypes.Length / 2); for(int index = 0; index < exportedTypes.Length; ++index) {
for(int index = 0; index < exportedTypes.Length; ++index) { Type exportedType = exportedTypes[index];
Type exportedType = exportedTypes[index]; if(exportedType.Namespace == filteredNamespace) {
if(exportedType.Namespace == filteredNamespace) { filteredTypes.Add(exportedType);
filteredTypes.Add(exportedType); }
} }
}
return filteredTypes.ToArray();
return filteredTypes.ToArray(); }
}
/// <summary>The application's currently active root window</summary>
/// <summary>The application's currently active root window</summary> private Form activeWindow;
private Form activeWindow; /// <summary>Invoked when a root window is put in the foreground</summary>
/// <summary>Invoked when a root window is put in the foreground</summary> private EventHandler rootWindowActivatedDelegate;
private EventHandler rootWindowActivatedDelegate; /// <summary>Invoked when a root window has been closed</summary>
/// <summary>Invoked when a root window has been closed</summary> private EventHandler rootWindowClosedDelegate;
private EventHandler rootWindowClosedDelegate; /// <summary>View model binder that will be used on all created views</summary>
/// <summary>View model binder that will be used on all created views</summary> private IAutoBinder autoBinder;
private IAutoBinder autoBinder; /// <summary>Caches the view types to use for a view model</summary>
/// <summary>Caches the view types to use for a view model</summary> private ConcurrentDictionary<Type, Type> viewTypesForViewModels;
private ConcurrentDictionary<Type, Type> viewTypesForViewModels;
}
}
} // namespace Nuclex.Windows.Forms
} // namespace Nuclex.Windows.Forms