diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 3b320e0..8de62b6 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -1,31 +1,50 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Nuclex.Windows.Forms")] -[assembly: AssemblyProduct("Nuclex.Windows.Forms")] -[assembly: AssemblyDescription("Lean and elegant MVVM library with extras for WinForms")] -[assembly: AssemblyCompany("Nuclex Development Labs")] -[assembly: AssemblyCopyright("Copyright © Nuclex Development Labs 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("08254ad9-394e-4638-8412-098c8c4a4c39")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -[assembly: AssemblyVersion("1.0.0.0")] +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Nuclex.Windows.Forms")] +[assembly: AssemblyProduct("Nuclex.Windows.Forms")] +[assembly: AssemblyDescription("Lean and elegant MVVM library with extras for WinForms")] +[assembly: AssemblyCompany("Nuclex Development Labs")] +[assembly: AssemblyCopyright("Copyright © Markus Ewald / Nuclex Development Labs 2002-2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("08254ad9-394e-4638-8412-098c8c4a4c39")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] diff --git a/Source/AsyncProgressBar/AsyncProgressBar.Designer.cs b/Source/AsyncProgressBar/AsyncProgressBar.Designer.cs index 12904bc..7a858cb 100644 --- a/Source/AsyncProgressBar/AsyncProgressBar.Designer.cs +++ b/Source/AsyncProgressBar/AsyncProgressBar.Designer.cs @@ -1,53 +1,52 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -namespace Nuclex.Windows.Forms { - - partial class AsyncProgressBar { - - /// Required designer variable. - private System.ComponentModel.IContainer components = null; - - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - /// - protected override void Dispose(bool disposing) { - if(disposing && (components != null)) { - components.Dispose(); - } - - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() { - components = new System.ComponentModel.Container(); - } - - #endregion - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +namespace Nuclex.Windows.Forms { + + partial class AsyncProgressBar { + + /// Required designer variable. + private System.ComponentModel.IContainer components = null; + + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + /// + protected override void Dispose(bool disposing) { + if(disposing && (components != null)) { + components.Dispose(); + } + + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + components = new System.ComponentModel.Container(); + } + + #endregion + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/AsyncProgressBar/AsyncProgressBar.Test.cs b/Source/AsyncProgressBar/AsyncProgressBar.Test.cs index 0995923..d2579cf 100644 --- a/Source/AsyncProgressBar/AsyncProgressBar.Test.cs +++ b/Source/AsyncProgressBar/AsyncProgressBar.Test.cs @@ -1,71 +1,70 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.IO; -using System.Windows.Forms; - -using NUnit.Framework; - -using Nuclex.Support; - -namespace Nuclex.Windows.Forms { - - /// Unit Test for the asynchronously updating progress bar - [TestFixture, Explicit] - public class AsyncProgressBarTest { - - /// - /// Verifies that asynchronous progress assignment is working - /// - [Test] - public void TestProgressAssignment() { - using(AsyncProgressBar progressBar = new AsyncProgressBar()) { - - // Let the control create its window handle - progressBar.CreateControl(); - progressBar.Minimum = 0; - progressBar.Maximum = 100; - - Assert.AreEqual(0, progressBar.Value); - - // Assign the new value. This will be done asynchronously, so we call - // Application.DoEvents() to execute the message pump once, guaranteeing - // that the call will have been executed after Application.DoEvents() returns. - progressBar.AsyncSetValue(0.33f); - Application.DoEvents(); - - Assert.AreEqual(33, progressBar.Value); - - progressBar.AsyncSetValue(0.66f); - Application.DoEvents(); - - Assert.AreEqual(66, progressBar.Value); - - } - } - - } - -} // namespace Nuclex.Windows.Forms - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.IO; +using System.Windows.Forms; + +using NUnit.Framework; + +using Nuclex.Support; + +namespace Nuclex.Windows.Forms { + + /// Unit Test for the asynchronously updating progress bar + [TestFixture, Explicit] + public class AsyncProgressBarTest { + + /// + /// Verifies that asynchronous progress assignment is working + /// + [Test] + public void TestProgressAssignment() { + using(AsyncProgressBar progressBar = new AsyncProgressBar()) { + + // Let the control create its window handle + progressBar.CreateControl(); + progressBar.Minimum = 0; + progressBar.Maximum = 100; + + Assert.AreEqual(0, progressBar.Value); + + // Assign the new value. This will be done asynchronously, so we call + // Application.DoEvents() to execute the message pump once, guaranteeing + // that the call will have been executed after Application.DoEvents() returns. + progressBar.AsyncSetValue(0.33f); + Application.DoEvents(); + + Assert.AreEqual(33, progressBar.Value); + + progressBar.AsyncSetValue(0.66f); + Application.DoEvents(); + + Assert.AreEqual(66, progressBar.Value); + + } + } + + } + +} // namespace Nuclex.Windows.Forms + +#endif // UNITTEST diff --git a/Source/AsyncProgressBar/AsyncProgressBar.cs b/Source/AsyncProgressBar/AsyncProgressBar.cs index e3b8fdf..237ff04 100644 --- a/Source/AsyncProgressBar/AsyncProgressBar.cs +++ b/Source/AsyncProgressBar/AsyncProgressBar.cs @@ -1,140 +1,139 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Threading; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms { - - /// Progress bar with optimized multi-threading behavior - /// - /// - /// If a background thread is generating lots of progress updates, using synchronized - /// calls can drastically reduce performance. This progress bar optimizes that case - /// by performing the update asynchronously and keeping only the most recent update - /// when multiple updates arrive while the asynchronous update call is still running. - /// - /// - /// This design eliminates useless queueing of progress updates, thereby reducing - /// CPU load occuring in the UI thread and at the same time avoids blocking the - /// worker thread, increasing its performance. - /// - /// - public partial class AsyncProgressBar : ProgressBar { - - /// Initializes a new asynchronous progress bar - public AsyncProgressBar() { - InitializeComponent(); - - this.Disposed += new EventHandler(progressBarDisposed); - this.updateProgressDelegate = new MethodInvoker(updateProgress); - - // Could probably use VolatileWrite() as well, but for consistency reasons - // this is an Interlocked call, too. Mixing different synchronization measures - // for a variable raises a red flag whenever I see it :) - Interlocked.Exchange(ref this.newProgress, -1.0f); - } - - /// Called when the progress bar is being disposed - /// Progress bar that is being disposed - /// Not used - private void progressBarDisposed(object sender, EventArgs arguments) { - - // CHECK: This method is only called on an explicit Dispose() of the control. - // It is legal to call Control.BeginInvoke() without calling Control.EndInvoke(), - // so the code is quite correct even if no Dispose() occurs, but is it also clean? - // http://www.interact-sw.co.uk/iangblog/2005/05/16/endinvokerequired - - // Since this has to occur in the UI thread, there's no way that updateProgress() - // could be executing just now. But the final call to updateProgress() will not - // have EndInvoke() called on it yet, so we do this here before the control - // is finally disposed. - if(this.progressUpdateAsyncResult != null) { - EndInvoke(this.progressUpdateAsyncResult); - this.progressUpdateAsyncResult = null; - } - - } - - /// Asynchronously updates the value to be shown in the progress bar - /// New value to set the progress bar to - /// - /// This will schedule an asynchronous update of the progress bar in the UI thread. - /// If you change the progress value again before the progress bar has completed its - /// update cycle, the original progress value will be skipped and the progress bar - /// jumps directly to the latest progress value. Updates are not queued, there is - /// at most one update waiting on the UI thread. It is also strictly guaranteed that - /// the last most progress value set will be shown and never skipped. - /// - public void AsyncSetValue(float value) { - - // Update the value to be shown on the progress bar. If this happens multiple - // times, that's not a problem, the progress bar updates as fast as it can - // and always tries to show the most recent value assigned. - float oldValue = Interlocked.Exchange(ref this.newProgress, value); - - // If the previous value was -1, the UI thread has already taken out the most recent - // value and assigned it (or is about to assign it) to the progress bar control. - // In this case, we'll wait until the current update has completed and immediately - // begin the next update - since we know that the value the UI thread has extracted - // is no longer the most recent one. - if(oldValue == -1.0f) { - if(this.progressUpdateAsyncResult != null) { - EndInvoke(this.progressUpdateAsyncResult); - } - - this.progressUpdateAsyncResult = BeginInvoke(this.updateProgressDelegate); - } - - } - - /// Synchronously updates the value visualized in the progress bar - private void updateProgress() { - - // Cache these to shorten the code that follows :) - int minimum = base.Minimum; - int maximum = base.Maximum; - - // Take out the most recent value that has been given to the asynchronous progress - // bar up until now and replace it by -1. This enables the updater to see when - // the update has actually been performed and whether it needs to start a new - // invocation to ensure the most recent value will remain at the end. - float progress = Interlocked.Exchange(ref this.newProgress, -1.0f); - - // Restrain the value to the progress bar's configured range and assign it. - // This is done to prevent exceptions in the UI thread (theoretically the user - // could change the progress bar's min and max just before the UI thread executes - // this method, so we cannot validate the value in AsyncSetValue()) - int value = (int)(progress * (maximum - minimum)) + minimum; - base.Value = Math.Min(Math.Max(value, minimum), maximum); - - } - - /// New progress being assigned to the progress bar - private float newProgress; - /// Delegate for the progress update method - private MethodInvoker updateProgressDelegate; - /// Async result for the invoked control state update method - private volatile IAsyncResult progressUpdateAsyncResult; - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms { + + /// Progress bar with optimized multi-threading behavior + /// + /// + /// If a background thread is generating lots of progress updates, using synchronized + /// calls can drastically reduce performance. This progress bar optimizes that case + /// by performing the update asynchronously and keeping only the most recent update + /// when multiple updates arrive while the asynchronous update call is still running. + /// + /// + /// This design eliminates useless queueing of progress updates, thereby reducing + /// CPU load occuring in the UI thread and at the same time avoids blocking the + /// worker thread, increasing its performance. + /// + /// + public partial class AsyncProgressBar : ProgressBar { + + /// Initializes a new asynchronous progress bar + public AsyncProgressBar() { + InitializeComponent(); + + this.Disposed += new EventHandler(progressBarDisposed); + this.updateProgressDelegate = new MethodInvoker(updateProgress); + + // Could probably use VolatileWrite() as well, but for consistency reasons + // this is an Interlocked call, too. Mixing different synchronization measures + // for a variable raises a red flag whenever I see it :) + Interlocked.Exchange(ref this.newProgress, -1.0f); + } + + /// Called when the progress bar is being disposed + /// Progress bar that is being disposed + /// Not used + private void progressBarDisposed(object sender, EventArgs arguments) { + + // CHECK: This method is only called on an explicit Dispose() of the control. + // It is legal to call Control.BeginInvoke() without calling Control.EndInvoke(), + // so the code is quite correct even if no Dispose() occurs, but is it also clean? + // http://www.interact-sw.co.uk/iangblog/2005/05/16/endinvokerequired + + // Since this has to occur in the UI thread, there's no way that updateProgress() + // could be executing just now. But the final call to updateProgress() will not + // have EndInvoke() called on it yet, so we do this here before the control + // is finally disposed. + if(this.progressUpdateAsyncResult != null) { + EndInvoke(this.progressUpdateAsyncResult); + this.progressUpdateAsyncResult = null; + } + + } + + /// Asynchronously updates the value to be shown in the progress bar + /// New value to set the progress bar to + /// + /// This will schedule an asynchronous update of the progress bar in the UI thread. + /// If you change the progress value again before the progress bar has completed its + /// update cycle, the original progress value will be skipped and the progress bar + /// jumps directly to the latest progress value. Updates are not queued, there is + /// at most one update waiting on the UI thread. It is also strictly guaranteed that + /// the last most progress value set will be shown and never skipped. + /// + public void AsyncSetValue(float value) { + + // Update the value to be shown on the progress bar. If this happens multiple + // times, that's not a problem, the progress bar updates as fast as it can + // and always tries to show the most recent value assigned. + float oldValue = Interlocked.Exchange(ref this.newProgress, value); + + // If the previous value was -1, the UI thread has already taken out the most recent + // value and assigned it (or is about to assign it) to the progress bar control. + // In this case, we'll wait until the current update has completed and immediately + // begin the next update - since we know that the value the UI thread has extracted + // is no longer the most recent one. + if(oldValue == -1.0f) { + if(this.progressUpdateAsyncResult != null) { + EndInvoke(this.progressUpdateAsyncResult); + } + + this.progressUpdateAsyncResult = BeginInvoke(this.updateProgressDelegate); + } + + } + + /// Synchronously updates the value visualized in the progress bar + private void updateProgress() { + + // Cache these to shorten the code that follows :) + int minimum = base.Minimum; + int maximum = base.Maximum; + + // Take out the most recent value that has been given to the asynchronous progress + // bar up until now and replace it by -1. This enables the updater to see when + // the update has actually been performed and whether it needs to start a new + // invocation to ensure the most recent value will remain at the end. + float progress = Interlocked.Exchange(ref this.newProgress, -1.0f); + + // Restrain the value to the progress bar's configured range and assign it. + // This is done to prevent exceptions in the UI thread (theoretically the user + // could change the progress bar's min and max just before the UI thread executes + // this method, so we cannot validate the value in AsyncSetValue()) + int value = (int)(progress * (maximum - minimum)) + minimum; + base.Value = Math.Min(Math.Max(value, minimum), maximum); + + } + + /// New progress being assigned to the progress bar + private float newProgress; + /// Delegate for the progress update method + private MethodInvoker updateProgressDelegate; + /// Async result for the invoked control state update method + private volatile IAsyncResult progressUpdateAsyncResult; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/AutoBinding/ConventionBinder.cs b/Source/AutoBinding/ConventionBinder.cs index 2faab8f..ef54ff8 100644 --- a/Source/AutoBinding/ConventionBinder.cs +++ b/Source/AutoBinding/ConventionBinder.cs @@ -1,47 +1,66 @@ -using Nuclex.Windows.Forms.Views; -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms.AutoBinding { - - /// - /// Binds a view to its model using a convention-over-configuration approach - /// - public class ConventionBinder : IAutoBinder { - - /// Binds the specified view to an explicitly selected view model - /// - /// Type of view model the view will be bound to - /// - /// View that will be bound to a view model - /// View model the view will be bound to - public void Bind(Control view, TViewModel viewModel) - where TViewModel : class { - bind(view, viewModel); - } - - /// - /// Binds the specified view to the view model specified in its DataContext - /// - /// View that will be bound - public void Bind(Control viewControl) { - IView viewControlAsView = viewControl as IView; - if(viewControlAsView == null) { - throw new InvalidOperationException( - "The specified view has no view model associated. Either assign your " + - "view model to the view's data context beforehand or use the overload " + - "of Bind() that allows you to explicitly specify the view model." - ); - } - - bind(viewControl, viewControlAsView.DataContext); - } - - /// Binds a view to a view model - /// View that will be bound - /// View model the view will be bound to - private void bind(Control view, object viewModel) { - } - - } -} // namespace Nuclex.Windows.Forms.AutoBinding +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using Nuclex.Windows.Forms.Views; +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms.AutoBinding { + + /// + /// Binds a view to its model using a convention-over-configuration approach + /// + public class ConventionBinder : IAutoBinder { + + /// Binds the specified view to an explicitly selected view model + /// + /// Type of view model the view will be bound to + /// + /// View that will be bound to a view model + /// View model the view will be bound to + public void Bind(Control view, TViewModel viewModel) + where TViewModel : class { + bind(view, viewModel); + } + + /// + /// Binds the specified view to the view model specified in its DataContext + /// + /// View that will be bound + public void Bind(Control viewControl) { + IView viewControlAsView = viewControl as IView; + if(viewControlAsView == null) { + throw new InvalidOperationException( + "The specified view has no view model associated. Either assign your " + + "view model to the view's data context beforehand or use the overload " + + "of Bind() that allows you to explicitly specify the view model." + ); + } + + bind(viewControl, viewControlAsView.DataContext); + } + + /// Binds a view to a view model + /// View that will be bound + /// View model the view will be bound to + private void bind(Control view, object viewModel) { + } + + } +} // namespace Nuclex.Windows.Forms.AutoBinding diff --git a/Source/AutoBinding/IAutoBinder.cs b/Source/AutoBinding/IAutoBinder.cs index 9220ea2..2a891e6 100644 --- a/Source/AutoBinding/IAutoBinder.cs +++ b/Source/AutoBinding/IAutoBinder.cs @@ -1,28 +1,47 @@ -using System; - -using System.Windows.Forms; -using Nuclex.Windows.Forms.Views; - -namespace Nuclex.Windows.Forms.AutoBinding { - - /// Binds views to their view models - public interface IAutoBinder { - - /// Binds the specified view to an explicitly selected view model - /// - /// Type of view model the view will be bound to - /// - /// View that will be bound to a view model - /// View model the view will be bound to - void Bind(Control view, TViewModel viewModel) - where TViewModel : class; - - /// - /// Binds the specified view to the view model specified in its DataContext - /// - /// View that will be bound - void Bind(Control view); - - } - -} // namespace Nuclex.Windows.Forms.AutoBinding +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +using System.Windows.Forms; +using Nuclex.Windows.Forms.Views; + +namespace Nuclex.Windows.Forms.AutoBinding { + + /// Binds views to their view models + public interface IAutoBinder { + + /// Binds the specified view to an explicitly selected view model + /// + /// Type of view model the view will be bound to + /// + /// View that will be bound to a view model + /// View model the view will be bound to + void Bind(Control view, TViewModel viewModel) + where TViewModel : class; + + /// + /// Binds the specified view to the view model specified in its DataContext + /// + /// View that will be bound + void Bind(Control view); + + } + +} // namespace Nuclex.Windows.Forms.AutoBinding diff --git a/Source/CommonDialogs/CommonDialogManager.cs b/Source/CommonDialogs/CommonDialogManager.cs index 3d50d78..4bd4027 100644 --- a/Source/CommonDialogs/CommonDialogManager.cs +++ b/Source/CommonDialogs/CommonDialogManager.cs @@ -1,187 +1,186 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Text; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms.CommonDialogs { - - /// Displays common dialogs for selecting files and directories - public class CommonDialogManager : ICommonDialogService { - - /// Initializes a new task dialog message service - public CommonDialogManager() : this(NullActiveWindowTracker.Default) { } - - /// Initializes a new task dialog message service - /// - /// Active window tracker used to obtain the parent window for message boxes - /// - public CommonDialogManager(IActiveWindowTracker tracker) { - this.tracker = tracker; - } - - /// Asks the user for a location to save a file under - /// Caption of the dialog - /// - /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" - /// - /// The full path of the file the user wishes to save as - public string AskForSaveLocation(string caption, params string[] masks) { - var saveDialog = new SaveFileDialog() { - Title = caption, - Filter = combineMasks(masks) - }; - - DialogResult result; - { - Form activeWindow = this.tracker.ActiveWindow; - if(activeWindow == null) { - result = saveDialog.ShowDialog(); - } else { - result = saveDialog.ShowDialog(activeWindow); - } - } - - if(result == DialogResult.OK) { - return saveDialog.FileName; - } else { - return null; - } - } - - /// Asks the user to select a file to open - /// Caption of the dialog - /// - /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" - /// - /// The full path of the file the user selected - public string AskForFileToOpen(string caption, params string[] masks) { - var openDialog = new OpenFileDialog() { - Title = caption, - Filter = combineMasks(masks), - CheckFileExists = true, - CheckPathExists = true, - Multiselect = false - }; - - DialogResult result; - { - Form activeWindow = this.tracker.ActiveWindow; - if(activeWindow == null) { - result = openDialog.ShowDialog(); - } else { - result = openDialog.ShowDialog(activeWindow); - } - } - - if(result == DialogResult.OK) { - return openDialog.FileName; - } else { - return null; - } - } - - /// Asks the user to select one or more files to open - /// Caption of the dialog - /// - /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" - /// - /// The full path of all files the user selected - public string[] AskForFilesToOpen(string caption, params string[] masks) { - var openDialog = new OpenFileDialog() { - Title = caption, - Filter = combineMasks(masks), - CheckFileExists = true, - CheckPathExists = true, - Multiselect = true - }; - - DialogResult result; - { - Form activeWindow = this.tracker.ActiveWindow; - if(activeWindow == null) { - result = openDialog.ShowDialog(); - } else { - result = openDialog.ShowDialog(activeWindow); - } - } - - if(result == DialogResult.OK) { - return openDialog.FileNames; - } else { - return null; - } - } - - /// Asks the user to select a directory - /// The directory the user has selected - public string AskForDirectory(string caption) { - var folderDialog = new System.Windows.Forms.FolderBrowserDialog() { - Description = caption - }; - - DialogResult result; - { - Form activeWindow = this.tracker.ActiveWindow; - if(activeWindow == null) { - result = folderDialog.ShowDialog(); - } else { - result = folderDialog.ShowDialog(activeWindow); - } - } - - if(result == DialogResult.OK) { - return folderDialog.SelectedPath; - } else { - return null; - } - } - - /// Combines an array of file masks into a single mask - /// Masks that will be combined - /// That combined masks - private static string combineMasks(string[] masks) { - if((masks == null) || (masks.Length == 0)) { - return null; - } - - int requiredCapacity = 0; - for(int index = 0; index < masks.Length; ++index) { - requiredCapacity += masks[index].Length; - requiredCapacity += 1; - } - - var maskBuilder = new StringBuilder(requiredCapacity); - maskBuilder.Append(masks[0]); - for(int index = 1; index < masks.Length; ++index) { - maskBuilder.Append('|'); - maskBuilder.Append(masks[index]); - } - - return maskBuilder.ToString(); - } - - /// Provides the currently active top-level window - private IActiveWindowTracker tracker; - - } - -} // namespace Nuclex.Windows.Forms.CommonDialogs +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Text; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms.CommonDialogs { + + /// Displays common dialogs for selecting files and directories + public class CommonDialogManager : ICommonDialogService { + + /// Initializes a new task dialog message service + public CommonDialogManager() : this(NullActiveWindowTracker.Default) { } + + /// Initializes a new task dialog message service + /// + /// Active window tracker used to obtain the parent window for message boxes + /// + public CommonDialogManager(IActiveWindowTracker tracker) { + this.tracker = tracker; + } + + /// Asks the user for a location to save a file under + /// Caption of the dialog + /// + /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" + /// + /// The full path of the file the user wishes to save as + public string AskForSaveLocation(string caption, params string[] masks) { + var saveDialog = new SaveFileDialog() { + Title = caption, + Filter = combineMasks(masks) + }; + + DialogResult result; + { + Form activeWindow = this.tracker.ActiveWindow; + if(activeWindow == null) { + result = saveDialog.ShowDialog(); + } else { + result = saveDialog.ShowDialog(activeWindow); + } + } + + if(result == DialogResult.OK) { + return saveDialog.FileName; + } else { + return null; + } + } + + /// Asks the user to select a file to open + /// Caption of the dialog + /// + /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" + /// + /// The full path of the file the user selected + public string AskForFileToOpen(string caption, params string[] masks) { + var openDialog = new OpenFileDialog() { + Title = caption, + Filter = combineMasks(masks), + CheckFileExists = true, + CheckPathExists = true, + Multiselect = false + }; + + DialogResult result; + { + Form activeWindow = this.tracker.ActiveWindow; + if(activeWindow == null) { + result = openDialog.ShowDialog(); + } else { + result = openDialog.ShowDialog(activeWindow); + } + } + + if(result == DialogResult.OK) { + return openDialog.FileName; + } else { + return null; + } + } + + /// Asks the user to select one or more files to open + /// Caption of the dialog + /// + /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" + /// + /// The full path of all files the user selected + public string[] AskForFilesToOpen(string caption, params string[] masks) { + var openDialog = new OpenFileDialog() { + Title = caption, + Filter = combineMasks(masks), + CheckFileExists = true, + CheckPathExists = true, + Multiselect = true + }; + + DialogResult result; + { + Form activeWindow = this.tracker.ActiveWindow; + if(activeWindow == null) { + result = openDialog.ShowDialog(); + } else { + result = openDialog.ShowDialog(activeWindow); + } + } + + if(result == DialogResult.OK) { + return openDialog.FileNames; + } else { + return null; + } + } + + /// Asks the user to select a directory + /// The directory the user has selected + public string AskForDirectory(string caption) { + var folderDialog = new System.Windows.Forms.FolderBrowserDialog() { + Description = caption + }; + + DialogResult result; + { + Form activeWindow = this.tracker.ActiveWindow; + if(activeWindow == null) { + result = folderDialog.ShowDialog(); + } else { + result = folderDialog.ShowDialog(activeWindow); + } + } + + if(result == DialogResult.OK) { + return folderDialog.SelectedPath; + } else { + return null; + } + } + + /// Combines an array of file masks into a single mask + /// Masks that will be combined + /// That combined masks + private static string combineMasks(string[] masks) { + if((masks == null) || (masks.Length == 0)) { + return null; + } + + int requiredCapacity = 0; + for(int index = 0; index < masks.Length; ++index) { + requiredCapacity += masks[index].Length; + requiredCapacity += 1; + } + + var maskBuilder = new StringBuilder(requiredCapacity); + maskBuilder.Append(masks[0]); + for(int index = 1; index < masks.Length; ++index) { + maskBuilder.Append('|'); + maskBuilder.Append(masks[index]); + } + + return maskBuilder.ToString(); + } + + /// Provides the currently active top-level window + private IActiveWindowTracker tracker; + + } + +} // namespace Nuclex.Windows.Forms.CommonDialogs diff --git a/Source/CommonDialogs/ICommonDialogService.cs b/Source/CommonDialogs/ICommonDialogService.cs index 6f94b26..d5772e8 100644 --- a/Source/CommonDialogs/ICommonDialogService.cs +++ b/Source/CommonDialogs/ICommonDialogService.cs @@ -1,58 +1,57 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Windows.Forms.CommonDialogs { - - /// Displays common dialogs for selecting files and directories - public interface ICommonDialogService { - - /// Asks the user for a location to save a file under - /// Caption of the dialog - /// - /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" - /// - /// The full path of the file the user wishes to save as - string AskForSaveLocation(string caption, params string[] masks); - - /// Asks the user to select a file to open - /// Caption of the dialog - /// - /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" - /// - /// The full path of the file the user selected - string AskForFileToOpen(string caption, params string[] masks); - - /// Asks the user to select one or more files to open - /// Caption of the dialog - /// - /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" - /// - /// The full path of all files the user selected - string[] AskForFilesToOpen(string caption, params string[] masks); - - /// Asks the user to select a directory - /// The directory the user has selected - string AskForDirectory(string caption); - - } - -} // namespace Nuclex.Windows.Forms.CommonDialogs +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Windows.Forms.CommonDialogs { + + /// Displays common dialogs for selecting files and directories + public interface ICommonDialogService { + + /// Asks the user for a location to save a file under + /// Caption of the dialog + /// + /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" + /// + /// The full path of the file the user wishes to save as + string AskForSaveLocation(string caption, params string[] masks); + + /// Asks the user to select a file to open + /// Caption of the dialog + /// + /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" + /// + /// The full path of the file the user selected + string AskForFileToOpen(string caption, params string[] masks); + + /// Asks the user to select one or more files to open + /// Caption of the dialog + /// + /// File masks in the form "Description|*.dat" or "Description2|*.da2;*.da3" + /// + /// The full path of all files the user selected + string[] AskForFilesToOpen(string caption, params string[] masks); + + /// Asks the user to select a directory + /// The directory the user has selected + string AskForDirectory(string caption); + + } + +} // namespace Nuclex.Windows.Forms.CommonDialogs diff --git a/Source/ContainerListView/ContainerListView.Designer.cs b/Source/ContainerListView/ContainerListView.Designer.cs index a39e339..d3aef6a 100644 --- a/Source/ContainerListView/ContainerListView.Designer.cs +++ b/Source/ContainerListView/ContainerListView.Designer.cs @@ -1,53 +1,52 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -namespace Nuclex.Windows.Forms { - - partial class ContainerListView { - - /// Required designer variable. - private System.ComponentModel.IContainer components = null; - - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - /// - protected override void Dispose(bool disposing) { - if(disposing && (components != null)) { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() { - components = new System.ComponentModel.Container(); - } - - #endregion - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +namespace Nuclex.Windows.Forms { + + partial class ContainerListView { + + /// Required designer variable. + private System.ComponentModel.IContainer components = null; + + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + /// + protected override void Dispose(bool disposing) { + if(disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + components = new System.ComponentModel.Container(); + } + + #endregion + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/ContainerListView/ContainerListView.Test.cs b/Source/ContainerListView/ContainerListView.Test.cs index 6867ff0..0f031cd 100644 --- a/Source/ContainerListView/ContainerListView.Test.cs +++ b/Source/ContainerListView/ContainerListView.Test.cs @@ -1,83 +1,82 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.IO; -using System.Windows.Forms; - -using NUnit.Framework; - -using Nuclex.Support; - -namespace Nuclex.Windows.Forms { - - /// Unit Test for the control container list view - [TestFixture, Explicit] - public class ContainerListViewTest { - - /// - /// Verifies that the asynchronous progress bar's constructor is working - /// - [Test] - public void TestConstructor() { - using(ContainerListView listView = new ContainerListView()) { - - // Let the control create its window handle - listView.CreateControl(); - listView.Columns.Add("Numeric"); - listView.Columns.Add("Spelled"); - listView.Columns.Add("Nonsense"); - - addRow(listView, "1", "One"); - addRow(listView, "2", "Two"); - addRow(listView, "3", "Three"); - - using(CheckBox checkBox = new CheckBox()) { - listView.EmbeddedControls.Add(new ListViewEmbeddedControl(checkBox, 2, 0)); - listView.EmbeddedControls.Clear(); - - listView.Refresh(); - - ListViewEmbeddedControl embeddedControl = new ListViewEmbeddedControl( - checkBox, 2, 0 - ); - listView.EmbeddedControls.Add(embeddedControl); - listView.EmbeddedControls.Remove(embeddedControl); - - listView.Refresh(); - } - - } - } - - /// Adds a row to a control container list view - /// List view control the row will be added to - /// Values that will appear in the individual columns - private void addRow(ContainerListView listView, params string[] columns) { - listView.Items.Add(new ListViewItem(columns)); - } - - } - -} // namespace Nuclex.Windows.Forms - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.IO; +using System.Windows.Forms; + +using NUnit.Framework; + +using Nuclex.Support; + +namespace Nuclex.Windows.Forms { + + /// Unit Test for the control container list view + [TestFixture, Explicit] + public class ContainerListViewTest { + + /// + /// Verifies that the asynchronous progress bar's constructor is working + /// + [Test] + public void TestConstructor() { + using(ContainerListView listView = new ContainerListView()) { + + // Let the control create its window handle + listView.CreateControl(); + listView.Columns.Add("Numeric"); + listView.Columns.Add("Spelled"); + listView.Columns.Add("Nonsense"); + + addRow(listView, "1", "One"); + addRow(listView, "2", "Two"); + addRow(listView, "3", "Three"); + + using(CheckBox checkBox = new CheckBox()) { + listView.EmbeddedControls.Add(new ListViewEmbeddedControl(checkBox, 2, 0)); + listView.EmbeddedControls.Clear(); + + listView.Refresh(); + + ListViewEmbeddedControl embeddedControl = new ListViewEmbeddedControl( + checkBox, 2, 0 + ); + listView.EmbeddedControls.Add(embeddedControl); + listView.EmbeddedControls.Remove(embeddedControl); + + listView.Refresh(); + } + + } + } + + /// Adds a row to a control container list view + /// List view control the row will be added to + /// Values that will appear in the individual columns + private void addRow(ContainerListView listView, params string[] columns) { + listView.Items.Add(new ListViewItem(columns)); + } + + } + +} // namespace Nuclex.Windows.Forms + +#endif // UNITTEST diff --git a/Source/ContainerListView/ContainerListView.cs b/Source/ContainerListView/ContainerListView.cs index efb14da..0f8ea89 100644 --- a/Source/ContainerListView/ContainerListView.cs +++ b/Source/ContainerListView/ContainerListView.cs @@ -1,247 +1,246 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Data; -using System.Text; -using System.Windows.Forms; -using System.Runtime.InteropServices; - -using Nuclex.Support.Collections; - -namespace Nuclex.Windows.Forms { - - /// ListView allowing for other controls to be embedded in its cells - /// - /// - /// There basically were two possible design choices: Provide a specialized - /// ListViewSubItem that carries a Control instead of a string or manage the - /// embedded controls seperate of the ListView's items. - /// - /// - /// The first option requires a complete rewrite of the ListViewItem class - /// and its related support classes, all of which are surprisingly large and - /// complex. Thus, I chose the less clean but more doable latter option. - /// - /// - /// This control is useful for simple item lists where you want to provide - /// a combobox, checkbox or other control to the user for a certain column. - /// It will not perform well for lists with hundreds of items since it - /// requires a control to be created per row and management of the embedded - /// controls is designed for limited usage. - /// - /// - public partial class ContainerListView : System.Windows.Forms.ListView { - - /// Message sent to a control to let it paint itself - private const int WM_PAINT = 0x000F; - - /// Initializes a new ContainerListView - public ContainerListView() { - this.embeddedControlClickedDelegate = new EventHandler(embeddedControlClicked); - - this.embeddedControls = new ObservableList(); - this.embeddedControls.ItemAdded += - new EventHandler>(embeddedControlAdded); - this.embeddedControls.ItemRemoved += - new EventHandler>(embeddedControlRemoved); - this.embeddedControls.Clearing += new EventHandler(embeddedControlsClearing); - - InitializeComponent(); - - // Eliminate flickering - SetStyle(ControlStyles.OptimizedDoubleBuffer, true); - SetStyle(ControlStyles.AllPaintingInWmPaint, true); - - base.View = View.Details; - - this.columnHeaderHeight = Font.Height; - } - - /// Controls being embedded in the ListView - public ICollection EmbeddedControls { - get { return this.embeddedControls; } - } - - /// Updates the controls embeded into the list view - public void UpdateEmbeddedControls() { - if(View != View.Details) { - for(int index = 0; index < this.embeddedControls.Count; ++index) { - this.embeddedControls[index].Control.Visible = false; - } - } else { - for(int index = 0; index < this.embeddedControls.Count; ++index) { - ListViewEmbeddedControl embeddedControl = this.embeddedControls[index]; - - Rectangle cellBounds = this.GetSubItemBounds( - Items[embeddedControl.Row], embeddedControl.Column - ); - - bool intersectsColumnHeader = - (base.HeaderStyle != ColumnHeaderStyle.None) && - (cellBounds.Top < base.Font.Height); - - embeddedControl.Control.Visible = !intersectsColumnHeader; - embeddedControl.Control.Bounds = cellBounds; - } - } - } - - /// Calculates the boundaries of a cell in the list view - /// Item in the list view from which to calculate the cell - /// Index der cell whose boundaries to calculate - /// The boundaries of the specified list view cell - /// - /// When the specified sub item index is not in the range of valid sub items - /// - protected Rectangle GetSubItemBounds(ListViewItem item, int subItem) { - int[] order = GetColumnOrder(); - if(order == null) { // No Columns - return Rectangle.Empty; - } - - if(subItem >= order.Length) { - throw new IndexOutOfRangeException("SubItem " + subItem + " out of range"); - } - - // Determine the border of the entire ListViewItem, including all sub items - Rectangle itemBounds = item.GetBounds(ItemBoundsPortion.Entire); - int subItemX = itemBounds.Left; - - // Find the horizontal position of the sub item. Because the column order can vary, - // we need to use Columns[order[i]] instead of simply doing Columns[i] here! - ColumnHeader columnHeader; - int i; - for(i = 0; i < order.Length; ++i) { - columnHeader = this.Columns[order[i]]; - if(columnHeader.Index == subItem) { - break; - } - - subItemX += columnHeader.Width; - } - - return new Rectangle( - subItemX, itemBounds.Top, this.Columns[order[i]].Width, itemBounds.Height - ); - } - - /// Responds to window messages sent by the operating system - /// Window message that will be processed - protected override void WndProc(ref Message message) { - switch(message.Msg) { - case WM_PAINT: { - UpdateEmbeddedControls(); - break; - } - } - - base.WndProc(ref message); - } - - /// Called when the list of embedded controls has been cleared - /// Collection that has been cleared of its controls - /// Not used - private void embeddedControlsClearing(object sender, EventArgs arguments) { - this.BeginUpdate(); - try { - foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) { - embeddedControl.Control.Click -= this.embeddedControlClickedDelegate; - this.Controls.Remove(embeddedControl.Control); - } - } - finally { - this.EndUpdate(); - } - } - - /// Called when a control gets removed from the embedded controls list - /// List from which the control has been removed - /// - /// Event arguments providing a reference to the removed control - /// - private void embeddedControlAdded( - object sender, ItemEventArgs arguments - ) { - arguments.Item.Control.Click += this.embeddedControlClickedDelegate; - this.Controls.Add(arguments.Item.Control); - } - - /// Called when a control gets added to the embedded controls list - /// List to which the control has been added - /// - /// Event arguments providing a reference to the added control - /// - private void embeddedControlRemoved( - object sender, ItemEventArgs arguments - ) { - if(this.Controls.Contains(arguments.Item.Control)) { - arguments.Item.Control.Click -= this.embeddedControlClickedDelegate; - this.Controls.Remove(arguments.Item.Control); - } - } - - /// Called when an embedded control has been clicked on - /// Embedded control that has been clicked - /// Not used - private void embeddedControlClicked(object sender, EventArgs arguments) { - this.BeginUpdate(); - - try { - SelectedItems.Clear(); - - foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) { - if(ReferenceEquals(embeddedControl.Control, sender)) { - if((embeddedControl.Row > 0) && (embeddedControl.Row < Items.Count)) { - Items[embeddedControl.Row].Selected = true; - } - } - } - } - finally { - this.EndUpdate(); - } - } - - /// Obtains the current column order of the list - /// An array indicating the order of the list's columns - private int[] GetColumnOrder() { - int[] order = new int[this.Columns.Count]; - - for(int index = 0; index < this.Columns.Count; ++index) { - order[this.Columns[index].DisplayIndex] = index; - } - - return order; - } - - /// Height of the list view's column header - private int columnHeaderHeight; - /// Event handler for when embedded controls are clicked on - private EventHandler embeddedControlClickedDelegate; - /// Controls being embedded in this ListView - private ObservableList embeddedControls; - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Data; +using System.Text; +using System.Windows.Forms; +using System.Runtime.InteropServices; + +using Nuclex.Support.Collections; + +namespace Nuclex.Windows.Forms { + + /// ListView allowing for other controls to be embedded in its cells + /// + /// + /// There basically were two possible design choices: Provide a specialized + /// ListViewSubItem that carries a Control instead of a string or manage the + /// embedded controls seperate of the ListView's items. + /// + /// + /// The first option requires a complete rewrite of the ListViewItem class + /// and its related support classes, all of which are surprisingly large and + /// complex. Thus, I chose the less clean but more doable latter option. + /// + /// + /// This control is useful for simple item lists where you want to provide + /// a combobox, checkbox or other control to the user for a certain column. + /// It will not perform well for lists with hundreds of items since it + /// requires a control to be created per row and management of the embedded + /// controls is designed for limited usage. + /// + /// + public partial class ContainerListView : System.Windows.Forms.ListView { + + /// Message sent to a control to let it paint itself + private const int WM_PAINT = 0x000F; + + /// Initializes a new ContainerListView + public ContainerListView() { + this.embeddedControlClickedDelegate = new EventHandler(embeddedControlClicked); + + this.embeddedControls = new ObservableList(); + this.embeddedControls.ItemAdded += + new EventHandler>(embeddedControlAdded); + this.embeddedControls.ItemRemoved += + new EventHandler>(embeddedControlRemoved); + this.embeddedControls.Clearing += new EventHandler(embeddedControlsClearing); + + InitializeComponent(); + + // Eliminate flickering + SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + SetStyle(ControlStyles.AllPaintingInWmPaint, true); + + base.View = View.Details; + + this.columnHeaderHeight = Font.Height; + } + + /// Controls being embedded in the ListView + public ICollection EmbeddedControls { + get { return this.embeddedControls; } + } + + /// Updates the controls embeded into the list view + public void UpdateEmbeddedControls() { + if(View != View.Details) { + for(int index = 0; index < this.embeddedControls.Count; ++index) { + this.embeddedControls[index].Control.Visible = false; + } + } else { + for(int index = 0; index < this.embeddedControls.Count; ++index) { + ListViewEmbeddedControl embeddedControl = this.embeddedControls[index]; + + Rectangle cellBounds = this.GetSubItemBounds( + Items[embeddedControl.Row], embeddedControl.Column + ); + + bool intersectsColumnHeader = + (base.HeaderStyle != ColumnHeaderStyle.None) && + (cellBounds.Top < base.Font.Height); + + embeddedControl.Control.Visible = !intersectsColumnHeader; + embeddedControl.Control.Bounds = cellBounds; + } + } + } + + /// Calculates the boundaries of a cell in the list view + /// Item in the list view from which to calculate the cell + /// Index der cell whose boundaries to calculate + /// The boundaries of the specified list view cell + /// + /// When the specified sub item index is not in the range of valid sub items + /// + protected Rectangle GetSubItemBounds(ListViewItem item, int subItem) { + int[] order = GetColumnOrder(); + if(order == null) { // No Columns + return Rectangle.Empty; + } + + if(subItem >= order.Length) { + throw new IndexOutOfRangeException("SubItem " + subItem + " out of range"); + } + + // Determine the border of the entire ListViewItem, including all sub items + Rectangle itemBounds = item.GetBounds(ItemBoundsPortion.Entire); + int subItemX = itemBounds.Left; + + // Find the horizontal position of the sub item. Because the column order can vary, + // we need to use Columns[order[i]] instead of simply doing Columns[i] here! + ColumnHeader columnHeader; + int i; + for(i = 0; i < order.Length; ++i) { + columnHeader = this.Columns[order[i]]; + if(columnHeader.Index == subItem) { + break; + } + + subItemX += columnHeader.Width; + } + + return new Rectangle( + subItemX, itemBounds.Top, this.Columns[order[i]].Width, itemBounds.Height + ); + } + + /// Responds to window messages sent by the operating system + /// Window message that will be processed + protected override void WndProc(ref Message message) { + switch(message.Msg) { + case WM_PAINT: { + UpdateEmbeddedControls(); + break; + } + } + + base.WndProc(ref message); + } + + /// Called when the list of embedded controls has been cleared + /// Collection that has been cleared of its controls + /// Not used + private void embeddedControlsClearing(object sender, EventArgs arguments) { + this.BeginUpdate(); + try { + foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) { + embeddedControl.Control.Click -= this.embeddedControlClickedDelegate; + this.Controls.Remove(embeddedControl.Control); + } + } + finally { + this.EndUpdate(); + } + } + + /// Called when a control gets removed from the embedded controls list + /// List from which the control has been removed + /// + /// Event arguments providing a reference to the removed control + /// + private void embeddedControlAdded( + object sender, ItemEventArgs arguments + ) { + arguments.Item.Control.Click += this.embeddedControlClickedDelegate; + this.Controls.Add(arguments.Item.Control); + } + + /// Called when a control gets added to the embedded controls list + /// List to which the control has been added + /// + /// Event arguments providing a reference to the added control + /// + private void embeddedControlRemoved( + object sender, ItemEventArgs arguments + ) { + if(this.Controls.Contains(arguments.Item.Control)) { + arguments.Item.Control.Click -= this.embeddedControlClickedDelegate; + this.Controls.Remove(arguments.Item.Control); + } + } + + /// Called when an embedded control has been clicked on + /// Embedded control that has been clicked + /// Not used + private void embeddedControlClicked(object sender, EventArgs arguments) { + this.BeginUpdate(); + + try { + SelectedItems.Clear(); + + foreach(ListViewEmbeddedControl embeddedControl in this.embeddedControls) { + if(ReferenceEquals(embeddedControl.Control, sender)) { + if((embeddedControl.Row > 0) && (embeddedControl.Row < Items.Count)) { + Items[embeddedControl.Row].Selected = true; + } + } + } + } + finally { + this.EndUpdate(); + } + } + + /// Obtains the current column order of the list + /// An array indicating the order of the list's columns + private int[] GetColumnOrder() { + int[] order = new int[this.Columns.Count]; + + for(int index = 0; index < this.Columns.Count; ++index) { + order[this.Columns[index].DisplayIndex] = index; + } + + return order; + } + + /// Height of the list view's column header + private int columnHeaderHeight; + /// Event handler for when embedded controls are clicked on + private EventHandler embeddedControlClickedDelegate; + /// Controls being embedded in this ListView + private ObservableList embeddedControls; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/ContainerListView/ListViewEmbeddedControl.cs b/Source/ContainerListView/ListViewEmbeddedControl.cs index 1d0536b..6d7cd91 100644 --- a/Source/ContainerListView/ListViewEmbeddedControl.cs +++ b/Source/ContainerListView/ListViewEmbeddedControl.cs @@ -1,64 +1,63 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms { - - /// Stores informations about an embedded control - public class ListViewEmbeddedControl { - - /// Initializes a new embedded control holder - /// Control being embedded in a list view - /// List row at which the control will be embedded - /// List column at which the control will be embedded - public ListViewEmbeddedControl(Control control, int row, int column) { - this.control = control; - this.row = row; - this.column = column; - } - - /// Control that is being embedded in the ListView - public Control Control { - get { return this.control; } - } - - /// Row the control has been embedded in - public int Row { - get { return this.row; } - } - - /// Column the control has been embedded in - public int Column { - get { return this.column; } - } - - /// Embedded control - private Control control; - /// Row where the control is embedded - private int row; - /// Column where the control is embedded - private int column; - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms { + + /// Stores informations about an embedded control + public class ListViewEmbeddedControl { + + /// Initializes a new embedded control holder + /// Control being embedded in a list view + /// List row at which the control will be embedded + /// List column at which the control will be embedded + public ListViewEmbeddedControl(Control control, int row, int column) { + this.control = control; + this.row = row; + this.column = column; + } + + /// Control that is being embedded in the ListView + public Control Control { + get { return this.control; } + } + + /// Row the control has been embedded in + public int Row { + get { return this.row; } + } + + /// Column the control has been embedded in + public int Column { + get { return this.column; } + } + + /// Embedded control + private Control control; + /// Row where the control is embedded + private int row; + /// Column where the control is embedded + private int column; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/Controls/ProgressSpinner.Designer.cs b/Source/Controls/ProgressSpinner.Designer.cs index f295673..48c4694 100644 --- a/Source/Controls/ProgressSpinner.Designer.cs +++ b/Source/Controls/ProgressSpinner.Designer.cs @@ -1,49 +1,68 @@ -namespace Nuclex.Windows.Forms.Controls { - - partial class ProgressSpinner { - - /// Required designer variable. - private System.ComponentModel.IContainer components = null; - - /// Clean up any resources being used. - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) { - if(disposing && (components != null)) { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() { - this.animationUpdateTimer = new System.Windows.Forms.Timer(); - this.SuspendLayout(); - // - // animationUpdateTimer - // - this.animationUpdateTimer.Tick += new System.EventHandler(this.animationTimerTicked); - // - // ProgressSpinner - // - this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.BackColor = System.Drawing.Color.Transparent; - this.DoubleBuffered = true; - this.Name = "ProgressSpinner"; - this.ResumeLayout(false); - - } - - #endregion - - /// Timer used to update the progress animation - private System.Windows.Forms.Timer animationUpdateTimer; - - } - -} // namespace Nuclex.Windows.Forms.Controls +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +namespace Nuclex.Windows.Forms.Controls { + + partial class ProgressSpinner { + + /// Required designer variable. + private System.ComponentModel.IContainer components = null; + + /// Clean up any resources being used. + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) { + if(disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.animationUpdateTimer = new System.Windows.Forms.Timer(); + this.SuspendLayout(); + // + // animationUpdateTimer + // + this.animationUpdateTimer.Tick += new System.EventHandler(this.animationTimerTicked); + // + // ProgressSpinner + // + this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 20F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.Transparent; + this.DoubleBuffered = true; + this.Name = "ProgressSpinner"; + this.ResumeLayout(false); + + } + + #endregion + + /// Timer used to update the progress animation + private System.Windows.Forms.Timer animationUpdateTimer; + + } + +} // namespace Nuclex.Windows.Forms.Controls diff --git a/Source/Controls/ProgressSpinner.cs b/Source/Controls/ProgressSpinner.cs index 967bc58..eb94280 100644 --- a/Source/Controls/ProgressSpinner.cs +++ b/Source/Controls/ProgressSpinner.cs @@ -1,297 +1,316 @@ -using System; -using System.Data; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Linq; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms.Controls { - - /// Displays a progress spinner to entertain the user while waiting - public partial class ProgressSpinner : UserControl { - - /// Number of dots the progress spinner will display - private const int DotCount = 8; - /// Size of a normal dot (only ever assumed by the trailing dot) - private const int DotRadius = 4; - /// - /// The leading dot will be DotCount times this larger than a normal dot - /// - private const int ScaleFactor = 1; - - /// Initializes a new progress spinner - public ProgressSpinner() { - SetStyle( - ( - ControlStyles.AllPaintingInWmPaint | - ControlStyles.OptimizedDoubleBuffer | - ControlStyles.ResizeRedraw | ControlStyles.UserPaint | - ControlStyles.SupportsTransparentBackColor - ), - true - ); - - InitializeComponent(); - - Disposed += new EventHandler(OnDisposed); - - if(!DesignMode) { - StartSpinner(); - } - } - - /// Releases all resources owned by the control when it is destroyed - /// Control that is being destroyed - /// Not used - private void OnDisposed(object sender, EventArgs arguments) { - if(this.dotOutlinePen != null) { - this.dotOutlinePen.Dispose(); - this.dotOutlinePen = null; - } - if(this.dotFillBrush != null) { - this.dotFillBrush.Dispose(); - this.dotFillBrush = null; - } - } - - /// Starts the spinner's animation - public void StartSpinner() { - this.spinnerRunning = true; - this.animationUpdateTimer.Enabled = true; - } - - /// Stops the spinner's animation - public void StopSpinner() { - this.animationUpdateTimer.Enabled = false; - this.spinnerRunning = false; - } - - /// Color used to fill the dots - public Color DotFillColor { - get { return this.dotFillColor; } - set { - if(value != this.dotFillColor) { - this.dotFillColor = value; - if(this.dotFillBrush != null) { - this.dotFillBrush.Dispose(); - this.dotFillBrush = null; - } - } - } - } - - /// Color used for the dots' outline - public Color DotOutlineColor { - get { return this.dotOutlineColor; } - set { - if(value != this.dotOutlineColor) { - this.dotOutlineColor = value; - if(this.dotOutlinePen != null) { - this.dotOutlinePen.Dispose(); - this.dotOutlinePen = null; - } - } - } - } - - /// Calculates the optimal size for the spinner control - /// The optimal size for the spinner control to have - /// - /// 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. - /// - public Size GetOptimalSize() { - SizeF textRectangle; - using(var dummyImage = new Bitmap(1, 1)) { - using(Graphics graphics = Graphics.FromImage(dummyImage)) { - textRectangle = graphics.MeasureString( - this.statusText, this.statusFont - ); - } - } - - return new Size( - Math.Max(128, (int)(textRectangle.Width + 2.0f)), - this.statusFont.Height + 128 - ); - } - - /// Font that is used to display the status text - public Font StatusFont { - get { return this.statusFont; } - set { this.statusFont = value; } - } - - /// Text that will be displayed as the control's status - public string StatusText { - get { return this.statusText; } - set { this.statusText = value; } - } - - /// Called when the control is hidden or shown - /// Not used - protected override void OnVisibleChanged(EventArgs arguments) { - base.OnVisibleChanged(arguments); - this.animationUpdateTimer.Enabled = this.spinnerRunning && Visible; - } - - /// Called when the control should redraw itself - /// Provides access to the drawing surface and tools - protected override void OnPaint(PaintEventArgs arguments) { - paintControlsBehindMe(arguments); - paintAnimatedDots(arguments); - paintStatusMessage(arguments); - } - - /// Forcefully redraws the controls below this one - /// Provides access to the drawing surface and tools - /// - /// - /// WinForms has very poor transparency support. A transparent control will only - /// be transparent to its immediate parent (so the parent needs to be a container - /// control and hold the transparent control as its preferrably only child). - /// - /// - /// Worse yet, if you manually establish this relationship in your .Designer.cs - /// file, the Visual Studio WinForms designer will dismantle it next time you - /// edit something. This method fixes those issues by repainting all controls - /// that are behind this control and whose bounding box intersect this control. - /// - /// - private void paintControlsBehindMe(PaintEventArgs arguments) { - if(Parent != null && this.BackColor == Color.Transparent) { - using(var bmp = new Bitmap(Parent.Width, Parent.Height)) { - Parent.Controls.Cast() - .Where(c => Parent.Controls.GetChildIndex(c) > Parent.Controls.GetChildIndex(this)) - .Where(c => c.Bounds.IntersectsWith(this.Bounds)) - .OrderByDescending(c => Parent.Controls.GetChildIndex(c)) - .ToList() - .ForEach(c => c.DrawToBitmap(bmp, c.Bounds)); - - arguments.Graphics.DrawImage(bmp, -Left, -Top); - } - } - } - - /// Draws a simple animated dots animation - /// Provides access to the drawing surface and tools - private void paintAnimatedDots(PaintEventArgs arguments) { - if(this.dotOutlinePen == null) { - this.dotOutlinePen = new Pen(this.dotOutlineColor); - } - if(this.dotFillBrush == null) { - this.dotFillBrush = new SolidBrush(this.dotFillColor); - } - - SmoothingMode prevousSmoothingMode = arguments.Graphics.SmoothingMode; - arguments.Graphics.SmoothingMode = SmoothingMode.HighQuality; - try { - PointF center = new PointF(Width / 2.0f, (Height - this.statusFont.Height - 2) / 2.0f); - - int diameter = Math.Min(Width, Height - this.statusFont.Height - 2); - int bigRadius = diameter / 2 - DotRadius - (DotCount - 1) * ScaleFactor; - - // Draw the dots - float unitAngle = 360.0f / DotCount; - for(int index = 0; index < DotCount; ++index) { - int dotIndex = (index + leadingDotIndex) % DotCount; - - var dotPosition = new PointF( - center.X + (float)(bigRadius * Math.Cos(unitAngle * dotIndex * Math.PI / 180.0f)), - center.Y + (float)(bigRadius * Math.Sin(unitAngle * dotIndex * Math.PI / 180.0f)) - ); - - int currentDotRadius = DotRadius + index * ScaleFactor; - - var corner = new PointF( - dotPosition.X - currentDotRadius, dotPosition.Y - currentDotRadius - ); - arguments.Graphics.FillEllipse( - this.dotFillBrush, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius - ); - arguments.Graphics.DrawEllipse( - this.dotOutlinePen, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius - ); - } - } - finally { - arguments.Graphics.SmoothingMode = prevousSmoothingMode; - } - } - - /// Draws the status message under the animated dots - /// Provides access to the drawing surface and tools - private void paintStatusMessage(PaintEventArgs arguments) { - if(!string.IsNullOrEmpty(this.statusText)) { - SizeF textRectangle = arguments.Graphics.MeasureString( - this.statusText, this.statusFont - ); - - var messageArea = new RectangleF( - (Width - textRectangle.Width) / 2.0f, - Height - this.statusFont.Height - 1.0f, - textRectangle.Width, - this.statusFont.Height - ); - - // Draw text with a white halo. This is a little bit ugly... - { - messageArea.Offset(-1.0f, 0.0f); - arguments.Graphics.DrawString( - this.statusText, this.statusFont, Brushes.White, messageArea - ); - - messageArea.Offset(2.0f, 0.0f); - arguments.Graphics.DrawString( - this.statusText, this.statusFont, Brushes.White, messageArea - ); - - messageArea.Offset(-1.0f, -1.0f); - arguments.Graphics.DrawString( - this.statusText, this.statusFont, Brushes.White, messageArea - ); - - messageArea.Offset(0.0f, 2.0f); - arguments.Graphics.DrawString( - this.statusText, this.statusFont, Brushes.White, messageArea - ); - - messageArea.Offset(0.0f, -1.0f); - arguments.Graphics.DrawString( - this.statusText, this.statusFont, this.dotFillBrush, messageArea - ); - } - } - } - - - /// Called when the animation timer ticks to update the animation state - /// Animation timer that has ticked - /// Not used - 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 - } - - /// Whether the spinner has been started - private bool spinnerRunning; - /// Index of the currently leading dot - private int leadingDotIndex = 0; - /// Text that will be displayed under the control as the current status - private string statusText; - - /// Color in which the dots will be filled - private Color dotFillColor = Color.RoyalBlue; - /// Color that will be used for the dots' outline - private Color dotOutlineColor = Color.White; - /// Brush used to fill the dots - private Brush dotFillBrush; - /// Brush used for the dots' outline - private Pen dotOutlinePen; - /// Font that is used to display the status text - private Font statusFont = SystemFonts.SmallCaptionFont; - - } - -} // namespace Nuclex.Windows.Forms.Controls +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Data; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Linq; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms.Controls { + + /// Displays a progress spinner to entertain the user while waiting + public partial class ProgressSpinner : UserControl { + + /// Number of dots the progress spinner will display + private const int DotCount = 8; + /// Size of a normal dot (only ever assumed by the trailing dot) + private const int DotRadius = 4; + /// + /// The leading dot will be DotCount times this larger than a normal dot + /// + private const int ScaleFactor = 1; + + /// Initializes a new progress spinner + public ProgressSpinner() { + SetStyle( + ( + ControlStyles.AllPaintingInWmPaint | + ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | ControlStyles.UserPaint | + ControlStyles.SupportsTransparentBackColor + ), + true + ); + + InitializeComponent(); + + Disposed += new EventHandler(OnDisposed); + + if(!DesignMode) { + StartSpinner(); + } + } + + /// Releases all resources owned by the control when it is destroyed + /// Control that is being destroyed + /// Not used + private void OnDisposed(object sender, EventArgs arguments) { + if(this.dotOutlinePen != null) { + this.dotOutlinePen.Dispose(); + this.dotOutlinePen = null; + } + if(this.dotFillBrush != null) { + this.dotFillBrush.Dispose(); + this.dotFillBrush = null; + } + } + + /// Starts the spinner's animation + public void StartSpinner() { + this.spinnerRunning = true; + this.animationUpdateTimer.Enabled = true; + } + + /// Stops the spinner's animation + public void StopSpinner() { + this.animationUpdateTimer.Enabled = false; + this.spinnerRunning = false; + } + + /// Color used to fill the dots + public Color DotFillColor { + get { return this.dotFillColor; } + set { + if(value != this.dotFillColor) { + this.dotFillColor = value; + if(this.dotFillBrush != null) { + this.dotFillBrush.Dispose(); + this.dotFillBrush = null; + } + } + } + } + + /// Color used for the dots' outline + public Color DotOutlineColor { + get { return this.dotOutlineColor; } + set { + if(value != this.dotOutlineColor) { + this.dotOutlineColor = value; + if(this.dotOutlinePen != null) { + this.dotOutlinePen.Dispose(); + this.dotOutlinePen = null; + } + } + } + } + + /// Calculates the optimal size for the spinner control + /// The optimal size for the spinner control to have + /// + /// 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. + /// + public Size GetOptimalSize() { + SizeF textRectangle; + using(var dummyImage = new Bitmap(1, 1)) { + using(Graphics graphics = Graphics.FromImage(dummyImage)) { + textRectangle = graphics.MeasureString( + this.statusText, this.statusFont + ); + } + } + + return new Size( + Math.Max(128, (int)(textRectangle.Width + 2.0f)), + this.statusFont.Height + 128 + ); + } + + /// Font that is used to display the status text + public Font StatusFont { + get { return this.statusFont; } + set { this.statusFont = value; } + } + + /// Text that will be displayed as the control's status + public string StatusText { + get { return this.statusText; } + set { this.statusText = value; } + } + + /// Called when the control is hidden or shown + /// Not used + protected override void OnVisibleChanged(EventArgs arguments) { + base.OnVisibleChanged(arguments); + this.animationUpdateTimer.Enabled = this.spinnerRunning && Visible; + } + + /// Called when the control should redraw itself + /// Provides access to the drawing surface and tools + protected override void OnPaint(PaintEventArgs arguments) { + paintControlsBehindMe(arguments); + paintAnimatedDots(arguments); + paintStatusMessage(arguments); + } + + /// Forcefully redraws the controls below this one + /// Provides access to the drawing surface and tools + /// + /// + /// WinForms has very poor transparency support. A transparent control will only + /// be transparent to its immediate parent (so the parent needs to be a container + /// control and hold the transparent control as its preferrably only child). + /// + /// + /// Worse yet, if you manually establish this relationship in your .Designer.cs + /// file, the Visual Studio WinForms designer will dismantle it next time you + /// edit something. This method fixes those issues by repainting all controls + /// that are behind this control and whose bounding box intersect this control. + /// + /// + private void paintControlsBehindMe(PaintEventArgs arguments) { + if(Parent != null && this.BackColor == Color.Transparent) { + using(var bmp = new Bitmap(Parent.Width, Parent.Height)) { + Parent.Controls.Cast() + .Where(c => Parent.Controls.GetChildIndex(c) > Parent.Controls.GetChildIndex(this)) + .Where(c => c.Bounds.IntersectsWith(this.Bounds)) + .OrderByDescending(c => Parent.Controls.GetChildIndex(c)) + .ToList() + .ForEach(c => c.DrawToBitmap(bmp, c.Bounds)); + + arguments.Graphics.DrawImage(bmp, -Left, -Top); + } + } + } + + /// Draws a simple animated dots animation + /// Provides access to the drawing surface and tools + private void paintAnimatedDots(PaintEventArgs arguments) { + if(this.dotOutlinePen == null) { + this.dotOutlinePen = new Pen(this.dotOutlineColor); + } + if(this.dotFillBrush == null) { + this.dotFillBrush = new SolidBrush(this.dotFillColor); + } + + SmoothingMode prevousSmoothingMode = arguments.Graphics.SmoothingMode; + arguments.Graphics.SmoothingMode = SmoothingMode.HighQuality; + try { + PointF center = new PointF(Width / 2.0f, (Height - this.statusFont.Height - 2) / 2.0f); + + int diameter = Math.Min(Width, Height - this.statusFont.Height - 2); + int bigRadius = diameter / 2 - DotRadius - (DotCount - 1) * ScaleFactor; + + // Draw the dots + float unitAngle = 360.0f / DotCount; + for(int index = 0; index < DotCount; ++index) { + int dotIndex = (index + leadingDotIndex) % DotCount; + + var dotPosition = new PointF( + center.X + (float)(bigRadius * Math.Cos(unitAngle * dotIndex * Math.PI / 180.0f)), + center.Y + (float)(bigRadius * Math.Sin(unitAngle * dotIndex * Math.PI / 180.0f)) + ); + + int currentDotRadius = DotRadius + index * ScaleFactor; + + var corner = new PointF( + dotPosition.X - currentDotRadius, dotPosition.Y - currentDotRadius + ); + arguments.Graphics.FillEllipse( + this.dotFillBrush, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius + ); + arguments.Graphics.DrawEllipse( + this.dotOutlinePen, corner.X, corner.Y, 2 * currentDotRadius, 2 * currentDotRadius + ); + } + } + finally { + arguments.Graphics.SmoothingMode = prevousSmoothingMode; + } + } + + /// Draws the status message under the animated dots + /// Provides access to the drawing surface and tools + private void paintStatusMessage(PaintEventArgs arguments) { + if(!string.IsNullOrEmpty(this.statusText)) { + SizeF textRectangle = arguments.Graphics.MeasureString( + this.statusText, this.statusFont + ); + + var messageArea = new RectangleF( + (Width - textRectangle.Width) / 2.0f, + Height - this.statusFont.Height - 1.0f, + textRectangle.Width, + this.statusFont.Height + ); + + // Draw text with a white halo. This is a little bit ugly... + { + messageArea.Offset(-1.0f, 0.0f); + arguments.Graphics.DrawString( + this.statusText, this.statusFont, Brushes.White, messageArea + ); + + messageArea.Offset(2.0f, 0.0f); + arguments.Graphics.DrawString( + this.statusText, this.statusFont, Brushes.White, messageArea + ); + + messageArea.Offset(-1.0f, -1.0f); + arguments.Graphics.DrawString( + this.statusText, this.statusFont, Brushes.White, messageArea + ); + + messageArea.Offset(0.0f, 2.0f); + arguments.Graphics.DrawString( + this.statusText, this.statusFont, Brushes.White, messageArea + ); + + messageArea.Offset(0.0f, -1.0f); + arguments.Graphics.DrawString( + this.statusText, this.statusFont, this.dotFillBrush, messageArea + ); + } + } + } + + + /// Called when the animation timer ticks to update the animation state + /// Animation timer that has ticked + /// Not used + 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 + } + + /// Whether the spinner has been started + private bool spinnerRunning; + /// Index of the currently leading dot + private int leadingDotIndex = 0; + /// Text that will be displayed under the control as the current status + private string statusText; + + /// Color in which the dots will be filled + private Color dotFillColor = Color.RoyalBlue; + /// Color that will be used for the dots' outline + private Color dotOutlineColor = Color.White; + /// Brush used to fill the dots + private Brush dotFillBrush; + /// Brush used for the dots' outline + private Pen dotOutlinePen; + /// Font that is used to display the status text + private Font statusFont = SystemFonts.SmallCaptionFont; + + } + +} // namespace Nuclex.Windows.Forms.Controls diff --git a/Source/IActiveWindowTracker.cs b/Source/IActiveWindowTracker.cs index aac4abe..a715709 100644 --- a/Source/IActiveWindowTracker.cs +++ b/Source/IActiveWindowTracker.cs @@ -1,38 +1,37 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms { - - /// Enables consumer to look up the currently active window - public interface IActiveWindowTracker { - - /// The currently active top-level or modal window - /// - /// If windows live in multiple threads, the property change notification for - /// this property, if supported, might be fired from a different thread. - /// - Form ActiveWindow { get; } - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms { + + /// Enables consumer to look up the currently active window + public interface IActiveWindowTracker { + + /// The currently active top-level or modal window + /// + /// If windows live in multiple threads, the property change notification for + /// this property, if supported, might be fired from a different thread. + /// + Form ActiveWindow { get; } + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/IWindowManager.cs b/Source/IWindowManager.cs index 31e75bd..4b08479 100644 --- a/Source/IWindowManager.cs +++ b/Source/IWindowManager.cs @@ -1,93 +1,92 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms { - - /// Interface for a window manager used in an MVVM environment - public interface IWindowManager : IActiveWindowTracker { - - /// Opens a view as a new root window of the application - /// - /// Type of view model a root window will be opened for - /// - /// - /// 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) - /// - /// - /// Whether the view model should be disposed when the view is closed - /// - /// The window that has been opened by the window manager - Form OpenRoot( - TViewModel viewModel = null, bool disposeOnClose = true - ) where TViewModel : class; - - /// Displays a view as a modal window - /// - /// Type of the view model for which a view will be displayed - /// - /// - /// 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) - /// - /// - /// Whether the view model should be disposed when the view is closed - /// - /// The return value of the modal window - bool? ShowModal( - TViewModel viewModel = null, bool disposeOnClose = true - ) where TViewModel : class; - - /// Creates the view for the specified view model - /// - /// Type of view model for which a view will be created - /// - /// - /// 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) - /// - /// The view for the specified view model - Control CreateView(TViewModel viewModel = null) - where TViewModel : class; - - /// Creates a view model without a matching view - /// Type of view model that will be created - /// The new view model - /// - /// - /// This is useful if a view model needs to create child view models (i.e. paged container - /// and wants to ensure the same dependency injector (if any) if used as the window - /// manager uses for other view models it creates. - /// - /// - /// This way, view models can set up their child view models without having to immediately - /// bind a view to them. Later on, views can use the window manager to create a matching - /// child view and store it in a container. - /// - /// - TViewModel CreateViewModel() - where TViewModel : class; - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms { + + /// Interface for a window manager used in an MVVM environment + public interface IWindowManager : IActiveWindowTracker { + + /// Opens a view as a new root window of the application + /// + /// Type of view model a root window will be opened for + /// + /// + /// 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) + /// + /// + /// Whether the view model should be disposed when the view is closed + /// + /// The window that has been opened by the window manager + Form OpenRoot( + TViewModel viewModel = null, bool disposeOnClose = true + ) where TViewModel : class; + + /// Displays a view as a modal window + /// + /// Type of the view model for which a view will be displayed + /// + /// + /// 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) + /// + /// + /// Whether the view model should be disposed when the view is closed + /// + /// The return value of the modal window + bool? ShowModal( + TViewModel viewModel = null, bool disposeOnClose = true + ) where TViewModel : class; + + /// Creates the view for the specified view model + /// + /// Type of view model for which a view will be created + /// + /// + /// 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) + /// + /// The view for the specified view model + Control CreateView(TViewModel viewModel = null) + where TViewModel : class; + + /// Creates a view model without a matching view + /// Type of view model that will be created + /// The new view model + /// + /// + /// This is useful if a view model needs to create child view models (i.e. paged container + /// and wants to ensure the same dependency injector (if any) if used as the window + /// manager uses for other view models it creates. + /// + /// + /// This way, view models can set up their child view models without having to immediately + /// bind a view to them. Later on, views can use the window manager to create a matching + /// child view and store it in a container. + /// + /// + TViewModel CreateViewModel() + where TViewModel : class; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/LateCheckedSynchronizer.cs b/Source/LateCheckedSynchronizer.cs index 65bbaf2..af371cf 100644 --- a/Source/LateCheckedSynchronizer.cs +++ b/Source/LateCheckedSynchronizer.cs @@ -1,157 +1,156 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.ComponentModel; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms { - - /// - /// Proxy stand-in to delay checking for the main window until it has been created - /// - /// - /// - /// The issue: when the view model for the main window is created, the main window - /// may only exist as a .NET object, without the underlying operating system window - /// (done in checkable via - /// ). Not only will things - /// like fail, we can't - /// even locate the main window at that stage. - /// - /// - /// Thus, if the main window cannot be found at the time a view model is created, - /// this late-checking synchronizer will jump into its place and re-check for - /// the main window only when something needs to be executed in the UI thread. - /// - /// - class LateCheckedSynchronizer : ISynchronizeInvoke { - - /// Initializes a new late-checked main window synchronizer - /// - public LateCheckedSynchronizer(Action uiContextFoundCallback) { - this.uiContextFoundCallback = uiContextFoundCallback; - } - - /// Finds the application's main window - /// Main window of the application or null if none has been created - /// - /// The application's main window, if it has been created yet - /// - public static Form GetMainWindow() { - IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle; - - // We can get two things: a list of all open windows and the handle of - // the window that the process has registered as main window. Use the latter - // to pick the correct window from the former. - FormCollection openForms = Application.OpenForms; - int openFormCount = openForms.Count; - for(int index = 0; index < openFormCount; ++index) { - Form form = openForms[index]; - - IntPtr handle; - if(form.InvokeRequired) { - handle = (IntPtr)form.Invoke(new Func(getWindowHandle), form); - } else { - handle = getWindowHandle(form); - } - if(handle != IntPtr.Zero) { - if(handle == mainWindowHandle) { - return form; - } - } - } - - // No matching main window found: use the first one in good faith or fail. - if(openFormCount > 0) { - return openForms[0]; - } else { - return null; - } - } - - /// Checks whether the calling thread needs to use Invoke() - public bool InvokeRequired { - get { return getMainWindowOrFail().InvokeRequired; } - } - - /// Schedules a method to be run by the main UI thread - /// Method that will be scheduled to run - /// Arguments that will be passed to the method - /// An asynchronous result handle that can be used to track the call - public IAsyncResult BeginInvoke(Delegate method, object[] args) { - return getMainWindowOrFail().BeginInvoke(method, args); - } - - /// Waits for a call scheduled on the main UI thread to complete - /// Asynchronous result handle returned by BeginInvoke() - /// The value returned by the method ran in the main UI thread - public object EndInvoke(IAsyncResult result) { - return getMainWindowOrFail().EndInvoke(result); - } - - /// Executes a method on the main UI thread and waits for it to complete - /// Method that will be run by the main UI thread - /// Arguments that will be passed to the method - /// The value returned by the method - public object Invoke(Delegate method, object[] arguments) { - return getMainWindowOrFail().Invoke(method, arguments); - } - - /// Retrieves the application's current main window - /// The application's current main window - /// - /// If there is no main window, an exception will be thrown - /// - private Form getMainWindowOrFail() { - Form mainWindow = GetMainWindow(); - if(mainWindow == null) { - throw new InvalidOperationException( - "Could not schedule work for the UI thread because no WinForms UI main window " + - "was found. Create a main window first or specify the UI synchronization context " + - "explicitly to the view model." - ); - } - - if(this.uiContextFoundCallback != null) { - this.uiContextFoundCallback(mainWindow); - this.uiContextFoundCallback = null; - } - - return mainWindow; - } - - /// Returns a Form's window handle without forcing its creation - /// Form whose window handle will be returned - /// The form's window handle of IntPtr.Zero if it has none - private static IntPtr getWindowHandle(Form form) { - if(form.IsHandleCreated) { - return form.Handle; - } else { - return IntPtr.Zero; - } - } - - /// Called when the late-checked synchronizer finds the main window - private Action uiContextFoundCallback; - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.ComponentModel; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms { + + /// + /// Proxy stand-in to delay checking for the main window until it has been created + /// + /// + /// + /// The issue: when the view model for the main window is created, the main window + /// may only exist as a .NET object, without the underlying operating system window + /// (done in checkable via + /// ). Not only will things + /// like fail, we can't + /// even locate the main window at that stage. + /// + /// + /// Thus, if the main window cannot be found at the time a view model is created, + /// this late-checking synchronizer will jump into its place and re-check for + /// the main window only when something needs to be executed in the UI thread. + /// + /// + class LateCheckedSynchronizer : ISynchronizeInvoke { + + /// Initializes a new late-checked main window synchronizer + /// + public LateCheckedSynchronizer(Action uiContextFoundCallback) { + this.uiContextFoundCallback = uiContextFoundCallback; + } + + /// Finds the application's main window + /// Main window of the application or null if none has been created + /// + /// The application's main window, if it has been created yet + /// + public static Form GetMainWindow() { + IntPtr mainWindowHandle = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle; + + // We can get two things: a list of all open windows and the handle of + // the window that the process has registered as main window. Use the latter + // to pick the correct window from the former. + FormCollection openForms = Application.OpenForms; + int openFormCount = openForms.Count; + for(int index = 0; index < openFormCount; ++index) { + Form form = openForms[index]; + + IntPtr handle; + if(form.InvokeRequired) { + handle = (IntPtr)form.Invoke(new Func(getWindowHandle), form); + } else { + handle = getWindowHandle(form); + } + if(handle != IntPtr.Zero) { + if(handle == mainWindowHandle) { + return form; + } + } + } + + // No matching main window found: use the first one in good faith or fail. + if(openFormCount > 0) { + return openForms[0]; + } else { + return null; + } + } + + /// Checks whether the calling thread needs to use Invoke() + public bool InvokeRequired { + get { return getMainWindowOrFail().InvokeRequired; } + } + + /// Schedules a method to be run by the main UI thread + /// Method that will be scheduled to run + /// Arguments that will be passed to the method + /// An asynchronous result handle that can be used to track the call + public IAsyncResult BeginInvoke(Delegate method, object[] args) { + return getMainWindowOrFail().BeginInvoke(method, args); + } + + /// Waits for a call scheduled on the main UI thread to complete + /// Asynchronous result handle returned by BeginInvoke() + /// The value returned by the method ran in the main UI thread + public object EndInvoke(IAsyncResult result) { + return getMainWindowOrFail().EndInvoke(result); + } + + /// Executes a method on the main UI thread and waits for it to complete + /// Method that will be run by the main UI thread + /// Arguments that will be passed to the method + /// The value returned by the method + public object Invoke(Delegate method, object[] arguments) { + return getMainWindowOrFail().Invoke(method, arguments); + } + + /// Retrieves the application's current main window + /// The application's current main window + /// + /// If there is no main window, an exception will be thrown + /// + private Form getMainWindowOrFail() { + Form mainWindow = GetMainWindow(); + if(mainWindow == null) { + throw new InvalidOperationException( + "Could not schedule work for the UI thread because no WinForms UI main window " + + "was found. Create a main window first or specify the UI synchronization context " + + "explicitly to the view model." + ); + } + + if(this.uiContextFoundCallback != null) { + this.uiContextFoundCallback(mainWindow); + this.uiContextFoundCallback = null; + } + + return mainWindow; + } + + /// Returns a Form's window handle without forcing its creation + /// Form whose window handle will be returned + /// The form's window handle of IntPtr.Zero if it has none + private static IntPtr getWindowHandle(Form form) { + if(form.IsHandleCreated) { + return form.Handle; + } else { + return IntPtr.Zero; + } + } + + /// Called when the late-checked synchronizer finds the main window + private Action uiContextFoundCallback; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/Messages/IMessageService.cs b/Source/Messages/IMessageService.cs index 1ac53c1..36d0f6b 100644 --- a/Source/Messages/IMessageService.cs +++ b/Source/Messages/IMessageService.cs @@ -1,55 +1,54 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms.Messages { - - /// Performs simple user interaction - /// - /// 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. - /// - public interface IMessageService { - - /// Triggered when a message is about to be displayed to the user - event EventHandler MessageDisplaying; - - /// Triggered when the user has acknowledged the current message - event EventHandler MessageAcknowledged; - - /// Asks the user a question that can be answered via several buttons - /// Image that will be shown on the message box - /// Text that will be shown to the user - /// Buttons available for the user to click on - /// The button the user has clicked on - DialogResult ShowQuestion( - MessageBoxIcon image, MessageText text, MessageBoxButtons buttons - ); - - /// Displays a notification to the user - /// Image that will be shown on the message bx - /// Text that will be shown to the user - void ShowNotification(MessageBoxIcon image, MessageText text); - - } - -} // namespace Nuclex.Windows.Forms.Messages +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms.Messages { + + /// Performs simple user interaction + /// + /// 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. + /// + public interface IMessageService { + + /// Triggered when a message is about to be displayed to the user + event EventHandler MessageDisplaying; + + /// Triggered when the user has acknowledged the current message + event EventHandler MessageAcknowledged; + + /// Asks the user a question that can be answered via several buttons + /// Image that will be shown on the message box + /// Text that will be shown to the user + /// Buttons available for the user to click on + /// The button the user has clicked on + DialogResult ShowQuestion( + MessageBoxIcon image, MessageText text, MessageBoxButtons buttons + ); + + /// Displays a notification to the user + /// Image that will be shown on the message bx + /// Text that will be shown to the user + void ShowNotification(MessageBoxIcon image, MessageText text); + + } + +} // namespace Nuclex.Windows.Forms.Messages diff --git a/Source/Messages/MessageEventArgs.Test.cs b/Source/Messages/MessageEventArgs.Test.cs index 9ef7ba3..f25895d 100644 --- a/Source/Messages/MessageEventArgs.Test.cs +++ b/Source/Messages/MessageEventArgs.Test.cs @@ -1,49 +1,48 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -using NUnit.Framework; - -namespace Nuclex.Windows.Forms.Messages { - - /// Unit tests for the message box event argument container - [TestFixture] - internal class MessageEventArgsTest { - - /// Verifies that the image associated with the message gets stored - [Test] - public void ImageIsStored() { - var arguments = new MessageEventArgs(MessageBoxIcon.Exclamation, null); - Assert.AreEqual(MessageBoxIcon.Exclamation, arguments.Image); - } - - /// Verifies that the text associated with the message gets stored - [Test] - public void TextIsStored() { - var text = new MessageText(); - var arguments = new MessageEventArgs(MessageBoxIcon.None, text); - Assert.AreSame(text, arguments.Text); - } - - } - -} // namespace Nuclex.Windows.Forms.Messages +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +using NUnit.Framework; + +namespace Nuclex.Windows.Forms.Messages { + + /// Unit tests for the message box event argument container + [TestFixture] + internal class MessageEventArgsTest { + + /// Verifies that the image associated with the message gets stored + [Test] + public void ImageIsStored() { + var arguments = new MessageEventArgs(MessageBoxIcon.Exclamation, null); + Assert.AreEqual(MessageBoxIcon.Exclamation, arguments.Image); + } + + /// Verifies that the text associated with the message gets stored + [Test] + public void TextIsStored() { + var text = new MessageText(); + var arguments = new MessageEventArgs(MessageBoxIcon.None, text); + Assert.AreSame(text, arguments.Text); + } + + } + +} // namespace Nuclex.Windows.Forms.Messages diff --git a/Source/Messages/MessageEventArgs.cs b/Source/Messages/MessageEventArgs.cs index 0fba56e..74dbe4b 100644 --- a/Source/Messages/MessageEventArgs.cs +++ b/Source/Messages/MessageEventArgs.cs @@ -1,54 +1,53 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms.Messages { - - /// Provides a displayed message and its severity to event subscribers - public class MessageEventArgs : EventArgs { - - /// Initializes a new message box event argument container - /// Image the message box will be displaying - /// Text that will be displayed in the message box - public MessageEventArgs(MessageBoxIcon image, MessageText text) { - this.image = image; - this.text = text; - } - - /// Image that indicates the severity of the message being displayed - public MessageBoxIcon Image { - get { return this.image; } - } - - /// Text that is being displayed in the message box - public MessageText Text { - get { return this.text; } - } - - /// Image that indicates the severity of the message being displayed - private MessageBoxIcon image; - /// Text that is being displayed in the message box - private MessageText text; - - } - -} // namespace Nuclex.Windows.Forms.Messages +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms.Messages { + + /// Provides a displayed message and its severity to event subscribers + public class MessageEventArgs : EventArgs { + + /// Initializes a new message box event argument container + /// Image the message box will be displaying + /// Text that will be displayed in the message box + public MessageEventArgs(MessageBoxIcon image, MessageText text) { + this.image = image; + this.text = text; + } + + /// Image that indicates the severity of the message being displayed + public MessageBoxIcon Image { + get { return this.image; } + } + + /// Text that is being displayed in the message box + public MessageText Text { + get { return this.text; } + } + + /// Image that indicates the severity of the message being displayed + private MessageBoxIcon image; + /// Text that is being displayed in the message box + private MessageText text; + + } + +} // namespace Nuclex.Windows.Forms.Messages diff --git a/Source/Messages/MessageServiceHelper.cs b/Source/Messages/MessageServiceHelper.cs index 526e9b9..aa5fc05 100644 --- a/Source/Messages/MessageServiceHelper.cs +++ b/Source/Messages/MessageServiceHelper.cs @@ -1,108 +1,107 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms.Messages { - - /// Contains helper methods for the message service - public static class MessageServiceHelper { - - /// Asks the user a question that can be answered with yes or no - /// - /// Message service that will be used to display the question - /// - /// Text that will be shown on the message box - /// The button the user has clicked on - public static DialogResult AskYesNo( - this IMessageService messageService, MessageText text - ) { - return messageService.ShowQuestion( - MessageBoxIcon.Question, text, MessageBoxButtons.YesNo - ); - } - - /// Asks the user a question that can be answered with ok or cancel - /// - /// Message service that will be used to display the question - /// - /// Text that will be shown on the message box - /// The button the user has clicked on - public static DialogResult AskOkCancel( - this IMessageService messageService, MessageText text - ) { - return messageService.ShowQuestion( - MessageBoxIcon.Question, text, MessageBoxButtons.OKCancel - ); - } - - /// - /// Asks the user a question that can be answered with yes, no or cancel - /// - /// - /// Message service that will be used to display the question - /// - /// Text that will be shown on the message box - /// The button the user has clicked on - public static DialogResult AskYesNoCancel( - this IMessageService messageService, MessageText text - ) { - return messageService.ShowQuestion( - MessageBoxIcon.Question, text, MessageBoxButtons.YesNoCancel - ); - } - - /// Displays an informative message - /// - /// Message service that will be used to display the warning - /// - /// Text to be displayed on the warning message - public static void Inform( - this IMessageService messageService, MessageText text - ) { - messageService.ShowNotification(MessageBoxIcon.Information, text); - } - - /// Displays a warning - /// - /// Message service that will be used to display the warning - /// - /// Text to be displayed on the warning message - public static void Warn( - this IMessageService messageService, MessageText text - ) { - messageService.ShowNotification(MessageBoxIcon.Warning, text); - } - - /// Reports an error - /// - /// Message service that will be used to display the warning - /// - /// Text to be displayed on the warning message - public static void ReportError( - this IMessageService messageService, MessageText text - ) { - messageService.ShowNotification(MessageBoxIcon.Error, text); - } - - } - -} // namespace Nuclex.Windows.Forms.Messages +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms.Messages { + + /// Contains helper methods for the message service + public static class MessageServiceHelper { + + /// Asks the user a question that can be answered with yes or no + /// + /// Message service that will be used to display the question + /// + /// Text that will be shown on the message box + /// The button the user has clicked on + public static DialogResult AskYesNo( + this IMessageService messageService, MessageText text + ) { + return messageService.ShowQuestion( + MessageBoxIcon.Question, text, MessageBoxButtons.YesNo + ); + } + + /// Asks the user a question that can be answered with ok or cancel + /// + /// Message service that will be used to display the question + /// + /// Text that will be shown on the message box + /// The button the user has clicked on + public static DialogResult AskOkCancel( + this IMessageService messageService, MessageText text + ) { + return messageService.ShowQuestion( + MessageBoxIcon.Question, text, MessageBoxButtons.OKCancel + ); + } + + /// + /// Asks the user a question that can be answered with yes, no or cancel + /// + /// + /// Message service that will be used to display the question + /// + /// Text that will be shown on the message box + /// The button the user has clicked on + public static DialogResult AskYesNoCancel( + this IMessageService messageService, MessageText text + ) { + return messageService.ShowQuestion( + MessageBoxIcon.Question, text, MessageBoxButtons.YesNoCancel + ); + } + + /// Displays an informative message + /// + /// Message service that will be used to display the warning + /// + /// Text to be displayed on the warning message + public static void Inform( + this IMessageService messageService, MessageText text + ) { + messageService.ShowNotification(MessageBoxIcon.Information, text); + } + + /// Displays a warning + /// + /// Message service that will be used to display the warning + /// + /// Text to be displayed on the warning message + public static void Warn( + this IMessageService messageService, MessageText text + ) { + messageService.ShowNotification(MessageBoxIcon.Warning, text); + } + + /// Reports an error + /// + /// Message service that will be used to display the warning + /// + /// Text to be displayed on the warning message + public static void ReportError( + this IMessageService messageService, MessageText text + ) { + messageService.ShowNotification(MessageBoxIcon.Error, text); + } + + } + +} // namespace Nuclex.Windows.Forms.Messages diff --git a/Source/Messages/MessageText.Test.cs b/Source/Messages/MessageText.Test.cs index 767b1f8..0c225eb 100644 --- a/Source/Messages/MessageText.Test.cs +++ b/Source/Messages/MessageText.Test.cs @@ -1,51 +1,50 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows; - -using NUnit.Framework; - -namespace Nuclex.Windows.Forms.Messages { - - /// Unit tests for the message text container - [TestFixture] - internal class MessageTextTest { - - /// Ensures that the message text class provides a copy constructor - [Test] - public void HasCopyConstructor() { - var text = new MessageText() { - Caption = "Caption", - Message = "Message", - Details = "Details", - ExpandedDetails = "ExpandedDetails" - }; - var copy = new MessageText(text); - - Assert.AreEqual(text.Caption, copy.Caption); - Assert.AreEqual(text.Message, copy.Message); - Assert.AreEqual(text.Details, copy.Details); - Assert.AreEqual(text.ExpandedDetails, copy.ExpandedDetails); - } - - } - -} // namespace Nuclex.Windows.Forms.Messages +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows; + +using NUnit.Framework; + +namespace Nuclex.Windows.Forms.Messages { + + /// Unit tests for the message text container + [TestFixture] + internal class MessageTextTest { + + /// Ensures that the message text class provides a copy constructor + [Test] + public void HasCopyConstructor() { + var text = new MessageText() { + Caption = "Caption", + Message = "Message", + Details = "Details", + ExpandedDetails = "ExpandedDetails" + }; + var copy = new MessageText(text); + + Assert.AreEqual(text.Caption, copy.Caption); + Assert.AreEqual(text.Message, copy.Message); + Assert.AreEqual(text.Details, copy.Details); + Assert.AreEqual(text.ExpandedDetails, copy.ExpandedDetails); + } + + } + +} // namespace Nuclex.Windows.Forms.Messages diff --git a/Source/Messages/MessageText.cs b/Source/Messages/MessageText.cs index dea8405..56d4119 100644 --- a/Source/Messages/MessageText.cs +++ b/Source/Messages/MessageText.cs @@ -1,55 +1,54 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Windows.Forms.Messages { - - /// Text that will be displayed in a message box - public class MessageText { - - /// Initializs a new message text - public MessageText() { } - - /// Initializes a new message text by copying another instance - /// Instance that will be copied - public MessageText(MessageText other) { - Caption = other.Caption; - Message = other.Message; - Details = other.Details; - ExpandedDetails = other.ExpandedDetails; - } - - /// The caption used when the is displayed in a message box - public string Caption { get; set; } - /// Main message being displayed to the user - public string Message { get; set; } - /// Message details shown below the main message - public string Details { get; set; } - /// - /// Additional informations the user can display by expanding - /// the message dialog. Can be null, in which case the message dialog - /// will not be expandable. - /// - public string ExpandedDetails { get; set; } - - } - -} // namespace Nuclex.Windows.Forms.Messages +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Windows.Forms.Messages { + + /// Text that will be displayed in a message box + public class MessageText { + + /// Initializs a new message text + public MessageText() { } + + /// Initializes a new message text by copying another instance + /// Instance that will be copied + public MessageText(MessageText other) { + Caption = other.Caption; + Message = other.Message; + Details = other.Details; + ExpandedDetails = other.ExpandedDetails; + } + + /// The caption used when the is displayed in a message box + public string Caption { get; set; } + /// Main message being displayed to the user + public string Message { get; set; } + /// Message details shown below the main message + public string Details { get; set; } + /// + /// Additional informations the user can display by expanding + /// the message dialog. Can be null, in which case the message dialog + /// will not be expandable. + /// + public string ExpandedDetails { get; set; } + + } + +} // namespace Nuclex.Windows.Forms.Messages diff --git a/Source/Messages/StandardMessageBoxManager.cs b/Source/Messages/StandardMessageBoxManager.cs index b5e3b3f..aed0bde 100644 --- a/Source/Messages/StandardMessageBoxManager.cs +++ b/Source/Messages/StandardMessageBoxManager.cs @@ -1,162 +1,161 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms.Messages { - - /// Uses task dialogs to display message boxes - public class StandardMessageBoxManager : IMessageService { - - #region class MessageScope - - /// Triggers the message displayed and acknowledged events - private class MessageScope : IDisposable { - - /// - /// Initializes a new message scope, triggering the message displayed event - /// - /// Message service the scope belongs to - /// Image of the message being displayed - /// Text contained in the message being displayed - public MessageScope( - StandardMessageBoxManager self, MessageBoxIcon image, MessageText text - ) { - EventHandler messageDisplayed = self.MessageDisplaying; - if(messageDisplayed != null) { - messageDisplayed(this, new MessageEventArgs(image, text)); - } - - this.self = self; - } - - /// Triggers the message acknowledged event - public void Dispose() { - EventHandler messageAcknowledged = self.MessageAcknowledged; - if(messageAcknowledged != null) { - messageAcknowledged(this, EventArgs.Empty); - } - } - - /// Message service the scope belongs to - private StandardMessageBoxManager self; - - } - - #endregion // class MessageScope - - /// Delegate for the standard message box show function - /// Window that will modally display the message box - /// Text that will be presented to the user - /// Contents of the message box' title bar - /// Buttons available for the user to choose from - /// Icon that will be displayed next to the text - /// The choice made by the user if multiple buttons were provided - private delegate DialogResult ShowMessageBoxDelegate( - IWin32Window owner, - string text, - string caption, - MessageBoxButtons buttons, - MessageBoxIcon icon - ); - - /// Triggered when a message is displayed to the user - public event EventHandler MessageDisplaying; - - /// Triggered when the user has acknowledged the current message - public event EventHandler MessageAcknowledged; - - /// Initializes a new task dialog message service - public StandardMessageBoxManager() : this(NullActiveWindowTracker.Default) { } - - /// Initializes a new task dialog message service - /// - /// Active window tracker used to obtain the parent window for message boxes - /// - public StandardMessageBoxManager(IActiveWindowTracker tracker) { - this.tracker = tracker; - this.showMessageDelegate = new ShowMessageBoxDelegate(MessageBox.Show); - } - - /// Asks the user a question that can be answered via several buttons - /// Image that will be shown on the message box - /// Text that will be shown to the user - /// Buttons available for the user to click on - /// The button the user has clicked on - public DialogResult ShowQuestion( - MessageBoxIcon image, MessageText text, MessageBoxButtons buttons - ) { - using(var scope = new MessageScope(this, image, text)) { - return showMessageBoxInActiveUiThread( - text.Message, - text.Caption, - buttons, - image - ); - } - } - - /// Displays a notification to the user - /// Image that will be shown on the message bx - /// Text that will be shown to the user - public void ShowNotification(MessageBoxIcon image, MessageText text) { - using(var scope = new MessageScope(this, image, text)) { - showMessageBoxInActiveUiThread( - text.Message, - text.Caption, - MessageBoxButtons.OK, - image - ); - } - } - - /// Displays the message box in the active view's thread - /// Text that will be presented to the user - /// Contents of the message box' title bar - /// Buttons available for the user to choose from - /// Image that will be displayed next to the text - /// - private DialogResult showMessageBoxInActiveUiThread( - string message, - string caption, - MessageBoxButtons buttons, - MessageBoxIcon image - ) { - Form mainWindow = this.tracker.ActiveWindow; - if(mainWindow != null) { - return (DialogResult)mainWindow.Invoke( - this.showMessageDelegate, - (IWin32Window)mainWindow, message, caption, buttons, image - ); - } - - // No window tracker or unknown main window -- just show the message box - return MessageBox.Show(message, caption, buttons, image); - } - - /// Provides the currently active top-level window - private IActiveWindowTracker tracker; - /// Delegate for the MessageBox.Show() method - private ShowMessageBoxDelegate showMessageDelegate; - - } - -} // namespace Nuclex.Windows.Forms.Messages +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms.Messages { + + /// Uses task dialogs to display message boxes + public class StandardMessageBoxManager : IMessageService { + + #region class MessageScope + + /// Triggers the message displayed and acknowledged events + private class MessageScope : IDisposable { + + /// + /// Initializes a new message scope, triggering the message displayed event + /// + /// Message service the scope belongs to + /// Image of the message being displayed + /// Text contained in the message being displayed + public MessageScope( + StandardMessageBoxManager self, MessageBoxIcon image, MessageText text + ) { + EventHandler messageDisplayed = self.MessageDisplaying; + if(messageDisplayed != null) { + messageDisplayed(this, new MessageEventArgs(image, text)); + } + + this.self = self; + } + + /// Triggers the message acknowledged event + public void Dispose() { + EventHandler messageAcknowledged = self.MessageAcknowledged; + if(messageAcknowledged != null) { + messageAcknowledged(this, EventArgs.Empty); + } + } + + /// Message service the scope belongs to + private StandardMessageBoxManager self; + + } + + #endregion // class MessageScope + + /// Delegate for the standard message box show function + /// Window that will modally display the message box + /// Text that will be presented to the user + /// Contents of the message box' title bar + /// Buttons available for the user to choose from + /// Icon that will be displayed next to the text + /// The choice made by the user if multiple buttons were provided + private delegate DialogResult ShowMessageBoxDelegate( + IWin32Window owner, + string text, + string caption, + MessageBoxButtons buttons, + MessageBoxIcon icon + ); + + /// Triggered when a message is displayed to the user + public event EventHandler MessageDisplaying; + + /// Triggered when the user has acknowledged the current message + public event EventHandler MessageAcknowledged; + + /// Initializes a new task dialog message service + public StandardMessageBoxManager() : this(NullActiveWindowTracker.Default) { } + + /// Initializes a new task dialog message service + /// + /// Active window tracker used to obtain the parent window for message boxes + /// + public StandardMessageBoxManager(IActiveWindowTracker tracker) { + this.tracker = tracker; + this.showMessageDelegate = new ShowMessageBoxDelegate(MessageBox.Show); + } + + /// Asks the user a question that can be answered via several buttons + /// Image that will be shown on the message box + /// Text that will be shown to the user + /// Buttons available for the user to click on + /// The button the user has clicked on + public DialogResult ShowQuestion( + MessageBoxIcon image, MessageText text, MessageBoxButtons buttons + ) { + using(var scope = new MessageScope(this, image, text)) { + return showMessageBoxInActiveUiThread( + text.Message, + text.Caption, + buttons, + image + ); + } + } + + /// Displays a notification to the user + /// Image that will be shown on the message bx + /// Text that will be shown to the user + public void ShowNotification(MessageBoxIcon image, MessageText text) { + using(var scope = new MessageScope(this, image, text)) { + showMessageBoxInActiveUiThread( + text.Message, + text.Caption, + MessageBoxButtons.OK, + image + ); + } + } + + /// Displays the message box in the active view's thread + /// Text that will be presented to the user + /// Contents of the message box' title bar + /// Buttons available for the user to choose from + /// Image that will be displayed next to the text + /// + private DialogResult showMessageBoxInActiveUiThread( + string message, + string caption, + MessageBoxButtons buttons, + MessageBoxIcon image + ) { + Form mainWindow = this.tracker.ActiveWindow; + if(mainWindow != null) { + return (DialogResult)mainWindow.Invoke( + this.showMessageDelegate, + (IWin32Window)mainWindow, message, caption, buttons, image + ); + } + + // No window tracker or unknown main window -- just show the message box + return MessageBox.Show(message, caption, buttons, image); + } + + /// Provides the currently active top-level window + private IActiveWindowTracker tracker; + /// Delegate for the MessageBox.Show() method + private ShowMessageBoxDelegate showMessageDelegate; + + } + +} // namespace Nuclex.Windows.Forms.Messages diff --git a/Source/NullActiveWindowTracker.cs b/Source/NullActiveWindowTracker.cs index c5ebd03..b595884 100644 --- a/Source/NullActiveWindowTracker.cs +++ b/Source/NullActiveWindowTracker.cs @@ -1,37 +1,36 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms { - - /// Dummy implementation of the active window tracker service - internal class NullActiveWindowTracker : IActiveWindowTracker { - - /// The default instance of the dummy window tracker - public static readonly NullActiveWindowTracker Default = new NullActiveWindowTracker(); - - /// The currently active top-level or modal window - public Form ActiveWindow { get { return null; } } - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms { + + /// Dummy implementation of the active window tracker service + internal class NullActiveWindowTracker : IActiveWindowTracker { + + /// The default instance of the dummy window tracker + public static readonly NullActiveWindowTracker Default = new NullActiveWindowTracker(); + + /// The currently active top-level or modal window + public Form ActiveWindow { get { return null; } } + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/ProgressReporter/ProgressReporterForm.Designer.cs b/Source/ProgressReporter/ProgressReporterForm.Designer.cs index 7a8d2d6..f56f6f1 100644 --- a/Source/ProgressReporter/ProgressReporterForm.Designer.cs +++ b/Source/ProgressReporter/ProgressReporterForm.Designer.cs @@ -1,120 +1,119 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -namespace Nuclex.Windows.Forms { - partial class ProgressReporterForm { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) { - if(disposing && (components != null)) { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() { - this.components = new System.ComponentModel.Container(); - this.cancelButton = new System.Windows.Forms.Button(); - this.progressBar = new Nuclex.Windows.Forms.AsyncProgressBar(); - this.statusLabel = new System.Windows.Forms.Label(); - this.controlCreationTimer = new System.Windows.Forms.Timer(this.components); - this.SuspendLayout(); - // - // cancelButton - // - this.cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Top; - this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.cancelButton.Location = new System.Drawing.Point(151, 55); - this.cancelButton.Name = "cancelButton"; - this.cancelButton.Size = new System.Drawing.Size(75, 23); - this.cancelButton.TabIndex = 0; - this.cancelButton.Text = "&Cancel"; - this.cancelButton.UseVisualStyleBackColor = true; - this.cancelButton.Click += new System.EventHandler(this.cancelClicked); - // - // progressBar - // - this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.progressBar.Location = new System.Drawing.Point(12, 26); - this.progressBar.Name = "progressBar"; - this.progressBar.Size = new System.Drawing.Size(352, 23); - this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; - this.progressBar.TabIndex = 1; - // - // statusLabel - // - this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.statusLabel.Location = new System.Drawing.Point(12, 9); - this.statusLabel.Name = "statusLabel"; - this.statusLabel.Size = new System.Drawing.Size(352, 14); - this.statusLabel.TabIndex = 2; - this.statusLabel.Text = "Please Wait..."; - this.statusLabel.TextAlign = System.Drawing.ContentAlignment.TopCenter; - // - // controlCreationTimer - // - this.controlCreationTimer.Enabled = true; - this.controlCreationTimer.Interval = 1; - this.controlCreationTimer.Tick += new System.EventHandler(this.controlCreationTimerTicked); - // - // ProgressReporterForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.CancelButton = this.cancelButton; - this.ClientSize = new System.Drawing.Size(376, 90); - this.ControlBox = false; - this.Controls.Add(this.statusLabel); - this.Controls.Add(this.progressBar); - this.Controls.Add(this.cancelButton); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.MaximizeBox = false; - this.MinimizeBox = false; - this.Name = "ProgressReporterForm"; - this.ShowIcon = false; - this.ShowInTaskbar = false; - this.Text = "Progress"; - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.Button cancelButton; - private Nuclex.Windows.Forms.AsyncProgressBar progressBar; - private System.Windows.Forms.Label statusLabel; - private System.Windows.Forms.Timer controlCreationTimer; - } +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +namespace Nuclex.Windows.Forms { + partial class ProgressReporterForm { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) { + if(disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); + this.cancelButton = new System.Windows.Forms.Button(); + this.progressBar = new Nuclex.Windows.Forms.AsyncProgressBar(); + this.statusLabel = new System.Windows.Forms.Label(); + this.controlCreationTimer = new System.Windows.Forms.Timer(this.components); + this.SuspendLayout(); + // + // cancelButton + // + this.cancelButton.Anchor = System.Windows.Forms.AnchorStyles.Top; + this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.cancelButton.Location = new System.Drawing.Point(151, 55); + this.cancelButton.Name = "cancelButton"; + this.cancelButton.Size = new System.Drawing.Size(75, 23); + this.cancelButton.TabIndex = 0; + this.cancelButton.Text = "&Cancel"; + this.cancelButton.UseVisualStyleBackColor = true; + this.cancelButton.Click += new System.EventHandler(this.cancelClicked); + // + // progressBar + // + this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.progressBar.Location = new System.Drawing.Point(12, 26); + this.progressBar.Name = "progressBar"; + this.progressBar.Size = new System.Drawing.Size(352, 23); + this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.progressBar.TabIndex = 1; + // + // statusLabel + // + this.statusLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.statusLabel.Location = new System.Drawing.Point(12, 9); + this.statusLabel.Name = "statusLabel"; + this.statusLabel.Size = new System.Drawing.Size(352, 14); + this.statusLabel.TabIndex = 2; + this.statusLabel.Text = "Please Wait..."; + this.statusLabel.TextAlign = System.Drawing.ContentAlignment.TopCenter; + // + // controlCreationTimer + // + this.controlCreationTimer.Enabled = true; + this.controlCreationTimer.Interval = 1; + this.controlCreationTimer.Tick += new System.EventHandler(this.controlCreationTimerTicked); + // + // ProgressReporterForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.cancelButton; + this.ClientSize = new System.Drawing.Size(376, 90); + this.ControlBox = false; + this.Controls.Add(this.statusLabel); + this.Controls.Add(this.progressBar); + this.Controls.Add(this.cancelButton); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "ProgressReporterForm"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.Text = "Progress"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button cancelButton; + private Nuclex.Windows.Forms.AsyncProgressBar progressBar; + private System.Windows.Forms.Label statusLabel; + private System.Windows.Forms.Timer controlCreationTimer; + } } \ No newline at end of file diff --git a/Source/ProgressReporter/ProgressReporterForm.cs b/Source/ProgressReporter/ProgressReporterForm.cs index f3749c8..4e034a8 100644 --- a/Source/ProgressReporter/ProgressReporterForm.cs +++ b/Source/ProgressReporter/ProgressReporterForm.cs @@ -1,273 +1,272 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.ComponentModel; -using System.Threading; -using System.Windows.Forms; - -using Nuclex.Support.Scheduling; -using Nuclex.Support.Tracking; - -namespace Nuclex.Windows.Forms { - - /// - /// Blocking progress dialog that prevents the user from accessing the application - /// window during a modal asynchronous processes. - /// - /// - /// - /// class Test : Nuclex.Support.Scheduling.ThreadOperation { - /// - /// static void Main() { - /// Test myTest = new Test(); - /// myTest.Begin(); - /// Nuclex.Windows.Forms.ProgressReporterForm.Track(myTest); - /// myTest.End(); - /// } - /// - /// protected override void Execute() { - /// for(int i = 0; i < 10000000; ++i) { - /// OnAsyncProgressUpdated((float)i / 10000000.0f); - /// } - /// } - /// - /// } - /// - /// - public partial class ProgressReporterForm : Form { - - /// Initializes a new progress reporter - internal ProgressReporterForm() { - InitializeComponent(); - - this.asyncEndedDelegate = new EventHandler(asyncEnded); - this.asyncProgressChangedDelegate = new EventHandler( - asyncProgressChanged - ); - } - - /// - /// Shows the progress reporter until the specified transaction has ended. - /// - /// - /// Transaction for whose duration to show the progress reporter - /// - public static void Track(Transaction transaction) { - Track(null, transaction); - } - - /// - /// Shows the progress reporter until the specified transaction has ended. - /// - /// - /// Text to be shown in the progress reporter's title bar - /// - /// - /// Process for whose duration to show the progress reporter - /// - public static void Track(string windowTitle, Transaction transaction) { - - // Small optimization to avoid the lengthy control creation when the background - // process has already ended. This is an accepted race condition: If the process - // finishes right after this line, it doesn't change the outcome, it just - // causes the progress dialog to be constructed needlessly. - if(transaction.Ended) { - return; - } - - // Open the form and let it monitor the transaction's state - using(ProgressReporterForm theForm = new ProgressReporterForm()) { - theForm.track(windowTitle, transaction); - } - - } - - /// Called when the user tries to close the form manually - /// - /// Contains a flag that can be used to abort the close attempt - /// - protected override void OnClosing(CancelEventArgs arguments) { - base.OnClosing(arguments); - - // Only allow the form to close when the form is ready to close and the - // transaction being tracked has also finished. - arguments.Cancel = (Thread.VolatileRead(ref this.state) < 2); - } - - /// - /// Shows the progress reporter until the specified transaction has ended. - /// - /// - /// Text to be shown in the progress reporter's title bar - /// - /// - /// Transaction for whose duration to show the progress reporter - /// - private void track(string windowTitle, Transaction transaction) { - - // Set the window title if the user wants to use a custom one - if(windowTitle != null) { - Text = windowTitle; - } - - // Only enable the cancel button if the transaction can be aborted - this.abortReceiver = (transaction as IAbortable); - this.cancelButton.Enabled = (this.abortReceiver != null); - - // Make sure the progress bar control has been created (otherwise, we've got - // a chance that BeginInvoke() would fail if the first progress notification - // arrived before we called ShowDialog()!) - { IntPtr tempDummy = this.progressBar.Handle; } - - // Subscribe the form to the transaction it is supposed to monitor. - // Careful: With the new design, this can cause the asyncEndedDelegate() - // callback to be called immediately and synchronously! - transaction.AsyncEnded += this.asyncEndedDelegate; - IProgressReporter progressReporter = transaction as IProgressReporter; - if(progressReporter != null) { - progressReporter.AsyncProgressChanged += this.asyncProgressChangedDelegate; - } - - // The transaction might have ended before this line was reached, if that's - // the case, we don't show the dialog at all. - if(!transaction.Ended) - ShowDialog(); - - // We're done, unsubscribe from the transaction's events again - progressReporter = transaction as IProgressReporter; - if(progressReporter != null) { - progressReporter.AsyncProgressChanged -= this.asyncProgressChangedDelegate; - } - transaction.AsyncEnded -= this.asyncEndedDelegate; - - } - - /// Called when the transaction has ended - /// Transaction that has ended - /// Not used - private void asyncEnded(object sender, EventArgs arguments) { - - // If the new state is 2, the form was ready to close (since the state - // is incremented once when the form becomes ready to be closed) - if(Interlocked.Increment(ref this.state) == 2) { - - // Close the dialog. Ensure the Close() method is invoked from the - // same thread the dialog was created in. - if(InvokeRequired) { - Invoke(new MethodInvoker(Close)); - } else { - Close(); - } - - } - - } - - /// Called when the tracked transaction's progress updates - /// Transaction whose progress has been updated - /// - /// Contains the new progress achieved by the transaction - /// - private void asyncProgressChanged(object sender, ProgressReportEventArgs arguments) { - - // See if this is the first progress update we're receiving. If yes, we need to - // switch the progress bar from marquee into its normal mode! - int haveProgress = Interlocked.Exchange(ref this.areProgressUpdatesIncoming, 1); - if(haveProgress == 0) { - this.progressBar.BeginInvoke( - (MethodInvoker)delegate() { this.progressBar.Style = ProgressBarStyle.Blocks; } - ); - } - - // Send the new progress to the progress bar - this.progressBar.AsyncSetValue(arguments.Progress); - - } - - /// - /// One-time timer callback that ensurs the form doesn't stay open when the - /// close request arrives at an inappropriate time. - /// - /// Timer that has ticked - /// Not used - private void controlCreationTimerTicked(object sender, EventArgs arguments) { - - // This timer is intended to run only once to find out when the dialog has - // been fully constructed and is running its message pump. So we'll disable - // it as soon as it has been triggered once. - this.controlCreationTimer.Enabled = false; - - // If the new state is 2, then the form was requested to close before it had - // been fully constructed, so we should close it now! - if(Interlocked.Increment(ref this.state) == 2) { - Close(); - } - - } - - /// - /// Aborts the background operation when the user clicks the cancel button - /// - /// Button that has been clicked - /// Not used - private void cancelClicked(object sender, EventArgs arguments) { - - if(this.abortReceiver != null) { - - // Do this first because the abort receiver might trigger the AsyncEnded() - // event in the calling thread (us!) and thus destroy our window even in - // the safe and synchronous UI thread :) - this.cancelButton.Enabled = false; - - // Now we're ready to abort! - this.abortReceiver.AsyncAbort(); - this.abortReceiver = null; - - } - - } - - /// Delegate for the asyncEnded() method - private EventHandler asyncEndedDelegate; - /// Delegate for the asyncProgressUpdated() method - private EventHandler asyncProgressChangedDelegate; - /// Whether the form can be closed and should be closed - /// - /// 0: Nothing happened yet - /// 1: Ready to close or close requested - /// 2: Ready to close and close requested, triggers close - /// - private int state; - /// Whether we're receiving progress updates from the transaction - /// - /// 0: No progress updates have arrived so far - /// 1: We have received at least one progress update from the transaction - /// - private int areProgressUpdatesIncoming; - /// - /// If set, reference to an object implementing IAbortable by which the - /// ongoing background process can be aborted. - /// - private IAbortable abortReceiver; - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.ComponentModel; +using System.Threading; +using System.Windows.Forms; + +using Nuclex.Support.Scheduling; +using Nuclex.Support.Tracking; + +namespace Nuclex.Windows.Forms { + + /// + /// Blocking progress dialog that prevents the user from accessing the application + /// window during a modal asynchronous processes. + /// + /// + /// + /// class Test : Nuclex.Support.Scheduling.ThreadOperation { + /// + /// static void Main() { + /// Test myTest = new Test(); + /// myTest.Begin(); + /// Nuclex.Windows.Forms.ProgressReporterForm.Track(myTest); + /// myTest.End(); + /// } + /// + /// protected override void Execute() { + /// for(int i = 0; i < 10000000; ++i) { + /// OnAsyncProgressUpdated((float)i / 10000000.0f); + /// } + /// } + /// + /// } + /// + /// + public partial class ProgressReporterForm : Form { + + /// Initializes a new progress reporter + internal ProgressReporterForm() { + InitializeComponent(); + + this.asyncEndedDelegate = new EventHandler(asyncEnded); + this.asyncProgressChangedDelegate = new EventHandler( + asyncProgressChanged + ); + } + + /// + /// Shows the progress reporter until the specified transaction has ended. + /// + /// + /// Transaction for whose duration to show the progress reporter + /// + public static void Track(Transaction transaction) { + Track(null, transaction); + } + + /// + /// Shows the progress reporter until the specified transaction has ended. + /// + /// + /// Text to be shown in the progress reporter's title bar + /// + /// + /// Process for whose duration to show the progress reporter + /// + public static void Track(string windowTitle, Transaction transaction) { + + // Small optimization to avoid the lengthy control creation when the background + // process has already ended. This is an accepted race condition: If the process + // finishes right after this line, it doesn't change the outcome, it just + // causes the progress dialog to be constructed needlessly. + if(transaction.Ended) { + return; + } + + // Open the form and let it monitor the transaction's state + using(ProgressReporterForm theForm = new ProgressReporterForm()) { + theForm.track(windowTitle, transaction); + } + + } + + /// Called when the user tries to close the form manually + /// + /// Contains a flag that can be used to abort the close attempt + /// + protected override void OnClosing(CancelEventArgs arguments) { + base.OnClosing(arguments); + + // Only allow the form to close when the form is ready to close and the + // transaction being tracked has also finished. + arguments.Cancel = (Thread.VolatileRead(ref this.state) < 2); + } + + /// + /// Shows the progress reporter until the specified transaction has ended. + /// + /// + /// Text to be shown in the progress reporter's title bar + /// + /// + /// Transaction for whose duration to show the progress reporter + /// + private void track(string windowTitle, Transaction transaction) { + + // Set the window title if the user wants to use a custom one + if(windowTitle != null) { + Text = windowTitle; + } + + // Only enable the cancel button if the transaction can be aborted + this.abortReceiver = (transaction as IAbortable); + this.cancelButton.Enabled = (this.abortReceiver != null); + + // Make sure the progress bar control has been created (otherwise, we've got + // a chance that BeginInvoke() would fail if the first progress notification + // arrived before we called ShowDialog()!) + { IntPtr tempDummy = this.progressBar.Handle; } + + // Subscribe the form to the transaction it is supposed to monitor. + // Careful: With the new design, this can cause the asyncEndedDelegate() + // callback to be called immediately and synchronously! + transaction.AsyncEnded += this.asyncEndedDelegate; + IProgressReporter progressReporter = transaction as IProgressReporter; + if(progressReporter != null) { + progressReporter.AsyncProgressChanged += this.asyncProgressChangedDelegate; + } + + // The transaction might have ended before this line was reached, if that's + // the case, we don't show the dialog at all. + if(!transaction.Ended) + ShowDialog(); + + // We're done, unsubscribe from the transaction's events again + progressReporter = transaction as IProgressReporter; + if(progressReporter != null) { + progressReporter.AsyncProgressChanged -= this.asyncProgressChangedDelegate; + } + transaction.AsyncEnded -= this.asyncEndedDelegate; + + } + + /// Called when the transaction has ended + /// Transaction that has ended + /// Not used + private void asyncEnded(object sender, EventArgs arguments) { + + // If the new state is 2, the form was ready to close (since the state + // is incremented once when the form becomes ready to be closed) + if(Interlocked.Increment(ref this.state) == 2) { + + // Close the dialog. Ensure the Close() method is invoked from the + // same thread the dialog was created in. + if(InvokeRequired) { + Invoke(new MethodInvoker(Close)); + } else { + Close(); + } + + } + + } + + /// Called when the tracked transaction's progress updates + /// Transaction whose progress has been updated + /// + /// Contains the new progress achieved by the transaction + /// + private void asyncProgressChanged(object sender, ProgressReportEventArgs arguments) { + + // See if this is the first progress update we're receiving. If yes, we need to + // switch the progress bar from marquee into its normal mode! + int haveProgress = Interlocked.Exchange(ref this.areProgressUpdatesIncoming, 1); + if(haveProgress == 0) { + this.progressBar.BeginInvoke( + (MethodInvoker)delegate() { this.progressBar.Style = ProgressBarStyle.Blocks; } + ); + } + + // Send the new progress to the progress bar + this.progressBar.AsyncSetValue(arguments.Progress); + + } + + /// + /// One-time timer callback that ensurs the form doesn't stay open when the + /// close request arrives at an inappropriate time. + /// + /// Timer that has ticked + /// Not used + private void controlCreationTimerTicked(object sender, EventArgs arguments) { + + // This timer is intended to run only once to find out when the dialog has + // been fully constructed and is running its message pump. So we'll disable + // it as soon as it has been triggered once. + this.controlCreationTimer.Enabled = false; + + // If the new state is 2, then the form was requested to close before it had + // been fully constructed, so we should close it now! + if(Interlocked.Increment(ref this.state) == 2) { + Close(); + } + + } + + /// + /// Aborts the background operation when the user clicks the cancel button + /// + /// Button that has been clicked + /// Not used + private void cancelClicked(object sender, EventArgs arguments) { + + if(this.abortReceiver != null) { + + // Do this first because the abort receiver might trigger the AsyncEnded() + // event in the calling thread (us!) and thus destroy our window even in + // the safe and synchronous UI thread :) + this.cancelButton.Enabled = false; + + // Now we're ready to abort! + this.abortReceiver.AsyncAbort(); + this.abortReceiver = null; + + } + + } + + /// Delegate for the asyncEnded() method + private EventHandler asyncEndedDelegate; + /// Delegate for the asyncProgressUpdated() method + private EventHandler asyncProgressChangedDelegate; + /// Whether the form can be closed and should be closed + /// + /// 0: Nothing happened yet + /// 1: Ready to close or close requested + /// 2: Ready to close and close requested, triggers close + /// + private int state; + /// Whether we're receiving progress updates from the transaction + /// + /// 0: No progress updates have arrived so far + /// 1: We have received at least one progress update from the transaction + /// + private int areProgressUpdatesIncoming; + /// + /// If set, reference to an object implementing IAbortable by which the + /// ongoing background process can be aborted. + /// + private IAbortable abortReceiver; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/TrackingBar/ToolStripTrackingBar.cs b/Source/TrackingBar/ToolStripTrackingBar.cs index f13d5a9..cf5ad11 100644 --- a/Source/TrackingBar/ToolStripTrackingBar.cs +++ b/Source/TrackingBar/ToolStripTrackingBar.cs @@ -1,114 +1,113 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.ComponentModel; -using System.Drawing; -using System.Windows.Forms; - -using Nuclex.Support.Tracking; - -namespace Nuclex.Windows.Forms { - - /// Tracking bar that can be hosted in a tool strip container - public class ToolStripTrackingBar : ToolStripControlHost { - - /// Initializes a new tool strip tracking bar - public ToolStripTrackingBar() : base(createTrackingBar()) { - hideControlAtRuntime(); - } - - /// Initializes a new tool strip tracking bar with a name - /// Name of the tracking bar control - public ToolStripTrackingBar(string name) : base(createTrackingBar(), name) { - hideControlAtRuntime(); - } - - /// The tracking bar control being hosted by the tool strip host - public TrackingBar TrackingBarControl { - get { return base.Control as TrackingBar; } - } - - /// Tracks the specified transaction in the tracking bar - /// Transaction to be tracked - public void Track(Transaction transaction) { - TrackingBarControl.Track(transaction); - } - - /// Tracks the specified transaction in the tracking bar - /// Transaction to be tracked - /// Weight of this transaction in the total progress - public void Track(Transaction transaction, float weight) { - TrackingBarControl.Track(transaction, weight); - } - - /// Stops tracking the specified transaction - /// Transaction to stop tracking - public void Untrack(Transaction transaction) { - TrackingBarControl.Untrack(transaction); - } - - /// Default size of the hosted control - protected override Size DefaultSize { - get { return new Size(100, 15); } - } - - /// Default margin to leave around the control in the tool strip - protected override Padding DefaultMargin { - get { - if((base.Owner != null) && (base.Owner is StatusStrip)) { - return new Padding(1, 3, 1, 3); - } - - return new Padding(1, 2, 1, 1); - } - } - - /// Creates a new tracking bar - /// A new tracking bar - private static TrackingBar createTrackingBar() { - TrackingBar trackingBar = new TrackingBar(); - trackingBar.Size = new Size(100, 15); - return trackingBar; - } - - /// Hides the control during runtime usage - private void hideControlAtRuntime() { - TrackingBarControl.VisibleChanged += new EventHandler(trackingBarVisibleChanged); - - LicenseUsageMode usageMode = System.ComponentModel.LicenseManager.UsageMode; - if(usageMode == LicenseUsageMode.Runtime) { - base.Visible = false; - } - } - - /// - /// Toggles the visibility of the tool strip host when the tracking bar control's - /// visibility changes. - /// - /// Tracking bar control whose visiblity has changed - /// Not used - private void trackingBarVisibleChanged(object sender, EventArgs arguments) { - base.Visible = TrackingBarControl.Visible; - } - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +using Nuclex.Support.Tracking; + +namespace Nuclex.Windows.Forms { + + /// Tracking bar that can be hosted in a tool strip container + public class ToolStripTrackingBar : ToolStripControlHost { + + /// Initializes a new tool strip tracking bar + public ToolStripTrackingBar() : base(createTrackingBar()) { + hideControlAtRuntime(); + } + + /// Initializes a new tool strip tracking bar with a name + /// Name of the tracking bar control + public ToolStripTrackingBar(string name) : base(createTrackingBar(), name) { + hideControlAtRuntime(); + } + + /// The tracking bar control being hosted by the tool strip host + public TrackingBar TrackingBarControl { + get { return base.Control as TrackingBar; } + } + + /// Tracks the specified transaction in the tracking bar + /// Transaction to be tracked + public void Track(Transaction transaction) { + TrackingBarControl.Track(transaction); + } + + /// Tracks the specified transaction in the tracking bar + /// Transaction to be tracked + /// Weight of this transaction in the total progress + public void Track(Transaction transaction, float weight) { + TrackingBarControl.Track(transaction, weight); + } + + /// Stops tracking the specified transaction + /// Transaction to stop tracking + public void Untrack(Transaction transaction) { + TrackingBarControl.Untrack(transaction); + } + + /// Default size of the hosted control + protected override Size DefaultSize { + get { return new Size(100, 15); } + } + + /// Default margin to leave around the control in the tool strip + protected override Padding DefaultMargin { + get { + if((base.Owner != null) && (base.Owner is StatusStrip)) { + return new Padding(1, 3, 1, 3); + } + + return new Padding(1, 2, 1, 1); + } + } + + /// Creates a new tracking bar + /// A new tracking bar + private static TrackingBar createTrackingBar() { + TrackingBar trackingBar = new TrackingBar(); + trackingBar.Size = new Size(100, 15); + return trackingBar; + } + + /// Hides the control during runtime usage + private void hideControlAtRuntime() { + TrackingBarControl.VisibleChanged += new EventHandler(trackingBarVisibleChanged); + + LicenseUsageMode usageMode = System.ComponentModel.LicenseManager.UsageMode; + if(usageMode == LicenseUsageMode.Runtime) { + base.Visible = false; + } + } + + /// + /// Toggles the visibility of the tool strip host when the tracking bar control's + /// visibility changes. + /// + /// Tracking bar control whose visiblity has changed + /// Not used + private void trackingBarVisibleChanged(object sender, EventArgs arguments) { + base.Visible = TrackingBarControl.Visible; + } + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/TrackingBar/TrackingBar.Designer.cs b/Source/TrackingBar/TrackingBar.Designer.cs index 4a03fe1..5d9c48c 100644 --- a/Source/TrackingBar/TrackingBar.Designer.cs +++ b/Source/TrackingBar/TrackingBar.Designer.cs @@ -1,51 +1,50 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -namespace Nuclex.Windows.Forms { - - partial class TrackingBar { - /// Required designer variable. - private System.ComponentModel.IContainer components = null; - - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - /// - protected override void Dispose(bool disposing) { - if(disposing && (components != null)) { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() { - components = new System.ComponentModel.Container(); - } - - #endregion - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +namespace Nuclex.Windows.Forms { + + partial class TrackingBar { + /// Required designer variable. + private System.ComponentModel.IContainer components = null; + + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + /// + protected override void Dispose(bool disposing) { + if(disposing && (components != null)) { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + components = new System.ComponentModel.Container(); + } + + #endregion + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/TrackingBar/TrackingBar.cs b/Source/TrackingBar/TrackingBar.cs index 18c4d0a..4aa2023 100644 --- a/Source/TrackingBar/TrackingBar.cs +++ b/Source/TrackingBar/TrackingBar.cs @@ -1,134 +1,133 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Data; -using System.Text; -using System.Windows.Forms; -using System.Threading; - -using Nuclex.Support.Tracking; - -namespace Nuclex.Windows.Forms { - - /// Progress bar for tracking the progress of background operations - public partial class TrackingBar : AsyncProgressBar { - - /// Initializes a new tracking bar - public TrackingBar() { - InitializeComponent(); - - // We start off being in the idle state (and thus, being invisible) - this.isIdle = true; - this.Visible = false; - - // Initialize the delegates we use to update the control's state and those - // we use to register ourselfes to the tracker's events - this.updateIdleStateDelegate = new MethodInvoker(updateIdleState); - this.asyncIdleStateChangedDelegate = new EventHandler( - asyncIdleStateChanged - ); - this.asyncProgressUpdateDelegate = new EventHandler( - asyncProgressUpdated - ); - - // Create the tracker and attach ourselfes to its events - this.tracker = new ProgressTracker(); - this.tracker.AsyncIdleStateChanged += this.asyncIdleStateChangedDelegate; - this.tracker.AsyncProgressChanged += this.asyncProgressUpdateDelegate; - } - - /// Tracks the specified transaction in the tracking bar - /// Transaction to be tracked - public void Track(Transaction transaction) { - this.tracker.Track(transaction); - } - - /// Tracks the specified transaction in the tracking bar - /// Transaction to be tracked - /// Weight of this transaction in the total progress - public void Track(Transaction transaction, float weight) { - this.tracker.Track(transaction, weight); - } - - /// Stops tracking the specified transaction - /// Transaction to stop tracking - public void Untrack(Transaction transaction) { - this.tracker.Untrack(transaction); - } - - /// - /// Called when the summed progressed of the tracked transaction has changed - /// - /// Transaction whose progress has changed - /// Contains the progress achieved by the transaction - private void asyncProgressUpdated( - object sender, ProgressReportEventArgs arguments - ) { - AsyncSetValue(arguments.Progress); - } - - /// Called when the tracker becomes enters of leaves the idle state - /// Tracker that has entered or left the idle state - /// Contains the new idle state - private void asyncIdleStateChanged(object sender, IdleStateEventArgs arguments) { - - // Do a fully synchronous update of the idle state. This update must not be - // lost because otherwise, the progress bar might stay on-screen when in fact, - // the background operation has already finished and nothing is happening anymore. - this.isIdle = arguments.Idle; - - // Update the bar's idle state - if(InvokeRequired) { - Invoke(this.updateIdleStateDelegate); - } else { - updateIdleState(); - } - - } - - /// - /// Updates the idle state of the progress bar - /// (controls whether the progress bar is shown or invisible) - /// - private void updateIdleState() { - - // Only show the progress bar when something is happening - base.Visible = !this.isIdle; - - } - - /// Whether the progress bar is in the idle state - private volatile bool isIdle; - /// Tracker used to sum and update the total progress - private ProgressTracker tracker; - /// Delegate for the idle state update method - private MethodInvoker updateIdleStateDelegate; - /// Delegate for the asyncIdleStateChanged() method - private EventHandler asyncIdleStateChangedDelegate; - /// Delegate for the asyncProgressUpdate() method - private EventHandler asyncProgressUpdateDelegate; - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Data; +using System.Text; +using System.Windows.Forms; +using System.Threading; + +using Nuclex.Support.Tracking; + +namespace Nuclex.Windows.Forms { + + /// Progress bar for tracking the progress of background operations + public partial class TrackingBar : AsyncProgressBar { + + /// Initializes a new tracking bar + public TrackingBar() { + InitializeComponent(); + + // We start off being in the idle state (and thus, being invisible) + this.isIdle = true; + this.Visible = false; + + // Initialize the delegates we use to update the control's state and those + // we use to register ourselfes to the tracker's events + this.updateIdleStateDelegate = new MethodInvoker(updateIdleState); + this.asyncIdleStateChangedDelegate = new EventHandler( + asyncIdleStateChanged + ); + this.asyncProgressUpdateDelegate = new EventHandler( + asyncProgressUpdated + ); + + // Create the tracker and attach ourselfes to its events + this.tracker = new ProgressTracker(); + this.tracker.AsyncIdleStateChanged += this.asyncIdleStateChangedDelegate; + this.tracker.AsyncProgressChanged += this.asyncProgressUpdateDelegate; + } + + /// Tracks the specified transaction in the tracking bar + /// Transaction to be tracked + public void Track(Transaction transaction) { + this.tracker.Track(transaction); + } + + /// Tracks the specified transaction in the tracking bar + /// Transaction to be tracked + /// Weight of this transaction in the total progress + public void Track(Transaction transaction, float weight) { + this.tracker.Track(transaction, weight); + } + + /// Stops tracking the specified transaction + /// Transaction to stop tracking + public void Untrack(Transaction transaction) { + this.tracker.Untrack(transaction); + } + + /// + /// Called when the summed progressed of the tracked transaction has changed + /// + /// Transaction whose progress has changed + /// Contains the progress achieved by the transaction + private void asyncProgressUpdated( + object sender, ProgressReportEventArgs arguments + ) { + AsyncSetValue(arguments.Progress); + } + + /// Called when the tracker becomes enters of leaves the idle state + /// Tracker that has entered or left the idle state + /// Contains the new idle state + private void asyncIdleStateChanged(object sender, IdleStateEventArgs arguments) { + + // Do a fully synchronous update of the idle state. This update must not be + // lost because otherwise, the progress bar might stay on-screen when in fact, + // the background operation has already finished and nothing is happening anymore. + this.isIdle = arguments.Idle; + + // Update the bar's idle state + if(InvokeRequired) { + Invoke(this.updateIdleStateDelegate); + } else { + updateIdleState(); + } + + } + + /// + /// Updates the idle state of the progress bar + /// (controls whether the progress bar is shown or invisible) + /// + private void updateIdleState() { + + // Only show the progress bar when something is happening + base.Visible = !this.isIdle; + + } + + /// Whether the progress bar is in the idle state + private volatile bool isIdle; + /// Tracker used to sum and update the total progress + private ProgressTracker tracker; + /// Delegate for the idle state update method + private MethodInvoker updateIdleStateDelegate; + /// Delegate for the asyncIdleStateChanged() method + private EventHandler asyncIdleStateChangedDelegate; + /// Delegate for the asyncProgressUpdate() method + private EventHandler asyncProgressUpdateDelegate; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/ViewModels/DialogViewModel.Test.cs b/Source/ViewModels/DialogViewModel.Test.cs index bb015f2..95fa38c 100644 --- a/Source/ViewModels/DialogViewModel.Test.cs +++ b/Source/ViewModels/DialogViewModel.Test.cs @@ -1,156 +1,155 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; - -using NUnit.Framework; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// Unit test for the dialog view model - [TestFixture] - public class DialogViewModelTest { - - #region class DialogViewModelSubscriber - - /// Subscriber for the events offered by a dialog view model - private class DialogViewModelSubscriber { - - /// Indicates that the user has accepted the dialog - public void Confirmed(object sender, EventArgs arguments) { - ++this.confirmCallCount; - } - - /// Indicates that the user has cancelled the dialog - public void Cancelled(object sender, EventArgs arguments) { - ++this.cancelCallCount; - } - - /// Indicates that the dialog was simply closed - public void Submitted(object sender, EventArgs arguments) { - ++this.submitCallCount; - } - - /// How many times the Confirmed() method was called - public int ConfirmCallCount { - get { return this.confirmCallCount; } - } - - /// How many times the Cancelled() method was called - public int CancelCallCount { - get { return this.cancelCallCount; } - } - - /// How many times the Submitted() method was called - public int SubmitCallCount { - get { return this.submitCallCount; } - } - - /// How many times the Confirmed() method was called - private int confirmCallCount; - /// How many times the Cancelled() method was called - private int cancelCallCount; - /// How many times the Submitted() method was called - private int submitCallCount; - - } - - #endregion // class DialogViewModelSubscriber - - /// Verifies that the dialog view model has a default constructor - [Test] - public void HasDefaultConstructor() { - Assert.DoesNotThrow( - delegate() { new DialogViewModel(); } - ); - } - - /// - /// Verifies that calling Confirm() on the dialog view model triggers - /// the 'Confirmed' event - /// - [Test] - public void ConfirmTriggersConfirmedEvent() { - var viewModel = new DialogViewModel(); - var subscriber = createSubscriber(viewModel); - - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - viewModel.Confirm(); - Assert.AreEqual(1, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - } - - /// - /// Verifies that calling Cancel() on the dialog view model triggers - /// the 'Cancelled' event - /// - [Test] - public void CancelTriggersCancelledEvent() { - var viewModel = new DialogViewModel(); - var subscriber = createSubscriber(viewModel); - - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - viewModel.Cancel(); - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(1, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - } - - /// - /// Verifies that calling Submitm() on the dialog view model triggers - /// the 'Submitted' event - /// - [Test] - public void SubmitTriggersSubmittedEvent() { - var viewModel = new DialogViewModel(); - var subscriber = createSubscriber(viewModel); - - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - viewModel.Submit(); - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(1, subscriber.SubmitCallCount); - } - - /// Constructs a new subscriber for the dialog view model's events - /// View model a subscriber will be created for - /// A subscriber for the events of the specified view model - private DialogViewModelSubscriber createSubscriber(DialogViewModel viewModel) { - var subscriber = new DialogViewModelSubscriber(); - viewModel.Confirmed += subscriber.Confirmed; - viewModel.Canceled += subscriber.Cancelled; - viewModel.Submitted += subscriber.Submitted; - return subscriber; - } - - } - -} // namespace Nuclex.Windows.Forms.ViewModels - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; + +using NUnit.Framework; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// Unit test for the dialog view model + [TestFixture] + public class DialogViewModelTest { + + #region class DialogViewModelSubscriber + + /// Subscriber for the events offered by a dialog view model + private class DialogViewModelSubscriber { + + /// Indicates that the user has accepted the dialog + public void Confirmed(object sender, EventArgs arguments) { + ++this.confirmCallCount; + } + + /// Indicates that the user has cancelled the dialog + public void Cancelled(object sender, EventArgs arguments) { + ++this.cancelCallCount; + } + + /// Indicates that the dialog was simply closed + public void Submitted(object sender, EventArgs arguments) { + ++this.submitCallCount; + } + + /// How many times the Confirmed() method was called + public int ConfirmCallCount { + get { return this.confirmCallCount; } + } + + /// How many times the Cancelled() method was called + public int CancelCallCount { + get { return this.cancelCallCount; } + } + + /// How many times the Submitted() method was called + public int SubmitCallCount { + get { return this.submitCallCount; } + } + + /// How many times the Confirmed() method was called + private int confirmCallCount; + /// How many times the Cancelled() method was called + private int cancelCallCount; + /// How many times the Submitted() method was called + private int submitCallCount; + + } + + #endregion // class DialogViewModelSubscriber + + /// Verifies that the dialog view model has a default constructor + [Test] + public void HasDefaultConstructor() { + Assert.DoesNotThrow( + delegate() { new DialogViewModel(); } + ); + } + + /// + /// Verifies that calling Confirm() on the dialog view model triggers + /// the 'Confirmed' event + /// + [Test] + public void ConfirmTriggersConfirmedEvent() { + var viewModel = new DialogViewModel(); + var subscriber = createSubscriber(viewModel); + + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + viewModel.Confirm(); + Assert.AreEqual(1, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + } + + /// + /// Verifies that calling Cancel() on the dialog view model triggers + /// the 'Cancelled' event + /// + [Test] + public void CancelTriggersCancelledEvent() { + var viewModel = new DialogViewModel(); + var subscriber = createSubscriber(viewModel); + + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + viewModel.Cancel(); + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(1, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + } + + /// + /// Verifies that calling Submitm() on the dialog view model triggers + /// the 'Submitted' event + /// + [Test] + public void SubmitTriggersSubmittedEvent() { + var viewModel = new DialogViewModel(); + var subscriber = createSubscriber(viewModel); + + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + viewModel.Submit(); + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(1, subscriber.SubmitCallCount); + } + + /// Constructs a new subscriber for the dialog view model's events + /// View model a subscriber will be created for + /// A subscriber for the events of the specified view model + private DialogViewModelSubscriber createSubscriber(DialogViewModel viewModel) { + var subscriber = new DialogViewModelSubscriber(); + viewModel.Confirmed += subscriber.Confirmed; + viewModel.Canceled += subscriber.Cancelled; + viewModel.Submitted += subscriber.Submitted; + return subscriber; + } + + } + +} // namespace Nuclex.Windows.Forms.ViewModels + +#endif // UNITTEST diff --git a/Source/ViewModels/DialogViewModel.cs b/Source/ViewModels/DialogViewModel.cs index 2cecb00..962f645 100644 --- a/Source/ViewModels/DialogViewModel.cs +++ b/Source/ViewModels/DialogViewModel.cs @@ -1,76 +1,75 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -using Nuclex.Support; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// Base class for the view model of dialogs (typically modal ones) - public class DialogViewModel : Observable { - - /// Indicates that the view should close with a positive result - /// - /// This event typically corresponds to the 'Ok' button in a dialog. - /// - public event EventHandler Confirmed; - - /// Indicates that the view should close with a negative result - /// - /// This event typically corresponds to the 'Cancel' button in a dialog. - /// - public event EventHandler Canceled; - - /// Indicates that the view should close - /// - /// 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. - /// - public event EventHandler Submitted; - - /// - /// Indicates that the dialog should be closed with a positive outcome - /// - public virtual void Confirm() { - if(Confirmed != null) { - Confirmed(this, EventArgs.Empty); - } - } - - /// - /// Indicates that the dialog should be closed with a negative outcome - /// - public virtual void Cancel() { - if(Canceled != null) { - Canceled(this, EventArgs.Empty); - } - } - - /// Indicates that the dialog should be closed - public virtual void Submit() { - if(Submitted != null) { - Submitted(this, EventArgs.Empty); - } - } - - } - -} // namespace Nuclex.Windows.Forms.ViewModels +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +using Nuclex.Support; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// Base class for the view model of dialogs (typically modal ones) + public class DialogViewModel : Observable { + + /// Indicates that the view should close with a positive result + /// + /// This event typically corresponds to the 'Ok' button in a dialog. + /// + public event EventHandler Confirmed; + + /// Indicates that the view should close with a negative result + /// + /// This event typically corresponds to the 'Cancel' button in a dialog. + /// + public event EventHandler Canceled; + + /// Indicates that the view should close + /// + /// 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. + /// + public event EventHandler Submitted; + + /// + /// Indicates that the dialog should be closed with a positive outcome + /// + public virtual void Confirm() { + if(Confirmed != null) { + Confirmed(this, EventArgs.Empty); + } + } + + /// + /// Indicates that the dialog should be closed with a negative outcome + /// + public virtual void Cancel() { + if(Canceled != null) { + Canceled(this, EventArgs.Empty); + } + } + + /// Indicates that the dialog should be closed + public virtual void Submit() { + if(Submitted != null) { + Submitted(this, EventArgs.Empty); + } + } + + } + +} // namespace Nuclex.Windows.Forms.ViewModels diff --git a/Source/ViewModels/IMultiPageViewModel.cs b/Source/ViewModels/IMultiPageViewModel.cs index 39007a1..2d23ee5 100644 --- a/Source/ViewModels/IMultiPageViewModel.cs +++ b/Source/ViewModels/IMultiPageViewModel.cs @@ -1,14 +1,33 @@ -using System; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// Interface for vew models that can switch between different pages - public interface IMultiPageViewModel { - - /// Retrieves (and, if needed, creates) the view model for the active page - /// A view model for the active page on the multi-page view model - object GetActivePageViewModel(); - - } - -} // namespace Nuclex.Windows.Forms.ViewModels +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// Interface for vew models that can switch between different pages + public interface IMultiPageViewModel { + + /// Retrieves (and, if needed, creates) the view model for the active page + /// A view model for the active page on the multi-page view model + object GetActivePageViewModel(); + + } + +} // namespace Nuclex.Windows.Forms.ViewModels diff --git a/Source/ViewModels/MultiPageViewModel.cs b/Source/ViewModels/MultiPageViewModel.cs index 355a7cb..05f310d 100644 --- a/Source/ViewModels/MultiPageViewModel.cs +++ b/Source/ViewModels/MultiPageViewModel.cs @@ -1,130 +1,129 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Concurrent; - -using Nuclex.Support; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// Base class for view models that have multiple child view models - /// Enum type by which pages can be indicated - public abstract class MultiPageViewModel : - Observable, IMultiPageViewModel, IDisposable { - - /// Initializes a new multi-page view model - /// - /// Window manager the view model uses to create child views - /// - /// - /// Whether child view models will be kept alive and reused - /// - public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) { - this.windowManager = windowManager; - if(cachePageViewModels) { - this.cachedViewModels = new ConcurrentDictionary(); - } - } - - /// Immediately releases all resources owned by the instance - public virtual void Dispose() { - if(this.cachedViewModels != null) { - foreach(object cacheViewModel in this.cachedViewModels.Values) { - disposeIfSupported(cacheViewModel); - } - this.activePageViewModel = null; - - this.cachedViewModels.Clear(); - this.cachedViewModels = null; - } else if(this.activePageViewModel != null) { - disposeIfSupported(this.activePageViewModel); - this.activePageViewModel = null; - } - } - - /// Child page that is currently being displayed by the view model - public TPageEnumeration ActivePage { - get { return this.activePage; } - set { - if(!this.activePage.Equals(value)) { - this.activePage = value; - if(this.activePageViewModel != null) { - if(this.cachedViewModels == null) { - disposeIfSupported(this.activePageViewModel); - } - this.activePageViewModel = null; - } - OnPropertyChanged(nameof(ActivePage)); - } - } - } - - /// Retrieves (and, if needed, creates) the view model for the active page - /// A view model for the active page on the multi-page view model - public object GetActivePageViewModel() { - if(this.cachedViewModels == null) { - if(this.activePageViewModel == null) { - this.activePageViewModel = CreateViewModelForPage(this.activePage); - } - } else if(this.activePageViewModel == null) { - this.activePageViewModel = this.cachedViewModels.GetOrAdd( - this.activePage, - delegate(TPageEnumeration activePage) { - return CreateViewModelForPage(this.activePage); - } - ); - } - - return this.activePageViewModel; - } - - /// Windowmanager that can create view models and display other views - protected IWindowManager WindowManager { - get { return this.windowManager; } - } - - /// Creates a view model for the specified page - /// Page for which a view model will be created - /// The view model for the specified page - protected abstract object CreateViewModelForPage(TPageEnumeration page); - - /// Disposes the specified object if it is disposable - /// Object that will be disposed if supported - private static void disposeIfSupported(object potentiallyDisposable) { - var disposable = potentiallyDisposable as IDisposable; - if(disposable != null) { - disposable.Dispose(); - } - } - - /// Page that is currently active in the multi-page view model - private TPageEnumeration activePage; - /// Window manager that can be used to display other views - private IWindowManager windowManager; - - /// View model for the active page - private object activePageViewModel; - /// Cached page view models, if caching is enabled - private ConcurrentDictionary cachedViewModels; - - } - -} // namespace Nuclex.Windows.Forms.ViewModels +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Concurrent; + +using Nuclex.Support; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// Base class for view models that have multiple child view models + /// Enum type by which pages can be indicated + public abstract class MultiPageViewModel : + Observable, IMultiPageViewModel, IDisposable { + + /// Initializes a new multi-page view model + /// + /// Window manager the view model uses to create child views + /// + /// + /// Whether child view models will be kept alive and reused + /// + public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) { + this.windowManager = windowManager; + if(cachePageViewModels) { + this.cachedViewModels = new ConcurrentDictionary(); + } + } + + /// Immediately releases all resources owned by the instance + public virtual void Dispose() { + if(this.cachedViewModels != null) { + foreach(object cacheViewModel in this.cachedViewModels.Values) { + disposeIfSupported(cacheViewModel); + } + this.activePageViewModel = null; + + this.cachedViewModels.Clear(); + this.cachedViewModels = null; + } else if(this.activePageViewModel != null) { + disposeIfSupported(this.activePageViewModel); + this.activePageViewModel = null; + } + } + + /// Child page that is currently being displayed by the view model + public TPageEnumeration ActivePage { + get { return this.activePage; } + set { + if(!this.activePage.Equals(value)) { + this.activePage = value; + if(this.activePageViewModel != null) { + if(this.cachedViewModels == null) { + disposeIfSupported(this.activePageViewModel); + } + this.activePageViewModel = null; + } + OnPropertyChanged(nameof(ActivePage)); + } + } + } + + /// Retrieves (and, if needed, creates) the view model for the active page + /// A view model for the active page on the multi-page view model + public object GetActivePageViewModel() { + if(this.cachedViewModels == null) { + if(this.activePageViewModel == null) { + this.activePageViewModel = CreateViewModelForPage(this.activePage); + } + } else if(this.activePageViewModel == null) { + this.activePageViewModel = this.cachedViewModels.GetOrAdd( + this.activePage, + delegate(TPageEnumeration activePage) { + return CreateViewModelForPage(this.activePage); + } + ); + } + + return this.activePageViewModel; + } + + /// Windowmanager that can create view models and display other views + protected IWindowManager WindowManager { + get { return this.windowManager; } + } + + /// Creates a view model for the specified page + /// Page for which a view model will be created + /// The view model for the specified page + protected abstract object CreateViewModelForPage(TPageEnumeration page); + + /// Disposes the specified object if it is disposable + /// Object that will be disposed if supported + private static void disposeIfSupported(object potentiallyDisposable) { + var disposable = potentiallyDisposable as IDisposable; + if(disposable != null) { + disposable.Dispose(); + } + } + + /// Page that is currently active in the multi-page view model + private TPageEnumeration activePage; + /// Window manager that can be used to display other views + private IWindowManager windowManager; + + /// View model for the active page + private object activePageViewModel; + /// Cached page view models, if caching is enabled + private ConcurrentDictionary cachedViewModels; + + } + +} // namespace Nuclex.Windows.Forms.ViewModels diff --git a/Source/ViewModels/ThreadedAction.Test.cs b/Source/ViewModels/ThreadedAction.Test.cs index b00929f..606385a 100644 --- a/Source/ViewModels/ThreadedAction.Test.cs +++ b/Source/ViewModels/ThreadedAction.Test.cs @@ -1,271 +1,270 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.ComponentModel; -using System.Threading; - -using NUnit.Framework; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// Unit test for the threaded action class - [TestFixture] - public class ThreadedActionTest { - - #region class DummyContext - - /// Synchronization context that does absolutely nothing - private class DummyContext : ISynchronizeInvoke { - - #region class SimpleAsyncResult - - /// Barebones implementation of an asynchronous result - private class SimpleAsyncResult : IAsyncResult { - - /// Ehether the asynchronous operation is complete - /// - /// Always true because it completes synchronously - /// - public bool IsCompleted { get { return true; } } - - /// - /// Wait handle that can be used to wait for the asynchronous operation - /// - public WaitHandle AsyncWaitHandle { - get { throw new NotImplementedException("Not implemented"); } - } - - /// Custom state that can be used to pass information around - public object AsyncState { - get { throw new NotImplementedException("Not implemented"); } - } - - /// Whether the asynchronous operation completed synchronously - public bool CompletedSynchronously { get { return true; } } - - /// The value returned from the asynchronous operation - public object ReturnedValue; - - } - - #endregion // class SimpleAsyncResult - - /// Whether the calling thread needs to use Invoke() - public bool InvokeRequired { - get { return true; } - } - - /// Schedules the specified method for execution in the target thread - /// Method the target thread will execute when it is idle - /// Arguments that will be passed to the method - /// - /// An asynchronous result handle that can be used to check on the status of - /// the call and wait for its completion - /// - public IAsyncResult BeginInvoke(Delegate method, object[] arguments) { - var asyncResult = new SimpleAsyncResult(); - asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments); - return asyncResult; - } - - /// Waits for the asychronous call to complete - /// - /// Asynchronous result handle returned by the method - /// - /// The original result returned by the asychronously called method - public object EndInvoke(IAsyncResult result) { - return ((SimpleAsyncResult)result).ReturnedValue; - } - - /// - /// Schedules the specified method for execution in the target thread and waits - /// for it to complete - /// - /// Method that will be executed by the target thread - /// Arguments that will be passed to the method - /// The result returned by the specified method - public object Invoke(Delegate method, object[] arguments) { - return method.Method.Invoke(method.Target, arguments); - } - - } - - #endregion // class DummyContext - - #region class DummyThreadedAction - - /// Implementation of a threaded action for the unit test - private class DummyThreadedAction : ThreadedAction { - - /// - /// Initializes a new threaded action, letting the base class figure out the UI thread - /// - public DummyThreadedAction() : base() { - this.finishedGate = new ManualResetEvent(initialState: false); - } - - /// - /// Initializes a new view model using the specified UI context explicitly - /// - public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) { - this.finishedGate = new ManualResetEvent(initialState: false); - } - - /// Immediately releases all resources owned by the instance - public override void Dispose() { - base.Dispose(); - - if(this.finishedGate != null) { - this.finishedGate.Dispose(); - this.finishedGate = null; - } - } - - /// Waits until the first background operation is finished - /// - /// True if the background operation is finished, false if it is ongoing - /// - public bool WaitUntilFinished() { - return this.finishedGate.WaitOne(100); - } - - /// Selects the value that will be assigned when the action runs - /// Value the action will assigned when it runs - public void SetValueToAssign(int valueToAssign) { - this.valueToAssign = valueToAssign; - } - - /// Sets up an error the action will fail with when run - /// Error the action will fail with - public void SetErrorToFailWith(Exception errorToFailWith) { - this.errorToFailWith = errorToFailWith; - } - - /// Last error that was reported by the threaded view model - public Exception ReportedError { - get { return this.reportedError; } - } - - /// Value that has been assigned from the background thread - public int AssignedValue { - get { return this.assignedValue; } - } - - /// Executes the threaded action from the background thread - /// Token by which execution can be canceled - protected override void Run(CancellationToken cancellationToken) { - if(this.errorToFailWith != null) { - throw this.errorToFailWith; - } - - this.assignedValue = this.valueToAssign; - this.finishedGate.Set(); - } - - /// Called when an error occurs in the background thread - /// Exception that was thrown in the background thread - protected override void ReportError(Exception exception) { - this.reportedError = exception; - this.finishedGate.Set(); - } - - /// Error the action will fail with, if set - private Exception errorToFailWith; - /// Value the action will assign to its same-named field - private int valueToAssign; - - /// Last error that was reported by the threaded view model - private volatile Exception reportedError; - /// Triggered when the - private ManualResetEvent finishedGate; - /// Value that is assigned through the background thread - private volatile int assignedValue; - - } - - #endregion // class DummyThreadedAction - - /// Verifies that the threaded action has a default constructor - [Test, Explicit] - public void HasDefaultConstructor() { - using(var mainForm = new System.Windows.Forms.Form()) { - mainForm.Show(); - try { - mainForm.Visible = false; - using(new DummyThreadedAction()) { } - } - finally { - mainForm.Close(); - } - } - } - - /// - /// Verifies that the threaded action can be constructed with a custom UI context - /// - [Test] - public void HasCustomSychronizationContextConstructor() { - using(new DummyThreadedAction(new DummyContext())) { } - } - - /// Checks that a new threadd action starts out idle and not busy - [Test] - public void NewInstanceIsNotBusy() { - using(var action = new DummyThreadedAction(new DummyContext())) { - Assert.IsFalse(action.IsBusy); - } - } - - /// - /// Verifies that errors happening in the background processing threads are - /// reported to the main thread - /// - [Test] - public void ErrorsInBackgroundThreadAreReported() { - using(var action = new DummyThreadedAction(new DummyContext())) { - var testError = new ArgumentException("Mooh"); - action.SetErrorToFailWith(testError); - action.Start(); - action.WaitUntilFinished(); - Assert.AreSame(testError, action.ReportedError); - } - } - - /// - /// Verifies that the background thread actually executes and can do work - /// - [Test] - public void BackgroundThreadExecutesTasks() { - using(var action = new DummyThreadedAction(new DummyContext())) { - action.SetValueToAssign(42001); - action.Start(); - action.WaitUntilFinished(); - Assert.AreEqual(42001, action.AssignedValue); - } - } - - } - -} // namespace Nuclex.Windows.Forms.ViewModels - +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.ComponentModel; +using System.Threading; + +using NUnit.Framework; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// Unit test for the threaded action class + [TestFixture] + public class ThreadedActionTest { + + #region class DummyContext + + /// Synchronization context that does absolutely nothing + private class DummyContext : ISynchronizeInvoke { + + #region class SimpleAsyncResult + + /// Barebones implementation of an asynchronous result + private class SimpleAsyncResult : IAsyncResult { + + /// Ehether the asynchronous operation is complete + /// + /// Always true because it completes synchronously + /// + public bool IsCompleted { get { return true; } } + + /// + /// Wait handle that can be used to wait for the asynchronous operation + /// + public WaitHandle AsyncWaitHandle { + get { throw new NotImplementedException("Not implemented"); } + } + + /// Custom state that can be used to pass information around + public object AsyncState { + get { throw new NotImplementedException("Not implemented"); } + } + + /// Whether the asynchronous operation completed synchronously + public bool CompletedSynchronously { get { return true; } } + + /// The value returned from the asynchronous operation + public object ReturnedValue; + + } + + #endregion // class SimpleAsyncResult + + /// Whether the calling thread needs to use Invoke() + public bool InvokeRequired { + get { return true; } + } + + /// Schedules the specified method for execution in the target thread + /// Method the target thread will execute when it is idle + /// Arguments that will be passed to the method + /// + /// An asynchronous result handle that can be used to check on the status of + /// the call and wait for its completion + /// + public IAsyncResult BeginInvoke(Delegate method, object[] arguments) { + var asyncResult = new SimpleAsyncResult(); + asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments); + return asyncResult; + } + + /// Waits for the asychronous call to complete + /// + /// Asynchronous result handle returned by the method + /// + /// The original result returned by the asychronously called method + public object EndInvoke(IAsyncResult result) { + return ((SimpleAsyncResult)result).ReturnedValue; + } + + /// + /// Schedules the specified method for execution in the target thread and waits + /// for it to complete + /// + /// Method that will be executed by the target thread + /// Arguments that will be passed to the method + /// The result returned by the specified method + public object Invoke(Delegate method, object[] arguments) { + return method.Method.Invoke(method.Target, arguments); + } + + } + + #endregion // class DummyContext + + #region class DummyThreadedAction + + /// Implementation of a threaded action for the unit test + private class DummyThreadedAction : ThreadedAction { + + /// + /// Initializes a new threaded action, letting the base class figure out the UI thread + /// + public DummyThreadedAction() : base() { + this.finishedGate = new ManualResetEvent(initialState: false); + } + + /// + /// Initializes a new view model using the specified UI context explicitly + /// + public DummyThreadedAction(ISynchronizeInvoke uiContext) : base(uiContext) { + this.finishedGate = new ManualResetEvent(initialState: false); + } + + /// Immediately releases all resources owned by the instance + public override void Dispose() { + base.Dispose(); + + if(this.finishedGate != null) { + this.finishedGate.Dispose(); + this.finishedGate = null; + } + } + + /// Waits until the first background operation is finished + /// + /// True if the background operation is finished, false if it is ongoing + /// + public bool WaitUntilFinished() { + return this.finishedGate.WaitOne(100); + } + + /// Selects the value that will be assigned when the action runs + /// Value the action will assigned when it runs + public void SetValueToAssign(int valueToAssign) { + this.valueToAssign = valueToAssign; + } + + /// Sets up an error the action will fail with when run + /// Error the action will fail with + public void SetErrorToFailWith(Exception errorToFailWith) { + this.errorToFailWith = errorToFailWith; + } + + /// Last error that was reported by the threaded view model + public Exception ReportedError { + get { return this.reportedError; } + } + + /// Value that has been assigned from the background thread + public int AssignedValue { + get { return this.assignedValue; } + } + + /// Executes the threaded action from the background thread + /// Token by which execution can be canceled + protected override void Run(CancellationToken cancellationToken) { + if(this.errorToFailWith != null) { + throw this.errorToFailWith; + } + + this.assignedValue = this.valueToAssign; + this.finishedGate.Set(); + } + + /// Called when an error occurs in the background thread + /// Exception that was thrown in the background thread + protected override void ReportError(Exception exception) { + this.reportedError = exception; + this.finishedGate.Set(); + } + + /// Error the action will fail with, if set + private Exception errorToFailWith; + /// Value the action will assign to its same-named field + private int valueToAssign; + + /// Last error that was reported by the threaded view model + private volatile Exception reportedError; + /// Triggered when the + private ManualResetEvent finishedGate; + /// Value that is assigned through the background thread + private volatile int assignedValue; + + } + + #endregion // class DummyThreadedAction + + /// Verifies that the threaded action has a default constructor + [Test, Explicit] + public void HasDefaultConstructor() { + using(var mainForm = new System.Windows.Forms.Form()) { + mainForm.Show(); + try { + mainForm.Visible = false; + using(new DummyThreadedAction()) { } + } + finally { + mainForm.Close(); + } + } + } + + /// + /// Verifies that the threaded action can be constructed with a custom UI context + /// + [Test] + public void HasCustomSychronizationContextConstructor() { + using(new DummyThreadedAction(new DummyContext())) { } + } + + /// Checks that a new threadd action starts out idle and not busy + [Test] + public void NewInstanceIsNotBusy() { + using(var action = new DummyThreadedAction(new DummyContext())) { + Assert.IsFalse(action.IsBusy); + } + } + + /// + /// Verifies that errors happening in the background processing threads are + /// reported to the main thread + /// + [Test] + public void ErrorsInBackgroundThreadAreReported() { + using(var action = new DummyThreadedAction(new DummyContext())) { + var testError = new ArgumentException("Mooh"); + action.SetErrorToFailWith(testError); + action.Start(); + action.WaitUntilFinished(); + Assert.AreSame(testError, action.ReportedError); + } + } + + /// + /// Verifies that the background thread actually executes and can do work + /// + [Test] + public void BackgroundThreadExecutesTasks() { + using(var action = new DummyThreadedAction(new DummyContext())) { + action.SetValueToAssign(42001); + action.Start(); + action.WaitUntilFinished(); + Assert.AreEqual(42001, action.AssignedValue); + } + } + + } + +} // namespace Nuclex.Windows.Forms.ViewModels + #endif // UNITTEST \ No newline at end of file diff --git a/Source/ViewModels/ThreadedAction.cs b/Source/ViewModels/ThreadedAction.cs index 476f627..41f6fce 100644 --- a/Source/ViewModels/ThreadedAction.cs +++ b/Source/ViewModels/ThreadedAction.cs @@ -1,398 +1,397 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.ComponentModel; -using System.Threading; -using System.Windows.Forms; - -using Nuclex.Support; -using Nuclex.Support.Threading; - -// Possible problem: -// -// After Run() is called, the action may not actually run if -// it is using another thread runner and that one is cancelled. -// -// Thus, a second call to Run() has to schedule the action again, -// even if it might already be scheduled, but should also not execute -// the action a second time if is was indeed still scheduled. - -namespace Nuclex.Windows.Forms.ViewModels { - - /// Encapsulates an action that can run in a thread - /// - /// - /// Sometimes a view model wants to allow multiple actions to take place - /// at the same time. Think multiple panels on the view require updating - /// from a web service - you can make both requests at the same time - /// instead of sequentially. - /// - /// - /// This class is also of use for things that need to be done sequentially - /// by sharing the thread runner of the threaded view model. That way, - /// you still have cancellable actions you can run at will and they - /// automatically queue themselves to be executed one after another. - /// - /// - public abstract class ThreadedAction : Observable, IDisposable { - - #region class ThreadedActionThreadRunner - - /// Thread runner for the threaded action - private class ThreadedActionThreadRunner : ThreadRunner { - - /// Initializes a new thread runner for the threaded view model - public ThreadedActionThreadRunner(ThreadedAction viewModel) { - this.threadedAction = viewModel; - } - - /// Reports an error - /// Error that will be reported - protected override void ReportError(Exception exception) { - this.threadedAction.reportErrorFromThread(exception); - } - - /// Called when the status of the busy flag changes - protected override void BusyChanged() { - // Narf. Can't use this. - } - - /// View model the thread runner belongs to - private ThreadedAction threadedAction; - - } - - #endregion // class ThreadedActionThreadRunner - - /// Initializes all common fields of the instance - private ThreadedAction() { - this.callRunIfNotCancelledDelegate = new Action( - callThreadedExecuteIfNotCancelled - ); - this.reportErrorDelegate = new Action(ReportError); - } - - /// Initializes a threaded action that uses its own thread runner - public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() { - if(uiContext == null) { - this.uiContext = LateCheckedSynchronizer.GetMainWindow(); - if(this.uiContext == null) { - this.uiContext = new LateCheckedSynchronizer(updateUiContext); - } - } else { - this.uiContext = uiContext; - } - - this.ownThreadRunner = new ThreadedActionThreadRunner(this); - } - - /// - /// Initializes a threaded action that uses the view model's thread runner - /// - /// View model whose thread runner will be used - /// - /// UI dispatcher that can be used to run callbacks in the UI thread - /// - public ThreadedAction( - ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null - ) : this() { - if(uiContext == null) { - this.uiContext = LateCheckedSynchronizer.GetMainWindow(); - if(this.uiContext == null) { - this.uiContext = new LateCheckedSynchronizer(updateUiContext); - } - } else { - this.uiContext = uiContext; - } - - this.externalThreadRunner = viewModel.ThreadRunner; - } - - /// Immediately releases all resources owned by the instance - public virtual void Dispose() { - if(this.isBusy) { - Cancel(); - } - if(this.ownThreadRunner != null) { - this.ownThreadRunner.Dispose(); - this.ownThreadRunner = null; - } - if(this.currentCancellationTokenSource != null) { - this.currentCancellationTokenSource.Dispose(); - this.currentCancellationTokenSource = null; - } - } - - /// Whether the view model is currently busy executing a task - public bool IsBusy { - get { return this.isBusy; } - private set { - if(value != this.isBusy) { - this.isBusy = value; - OnPropertyChanged(nameof(IsBusy)); - } - } - } - - /// Cancels the running background task, if any - public void Cancel() { - lock(this.runningTaskSyncRoot) { - - // If the background task is not running, do nothing. This also allows - // us to avoid needless recreation of the same cancellation token source. - if(!this.isBusy) { - return; - } - - // If a task is currently running, cancel it - if(this.isRunning) { - if(this.currentCancellationTokenSource != null) { - this.currentCancellationTokenSource.Cancel(); - this.currentCancellationTokenSource = null; - } - } - - // If the task was scheduled to be repeated, we also have to mark - // the upcoming cancellation token source as canceled because the scheduled - // run will still be happening (it will just cancel out immediately). - if(this.nextCancellationTokenSource != null) { - this.nextCancellationTokenSource.Cancel(); - this.nextCancellationTokenSource = null; - } - this.isScheduledAgain = false; - - // If the task was not running, we can clear the busy state because it - // is not going to reach the running state. - if(!this.isRunning) { - this.isBusy = false; - } - - } - } - - /// - /// Starts the task, cancelling the running task before doing so - /// - public void Restart() { - bool reportBusyChange = false; - - lock(this.runningTaskSyncRoot) { - - // If we're already in the execution phase, schedule another execution right - // after this one is finished (because now, data might have changed after - // execution has finished). - if(this.isRunning) { - //System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution"); - if(this.currentCancellationTokenSource != null) { - this.currentCancellationTokenSource.Cancel(); - } - - this.currentCancellationTokenSource = this.nextCancellationTokenSource; - this.nextCancellationTokenSource = null; - this.isScheduledAgain = false; - } - - // If there's no cancellation token source, create one. If an execution - // was already scheduled and the cancellation token source is still valid, - // then reuse that in order to be able to cancel all scheduled executions. - if(this.currentCancellationTokenSource == null) { - //System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token"); - this.currentCancellationTokenSource = new CancellationTokenSource(); - } - - // Schedule another execution of the action - scheduleExecution(); - - reportBusyChange = (this.isBusy == false); - this.isBusy = true; - } - - if(reportBusyChange) { - OnPropertyChanged(nameof(IsBusy)); - } - } - - /// Starts the task - public void Start() { - bool reportBusyChange = false; - - lock(this.runningTaskSyncRoot) { - - // If we're already in the execution phase, schedule another execution right - // after this one is finished (because now, data might have changed after - // execution has finished). - if(this.isRunning) { - - // If we already created a new cancellation token source, keep it, - // otherwise create a new one for the next execution - if(!this.isScheduledAgain) { - this.nextCancellationTokenSource = new CancellationTokenSource(); - this.isScheduledAgain = true; - } - - } else { - - // If there's no cancellation token source, create one. If an execution - // was already scheduled and the cancellation token source is still valid, - // then reuse that in order to be able to cancel all scheduled executions. - if(this.currentCancellationTokenSource == null) { - this.currentCancellationTokenSource = new CancellationTokenSource(); - } - - // Schedule another execution of the action - scheduleExecution(); - - } - - reportBusyChange = (this.isBusy == false); - this.isBusy = true; - } - - if(reportBusyChange) { - OnPropertyChanged(nameof(IsBusy)); - } - } - - /// Reports an error - /// Error that will be reported - protected abstract void ReportError(Exception exception); - - /// Executes the threaded action from the background thread - /// Token by which execution can be canceled - protected abstract void Run(CancellationToken cancellationToken); - - /// - /// Calls the Run() method from the background thread and manages the flags - /// - /// - private void callThreadedExecuteIfNotCancelled( - CancellationTokenSource cancellationTokenSource - ) { - lock(this) { - if(cancellationTokenSource.Token.IsCancellationRequested) { - return; - } - - this.isRunning = true; - } - - try { - Run(cancellationTokenSource.Token); - } - finally { - bool reportBusyChange = false; - - lock(this) { - this.isRunning = false; - - // Cancel the current cancellation token because this execution may have - // been scheduled multiple times (there's no way for the Run() method to - // know if the currently scheduled execution was cancelled, so it is forced - // to reschedule on each call - accepting redundant schedules). - cancellationTokenSource.Cancel(); - - // Pick the next cancellation token source. Normally it is null, but this - // is more elegant because we can avoid an while if statement this way :) - this.currentCancellationTokenSource = nextCancellationTokenSource; - this.nextCancellationTokenSource = null; - - // If Start() was called while we were executing, another execution is required - // (because the data may have changed during the call to Start()). - if(this.isScheduledAgain) { - this.isScheduledAgain = false; - scheduleExecution(); - } else { // We're idle now - reportBusyChange = (this.isBusy == true); - this.isBusy = false; - } - } - - if(reportBusyChange) { - OnPropertyChanged(nameof(IsBusy)); - } - } - } - - /// Schedules one execution of the action - private void scheduleExecution() { - //System.Diagnostics.Debug.WriteLine("Scheduling execution"); - - ThreadRunner runner = this.externalThreadRunner; - if(runner != null) { - runner.RunInBackground( - this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource - ); - } - - runner = this.ownThreadRunner; - if(runner != null) { - runner.RunInBackground( - this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource - ); - } - } - - /// Reports an error that occurred in the runner's background thread - /// Exception that the thread has encountered - private void reportErrorFromThread(Exception exception) { - this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception }); - } - - /// Sets the UI context that will be used by the threaded action - /// The UI context the threaded action will use - private void updateUiContext(ISynchronizeInvoke uiContext) { - this.uiContext = uiContext; - } - - /// Synchronization context of the thread in which the view runs - private ISynchronizeInvoke uiContext; - /// Delegate for the ReportError() method - private Action reportErrorDelegate; - /// Delegate for the callThreadedExecuteIfNotCancelled() method - private Action callRunIfNotCancelledDelegate; - - /// Thread runner on which the action can run its background task - private ThreadedActionThreadRunner ownThreadRunner; - /// - /// External thread runner on which the action runs its background task if assigned - /// - private ThreadRunner externalThreadRunner; - - /// Synchronization root for the threaded execute method - private object runningTaskSyncRoot = new object(); - /// Used to cancel the currently running task - private CancellationTokenSource currentCancellationTokenSource; - /// Used to cancel the upcoming task if a re-run was scheduled - private CancellationTokenSource nextCancellationTokenSource; - /// Whether the background task is running or waiting to run - private volatile bool isBusy; - /// Whether execution is taking place right now - /// - /// If this flag is set and the Start() method is called, another run needs to - /// be scheduled. - /// - private bool isRunning; - /// Whether run was called while the action was already running - private bool isScheduledAgain; - - } - -} // namespace Nuclex.Windows.Forms.ViewModels +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.ComponentModel; +using System.Threading; +using System.Windows.Forms; + +using Nuclex.Support; +using Nuclex.Support.Threading; + +// Possible problem: +// +// After Run() is called, the action may not actually run if +// it is using another thread runner and that one is cancelled. +// +// Thus, a second call to Run() has to schedule the action again, +// even if it might already be scheduled, but should also not execute +// the action a second time if is was indeed still scheduled. + +namespace Nuclex.Windows.Forms.ViewModels { + + /// Encapsulates an action that can run in a thread + /// + /// + /// Sometimes a view model wants to allow multiple actions to take place + /// at the same time. Think multiple panels on the view require updating + /// from a web service - you can make both requests at the same time + /// instead of sequentially. + /// + /// + /// This class is also of use for things that need to be done sequentially + /// by sharing the thread runner of the threaded view model. That way, + /// you still have cancellable actions you can run at will and they + /// automatically queue themselves to be executed one after another. + /// + /// + public abstract class ThreadedAction : Observable, IDisposable { + + #region class ThreadedActionThreadRunner + + /// Thread runner for the threaded action + private class ThreadedActionThreadRunner : ThreadRunner { + + /// Initializes a new thread runner for the threaded view model + public ThreadedActionThreadRunner(ThreadedAction viewModel) { + this.threadedAction = viewModel; + } + + /// Reports an error + /// Error that will be reported + protected override void ReportError(Exception exception) { + this.threadedAction.reportErrorFromThread(exception); + } + + /// Called when the status of the busy flag changes + protected override void BusyChanged() { + // Narf. Can't use this. + } + + /// View model the thread runner belongs to + private ThreadedAction threadedAction; + + } + + #endregion // class ThreadedActionThreadRunner + + /// Initializes all common fields of the instance + private ThreadedAction() { + this.callRunIfNotCancelledDelegate = new Action( + callThreadedExecuteIfNotCancelled + ); + this.reportErrorDelegate = new Action(ReportError); + } + + /// Initializes a threaded action that uses its own thread runner + public ThreadedAction(ISynchronizeInvoke uiContext = null) : this() { + if(uiContext == null) { + this.uiContext = LateCheckedSynchronizer.GetMainWindow(); + if(this.uiContext == null) { + this.uiContext = new LateCheckedSynchronizer(updateUiContext); + } + } else { + this.uiContext = uiContext; + } + + this.ownThreadRunner = new ThreadedActionThreadRunner(this); + } + + /// + /// Initializes a threaded action that uses the view model's thread runner + /// + /// View model whose thread runner will be used + /// + /// UI dispatcher that can be used to run callbacks in the UI thread + /// + public ThreadedAction( + ThreadedViewModel viewModel, ISynchronizeInvoke uiContext = null + ) : this() { + if(uiContext == null) { + this.uiContext = LateCheckedSynchronizer.GetMainWindow(); + if(this.uiContext == null) { + this.uiContext = new LateCheckedSynchronizer(updateUiContext); + } + } else { + this.uiContext = uiContext; + } + + this.externalThreadRunner = viewModel.ThreadRunner; + } + + /// Immediately releases all resources owned by the instance + public virtual void Dispose() { + if(this.isBusy) { + Cancel(); + } + if(this.ownThreadRunner != null) { + this.ownThreadRunner.Dispose(); + this.ownThreadRunner = null; + } + if(this.currentCancellationTokenSource != null) { + this.currentCancellationTokenSource.Dispose(); + this.currentCancellationTokenSource = null; + } + } + + /// Whether the view model is currently busy executing a task + public bool IsBusy { + get { return this.isBusy; } + private set { + if(value != this.isBusy) { + this.isBusy = value; + OnPropertyChanged(nameof(IsBusy)); + } + } + } + + /// Cancels the running background task, if any + public void Cancel() { + lock(this.runningTaskSyncRoot) { + + // If the background task is not running, do nothing. This also allows + // us to avoid needless recreation of the same cancellation token source. + if(!this.isBusy) { + return; + } + + // If a task is currently running, cancel it + if(this.isRunning) { + if(this.currentCancellationTokenSource != null) { + this.currentCancellationTokenSource.Cancel(); + this.currentCancellationTokenSource = null; + } + } + + // If the task was scheduled to be repeated, we also have to mark + // the upcoming cancellation token source as canceled because the scheduled + // run will still be happening (it will just cancel out immediately). + if(this.nextCancellationTokenSource != null) { + this.nextCancellationTokenSource.Cancel(); + this.nextCancellationTokenSource = null; + } + this.isScheduledAgain = false; + + // If the task was not running, we can clear the busy state because it + // is not going to reach the running state. + if(!this.isRunning) { + this.isBusy = false; + } + + } + } + + /// + /// Starts the task, cancelling the running task before doing so + /// + public void Restart() { + bool reportBusyChange = false; + + lock(this.runningTaskSyncRoot) { + + // If we're already in the execution phase, schedule another execution right + // after this one is finished (because now, data might have changed after + // execution has finished). + if(this.isRunning) { + //System.Diagnostics.Debug.WriteLine("Restart() - interrupting execution"); + if(this.currentCancellationTokenSource != null) { + this.currentCancellationTokenSource.Cancel(); + } + + this.currentCancellationTokenSource = this.nextCancellationTokenSource; + this.nextCancellationTokenSource = null; + this.isScheduledAgain = false; + } + + // If there's no cancellation token source, create one. If an execution + // was already scheduled and the cancellation token source is still valid, + // then reuse that in order to be able to cancel all scheduled executions. + if(this.currentCancellationTokenSource == null) { + //System.Diagnostics.Debug.WriteLine("Restart() - creating new cancellation token"); + this.currentCancellationTokenSource = new CancellationTokenSource(); + } + + // Schedule another execution of the action + scheduleExecution(); + + reportBusyChange = (this.isBusy == false); + this.isBusy = true; + } + + if(reportBusyChange) { + OnPropertyChanged(nameof(IsBusy)); + } + } + + /// Starts the task + public void Start() { + bool reportBusyChange = false; + + lock(this.runningTaskSyncRoot) { + + // If we're already in the execution phase, schedule another execution right + // after this one is finished (because now, data might have changed after + // execution has finished). + if(this.isRunning) { + + // If we already created a new cancellation token source, keep it, + // otherwise create a new one for the next execution + if(!this.isScheduledAgain) { + this.nextCancellationTokenSource = new CancellationTokenSource(); + this.isScheduledAgain = true; + } + + } else { + + // If there's no cancellation token source, create one. If an execution + // was already scheduled and the cancellation token source is still valid, + // then reuse that in order to be able to cancel all scheduled executions. + if(this.currentCancellationTokenSource == null) { + this.currentCancellationTokenSource = new CancellationTokenSource(); + } + + // Schedule another execution of the action + scheduleExecution(); + + } + + reportBusyChange = (this.isBusy == false); + this.isBusy = true; + } + + if(reportBusyChange) { + OnPropertyChanged(nameof(IsBusy)); + } + } + + /// Reports an error + /// Error that will be reported + protected abstract void ReportError(Exception exception); + + /// Executes the threaded action from the background thread + /// Token by which execution can be canceled + protected abstract void Run(CancellationToken cancellationToken); + + /// + /// Calls the Run() method from the background thread and manages the flags + /// + /// + private void callThreadedExecuteIfNotCancelled( + CancellationTokenSource cancellationTokenSource + ) { + lock(this) { + if(cancellationTokenSource.Token.IsCancellationRequested) { + return; + } + + this.isRunning = true; + } + + try { + Run(cancellationTokenSource.Token); + } + finally { + bool reportBusyChange = false; + + lock(this) { + this.isRunning = false; + + // Cancel the current cancellation token because this execution may have + // been scheduled multiple times (there's no way for the Run() method to + // know if the currently scheduled execution was cancelled, so it is forced + // to reschedule on each call - accepting redundant schedules). + cancellationTokenSource.Cancel(); + + // Pick the next cancellation token source. Normally it is null, but this + // is more elegant because we can avoid an while if statement this way :) + this.currentCancellationTokenSource = nextCancellationTokenSource; + this.nextCancellationTokenSource = null; + + // If Start() was called while we were executing, another execution is required + // (because the data may have changed during the call to Start()). + if(this.isScheduledAgain) { + this.isScheduledAgain = false; + scheduleExecution(); + } else { // We're idle now + reportBusyChange = (this.isBusy == true); + this.isBusy = false; + } + } + + if(reportBusyChange) { + OnPropertyChanged(nameof(IsBusy)); + } + } + } + + /// Schedules one execution of the action + private void scheduleExecution() { + //System.Diagnostics.Debug.WriteLine("Scheduling execution"); + + ThreadRunner runner = this.externalThreadRunner; + if(runner != null) { + runner.RunInBackground( + this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource + ); + } + + runner = this.ownThreadRunner; + if(runner != null) { + runner.RunInBackground( + this.callRunIfNotCancelledDelegate, this.currentCancellationTokenSource + ); + } + } + + /// Reports an error that occurred in the runner's background thread + /// Exception that the thread has encountered + private void reportErrorFromThread(Exception exception) { + this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception }); + } + + /// Sets the UI context that will be used by the threaded action + /// The UI context the threaded action will use + private void updateUiContext(ISynchronizeInvoke uiContext) { + this.uiContext = uiContext; + } + + /// Synchronization context of the thread in which the view runs + private ISynchronizeInvoke uiContext; + /// Delegate for the ReportError() method + private Action reportErrorDelegate; + /// Delegate for the callThreadedExecuteIfNotCancelled() method + private Action callRunIfNotCancelledDelegate; + + /// Thread runner on which the action can run its background task + private ThreadedActionThreadRunner ownThreadRunner; + /// + /// External thread runner on which the action runs its background task if assigned + /// + private ThreadRunner externalThreadRunner; + + /// Synchronization root for the threaded execute method + private object runningTaskSyncRoot = new object(); + /// Used to cancel the currently running task + private CancellationTokenSource currentCancellationTokenSource; + /// Used to cancel the upcoming task if a re-run was scheduled + private CancellationTokenSource nextCancellationTokenSource; + /// Whether the background task is running or waiting to run + private volatile bool isBusy; + /// Whether execution is taking place right now + /// + /// If this flag is set and the Start() method is called, another run needs to + /// be scheduled. + /// + private bool isRunning; + /// Whether run was called while the action was already running + private bool isScheduledAgain; + + } + +} // namespace Nuclex.Windows.Forms.ViewModels diff --git a/Source/ViewModels/ThreadedDialogViewModel.Test.cs b/Source/ViewModels/ThreadedDialogViewModel.Test.cs index dbf3c00..5137ecf 100644 --- a/Source/ViewModels/ThreadedDialogViewModel.Test.cs +++ b/Source/ViewModels/ThreadedDialogViewModel.Test.cs @@ -1,174 +1,173 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; - -using NUnit.Framework; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// Unit test for the threaded dialog view model - [TestFixture] - public class ThreadedDialogViewModelTest { - - #region class DialogViewModelSubscriber - - /// Subscriber for the events offered by a dialog view model - private class DialogViewModelSubscriber { - - /// Indicates that the user has accepted the dialog - public void Confirmed(object sender, EventArgs arguments) { - ++this.confirmCallCount; - } - - /// Indicates that the user has cancelled the dialog - public void Cancelled(object sender, EventArgs arguments) { - ++this.cancelCallCount; - } - - /// Indicates that the dialog was simply closed - public void Submitted(object sender, EventArgs arguments) { - ++this.submitCallCount; - } - - /// How many times the Confirmed() method was called - public int ConfirmCallCount { - get { return this.confirmCallCount; } - } - - /// How many times the Cancelled() method was called - public int CancelCallCount { - get { return this.cancelCallCount; } - } - - /// How many times the Submitted() method was called - public int SubmitCallCount { - get { return this.submitCallCount; } - } - - /// How many times the Confirmed() method was called - private int confirmCallCount; - /// How many times the Cancelled() method was called - private int cancelCallCount; - /// How many times the Submitted() method was called - private int submitCallCount; - - } - - #endregion // class DialogViewModelSubscriber - - #region class TestViewModel - - private class TestViewModel : ThreadedDialogViewModel { - - public Exception ReportedError { - get { return this.reportedError; } - } - - protected override void ReportError(Exception exception) { - this.reportedError = exception; - } - - private Exception reportedError; - - } - - #endregion // class TestViewModel - - /// Verifies that the dialog view model has a default constructor - [Test] - public void HasDefaultConstructor() { - Assert.DoesNotThrow( - delegate() { new TestViewModel(); } - ); - } - - /// - /// Verifies that calling Confirm() on the dialog view model triggers - /// the 'Confirmed' event - /// - [Test] - public void ConfirmTriggersConfirmedEvent() { - var viewModel = new TestViewModel(); - var subscriber = createSubscriber(viewModel); - - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - viewModel.Confirm(); - Assert.AreEqual(1, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - } - - /// - /// Verifies that calling Cancel() on the dialog view model triggers - /// the 'Cancelled' event - /// - [Test] - public void CancelTriggersCancelledEvent() { - var viewModel = new TestViewModel(); - var subscriber = createSubscriber(viewModel); - - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - viewModel.Cancel(); - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(1, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - } - - /// - /// Verifies that calling Submitm() on the dialog view model triggers - /// the 'Submitted' event - /// - [Test] - public void SubmitTriggersSubmittedEvent() { - var viewModel = new TestViewModel(); - var subscriber = createSubscriber(viewModel); - - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(0, subscriber.SubmitCallCount); - viewModel.Submit(); - Assert.AreEqual(0, subscriber.ConfirmCallCount); - Assert.AreEqual(0, subscriber.CancelCallCount); - Assert.AreEqual(1, subscriber.SubmitCallCount); - } - - /// Constructs a new subscriber for the dialog view model's events - /// View model a subscriber will be created for - /// A subscriber for the events of the specified view model - private DialogViewModelSubscriber createSubscriber(ThreadedDialogViewModel viewModel) { - var subscriber = new DialogViewModelSubscriber(); - viewModel.Confirmed += subscriber.Confirmed; - viewModel.Canceled += subscriber.Cancelled; - viewModel.Submitted += subscriber.Submitted; - return subscriber; - } - - } - -} // namespace Nuclex.Windows.Forms.ViewModels - -#endif // UNITTEST +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; + +using NUnit.Framework; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// Unit test for the threaded dialog view model + [TestFixture] + public class ThreadedDialogViewModelTest { + + #region class DialogViewModelSubscriber + + /// Subscriber for the events offered by a dialog view model + private class DialogViewModelSubscriber { + + /// Indicates that the user has accepted the dialog + public void Confirmed(object sender, EventArgs arguments) { + ++this.confirmCallCount; + } + + /// Indicates that the user has cancelled the dialog + public void Cancelled(object sender, EventArgs arguments) { + ++this.cancelCallCount; + } + + /// Indicates that the dialog was simply closed + public void Submitted(object sender, EventArgs arguments) { + ++this.submitCallCount; + } + + /// How many times the Confirmed() method was called + public int ConfirmCallCount { + get { return this.confirmCallCount; } + } + + /// How many times the Cancelled() method was called + public int CancelCallCount { + get { return this.cancelCallCount; } + } + + /// How many times the Submitted() method was called + public int SubmitCallCount { + get { return this.submitCallCount; } + } + + /// How many times the Confirmed() method was called + private int confirmCallCount; + /// How many times the Cancelled() method was called + private int cancelCallCount; + /// How many times the Submitted() method was called + private int submitCallCount; + + } + + #endregion // class DialogViewModelSubscriber + + #region class TestViewModel + + private class TestViewModel : ThreadedDialogViewModel { + + public Exception ReportedError { + get { return this.reportedError; } + } + + protected override void ReportError(Exception exception) { + this.reportedError = exception; + } + + private Exception reportedError; + + } + + #endregion // class TestViewModel + + /// Verifies that the dialog view model has a default constructor + [Test] + public void HasDefaultConstructor() { + Assert.DoesNotThrow( + delegate() { new TestViewModel(); } + ); + } + + /// + /// Verifies that calling Confirm() on the dialog view model triggers + /// the 'Confirmed' event + /// + [Test] + public void ConfirmTriggersConfirmedEvent() { + var viewModel = new TestViewModel(); + var subscriber = createSubscriber(viewModel); + + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + viewModel.Confirm(); + Assert.AreEqual(1, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + } + + /// + /// Verifies that calling Cancel() on the dialog view model triggers + /// the 'Cancelled' event + /// + [Test] + public void CancelTriggersCancelledEvent() { + var viewModel = new TestViewModel(); + var subscriber = createSubscriber(viewModel); + + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + viewModel.Cancel(); + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(1, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + } + + /// + /// Verifies that calling Submitm() on the dialog view model triggers + /// the 'Submitted' event + /// + [Test] + public void SubmitTriggersSubmittedEvent() { + var viewModel = new TestViewModel(); + var subscriber = createSubscriber(viewModel); + + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(0, subscriber.SubmitCallCount); + viewModel.Submit(); + Assert.AreEqual(0, subscriber.ConfirmCallCount); + Assert.AreEqual(0, subscriber.CancelCallCount); + Assert.AreEqual(1, subscriber.SubmitCallCount); + } + + /// Constructs a new subscriber for the dialog view model's events + /// View model a subscriber will be created for + /// A subscriber for the events of the specified view model + private DialogViewModelSubscriber createSubscriber(ThreadedDialogViewModel viewModel) { + var subscriber = new DialogViewModelSubscriber(); + viewModel.Confirmed += subscriber.Confirmed; + viewModel.Canceled += subscriber.Cancelled; + viewModel.Submitted += subscriber.Submitted; + return subscriber; + } + + } + +} // namespace Nuclex.Windows.Forms.ViewModels + +#endif // UNITTEST diff --git a/Source/ViewModels/ThreadedDialogViewModel.cs b/Source/ViewModels/ThreadedDialogViewModel.cs index f0d8364..3ac55eb 100644 --- a/Source/ViewModels/ThreadedDialogViewModel.cs +++ b/Source/ViewModels/ThreadedDialogViewModel.cs @@ -1,74 +1,73 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// View model for a dialog that can execute tasks in a background thread - public abstract class ThreadedDialogViewModel : ThreadedViewModel { - - /// Indicates that the view should close with a positive result - /// - /// This event typically corresponds to the 'Ok' button in a dialog. - /// - public event EventHandler Confirmed; - - /// Indicates that the view should close with a negative result - /// - /// This event typically corresponds to the 'Cancel' button in a dialog. - /// - public event EventHandler Canceled; - - /// Indicates that the view should close - /// - /// 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. - /// - public event EventHandler Submitted; - - /// - /// Indicates that the dialog should be closed with a positive outcome - /// - public virtual void Confirm() { - if(Confirmed != null) { - Confirmed(this, EventArgs.Empty); - } - } - - /// - /// Indicates that the dialog should be closed with a negative outcome - /// - public virtual void Cancel() { - if(Canceled != null) { - Canceled(this, EventArgs.Empty); - } - } - - /// Indicates that the dialog should be closed - public virtual void Submit() { - if(Submitted != null) { - Submitted(this, EventArgs.Empty); - } - } - - } - -} // namespace Nuclex.Windows.Forms.ViewModels +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// View model for a dialog that can execute tasks in a background thread + public abstract class ThreadedDialogViewModel : ThreadedViewModel { + + /// Indicates that the view should close with a positive result + /// + /// This event typically corresponds to the 'Ok' button in a dialog. + /// + public event EventHandler Confirmed; + + /// Indicates that the view should close with a negative result + /// + /// This event typically corresponds to the 'Cancel' button in a dialog. + /// + public event EventHandler Canceled; + + /// Indicates that the view should close + /// + /// 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. + /// + public event EventHandler Submitted; + + /// + /// Indicates that the dialog should be closed with a positive outcome + /// + public virtual void Confirm() { + if(Confirmed != null) { + Confirmed(this, EventArgs.Empty); + } + } + + /// + /// Indicates that the dialog should be closed with a negative outcome + /// + public virtual void Cancel() { + if(Canceled != null) { + Canceled(this, EventArgs.Empty); + } + } + + /// Indicates that the dialog should be closed + public virtual void Submit() { + if(Submitted != null) { + Submitted(this, EventArgs.Empty); + } + } + + } + +} // namespace Nuclex.Windows.Forms.ViewModels diff --git a/Source/ViewModels/ThreadedViewModel.Test.cs b/Source/ViewModels/ThreadedViewModel.Test.cs index 34b9a03..c7d7828 100644 --- a/Source/ViewModels/ThreadedViewModel.Test.cs +++ b/Source/ViewModels/ThreadedViewModel.Test.cs @@ -1,262 +1,261 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -#if UNITTEST - -using System; -using System.ComponentModel; -using System.Threading; -using NUnit.Framework; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// Unit test for the threaded view model base class - [TestFixture] - public class ThreadedViewModelTest { - - #region class DummyContext - - /// Synchronization context that does absolutely nothing - private class DummyContext : ISynchronizeInvoke { - - #region class SimpleAsyncResult - - /// Barebones implementation of an asynchronous result - private class SimpleAsyncResult : IAsyncResult { - - /// Ehether the asynchronous operation is complete - /// - /// Always true because it completes synchronously - /// - public bool IsCompleted { get { return true; } } - - /// - /// Wait handle that can be used to wait for the asynchronous operation - /// - public WaitHandle AsyncWaitHandle { - get { throw new NotImplementedException("Not implemented"); } - } - - /// Custom state that can be used to pass information around - public object AsyncState { - get { throw new NotImplementedException("Not implemented"); } - } - - /// Whether the asynchronous operation completed synchronously - public bool CompletedSynchronously { get { return true; } } - - /// The value returned from the asynchronous operation - public object ReturnedValue; - - } - - #endregion // class SimpleAsyncResult - - /// Whether the calling thread needs to use Invoke() - public bool InvokeRequired { - get { return true; } - } - - /// Schedules the specified method for execution in the target thread - /// Method the target thread will execute when it is idle - /// Arguments that will be passed to the method - /// - /// An asynchronous result handle that can be used to check on the status of - /// the call and wait for its completion - /// - public IAsyncResult BeginInvoke(Delegate method, object[] arguments) { - var asyncResult = new SimpleAsyncResult(); - asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments); - return asyncResult; - } - - /// Waits for the asychronous call to complete - /// - /// Asynchronous result handle returned by the method - /// - /// The original result returned by the asychronously called method - public object EndInvoke(IAsyncResult result) { - return ((SimpleAsyncResult)result).ReturnedValue; - } - - /// - /// Schedules the specified method for execution in the target thread and waits - /// for it to complete - /// - /// Method that will be executed by the target thread - /// Arguments that will be passed to the method - /// The result returned by the specified method - public object Invoke(Delegate method, object[] arguments) { - return method.Method.Invoke(method.Target, arguments); - } - - } - - #endregion // class DummyContext - - #region class TestViewModel - - /// View model used to unit test the threaded view model base class - private class TestViewModel : ThreadedViewModel { - - /// - /// Initializes a new view model, letting the base class figure out the UI thread - /// - public TestViewModel() : base() { - this.finishedGate = new ManualResetEvent(initialState: false); - } - - /// - /// Initializes a new view model, using the specified context for the UI thread - /// - /// Synchronization context of the UI thread - public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) { - this.finishedGate = new ManualResetEvent(initialState: false); - } - - /// Immediately releases all resources owned by the instance - public override void Dispose() { - base.Dispose(); - - if(this.finishedGate != null) { - this.finishedGate.Dispose(); - this.finishedGate = null; - } - } - - /// Waits until the first background operation is finished - /// - /// True if the background operation is finished, false if it is ongoing - /// - public bool WaitUntilFinished() { - return this.finishedGate.WaitOne(100); - } - - /// Runs a background process that causes the specified error - /// Error that will be caused in the background process - public void CauseErrorInBackgroundThread(Exception error) { - RunInBackground( - delegate() { throw error; } - ); - } - - /// - /// Assigns the specified value to the same-named property from a background thread - /// - /// Value that will be assigned to the same-named property - public void AssignValueInBackgroundThread(int value) { - RunInBackground( - delegate () { - this.assignedValue = value; - this.finishedGate.Set(); - } - ); - } - - /// Last error that was reported by the threaded view model - public Exception ReportedError { - get { return this.reportedError; } - } - - /// Value that has been assigned from the background thread - public int AssignedValue { - get { return this.assignedValue; } - } - - /// Called when an error occurs in the background thread - /// Exception that was thrown in the background thread - protected override void ReportError(Exception exception) { - this.reportedError = exception; - this.finishedGate.Set(); - } - - /// Last error that was reported by the threaded view model - private volatile Exception reportedError; - /// Triggered when the - private ManualResetEvent finishedGate; - /// Value that is assigned through the background thread - private volatile int assignedValue; - - } - - #endregion // class TestViewModel - - /// Verifies that the threaded view model has a default constructor - [Test, Explicit] - public void HasDefaultConstructor() { - using(var mainForm = new System.Windows.Forms.Form()) { - mainForm.Show(); - try { - mainForm.Visible = false; - using(new TestViewModel()) { } - } - finally { - mainForm.Close(); - } - } - } - - /// - /// Verifies that the threaded view model can be constructed with a custom UI context - /// - [Test] - public void HasCustomSychronizationContextConstructor() { - using(new TestViewModel(new DummyContext())) { } - } - - /// Checks that a new view model starts out idle and not busy - [Test] - public void NewInstanceIsNotBusy() { - using(var viewModel = new TestViewModel(new DummyContext())) { - Assert.IsFalse(viewModel.IsBusy); - } - } - - /// - /// Verifies that errors happening in the background processing threads are - /// reported to the main thread - /// - [Test] - public void ErrorsInBackgroundThreadAreReported() { - using(var viewModel = new TestViewModel(new DummyContext())) { - var testError = new ArgumentException("Mooh"); - viewModel.CauseErrorInBackgroundThread(testError); - viewModel.WaitUntilFinished(); - Assert.AreSame(testError, viewModel.ReportedError); - } - } - - /// - /// Verifies that the background thread actually executes and can do work - /// - [Test] - public void BackgroundThreadExecutesTasks() { - using(var viewModel = new TestViewModel(new DummyContext())) { - viewModel.AssignValueInBackgroundThread(10042); - viewModel.WaitUntilFinished(); - Assert.AreEqual(10042, viewModel.AssignedValue); - } - } - - } - -} // namespace Nuclex.Windows.Forms.ViewModels - +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +#if UNITTEST + +using System; +using System.ComponentModel; +using System.Threading; +using NUnit.Framework; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// Unit test for the threaded view model base class + [TestFixture] + public class ThreadedViewModelTest { + + #region class DummyContext + + /// Synchronization context that does absolutely nothing + private class DummyContext : ISynchronizeInvoke { + + #region class SimpleAsyncResult + + /// Barebones implementation of an asynchronous result + private class SimpleAsyncResult : IAsyncResult { + + /// Ehether the asynchronous operation is complete + /// + /// Always true because it completes synchronously + /// + public bool IsCompleted { get { return true; } } + + /// + /// Wait handle that can be used to wait for the asynchronous operation + /// + public WaitHandle AsyncWaitHandle { + get { throw new NotImplementedException("Not implemented"); } + } + + /// Custom state that can be used to pass information around + public object AsyncState { + get { throw new NotImplementedException("Not implemented"); } + } + + /// Whether the asynchronous operation completed synchronously + public bool CompletedSynchronously { get { return true; } } + + /// The value returned from the asynchronous operation + public object ReturnedValue; + + } + + #endregion // class SimpleAsyncResult + + /// Whether the calling thread needs to use Invoke() + public bool InvokeRequired { + get { return true; } + } + + /// Schedules the specified method for execution in the target thread + /// Method the target thread will execute when it is idle + /// Arguments that will be passed to the method + /// + /// An asynchronous result handle that can be used to check on the status of + /// the call and wait for its completion + /// + public IAsyncResult BeginInvoke(Delegate method, object[] arguments) { + var asyncResult = new SimpleAsyncResult(); + asyncResult.ReturnedValue = method.Method.Invoke(method.Target, arguments); + return asyncResult; + } + + /// Waits for the asychronous call to complete + /// + /// Asynchronous result handle returned by the method + /// + /// The original result returned by the asychronously called method + public object EndInvoke(IAsyncResult result) { + return ((SimpleAsyncResult)result).ReturnedValue; + } + + /// + /// Schedules the specified method for execution in the target thread and waits + /// for it to complete + /// + /// Method that will be executed by the target thread + /// Arguments that will be passed to the method + /// The result returned by the specified method + public object Invoke(Delegate method, object[] arguments) { + return method.Method.Invoke(method.Target, arguments); + } + + } + + #endregion // class DummyContext + + #region class TestViewModel + + /// View model used to unit test the threaded view model base class + private class TestViewModel : ThreadedViewModel { + + /// + /// Initializes a new view model, letting the base class figure out the UI thread + /// + public TestViewModel() : base() { + this.finishedGate = new ManualResetEvent(initialState: false); + } + + /// + /// Initializes a new view model, using the specified context for the UI thread + /// + /// Synchronization context of the UI thread + public TestViewModel(ISynchronizeInvoke uiContext) : base(uiContext) { + this.finishedGate = new ManualResetEvent(initialState: false); + } + + /// Immediately releases all resources owned by the instance + public override void Dispose() { + base.Dispose(); + + if(this.finishedGate != null) { + this.finishedGate.Dispose(); + this.finishedGate = null; + } + } + + /// Waits until the first background operation is finished + /// + /// True if the background operation is finished, false if it is ongoing + /// + public bool WaitUntilFinished() { + return this.finishedGate.WaitOne(100); + } + + /// Runs a background process that causes the specified error + /// Error that will be caused in the background process + public void CauseErrorInBackgroundThread(Exception error) { + RunInBackground( + delegate() { throw error; } + ); + } + + /// + /// Assigns the specified value to the same-named property from a background thread + /// + /// Value that will be assigned to the same-named property + public void AssignValueInBackgroundThread(int value) { + RunInBackground( + delegate () { + this.assignedValue = value; + this.finishedGate.Set(); + } + ); + } + + /// Last error that was reported by the threaded view model + public Exception ReportedError { + get { return this.reportedError; } + } + + /// Value that has been assigned from the background thread + public int AssignedValue { + get { return this.assignedValue; } + } + + /// Called when an error occurs in the background thread + /// Exception that was thrown in the background thread + protected override void ReportError(Exception exception) { + this.reportedError = exception; + this.finishedGate.Set(); + } + + /// Last error that was reported by the threaded view model + private volatile Exception reportedError; + /// Triggered when the + private ManualResetEvent finishedGate; + /// Value that is assigned through the background thread + private volatile int assignedValue; + + } + + #endregion // class TestViewModel + + /// Verifies that the threaded view model has a default constructor + [Test, Explicit] + public void HasDefaultConstructor() { + using(var mainForm = new System.Windows.Forms.Form()) { + mainForm.Show(); + try { + mainForm.Visible = false; + using(new TestViewModel()) { } + } + finally { + mainForm.Close(); + } + } + } + + /// + /// Verifies that the threaded view model can be constructed with a custom UI context + /// + [Test] + public void HasCustomSychronizationContextConstructor() { + using(new TestViewModel(new DummyContext())) { } + } + + /// Checks that a new view model starts out idle and not busy + [Test] + public void NewInstanceIsNotBusy() { + using(var viewModel = new TestViewModel(new DummyContext())) { + Assert.IsFalse(viewModel.IsBusy); + } + } + + /// + /// Verifies that errors happening in the background processing threads are + /// reported to the main thread + /// + [Test] + public void ErrorsInBackgroundThreadAreReported() { + using(var viewModel = new TestViewModel(new DummyContext())) { + var testError = new ArgumentException("Mooh"); + viewModel.CauseErrorInBackgroundThread(testError); + viewModel.WaitUntilFinished(); + Assert.AreSame(testError, viewModel.ReportedError); + } + } + + /// + /// Verifies that the background thread actually executes and can do work + /// + [Test] + public void BackgroundThreadExecutesTasks() { + using(var viewModel = new TestViewModel(new DummyContext())) { + viewModel.AssignValueInBackgroundThread(10042); + viewModel.WaitUntilFinished(); + Assert.AreEqual(10042, viewModel.AssignedValue); + } + } + + } + +} // namespace Nuclex.Windows.Forms.ViewModels + #endif // UNITTEST \ No newline at end of file diff --git a/Source/ViewModels/ThreadedViewModel.cs b/Source/ViewModels/ThreadedViewModel.cs index 06a4ed0..9ed0cbc 100644 --- a/Source/ViewModels/ThreadedViewModel.cs +++ b/Source/ViewModels/ThreadedViewModel.cs @@ -1,232 +1,231 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.ComponentModel; -using System.Windows.Forms; - -using Nuclex.Support; -using Nuclex.Support.Threading; - -namespace Nuclex.Windows.Forms.ViewModels { - - /// View model that can execute tasks in a background thread - public abstract class ThreadedViewModel : Observable, IDisposable { - - #region class ViewModelThreadRunner - - /// Thread runner for the threaded view model - private class ViewModelThreadRunner : ThreadRunner { - - /// Initializes a new thread runner for the threaded view model - public ViewModelThreadRunner(ThreadedViewModel viewModel) { - this.viewModel = viewModel; - } - - /// Reports an error - /// Error that will be reported - protected override void ReportError(Exception exception) { - this.viewModel.reportErrorFromThread(exception); - } - - /// Called when the status of the busy flag changes - protected override void BusyChanged() { - this.viewModel.OnIsBusyChanged(); - } - - /// View model the thread runner belongs to - private ThreadedViewModel viewModel; - - } - - #endregion // class ViewModelThreadRunner - - /// Initializes a new view model for background processing - /// - /// UI dispatcher that can be used to run callbacks in the UI thread - /// - protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) { - if(uiContext == null) { - this.uiContext = LateCheckedSynchronizer.GetMainWindow(); - if(this.uiContext == null) { - this.uiContext = new LateCheckedSynchronizer(updateUiContext); - } - } else { - this.uiContext = uiContext; - } - - this.reportErrorDelegate = new Action(ReportError); - - this.threadRunner = new ViewModelThreadRunner(this); - } - - /// Immediately releases all resources owned by the instance - public virtual void Dispose() { - if(this.threadRunner != null) { - this.threadRunner.Dispose(); - this.threadRunner = null; - } - } - - /// Whether the view model is currently busy executing a task - public bool IsBusy { - get { return this.threadRunner.IsBusy; } - } - - /// Reports an error to the user - /// Error that will be reported - /// - /// - /// 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). - /// - /// - /// When is used, this method will also - /// be called in case an exception within the asynchronously running code goes unhandled. - /// This choice was made because, in the context of UI code, you would wrap any - /// operations that might fail in a try..catch pair anyway in order to inform - /// the user instead of aborting the entire application. - /// - /// - protected abstract void ReportError(Exception exception); - - /// Executes the specified operation in the background - /// Action that will be executed in the background - protected void RunInBackground(Action action) { - this.threadRunner.RunInBackground(action); - } - - /// Executes the specified operation in the background - /// Action that will be executed in the background - protected void RunInBackground(CancellableAction action) { - this.threadRunner.RunInBackground(action); - } - - /// Executes the specified operation in the background - /// Action that will be executed in the background - /// Parameter that will be passed to the action - protected void RunInBackground(Action action, P1 parameter1) { - this.threadRunner.RunInBackground(action, parameter1); - } - - /// Executes the specified operation in the background - /// Action that will be executed in the background - /// Parameter that will be passed to the action - protected void RunInBackground(CancellableAction action, P1 parameter1) { - this.threadRunner.RunInBackground(action, parameter1); - } - - /// Executes the specified operation in the background - /// Action that will be executed in the background - /// First parameter that will be passed to the action - /// Second parameter that will be passed to the action - protected void RunInBackground(Action action, P1 parameter1, P2 parameter2) { - this.threadRunner.RunInBackground(action, parameter1, parameter2); - } - - /// Executes the specified operation in the background - /// Action that will be executed in the background - /// First parameter that will be passed to the action - /// Second parameter that will be passed to the action - protected void RunInBackground( - CancellableAction action, P1 parameter1, P2 parameter2 - ) { - this.threadRunner.RunInBackground(action, parameter1, parameter2); - } - - /// Cancels the currently running background operation - protected void CancelBackgroundOperation() { - this.threadRunner.CancelBackgroundOperation(); - } - - /// Cancels all queued and the currently running background operation - protected void CancelAllBackgroundOperations() { - this.threadRunner.CancelAllBackgroundOperations(); - } - - /// Whether the background operation has been cancelled - //[Obsolete("Please use a method accepting a cancellation token instead of using this")] - protected bool IsBackgroundOperationCancelled { - get { return this.threadRunner.IsBackgroundOperationCancelled; } - } - - /// Throws an exception if the background operation was cancelled - //[Obsolete("Please use a method accepting a cancellation token instead of using this")] - protected void ThrowIfBackgroundOperationCancelled() { - this.threadRunner.ThrowIfBackgroundOperationCancelled(); - } - - /// Executes the specified action in the UI thread - /// Action that will be executed in the UI thread - protected void RunInUIThread(Action action) { - this.uiContext.Invoke(action, EmptyObjectArray); - } - - /// Executes the specified action in the UI thread - /// Action that will be executed in the UI thread - /// Parameter that will be passed to the action - protected void RunInUIThread(Action action, P1 parameter1) { - this.uiContext.Invoke(action, new object[1] { parameter1 }); - } - - /// Executes the specified action in the UI thread - /// Action that will be executed in the UI thread - /// First parameter that will be passed to the action - /// Second parameter that will be passed to the action - protected void RunInUIThread(Action action, P1 parameter1, P2 parameter2) { - this.uiContext.Invoke(action, new object[2] { parameter1, parameter2 }); - } - - /// Called when the thread runner's busy flag changes - protected virtual void OnIsBusyChanged() { - OnPropertyChanged(nameof(IsBusy)); - } - - // For the ThreadedAction class - there should be a better way! - /// Thread runner that manages the view model's thread - internal ThreadRunner ThreadRunner { - get { return this.threadRunner; } - } - - /// Reports an error that occurred in the runner's background thread - /// Exception that the thread has encountered - private void reportErrorFromThread(Exception exception) { - this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception }); - } - - /// Sets the UI context that will be used by the threaded action - /// The UI context the threaded action will use - private void updateUiContext(ISynchronizeInvoke uiContext) { - this.uiContext = uiContext; - } - - /// An array of zero objects - private static readonly object[] EmptyObjectArray = new object[0]; - - /// UI dispatcher of the thread in which the view runs - private ISynchronizeInvoke uiContext; - /// Delegate for the ReportError() method - private Action reportErrorDelegate; - /// Thread runner that manages the view model's thread - private ViewModelThreadRunner threadRunner; - - } - -} // namespace Nuclex.Windows.Forms.ViewModels +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.ComponentModel; +using System.Windows.Forms; + +using Nuclex.Support; +using Nuclex.Support.Threading; + +namespace Nuclex.Windows.Forms.ViewModels { + + /// View model that can execute tasks in a background thread + public abstract class ThreadedViewModel : Observable, IDisposable { + + #region class ViewModelThreadRunner + + /// Thread runner for the threaded view model + private class ViewModelThreadRunner : ThreadRunner { + + /// Initializes a new thread runner for the threaded view model + public ViewModelThreadRunner(ThreadedViewModel viewModel) { + this.viewModel = viewModel; + } + + /// Reports an error + /// Error that will be reported + protected override void ReportError(Exception exception) { + this.viewModel.reportErrorFromThread(exception); + } + + /// Called when the status of the busy flag changes + protected override void BusyChanged() { + this.viewModel.OnIsBusyChanged(); + } + + /// View model the thread runner belongs to + private ThreadedViewModel viewModel; + + } + + #endregion // class ViewModelThreadRunner + + /// Initializes a new view model for background processing + /// + /// UI dispatcher that can be used to run callbacks in the UI thread + /// + protected ThreadedViewModel(ISynchronizeInvoke uiContext = null) { + if(uiContext == null) { + this.uiContext = LateCheckedSynchronizer.GetMainWindow(); + if(this.uiContext == null) { + this.uiContext = new LateCheckedSynchronizer(updateUiContext); + } + } else { + this.uiContext = uiContext; + } + + this.reportErrorDelegate = new Action(ReportError); + + this.threadRunner = new ViewModelThreadRunner(this); + } + + /// Immediately releases all resources owned by the instance + public virtual void Dispose() { + if(this.threadRunner != null) { + this.threadRunner.Dispose(); + this.threadRunner = null; + } + } + + /// Whether the view model is currently busy executing a task + public bool IsBusy { + get { return this.threadRunner.IsBusy; } + } + + /// Reports an error to the user + /// Error that will be reported + /// + /// + /// 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). + /// + /// + /// When is used, this method will also + /// be called in case an exception within the asynchronously running code goes unhandled. + /// This choice was made because, in the context of UI code, you would wrap any + /// operations that might fail in a try..catch pair anyway in order to inform + /// the user instead of aborting the entire application. + /// + /// + protected abstract void ReportError(Exception exception); + + /// Executes the specified operation in the background + /// Action that will be executed in the background + protected void RunInBackground(Action action) { + this.threadRunner.RunInBackground(action); + } + + /// Executes the specified operation in the background + /// Action that will be executed in the background + protected void RunInBackground(CancellableAction action) { + this.threadRunner.RunInBackground(action); + } + + /// Executes the specified operation in the background + /// Action that will be executed in the background + /// Parameter that will be passed to the action + protected void RunInBackground(Action action, P1 parameter1) { + this.threadRunner.RunInBackground(action, parameter1); + } + + /// Executes the specified operation in the background + /// Action that will be executed in the background + /// Parameter that will be passed to the action + protected void RunInBackground(CancellableAction action, P1 parameter1) { + this.threadRunner.RunInBackground(action, parameter1); + } + + /// Executes the specified operation in the background + /// Action that will be executed in the background + /// First parameter that will be passed to the action + /// Second parameter that will be passed to the action + protected void RunInBackground(Action action, P1 parameter1, P2 parameter2) { + this.threadRunner.RunInBackground(action, parameter1, parameter2); + } + + /// Executes the specified operation in the background + /// Action that will be executed in the background + /// First parameter that will be passed to the action + /// Second parameter that will be passed to the action + protected void RunInBackground( + CancellableAction action, P1 parameter1, P2 parameter2 + ) { + this.threadRunner.RunInBackground(action, parameter1, parameter2); + } + + /// Cancels the currently running background operation + protected void CancelBackgroundOperation() { + this.threadRunner.CancelBackgroundOperation(); + } + + /// Cancels all queued and the currently running background operation + protected void CancelAllBackgroundOperations() { + this.threadRunner.CancelAllBackgroundOperations(); + } + + /// Whether the background operation has been cancelled + //[Obsolete("Please use a method accepting a cancellation token instead of using this")] + protected bool IsBackgroundOperationCancelled { + get { return this.threadRunner.IsBackgroundOperationCancelled; } + } + + /// Throws an exception if the background operation was cancelled + //[Obsolete("Please use a method accepting a cancellation token instead of using this")] + protected void ThrowIfBackgroundOperationCancelled() { + this.threadRunner.ThrowIfBackgroundOperationCancelled(); + } + + /// Executes the specified action in the UI thread + /// Action that will be executed in the UI thread + protected void RunInUIThread(Action action) { + this.uiContext.Invoke(action, EmptyObjectArray); + } + + /// Executes the specified action in the UI thread + /// Action that will be executed in the UI thread + /// Parameter that will be passed to the action + protected void RunInUIThread(Action action, P1 parameter1) { + this.uiContext.Invoke(action, new object[1] { parameter1 }); + } + + /// Executes the specified action in the UI thread + /// Action that will be executed in the UI thread + /// First parameter that will be passed to the action + /// Second parameter that will be passed to the action + protected void RunInUIThread(Action action, P1 parameter1, P2 parameter2) { + this.uiContext.Invoke(action, new object[2] { parameter1, parameter2 }); + } + + /// Called when the thread runner's busy flag changes + protected virtual void OnIsBusyChanged() { + OnPropertyChanged(nameof(IsBusy)); + } + + // For the ThreadedAction class - there should be a better way! + /// Thread runner that manages the view model's thread + internal ThreadRunner ThreadRunner { + get { return this.threadRunner; } + } + + /// Reports an error that occurred in the runner's background thread + /// Exception that the thread has encountered + private void reportErrorFromThread(Exception exception) { + this.uiContext.Invoke(this.reportErrorDelegate, new object[1] { exception }); + } + + /// Sets the UI context that will be used by the threaded action + /// The UI context the threaded action will use + private void updateUiContext(ISynchronizeInvoke uiContext) { + this.uiContext = uiContext; + } + + /// An array of zero objects + private static readonly object[] EmptyObjectArray = new object[0]; + + /// UI dispatcher of the thread in which the view runs + private ISynchronizeInvoke uiContext; + /// Delegate for the ReportError() method + private Action reportErrorDelegate; + /// Thread runner that manages the view model's thread + private ViewModelThreadRunner threadRunner; + + } + +} // namespace Nuclex.Windows.Forms.ViewModels diff --git a/Source/Views/IActiveWindowTracker.cs b/Source/Views/IActiveWindowTracker.cs index ad6e56d..e36b3ea 100644 --- a/Source/Views/IActiveWindowTracker.cs +++ b/Source/Views/IActiveWindowTracker.cs @@ -1,38 +1,37 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Windows.Forms; - -namespace Nuclex.Windows.Forms { - - /// Enables consumer to look up the currently active window - public interface IActiveWindowTracker { - - /// The currently active top-level or modal window - /// - /// If windows live in multiple threads, the property change notification for - /// this property, if supported, might be fired from a different thread. - /// - Form ActiveWindow { get; } - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Windows.Forms; + +namespace Nuclex.Windows.Forms { + + /// Enables consumer to look up the currently active window + public interface IActiveWindowTracker { + + /// The currently active top-level or modal window + /// + /// If windows live in multiple threads, the property change notification for + /// this property, if supported, might be fired from a different thread. + /// + Form ActiveWindow { get; } + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/Views/IView.cs b/Source/Views/IView.cs index 202498c..531b0ed 100644 --- a/Source/Views/IView.cs +++ b/Source/Views/IView.cs @@ -1,41 +1,40 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -namespace Nuclex.Windows.Forms.Views { - - /// View with support for data binding - public interface IView { - - /// Provides the data binding target for the view - /// - /// This property is identical to the same-named one in WPF, it provides - /// the view model to which the view should bind its controls. - /// - object DataContext { get; set; } - - // Whether the view owns its view model and it needs to be disposed after - // the view ceases to exist - //bool IsOwnedByView { get; set; } - - } - -} // namespace Nuclex.Windows.Forms.Views +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +namespace Nuclex.Windows.Forms.Views { + + /// View with support for data binding + public interface IView { + + /// Provides the data binding target for the view + /// + /// This property is identical to the same-named one in WPF, it provides + /// the view model to which the view should bind its controls. + /// + object DataContext { get; set; } + + // Whether the view owns its view model and it needs to be disposed after + // the view ceases to exist + //bool IsOwnedByView { get; set; } + + } + +} // namespace Nuclex.Windows.Forms.Views diff --git a/Source/Views/MultiPageViewForm.cs b/Source/Views/MultiPageViewForm.cs index 611c833..364192f 100644 --- a/Source/Views/MultiPageViewForm.cs +++ b/Source/Views/MultiPageViewForm.cs @@ -1,366 +1,385 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -using Nuclex.Support; -using Nuclex.Windows.Forms.ViewModels; - -namespace Nuclex.Windows.Forms.Views { - - /// Special view form that can display different child views - public class MultiPageViewForm : ViewForm { - - #region struct RedrawLockScope - - /// Prevents controls from redrawing themselves for a while - private struct RedrawLockScope : IDisposable { - - /// Window message that enables or disables control redraw - private const int WM_SETREDRAW = 11; - - /// Sends a window message to the specified window - /// Window a message will be sent to - /// ID of the message that will be sent - /// First argument to the window procedure - /// Second argument to the window procedure - /// The return value of the window procedure - [DllImport("user32")] - public static extern int SendMessage( - IntPtr windowHandle, int messageId, bool firstArgument, int secondArgument - ); - - /// Stops redrawing the specified control - /// Control to stop redrawing - public RedrawLockScope(Control control) { - if(Environment.OSVersion.Platform == PlatformID.Win32NT) { - SendMessage(control.Handle, WM_SETREDRAW, false, 0); - this.control = control; - } else { - this.control = null; - } - } - - /// Enables redrawing again when the lock scope is disposed - public void Dispose() { - if(this.control != null) { - SendMessage(this.control.Handle, WM_SETREDRAW, true, 0); - this.control.Invalidate(true); - } - } - - /// Control that has been stopped from redrawing itself - private Control control; - - } - - #endregion // struct RedrawLockScope - - /// Initializes a new multi page view window for the windows forms designer - public MultiPageViewForm() { - this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView)); - } - - /// Initializes a new multi page view window - /// - /// Window manager that is used to set up the child views - /// - /// Whether page views should be kept alive and reused - public MultiPageViewForm(IWindowManager windowManager, bool cachePageViews = false) { - this.windowManager = windowManager; - this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView)); - - if(cachePageViews) { - this.cachedViews = new Dictionary(); - } - } - - /// Called when the control is being disposed - /// - /// Whether the call was made by user code (vs. the garbage collector) - /// - protected override void Dispose(bool calledExplicitly) { - if(calledExplicitly) { - - // Disable the active view, if any - if(this.activePageView != null) { - if(this.childViewContainer != null) { - this.childViewContainer.Controls.Remove(this.activePageView); - } - } - - // If caching is disabled, dispose of the active child view, if any - if(this.cachedViews == null) { - if(this.activePageView != null) { - disposeIfSupported(this.activePageView); - this.activePageView = null; - } - } else { // Caching is enabled, dispose of any cached child views - foreach(Control childView in this.cachedViews.Values) { - disposeIfSupported(childView); - } - this.cachedViews.Clear(); - this.cachedViews = null; - this.activePageView = null; - } - } - - base.Dispose(calledExplicitly); - } - - /// Discovers the container control used to host the child views - /// The container control is which the child views will be hosted - /// - /// This is supposed to be overriden by the user, simply returning the container - /// control that should host the page views. If it isn't, however, we use some - /// heuristics to figure out the most likely candidate: it should be a container, - /// and it should cover most of the window's client area. - /// - protected virtual Control IdentifyPageContainer() { - Size halfWindowSize = Size; - halfWindowSize.Width /= 2; - halfWindowSize.Height /= 2; - - // First container control we found -- if we find no likely candidate, - // we simply use the first - Control firstContainer = null; - - // Check all top-level controls in the window. If there's a container that - // covers most of the window, it's our best bet - int controlCount = Controls.Count; - for(int index = 0; index < controlCount; ++index) { - Control control = Controls[index]; - - // Only check container controls - if((control is ContainerControl) || (control is Panel)) { - if(firstContainer == null) { - firstContainer = control; - } - - // If this control covers most of the view, it's our candidate! - Size controlSize = control.Size; - bool goodCandidate = ( - (controlSize.Width > halfWindowSize.Width) && - (controlSize.Height > halfWindowSize.Height) - ); - if(goodCandidate) { - return control; - } - } - } - - // If no candidate was found, return the first container control we encountered - // or create a new UserControl as the container if nothing was found at all. - if(firstContainer == null) { - firstContainer = new Panel(); - Controls.Add(firstContainer); - firstContainer.Dock = DockStyle.Fill; - } - - return firstContainer; - } - - /// Called when the window's data context is changed - /// Window whose data context was changed - /// Data context that was previously used - /// Data context that will be used from now on - protected override void OnDataContextChanged( - object sender, object oldDataContext, object newDataContext - ) { - - // Kill the currently active view if there was an old view model. - if(oldDataContext != null) { - disableActivePageView(); - } - - base.OnDataContextChanged(sender, oldDataContext, newDataContext); - - // If a valid view model was assigned, create a new view its active page view model - if(newDataContext != null) { - var dataContextAsMultiPageViewModel = newDataContext as IMultiPageViewModel; - if(dataContextAsMultiPageViewModel != null) { - activatePageView(dataContextAsMultiPageViewModel.GetActivePageViewModel()); - } - } - - } - - /// Called when a property of the view model is changed - /// View model in which a property was changed - /// Contains the name of the property that has changed - protected override void OnViewModelPropertyChanged( - object sender, PropertyChangedEventArgs arguments - ) { - base.OnViewModelPropertyChanged(sender, arguments); - - if(arguments.AreAffecting(nameof(MultiPageViewModel.ActivePage))) { - var viewModelAsMultiPageviewModel = DataContext as IMultiPageViewModel; - if(viewModelAsMultiPageviewModel != null) { - if(InvokeRequired) { - Invoke( - new Action(activatePageView), - viewModelAsMultiPageviewModel.GetActivePageViewModel() - ); - } else { - activatePageView(viewModelAsMultiPageviewModel.GetActivePageViewModel()); - } - } - } - } - - /// Currently active page view control - protected Control ActivePageView { - get { return this.activePageView; } - } - - /// The view model running the currently active page - protected object ActivePageViewModel { - get { - var activePageViewAsView = this.activePageView as IView; - if(activePageViewAsView == null) { - return null; - } else { - return activePageViewAsView.DataContext; - } - } - } - - /// Activates the page view for the specified page view model - /// - /// Page view model for which the page view will be activated - /// - private void activatePageView(object pageViewModel) { - object activePageViewModel = null; - { - var activePageViewAsView = this.activePageView as IView; - if(activePageViewAsView != null) { - activePageViewModel = activePageViewAsView.DataContext; - } - } - - // Try from the cheapest to the most expensive way to get to our goal, - // an activated view suiting the specified view model. - - // If we already have the target view model selected, do nothing - if(activePageViewModel == pageViewModel) { - return; - } - - // If the page view model for the old and the new page are of the same - // type, we can reuse the currently active page view - if((activePageViewModel != null) && (pageViewModel != null)) { - if(pageViewModel.GetType() == this.activePageView.GetType()) { - var activePageViewAsView = this.activePageView as IView; - if(activePageViewAsView != null) { - activePageViewAsView.DataContext = pageViewModel; - } - - return; - } - } - - // Worst, but usual, case: the new page view model might require - // a different view. Create or look up the new view and put it in the container - { - if(pageViewModel == null) { - disableActivePageView(); - } else { - Control pageViewContainer = getPageViewContainer(); - using(new RedrawLockScope(pageViewContainer)) { - disableActivePageView(); - - this.activePageView = getOrCreatePageView(pageViewModel); - pageViewContainer.Controls.Add(this.activePageView); - this.activePageView.Dock = DockStyle.Fill; - } - } - } - } - - /// Gets the cached child view or creates a new one if not cached - /// View model for which a child view will be returned - /// A child view suitable for the specified view model - private Control getOrCreatePageView(object viewModel) { - Type viewModelType = viewModel.GetType(); - - Control view; - - // If caching is enabled, check if we have a cached view - if(this.cachedViews != null) { - if(this.cachedViews.TryGetValue(viewModelType, out view)) { - return view; - } - } - - // Otherwise, call the window manager's CreateView() method - MethodInfo specializedCreateViewMethod = ( - this.createViewMethod.MakeGenericMethod(viewModelType) - ); - view = (Control)specializedCreateViewMethod.Invoke( - this.windowManager, new object[1] { viewModel } - ); - - // If caching is enabled, register the view in the cache - if(this.cachedViews != null) { - this.cachedViews.Add(viewModelType, view); - } - - return view; - } - - /// Disables the currently active page view control - private void disableActivePageView() { - if(this.activePageView != null) { - Control container = getPageViewContainer(); - container.Controls.Remove(this.activePageView); - - // If we don't reuse views, kill it now - if(this.cachedViews == null) { - disposeIfSupported(this.activePageView); - this.activePageView = null; - } else { - var activePageViewAsView = this.activePageView as IView; - if(activePageViewAsView != null) { - activePageViewAsView.DataContext = null; - } - } - } - } - - /// Fetches the container that holds the child views - /// The container for the child views - private Control getPageViewContainer() { - if(this.childViewContainer == null) { - this.childViewContainer = IdentifyPageContainer(); - } - - return this.childViewContainer; - } - - /// Disposes the specified object if it is disposable - /// Object that will be disposed if supported - private static void disposeIfSupported(object potentiallyDisposable) { - var disposable = potentiallyDisposable as IDisposable; - if(disposable != null) { - disposable.Dispose(); - } - } - - /// Window manager through which the child views are created - private IWindowManager windowManager; - /// Reflection info for the createView() method of the window manager - private MethodInfo createViewMethod; - - /// Container in which the child views will be hosted - private Control childViewContainer; - /// Cached views that will be reused when the view model activates them - private Dictionary cachedViews; - /// The currently active child view - private Control activePageView; - - } - -} // namespace Nuclex.Windows.Forms.Views +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +using Nuclex.Support; +using Nuclex.Windows.Forms.ViewModels; + +namespace Nuclex.Windows.Forms.Views { + + /// Special view form that can display different child views + public class MultiPageViewForm : ViewForm { + + #region struct RedrawLockScope + + /// Prevents controls from redrawing themselves for a while + private struct RedrawLockScope : IDisposable { + + /// Window message that enables or disables control redraw + private const int WM_SETREDRAW = 11; + + /// Sends a window message to the specified window + /// Window a message will be sent to + /// ID of the message that will be sent + /// First argument to the window procedure + /// Second argument to the window procedure + /// The return value of the window procedure + [DllImport("user32")] + public static extern int SendMessage( + IntPtr windowHandle, int messageId, bool firstArgument, int secondArgument + ); + + /// Stops redrawing the specified control + /// Control to stop redrawing + public RedrawLockScope(Control control) { + if(Environment.OSVersion.Platform == PlatformID.Win32NT) { + SendMessage(control.Handle, WM_SETREDRAW, false, 0); + this.control = control; + } else { + this.control = null; + } + } + + /// Enables redrawing again when the lock scope is disposed + public void Dispose() { + if(this.control != null) { + SendMessage(this.control.Handle, WM_SETREDRAW, true, 0); + this.control.Invalidate(true); + } + } + + /// Control that has been stopped from redrawing itself + private Control control; + + } + + #endregion // struct RedrawLockScope + + /// Initializes a new multi page view window for the windows forms designer + public MultiPageViewForm() { + this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView)); + } + + /// Initializes a new multi page view window + /// + /// Window manager that is used to set up the child views + /// + /// Whether page views should be kept alive and reused + public MultiPageViewForm(IWindowManager windowManager, bool cachePageViews = false) { + this.windowManager = windowManager; + this.createViewMethod = typeof(IWindowManager).GetMethod(nameof(IWindowManager.CreateView)); + + if(cachePageViews) { + this.cachedViews = new Dictionary(); + } + } + + /// Called when the control is being disposed + /// + /// Whether the call was made by user code (vs. the garbage collector) + /// + protected override void Dispose(bool calledExplicitly) { + if(calledExplicitly) { + + // Disable the active view, if any + if(this.activePageView != null) { + if(this.childViewContainer != null) { + this.childViewContainer.Controls.Remove(this.activePageView); + } + } + + // If caching is disabled, dispose of the active child view, if any + if(this.cachedViews == null) { + if(this.activePageView != null) { + disposeIfSupported(this.activePageView); + this.activePageView = null; + } + } else { // Caching is enabled, dispose of any cached child views + foreach(Control childView in this.cachedViews.Values) { + disposeIfSupported(childView); + } + this.cachedViews.Clear(); + this.cachedViews = null; + this.activePageView = null; + } + } + + base.Dispose(calledExplicitly); + } + + /// Discovers the container control used to host the child views + /// The container control is which the child views will be hosted + /// + /// This is supposed to be overriden by the user, simply returning the container + /// control that should host the page views. If it isn't, however, we use some + /// heuristics to figure out the most likely candidate: it should be a container, + /// and it should cover most of the window's client area. + /// + protected virtual Control IdentifyPageContainer() { + Size halfWindowSize = Size; + halfWindowSize.Width /= 2; + halfWindowSize.Height /= 2; + + // First container control we found -- if we find no likely candidate, + // we simply use the first + Control firstContainer = null; + + // Check all top-level controls in the window. If there's a container that + // covers most of the window, it's our best bet + int controlCount = Controls.Count; + for(int index = 0; index < controlCount; ++index) { + Control control = Controls[index]; + + // Only check container controls + if((control is ContainerControl) || (control is Panel)) { + if(firstContainer == null) { + firstContainer = control; + } + + // If this control covers most of the view, it's our candidate! + Size controlSize = control.Size; + bool goodCandidate = ( + (controlSize.Width > halfWindowSize.Width) && + (controlSize.Height > halfWindowSize.Height) + ); + if(goodCandidate) { + return control; + } + } + } + + // If no candidate was found, return the first container control we encountered + // or create a new UserControl as the container if nothing was found at all. + if(firstContainer == null) { + firstContainer = new Panel(); + Controls.Add(firstContainer); + firstContainer.Dock = DockStyle.Fill; + } + + return firstContainer; + } + + /// Called when the window's data context is changed + /// Window whose data context was changed + /// Data context that was previously used + /// Data context that will be used from now on + protected override void OnDataContextChanged( + object sender, object oldDataContext, object newDataContext + ) { + + // Kill the currently active view if there was an old view model. + if(oldDataContext != null) { + disableActivePageView(); + } + + base.OnDataContextChanged(sender, oldDataContext, newDataContext); + + // If a valid view model was assigned, create a new view its active page view model + if(newDataContext != null) { + var dataContextAsMultiPageViewModel = newDataContext as IMultiPageViewModel; + if(dataContextAsMultiPageViewModel != null) { + activatePageView(dataContextAsMultiPageViewModel.GetActivePageViewModel()); + } + } + + } + + /// Called when a property of the view model is changed + /// View model in which a property was changed + /// Contains the name of the property that has changed + protected override void OnViewModelPropertyChanged( + object sender, PropertyChangedEventArgs arguments + ) { + base.OnViewModelPropertyChanged(sender, arguments); + + if(arguments.AreAffecting(nameof(MultiPageViewModel.ActivePage))) { + var viewModelAsMultiPageviewModel = DataContext as IMultiPageViewModel; + if(viewModelAsMultiPageviewModel != null) { + if(InvokeRequired) { + Invoke( + new Action(activatePageView), + viewModelAsMultiPageviewModel.GetActivePageViewModel() + ); + } else { + activatePageView(viewModelAsMultiPageviewModel.GetActivePageViewModel()); + } + } + } + } + + /// Currently active page view control + protected Control ActivePageView { + get { return this.activePageView; } + } + + /// The view model running the currently active page + protected object ActivePageViewModel { + get { + var activePageViewAsView = this.activePageView as IView; + if(activePageViewAsView == null) { + return null; + } else { + return activePageViewAsView.DataContext; + } + } + } + + /// Activates the page view for the specified page view model + /// + /// Page view model for which the page view will be activated + /// + private void activatePageView(object pageViewModel) { + object activePageViewModel = null; + { + var activePageViewAsView = this.activePageView as IView; + if(activePageViewAsView != null) { + activePageViewModel = activePageViewAsView.DataContext; + } + } + + // Try from the cheapest to the most expensive way to get to our goal, + // an activated view suiting the specified view model. + + // If we already have the target view model selected, do nothing + if(activePageViewModel == pageViewModel) { + return; + } + + // If the page view model for the old and the new page are of the same + // type, we can reuse the currently active page view + if((activePageViewModel != null) && (pageViewModel != null)) { + if(pageViewModel.GetType() == this.activePageView.GetType()) { + var activePageViewAsView = this.activePageView as IView; + if(activePageViewAsView != null) { + activePageViewAsView.DataContext = pageViewModel; + } + + return; + } + } + + // Worst, but usual, case: the new page view model might require + // a different view. Create or look up the new view and put it in the container + { + if(pageViewModel == null) { + disableActivePageView(); + } else { + Control pageViewContainer = getPageViewContainer(); + using(new RedrawLockScope(pageViewContainer)) { + disableActivePageView(); + + this.activePageView = getOrCreatePageView(pageViewModel); + pageViewContainer.Controls.Add(this.activePageView); + this.activePageView.Dock = DockStyle.Fill; + } + } + } + } + + /// Gets the cached child view or creates a new one if not cached + /// View model for which a child view will be returned + /// A child view suitable for the specified view model + private Control getOrCreatePageView(object viewModel) { + Type viewModelType = viewModel.GetType(); + + Control view; + + // If caching is enabled, check if we have a cached view + if(this.cachedViews != null) { + if(this.cachedViews.TryGetValue(viewModelType, out view)) { + return view; + } + } + + // Otherwise, call the window manager's CreateView() method + MethodInfo specializedCreateViewMethod = ( + this.createViewMethod.MakeGenericMethod(viewModelType) + ); + view = (Control)specializedCreateViewMethod.Invoke( + this.windowManager, new object[1] { viewModel } + ); + + // If caching is enabled, register the view in the cache + if(this.cachedViews != null) { + this.cachedViews.Add(viewModelType, view); + } + + return view; + } + + /// Disables the currently active page view control + private void disableActivePageView() { + if(this.activePageView != null) { + Control container = getPageViewContainer(); + container.Controls.Remove(this.activePageView); + + // If we don't reuse views, kill it now + if(this.cachedViews == null) { + disposeIfSupported(this.activePageView); + this.activePageView = null; + } else { + var activePageViewAsView = this.activePageView as IView; + if(activePageViewAsView != null) { + activePageViewAsView.DataContext = null; + } + } + } + } + + /// Fetches the container that holds the child views + /// The container for the child views + private Control getPageViewContainer() { + if(this.childViewContainer == null) { + this.childViewContainer = IdentifyPageContainer(); + } + + return this.childViewContainer; + } + + /// Disposes the specified object if it is disposable + /// Object that will be disposed if supported + private static void disposeIfSupported(object potentiallyDisposable) { + var disposable = potentiallyDisposable as IDisposable; + if(disposable != null) { + disposable.Dispose(); + } + } + + /// Window manager through which the child views are created + private IWindowManager windowManager; + /// Reflection info for the createView() method of the window manager + private MethodInfo createViewMethod; + + /// Container in which the child views will be hosted + private Control childViewContainer; + /// Cached views that will be reused when the view model activates them + private Dictionary cachedViews; + /// The currently active child view + private Control activePageView; + + } + +} // namespace Nuclex.Windows.Forms.Views diff --git a/Source/Views/ViewControl.cs b/Source/Views/ViewControl.cs index 47b6adc..c31ea60 100644 --- a/Source/Views/ViewControl.cs +++ b/Source/Views/ViewControl.cs @@ -1,89 +1,88 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.ComponentModel; -using System.Windows.Forms; - -using Nuclex.Support; - -namespace Nuclex.Windows.Forms.Views { - - /// - /// Base class for MVVM user controls that act as views connected to a view model - /// - public class ViewControl : UserControl, IView { - - /// Initializes a new view control - public ViewControl() { - this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged; - } - - /// Called when the control's data context is changed - /// Control whose data context was changed - /// Data context that was previously used - /// Data context that will be used from now on - protected virtual void OnDataContextChanged( - object sender, object oldDataContext, object newDataContext - ) { - var oldViewModel = oldDataContext as INotifyPropertyChanged; - if(oldViewModel != null) { - oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate; - } - - var newViewModel = newDataContext as INotifyPropertyChanged; - if(newViewModel != null) { - newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate; - InvalidateAllViewModelProperties(); - } - } - - /// Refreshes all properties from the view model - protected void InvalidateAllViewModelProperties() { - OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard); - } - - /// Called when a property of the view model is changed - /// View model in which a property was changed - /// Contains the name of the property that has changed - protected virtual void OnViewModelPropertyChanged( - object sender, PropertyChangedEventArgs arguments - ) { } - - /// Provides the data binding target for the view - public object DataContext { - get { return this.dataContext; } - set { - if(value != this.dataContext) { - object oldDataContext = this.dataContext; - this.dataContext = value; - OnDataContextChanged(this, oldDataContext, value); - } - } - } - - /// Active data binding target, can be null - private object dataContext; - /// Delegate for the OnViewModelPropertyChanged() method - private PropertyChangedEventHandler onViewModelPropertyChangedDelegate; - - } - -} // namespace Nuclex.Windows.Forms.Views +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.ComponentModel; +using System.Windows.Forms; + +using Nuclex.Support; + +namespace Nuclex.Windows.Forms.Views { + + /// + /// Base class for MVVM user controls that act as views connected to a view model + /// + public class ViewControl : UserControl, IView { + + /// Initializes a new view control + public ViewControl() { + this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged; + } + + /// Called when the control's data context is changed + /// Control whose data context was changed + /// Data context that was previously used + /// Data context that will be used from now on + protected virtual void OnDataContextChanged( + object sender, object oldDataContext, object newDataContext + ) { + var oldViewModel = oldDataContext as INotifyPropertyChanged; + if(oldViewModel != null) { + oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate; + } + + var newViewModel = newDataContext as INotifyPropertyChanged; + if(newViewModel != null) { + newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate; + InvalidateAllViewModelProperties(); + } + } + + /// Refreshes all properties from the view model + protected void InvalidateAllViewModelProperties() { + OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard); + } + + /// Called when a property of the view model is changed + /// View model in which a property was changed + /// Contains the name of the property that has changed + protected virtual void OnViewModelPropertyChanged( + object sender, PropertyChangedEventArgs arguments + ) { } + + /// Provides the data binding target for the view + public object DataContext { + get { return this.dataContext; } + set { + if(value != this.dataContext) { + object oldDataContext = this.dataContext; + this.dataContext = value; + OnDataContextChanged(this, oldDataContext, value); + } + } + } + + /// Active data binding target, can be null + private object dataContext; + /// Delegate for the OnViewModelPropertyChanged() method + private PropertyChangedEventHandler onViewModelPropertyChangedDelegate; + + } + +} // namespace Nuclex.Windows.Forms.Views diff --git a/Source/Views/ViewForm.cs b/Source/Views/ViewForm.cs index 88a3578..625b218 100644 --- a/Source/Views/ViewForm.cs +++ b/Source/Views/ViewForm.cs @@ -1,89 +1,88 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.ComponentModel; -using System.Windows.Forms; - -using Nuclex.Support; - -namespace Nuclex.Windows.Forms.Views { - - /// - /// Base class for MVVM windows that act as views connected to a view model - /// - public class ViewForm : Form, IView { - - /// Initializes a new view control - public ViewForm() { - this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged; - } - - /// Provides the data binding target for the view - public object DataContext { - get { return this.dataContext; } - set { - if(value != this.dataContext) { - object oldDataContext = this.dataContext; - this.dataContext = value; - OnDataContextChanged(this, oldDataContext, value); - } - } - } - - /// Called when the window's data context is changed - /// Window whose data context was changed - /// Data context that was previously used - /// Data context that will be used from now on - protected virtual void OnDataContextChanged( - object sender, object oldDataContext, object newDataContext - ) { - var oldViewModel = oldDataContext as INotifyPropertyChanged; - if(oldViewModel != null) { - oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate; - } - - var newViewModel = newDataContext as INotifyPropertyChanged; - if(newViewModel != null) { - newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate; - InvalidateAllViewModelProperties(); - } - } - - /// Refreshes all properties from the view model - protected void InvalidateAllViewModelProperties() { - OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard); - } - - /// Called when a property of the view model is changed - /// View model in which a property was changed - /// Contains the name of the property that has changed - protected virtual void OnViewModelPropertyChanged( - object sender, PropertyChangedEventArgs arguments - ) { } - - /// Active data binding target, can be null - private object dataContext; - /// Delegate for the OnViewModelPropertyChanged() method - private PropertyChangedEventHandler onViewModelPropertyChangedDelegate; - - } - -} // namespace Nuclex.Windows.Forms.Views +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.ComponentModel; +using System.Windows.Forms; + +using Nuclex.Support; + +namespace Nuclex.Windows.Forms.Views { + + /// + /// Base class for MVVM windows that act as views connected to a view model + /// + public class ViewForm : Form, IView { + + /// Initializes a new view control + public ViewForm() { + this.onViewModelPropertyChangedDelegate = OnViewModelPropertyChanged; + } + + /// Provides the data binding target for the view + public object DataContext { + get { return this.dataContext; } + set { + if(value != this.dataContext) { + object oldDataContext = this.dataContext; + this.dataContext = value; + OnDataContextChanged(this, oldDataContext, value); + } + } + } + + /// Called when the window's data context is changed + /// Window whose data context was changed + /// Data context that was previously used + /// Data context that will be used from now on + protected virtual void OnDataContextChanged( + object sender, object oldDataContext, object newDataContext + ) { + var oldViewModel = oldDataContext as INotifyPropertyChanged; + if(oldViewModel != null) { + oldViewModel.PropertyChanged -= this.onViewModelPropertyChangedDelegate; + } + + var newViewModel = newDataContext as INotifyPropertyChanged; + if(newViewModel != null) { + newViewModel.PropertyChanged += this.onViewModelPropertyChangedDelegate; + InvalidateAllViewModelProperties(); + } + } + + /// Refreshes all properties from the view model + protected void InvalidateAllViewModelProperties() { + OnViewModelPropertyChanged(this.dataContext, PropertyChangedEventArgsHelper.Wildcard); + } + + /// Called when a property of the view model is changed + /// View model in which a property was changed + /// Contains the name of the property that has changed + protected virtual void OnViewModelPropertyChanged( + object sender, PropertyChangedEventArgs arguments + ) { } + + /// Active data binding target, can be null + private object dataContext; + /// Delegate for the OnViewModelPropertyChanged() method + private PropertyChangedEventHandler onViewModelPropertyChangedDelegate; + + } + +} // namespace Nuclex.Windows.Forms.Views diff --git a/Source/WindowManager.Test.cs b/Source/WindowManager.Test.cs index dd5bd48..d710fbc 100644 --- a/Source/WindowManager.Test.cs +++ b/Source/WindowManager.Test.cs @@ -1,41 +1,40 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; - -using NUnit.Framework; - -namespace Nuclex.Windows.Forms { - - /// Unit test for the window manager - [TestFixture] - public class WindowManagerTest { - - /// Verifies that the window manager provides a default constructor - [Test] - public void HasDefaultConstructor() { - Assert.DoesNotThrow( - () => new WindowManager() - ); - } - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +using NUnit.Framework; + +namespace Nuclex.Windows.Forms { + + /// Unit test for the window manager + [TestFixture] + public class WindowManagerTest { + + /// Verifies that the window manager provides a default constructor + [Test] + public void HasDefaultConstructor() { + Assert.DoesNotThrow( + () => new WindowManager() + ); + } + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/WindowManager.cs b/Source/WindowManager.cs index dba4b87..f944052 100644 --- a/Source/WindowManager.cs +++ b/Source/WindowManager.cs @@ -1,442 +1,441 @@ -#region CPL License -/* -Nuclex Framework -Copyright (C) 2002-2019 Nuclex Development Labs - -This library is free software; you can redistribute it and/or -modify it under the terms of the IBM Common Public License as -published by the IBM Corporation; either version 1.0 of the -License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -IBM Common Public License for more details. - -You should have received a copy of the IBM Common Public -License along with this library -*/ -#endregion - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Windows.Forms; - -using Nuclex.Support; -using Nuclex.Windows.Forms.AutoBinding; -using Nuclex.Windows.Forms.Views; - -namespace Nuclex.Windows.Forms { - - /// Manages an application's windows and views - public class WindowManager : Observable, IWindowManager { - - #region class CancellableDisposer - - /// Disposes an object that potentially implements IDisposable - private struct CancellableDisposer : IDisposable { - - /// Initializes a new cancellable disposer - /// - /// Object that potentially implements IDisposable - /// - public CancellableDisposer(object potentiallyDisposable = null) { - this.potentiallyDisposable = potentiallyDisposable; - } - - /// - /// Disposes the assigned object if the disposer has not been cancelled - /// - public void Dispose() { - var disposable = this.potentiallyDisposable as IDisposable; - if(disposable != null) { - disposable.Dispose(); - } - } - - /// Cancels the call to Dispose(), keeping the object alive - public void Dismiss() { - this.potentiallyDisposable = null; - } - - /// Assigns a new potentially disposable object - /// - /// Potentially disposable object that the disposer will dispose - /// - public void Set(object potentiallyDisposable) { - this.potentiallyDisposable = potentiallyDisposable; - } - - /// Object that will be disposed unless the disposer is cancelled - private object potentiallyDisposable; - - } - - #endregion // class CancellableDisposer - - /// Initializes a new window manager - /// - /// View model binder that will be used to bind all created views to their models - /// - public WindowManager(IAutoBinder autoBinder = null) { - this.autoBinder = autoBinder; - - this.rootWindowActivatedDelegate = rootWindowActivated; - this.rootWindowClosedDelegate = rootWindowClosed; - this.viewTypesForViewModels = new ConcurrentDictionary(); - } - - /// The currently active top-level or modal window - public Form ActiveWindow { - get { return this.activeWindow; } - private set { - if(value != this.activeWindow) { - this.activeWindow = value; - OnPropertyChanged(nameof(ActiveWindow)); - } - } - } - - /// Opens a view as a new root window of the application - /// - /// Type of view model a root window will be opened for - /// - /// - /// 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) - /// - /// - /// Whether the view model should be disposed when the view is closed - /// - /// The window that has been opened by the window manager - public Form OpenRoot( - TViewModel viewModel = null, bool disposeOnClose = true - ) where TViewModel : class { - - Form window = (Form)CreateView(viewModel); - window.Activated += this.rootWindowActivatedDelegate; - window.Closed += this.rootWindowClosedDelegate; - - // If we either created the view model or the user explicitly asked us to - // dispose his view model, tag the window so that we know to dispose it - // when we're done (but still allow the user to change his mind) - if((viewModel == null) || disposeOnClose) { - window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead? - //window.SetValue(DisposeViewModelOnCloseProperty, true); - } - - window.Show(); - - return window; - } - - /// Displays a view as a modal window - /// - /// Type of the view model for which a view will be displayed - /// - /// - /// 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) - /// - /// - /// Whether the view model should be disposed when the view is closed - /// - /// The return value of the modal window - public bool? ShowModal( - TViewModel viewModel = null, bool disposeOnClose = true - ) where TViewModel : class { - Form window = (Form)CreateView(viewModel); - window.Owner = this.activeWindow; - window.Activated += this.rootWindowActivatedDelegate; - - try { - // If we either created the view model or the user explicitly asked us to - // dispose his view model, tag the window so that we know to dispose it - // when we're done (but still allow the user to change his mind) - if((viewModel == null) || disposeOnClose) { - window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead? - //window.SetValue(DisposeViewModelOnCloseProperty, true); - } - - DialogResult result = window.ShowDialog(this.activeWindow); - if((result == DialogResult.OK) || (result == DialogResult.Yes)) { - return true; - } else if((result == DialogResult.Cancel) || (result == DialogResult.No)) { - return false; - } else { - return null; - } - } - finally { - window.Activated -= this.rootWindowActivatedDelegate; - ActiveWindow = window.Owner; - - if(shouldDisposeViewModelOnClose(window)) { - IView windowAsView = window as IView; - if(windowAsView != null) { - object viewModelAsObject = windowAsView.DataContext; - windowAsView.DataContext = null; - disposeIfDisposable(viewModelAsObject); - } - } - disposeIfDisposable(window); - } - } - - /// Creates the view for the specified view model - /// - /// Type of view model for which a view will be created - /// - /// - /// 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) - /// - /// The view for the specified view model - public virtual Control CreateView( - TViewModel viewModel = null - ) where TViewModel : class { - Type viewType = LocateViewForViewModel(typeof(TViewModel)); - Control viewControl = (Control)CreateInstance(viewType); - using(var viewDisposer = new CancellableDisposer(viewControl)) { - - // Create a view model if none was provided, and in either case assign - // the view model to the view (provided it implements IView). - using(var viewModelDisposer = new CancellableDisposer()) { - IView viewControlAsView = viewControl as IView; - if(viewModel == null) { // No view model provided, create one - if(viewControlAsView == null) { // View doesn't implement IView - viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); - viewModelDisposer.Set(viewModel); - } else if(viewControlAsView.DataContext == null) { // View has no view model - viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); - viewModelDisposer.Set(viewModel); - viewControlAsView.DataContext = viewModel; - } else { // There's an existing view model - viewModel = viewControlAsView.DataContext as TViewModel; - if(viewModel == null) { // The existing view model is another type - viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); - viewModelDisposer.Set(viewModel); - viewControlAsView.DataContext = viewModel; - } - } - } else if(viewControlAsView != null) { // Caller has provided a view model - viewControlAsView.DataContext = viewModel; - } - - // If an auto binder was provided, automatically bind the view to the view model - if(this.autoBinder != null) { - this.autoBinder.Bind(viewControl, viewModel); - } - - viewModelDisposer.Dismiss(); // Everything went well, we keep the view model - } - - viewDisposer.Dismiss(); // Everything went well, we keep the view - - } - - return viewControl; - } - - /// Creates a view model without a matching view - /// Type of view model that will be created - /// The new view model - /// - /// - /// This is useful if a view model needs to create child view models (i.e. paged container - /// and wants to ensure the same dependency injector (if any) if used as the window - /// manager uses for other view models it creates. - /// - /// - /// This way, view models can set up their child view models without having to immediately - /// bind a view to them. Later on, views can use the window manager to create a matching - /// child view and store it in a container. - /// - /// - public TViewModel CreateViewModel() where TViewModel : class { - return (TViewModel)CreateInstance(typeof(TViewModel)); - } - - /// Locates the view that will be used to a view model - /// - /// Type of view model for which the view will be located - /// - /// The type of view that should be used for the specified view model - protected virtual Type LocateViewForViewModel(Type viewModelType) { - Type viewType; - if(!this.viewTypesForViewModels.TryGetValue(viewModelType, out viewType)) { - string viewName = viewModelType.Name; - if(viewName.EndsWith("ViewModel")) { - viewName = viewName.Substring(0, viewName.Length - 9); - } - - Type[] exportedTypes = viewModelType.Assembly.GetExportedTypes(); - Type[] namespaceTypes = filterTypesByNamespace(exportedTypes, viewModelType.Namespace); - - // First, search the own namespace (because if two identical view models exist in - // different namespaces, the one in the same namespace is most likely the desired one) - viewType = findBestMatch( - namespaceTypes, - viewName + "View", - viewName + "Page", - viewName + "Form", - viewName + "Window", - viewName + "Dialog", - viewName + "Control" - ); - - // If the view model doesn't exist in the same namespace, expand the search to - // the entire assembly the view is in. - if(viewType == null) { - viewType = findBestMatch( - exportedTypes, - viewName + "View", - viewName + "Page", - viewName + "Form", - viewName + "Window", - viewName + "Dialog", - viewName + "Control" - ); - } - - // Still no view found? We give up! - if(viewType == null) { - throw new InvalidOperationException( - string.Format("Could not locate view for view model '{0}'", viewModelType.Name) - ); - } - - this.viewTypesForViewModels.TryAdd(viewModelType, viewType); - } - - return viewType; - } - - /// Creates an instance of the specified type - /// Type an instance will be created of - /// The created instance - /// - /// Use this to wire up your dependency injection container. By default, - /// the Activator class will be used to create instances which only works - /// if all of your view models are concrete classes. - /// - protected virtual object CreateInstance(Type type) { - return Activator.CreateInstance(type); - } - - /// Called when one of the application's root windows is closed - /// Window that has been closed - /// Not used - private void rootWindowClosed(object sender, EventArgs arguments) { - Form closedWindow = (Form)sender; - closedWindow.Closed -= this.rootWindowClosedDelegate; - closedWindow.Activated -= this.rootWindowActivatedDelegate; - - // If the view model was created just for this view or if the user asked us - // to dispose of his view model, do so now. - if(shouldDisposeViewModelOnClose(closedWindow)) { - IView windowAsView = closedWindow as IView; - if(windowAsView != null) { - object viewModelAsObject = windowAsView.DataContext; - windowAsView.DataContext = null; - disposeIfDisposable(viewModelAsObject); - } - } - - lock(this) { - ActiveWindow = null; - } - } - - /// Called when one of the application's root windows is activated - /// Window that has been put in the foreground - /// Not used - private void rootWindowActivated(object sender, EventArgs arguments) { - lock(this) { - ActiveWindow = (Form)sender; - } - } - - /// Tries to find the best match for a named type in a list of types - /// List of types the search will take place in - /// - /// The candidates the method will look for, starting with the best match - /// - /// The best match in the list of types, if any match was found - private static Type findBestMatch(Type[] types, params string[] typeNames) { - int bestMatchFound = typeNames.Length; - - Type type = null; - for(int index = 0; index < types.Length; ++index) { - for(int nameIndex = 0; nameIndex < bestMatchFound; ++nameIndex) { - if(types[index].Name == typeNames[nameIndex]) { - bestMatchFound = nameIndex; - type = types[index]; - - if(bestMatchFound == 0) { // There can be no better match - return type; - } - - break; - } - } - } - - return type; - } - - /// Disposes the specified object if it implements IDisposable - /// Type of object that will disposed if possible - /// Object that the method will attempt to dispose - private static void disposeIfDisposable(T instance) where T : class { - var disposable = instance as IDisposable; - if(disposable != null) { - disposable.Dispose(); - } - } - - /// Determines if the view owns the view model - /// View that will be checked for ownership - /// True if the view owns the view model - private static bool shouldDisposeViewModelOnClose(Control view) { - string tagAsString = view.Tag as string; - if(tagAsString != null) { - return tagAsString.Contains("DisposeViewModelOnClose"); - } else { - return false; - } - } - - /// Filters a list of types to contain only those in a specific namespace - /// List of exported types that will be filtered - /// - /// Namespace the types in the filtered list will be in - /// - /// A subset of the specified types that are in the provided namespace - private static Type[] filterTypesByNamespace(Type[] exportedTypes, string filteredNamespace) { - var filteredTypes = new List(exportedTypes.Length / 2); - for(int index = 0; index < exportedTypes.Length; ++index) { - Type exportedType = exportedTypes[index]; - if(exportedType.Namespace == filteredNamespace) { - filteredTypes.Add(exportedType); - } - } - - return filteredTypes.ToArray(); - } - - /// The application's currently active root window - private Form activeWindow; - /// Invoked when a root window is put in the foreground - private EventHandler rootWindowActivatedDelegate; - /// Invoked when a root window has been closed - private EventHandler rootWindowClosedDelegate; - /// View model binder that will be used on all created views - private IAutoBinder autoBinder; - /// Caches the view types to use for a view model - private ConcurrentDictionary viewTypesForViewModels; - - } - -} // namespace Nuclex.Windows.Forms +#region Apache License 2.0 +/* +Nuclex .NET Framework +Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Windows.Forms; + +using Nuclex.Support; +using Nuclex.Windows.Forms.AutoBinding; +using Nuclex.Windows.Forms.Views; + +namespace Nuclex.Windows.Forms { + + /// Manages an application's windows and views + public class WindowManager : Observable, IWindowManager { + + #region class CancellableDisposer + + /// Disposes an object that potentially implements IDisposable + private struct CancellableDisposer : IDisposable { + + /// Initializes a new cancellable disposer + /// + /// Object that potentially implements IDisposable + /// + public CancellableDisposer(object potentiallyDisposable = null) { + this.potentiallyDisposable = potentiallyDisposable; + } + + /// + /// Disposes the assigned object if the disposer has not been cancelled + /// + public void Dispose() { + var disposable = this.potentiallyDisposable as IDisposable; + if(disposable != null) { + disposable.Dispose(); + } + } + + /// Cancels the call to Dispose(), keeping the object alive + public void Dismiss() { + this.potentiallyDisposable = null; + } + + /// Assigns a new potentially disposable object + /// + /// Potentially disposable object that the disposer will dispose + /// + public void Set(object potentiallyDisposable) { + this.potentiallyDisposable = potentiallyDisposable; + } + + /// Object that will be disposed unless the disposer is cancelled + private object potentiallyDisposable; + + } + + #endregion // class CancellableDisposer + + /// Initializes a new window manager + /// + /// View model binder that will be used to bind all created views to their models + /// + public WindowManager(IAutoBinder autoBinder = null) { + this.autoBinder = autoBinder; + + this.rootWindowActivatedDelegate = rootWindowActivated; + this.rootWindowClosedDelegate = rootWindowClosed; + this.viewTypesForViewModels = new ConcurrentDictionary(); + } + + /// The currently active top-level or modal window + public Form ActiveWindow { + get { return this.activeWindow; } + private set { + if(value != this.activeWindow) { + this.activeWindow = value; + OnPropertyChanged(nameof(ActiveWindow)); + } + } + } + + /// Opens a view as a new root window of the application + /// + /// Type of view model a root window will be opened for + /// + /// + /// 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) + /// + /// + /// Whether the view model should be disposed when the view is closed + /// + /// The window that has been opened by the window manager + public Form OpenRoot( + TViewModel viewModel = null, bool disposeOnClose = true + ) where TViewModel : class { + + Form window = (Form)CreateView(viewModel); + window.Activated += this.rootWindowActivatedDelegate; + window.Closed += this.rootWindowClosedDelegate; + + // If we either created the view model or the user explicitly asked us to + // dispose his view model, tag the window so that we know to dispose it + // when we're done (but still allow the user to change his mind) + if((viewModel == null) || disposeOnClose) { + window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead? + //window.SetValue(DisposeViewModelOnCloseProperty, true); + } + + window.Show(); + + return window; + } + + /// Displays a view as a modal window + /// + /// Type of the view model for which a view will be displayed + /// + /// + /// 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) + /// + /// + /// Whether the view model should be disposed when the view is closed + /// + /// The return value of the modal window + public bool? ShowModal( + TViewModel viewModel = null, bool disposeOnClose = true + ) where TViewModel : class { + Form window = (Form)CreateView(viewModel); + window.Owner = this.activeWindow; + window.Activated += this.rootWindowActivatedDelegate; + + try { + // If we either created the view model or the user explicitly asked us to + // dispose his view model, tag the window so that we know to dispose it + // when we're done (but still allow the user to change his mind) + if((viewModel == null) || disposeOnClose) { + window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead? + //window.SetValue(DisposeViewModelOnCloseProperty, true); + } + + DialogResult result = window.ShowDialog(this.activeWindow); + if((result == DialogResult.OK) || (result == DialogResult.Yes)) { + return true; + } else if((result == DialogResult.Cancel) || (result == DialogResult.No)) { + return false; + } else { + return null; + } + } + finally { + window.Activated -= this.rootWindowActivatedDelegate; + ActiveWindow = window.Owner; + + if(shouldDisposeViewModelOnClose(window)) { + IView windowAsView = window as IView; + if(windowAsView != null) { + object viewModelAsObject = windowAsView.DataContext; + windowAsView.DataContext = null; + disposeIfDisposable(viewModelAsObject); + } + } + disposeIfDisposable(window); + } + } + + /// Creates the view for the specified view model + /// + /// Type of view model for which a view will be created + /// + /// + /// 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) + /// + /// The view for the specified view model + public virtual Control CreateView( + TViewModel viewModel = null + ) where TViewModel : class { + Type viewType = LocateViewForViewModel(typeof(TViewModel)); + Control viewControl = (Control)CreateInstance(viewType); + using(var viewDisposer = new CancellableDisposer(viewControl)) { + + // Create a view model if none was provided, and in either case assign + // the view model to the view (provided it implements IView). + using(var viewModelDisposer = new CancellableDisposer()) { + IView viewControlAsView = viewControl as IView; + if(viewModel == null) { // No view model provided, create one + if(viewControlAsView == null) { // View doesn't implement IView + viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); + viewModelDisposer.Set(viewModel); + } else if(viewControlAsView.DataContext == null) { // View has no view model + viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); + viewModelDisposer.Set(viewModel); + viewControlAsView.DataContext = viewModel; + } else { // There's an existing view model + viewModel = viewControlAsView.DataContext as TViewModel; + if(viewModel == null) { // The existing view model is another type + viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); + viewModelDisposer.Set(viewModel); + viewControlAsView.DataContext = viewModel; + } + } + } else if(viewControlAsView != null) { // Caller has provided a view model + viewControlAsView.DataContext = viewModel; + } + + // If an auto binder was provided, automatically bind the view to the view model + if(this.autoBinder != null) { + this.autoBinder.Bind(viewControl, viewModel); + } + + viewModelDisposer.Dismiss(); // Everything went well, we keep the view model + } + + viewDisposer.Dismiss(); // Everything went well, we keep the view + + } + + return viewControl; + } + + /// Creates a view model without a matching view + /// Type of view model that will be created + /// The new view model + /// + /// + /// This is useful if a view model needs to create child view models (i.e. paged container + /// and wants to ensure the same dependency injector (if any) if used as the window + /// manager uses for other view models it creates. + /// + /// + /// This way, view models can set up their child view models without having to immediately + /// bind a view to them. Later on, views can use the window manager to create a matching + /// child view and store it in a container. + /// + /// + public TViewModel CreateViewModel() where TViewModel : class { + return (TViewModel)CreateInstance(typeof(TViewModel)); + } + + /// Locates the view that will be used to a view model + /// + /// Type of view model for which the view will be located + /// + /// The type of view that should be used for the specified view model + protected virtual Type LocateViewForViewModel(Type viewModelType) { + Type viewType; + if(!this.viewTypesForViewModels.TryGetValue(viewModelType, out viewType)) { + string viewName = viewModelType.Name; + if(viewName.EndsWith("ViewModel")) { + viewName = viewName.Substring(0, viewName.Length - 9); + } + + Type[] exportedTypes = viewModelType.Assembly.GetExportedTypes(); + Type[] namespaceTypes = filterTypesByNamespace(exportedTypes, viewModelType.Namespace); + + // First, search the own namespace (because if two identical view models exist in + // different namespaces, the one in the same namespace is most likely the desired one) + viewType = findBestMatch( + namespaceTypes, + viewName + "View", + viewName + "Page", + viewName + "Form", + viewName + "Window", + viewName + "Dialog", + viewName + "Control" + ); + + // If the view model doesn't exist in the same namespace, expand the search to + // the entire assembly the view is in. + if(viewType == null) { + viewType = findBestMatch( + exportedTypes, + viewName + "View", + viewName + "Page", + viewName + "Form", + viewName + "Window", + viewName + "Dialog", + viewName + "Control" + ); + } + + // Still no view found? We give up! + if(viewType == null) { + throw new InvalidOperationException( + string.Format("Could not locate view for view model '{0}'", viewModelType.Name) + ); + } + + this.viewTypesForViewModels.TryAdd(viewModelType, viewType); + } + + return viewType; + } + + /// Creates an instance of the specified type + /// Type an instance will be created of + /// The created instance + /// + /// Use this to wire up your dependency injection container. By default, + /// the Activator class will be used to create instances which only works + /// if all of your view models are concrete classes. + /// + protected virtual object CreateInstance(Type type) { + return Activator.CreateInstance(type); + } + + /// Called when one of the application's root windows is closed + /// Window that has been closed + /// Not used + private void rootWindowClosed(object sender, EventArgs arguments) { + Form closedWindow = (Form)sender; + closedWindow.Closed -= this.rootWindowClosedDelegate; + closedWindow.Activated -= this.rootWindowActivatedDelegate; + + // If the view model was created just for this view or if the user asked us + // to dispose of his view model, do so now. + if(shouldDisposeViewModelOnClose(closedWindow)) { + IView windowAsView = closedWindow as IView; + if(windowAsView != null) { + object viewModelAsObject = windowAsView.DataContext; + windowAsView.DataContext = null; + disposeIfDisposable(viewModelAsObject); + } + } + + lock(this) { + ActiveWindow = null; + } + } + + /// Called when one of the application's root windows is activated + /// Window that has been put in the foreground + /// Not used + private void rootWindowActivated(object sender, EventArgs arguments) { + lock(this) { + ActiveWindow = (Form)sender; + } + } + + /// Tries to find the best match for a named type in a list of types + /// List of types the search will take place in + /// + /// The candidates the method will look for, starting with the best match + /// + /// The best match in the list of types, if any match was found + private static Type findBestMatch(Type[] types, params string[] typeNames) { + int bestMatchFound = typeNames.Length; + + Type type = null; + for(int index = 0; index < types.Length; ++index) { + for(int nameIndex = 0; nameIndex < bestMatchFound; ++nameIndex) { + if(types[index].Name == typeNames[nameIndex]) { + bestMatchFound = nameIndex; + type = types[index]; + + if(bestMatchFound == 0) { // There can be no better match + return type; + } + + break; + } + } + } + + return type; + } + + /// Disposes the specified object if it implements IDisposable + /// Type of object that will disposed if possible + /// Object that the method will attempt to dispose + private static void disposeIfDisposable(T instance) where T : class { + var disposable = instance as IDisposable; + if(disposable != null) { + disposable.Dispose(); + } + } + + /// Determines if the view owns the view model + /// View that will be checked for ownership + /// True if the view owns the view model + private static bool shouldDisposeViewModelOnClose(Control view) { + string tagAsString = view.Tag as string; + if(tagAsString != null) { + return tagAsString.Contains("DisposeViewModelOnClose"); + } else { + return false; + } + } + + /// Filters a list of types to contain only those in a specific namespace + /// List of exported types that will be filtered + /// + /// Namespace the types in the filtered list will be in + /// + /// A subset of the specified types that are in the provided namespace + private static Type[] filterTypesByNamespace(Type[] exportedTypes, string filteredNamespace) { + var filteredTypes = new List(exportedTypes.Length / 2); + for(int index = 0; index < exportedTypes.Length; ++index) { + Type exportedType = exportedTypes[index]; + if(exportedType.Namespace == filteredNamespace) { + filteredTypes.Add(exportedType); + } + } + + return filteredTypes.ToArray(); + } + + /// The application's currently active root window + private Form activeWindow; + /// Invoked when a root window is put in the foreground + private EventHandler rootWindowActivatedDelegate; + /// Invoked when a root window has been closed + private EventHandler rootWindowClosedDelegate; + /// View model binder that will be used on all created views + private IAutoBinder autoBinder; + /// Caches the view types to use for a view model + private ConcurrentDictionary viewTypesForViewModels; + + } + +} // namespace Nuclex.Windows.Forms