#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; } /// 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