#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