Added option to create service scopes per window or dialog by overriding the WindowManager.CreateScope() method

This commit is contained in:
Markus Ewald 2025-06-19 15:12:51 +02:00
parent f6457b1909
commit d057f70449
3 changed files with 149 additions and 35 deletions

View File

@ -118,7 +118,7 @@ you have set up in your Ninject kernel.
class MainViewModel { class MainViewModel {
public MainViewModel(IMyService myService, IMySettings mySettings) { public MainViewModel(IMyService myService, IMySettings mySettings) {
// ... // ...
} }
} }

45
Source/IWindowScope.cs Normal file
View File

@ -0,0 +1,45 @@
using System;
namespace Nuclex.Windows.Forms {
/// <summary>Constructs views and view model in a scope</summary>
/// <remarks>
/// <para>
/// By default the <see cref="WindowManager" /> uses its own
/// <see cref="WindowManager.CreateInstance" /> method to construct views
/// and view models via <see cref="Activator.CreateInstance(Type)" />,
/// which is enough to create forms (which the Windows Forms designer already
/// requires to have parameterless constructors) and view models, so long as
/// they also have parameterless constructors.
/// </para>
/// <para>
/// To support dependency injection via constructor parameters, you can
/// inherit from the <see cref="WindowManager" /> and provide your own override
/// of <see cref="WindowManager.CreateInstance" /> that constructs the required
/// instance via your dependency injector. This is decent until you have multiple
/// view models all accessing the same resource (i.e. a database) via threads.
/// </para>
/// <para>
/// In this final case, &quot;scopes&quot; have become a common solution. Each
/// scope has access to singleton services (these exist for the lifetime of
/// the entire application), but there are also scoped services which will have
/// new instances constructed within each scope. By implementing the
/// <see cref="WindowManager.CreateWindowScope" /> method, you can make
/// the window manager set up an implicit scope per window or dialog.
/// </para>
/// </remarks>
public interface IWindowScope : IDisposable {
/// <summary>Creates an instance of the specified type in the scope</summary>
/// <param name="type">Type an instance will be created of</param>
/// <returns>The created instance</returns>
/// <remarks>
/// 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.
/// </remarks>
object CreateInstance(Type type);
}
} // namespace Nuclex.Windows.Forms

View File

