diff --git a/Nuclex.Windows.Forms (net-4.0).csproj b/Nuclex.Windows.Forms (net-4.0).csproj index f77e696..2a4f824 100644 --- a/Nuclex.Windows.Forms (net-4.0).csproj +++ b/Nuclex.Windows.Forms (net-4.0).csproj @@ -67,13 +67,16 @@ ThreadedViewModel.cs + + UserControl Form + ProgressReporterForm.cs Designer diff --git a/Source/IActiveWindowTracker.cs b/Source/IActiveWindowTracker.cs new file mode 100644 index 0000000..aac4abe --- /dev/null +++ b/Source/IActiveWindowTracker.cs @@ -0,0 +1,38 @@ +#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 diff --git a/Source/IWindowManager.cs b/Source/IWindowManager.cs new file mode 100644 index 0000000..1685506 --- /dev/null +++ b/Source/IWindowManager.cs @@ -0,0 +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 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; + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/WindowManager.cs b/Source/WindowManager.cs new file mode 100644 index 0000000..72c12d4 --- /dev/null +++ b/Source/WindowManager.cs @@ -0,0 +1,354 @@ +#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.Views; + +namespace Nuclex.Windows.Forms { + + /// Manages an application's windows and views + public class WindowManager : Observable, IWindowManager { + + /// Initializes a new window manager + public WindowManager() { + 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); + + bool createdViewModel = false; + try { + IView viewControlAsView = viewControl as IView; + if(viewControlAsView != null) { + if(viewModel != null) { + viewControlAsView.DataContext = viewModel; + } else if(viewControlAsView.DataContext == null) { + viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); + viewControlAsView.DataContext = viewModel; + createdViewModel = true; + } + } + } + catch(Exception) { + if(createdViewModel) { // If we created it, we kill it. + disposeIfDisposable(viewModel); + } + disposeIfDisposable(viewControl); + + throw; + } + + return viewControl; + } + + /// 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 + "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 + "Form", + viewName + "Window", + viewName + "Dialog", + viewName + "Control" + ); + } + + 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; + /// Caches the view types to use for a view model + private ConcurrentDictionary viewTypesForViewModels; + + } + +} // namespace Nuclex.Windows.Forms