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