@ -39,6 +39,36 @@ namespace Nuclex.Windows.Forms {
#endif #endif
public class WindowManager : Observable, IWindowManager { public class WindowManager : Observable, IWindowManager {
#region class WindowManagerScope
/// <summary>Global scope that uses the WindowManager's CreateInstance()</summary>
public class WindowManagerScope : IWindowScope {
/// <summary>Initializes a new global window scope</summary>
/// <param name="windowManager">
/// Window manager whose CreateInstance() method will be used
/// </param>
public WindowManagerScope(WindowManager windowManager) {
this.windowManager = windowManager;
}
/// <summary>Creates an instance of the specified type in the scope</summary>
/// <param name="type">Type an instance will be created of</param>
/// <returns>The created instance</returns>
object IWindowScope.CreateInstance(Type type) {
return this.windowManager.CreateInstance(type);
}
/// <summary>Does nothing because this is the global fallback scope</summary>
void IDisposable.Dispose() {}
/// <summary>WindowManager whose CreateInstance() method will be wrapped</summary>
private WindowManager windowManager;
}
#endregion // class WindwoManagerScope
#region class CancellableDisposer #region class CancellableDisposer
/// <summary>Disposes an object that potentially implements IDisposable</summary> /// <summary>Disposes an object that potentially implements IDisposable</summary>
@ -91,6 +121,7 @@ namespace Nuclex.Windows.Forms {
this.rootWindowActivatedDelegate = rootWindowActivated; this.rootWindowActivatedDelegate = rootWindowActivated;
this.rootWindowClosedDelegate = rootWindowClosed; this.rootWindowClosedDelegate = rootWindowClosed;
this.windowManagerAsScope = new WindowManagerScope(this);
this.viewTypesForViewModels = new ConcurrentDictionary<Type, Type>(); this.viewTypesForViewModels = new ConcurrentDictionary<Type, Type>();
} }
@ -193,45 +224,55 @@ namespace Nuclex.Windows.Forms {
public virtual Control CreateView<TViewModel>( public virtual Control CreateView<TViewModel>(
TViewModel viewModel = null TViewModel viewModel = null
) where TViewModel : class { ) where TViewModel : class {
Type viewType = LocateViewForViewModel(typeof(TViewModel)); Control viewControl;
Control viewControl = (Control)CreateInstance(viewType); {
using(var viewDisposer = new CancellableDisposer(viewControl)) { Type viewType = LocateViewForViewModel(typeof(TViewModel));
// Create a view model if none was provided, and in either case assign IWindowScope scope = CreateWindowScope();
// the view model to the view (provided it implements IView). using(var scopeDisposer = new CancellableDisposer(scope)) {
using(var viewModelDisposer = new CancellableDisposer()) { viewControl = (Control)scope.CreateInstance(viewType);
IView viewControlAsView = viewControl as IView; using(var viewDisposer = new CancellableDisposer(viewControl)) {
if(viewModel == null) { // No view model provided, create one
if(viewControlAsView == null) { // View doesn't implement IView // Create a view model if none was provided, and in either case assign
viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); // the view model to the view (provided it implements IView).
viewModelDisposer.Set(viewModel); using(var viewModelDisposer = new CancellableDisposer()) {
} else if(viewControlAsView.DataContext == null) { // View has no view model IView viewControlAsView = viewControl as IView;
viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); if(viewModel == null) { // No view model provided, create one
viewModelDisposer.Set(viewModel); if(viewControlAsView == null) { // View doesn't implement IView
viewControlAsView.DataContext = viewModel; viewModel = (TViewModel)scope.CreateInstance(typeof(TViewModel));
} else { // There's an existing view model viewModelDisposer.Set(viewModel);
viewModel = viewControlAsView.DataContext as TViewModel; } else if(viewControlAsView.DataContext == null) { // View has no view model
if(viewModel == null) { // The existing view model is another type viewModel = (TViewModel)scope.CreateInstance(typeof(TViewModel));
viewModel = (TViewModel)CreateInstance(typeof(TViewModel)); viewModelDisposer.Set(viewModel);
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)scope.CreateInstance(typeof(TViewModel));
viewModelDisposer.Set(viewModel);
viewControlAsView.DataContext = viewModel;
}
}
} else if(viewControlAsView != null) { // Caller has provided a view model
viewControlAsView.DataContext = viewModel; 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
} }
} else if(viewControlAsView != null) { // Caller has provided a view model
viewControlAsView.DataContext = viewModel; viewDisposer.Dismiss(); // Everything went well, we keep the view
} }
// If an auto binder was provided, automatically bind the view to the view model scopeDisposer.Dismiss(); // Everything went well, we keep the scope
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 setupScopeDisposal(viewControl, scope);
} // beauty scope
}
return viewControl; return viewControl;
} }
@ -322,8 +363,18 @@ namespace Nuclex.Windows.Forms {
return Activator.CreateInstance(type); return Activator.CreateInstance(type);
} }
protected virtual object CreateServiceScope() { /// <summary>Creates an instance of the specified type in a new scope</summary>
return null; /// <param name="type">Type an instance will be created of</param>
/// <returns>The created instance and the scope in which it lives</returns>
/// <remarks>
/// This is identical to <see cref="CreateInstance" /> but, if used together
/// with a dependency injector, should also create a service scope. This way,
/// an implicit service scope will cover the lifetime of a view model and
/// any non-singleton services will use new instances, avoiding, for example,
/// that multiple dialogs access the same database connection simultaneously.
/// </remarks>
protected virtual IWindowScope CreateWindowScope() {
return this.windowManagerAsScope;
} }
/// <summary>Called when one of the application's root windows is closed</summary> /// <summary>Called when one of the application's root windows is closed</summary>
@ -338,6 +389,7 @@ namespace Nuclex.Windows.Forms {
ActiveWindow = null; ActiveWindow = null;
} }
// Application.Run() already does this and it's the user's responsibility anyways
//disposeIfDisposable(closedWindow); //disposeIfDisposable(closedWindow);
} }
@ -392,7 +444,7 @@ namespace Nuclex.Windows.Forms {
/// <param name="control"> /// <param name="control">
/// Control whose view model will be disposed when it is itself disposed /// Control whose view model will be disposed when it is itself disposed
/// </param> /// </param>
private void setupViewModelDisposal(Control control) { private static void setupViewModelDisposal(Control control) {
IView controlAsView = control as IView; IView controlAsView = control as IView;
if(controlAsView != null) { if(controlAsView != null) {
IDisposable disposableViewModel = controlAsView.DataContext as IDisposable; IDisposable disposableViewModel = controlAsView.DataContext as IDisposable;
@ -408,6 +460,21 @@ namespace Nuclex.Windows.Forms {
//window.SetValue(DisposeViewModelOnCloseProperty, true); //window.SetValue(DisposeViewModelOnCloseProperty, true);
} }
/// <summary>Attaches a scope disposer to a control</summary>
/// <param name="control">
/// Control that will dispose a scope when it is itself disposed
/// </param>
private void setupScopeDisposal(Control control, IWindowScope scope) {
if(!ReferenceEquals(scope, this.windowManagerAsScope)) {
IDisposable disposableScope = scope as IDisposable;
if(disposableScope != null) {
control.Disposed += delegate(object sender, EventArgs arguments) {
disposableScope.Dispose();
};
}
}
}
/// <summary>Filters a list of types to contain only those in a specific namespace</summary> /// <summary>Filters a list of types to contain only those in a specific namespace</summary>
/// <param name="exportedTypes">List of exported types that will be filtered</param> /// <param name="exportedTypes">List of exported types that will be filtered</param>
/// <param name="filteredNamespace"> /// <param name="filteredNamespace">
@ -432,6 +499,8 @@ namespace Nuclex.Windows.Forms {
private EventHandler rootWindowActivatedDelegate; private EventHandler rootWindowActivatedDelegate;
/// <summary>Invoked when a root window has been closed</summary> /// <summary>Invoked when a root window has been closed</summary>
private EventHandler rootWindowClosedDelegate; private EventHandler rootWindowClosedDelegate;
/// <summary>Scope that uses the WindowManager's global CreateInstance() method</summary>
private WindowManagerScope windowManagerAsScope;
/// <summary>View model binder that will be used on all created views</summary> /// <summary>View model binder that will be used on all created views</summary>
private IAutoBinder autoBinder; private IAutoBinder autoBinder;
/// <summary>Caches the view types to use for a view model</summary> /// <summary>Caches the view types to use for a view model</summary>