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

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
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
/// <summary>Disposes an object that potentially implements IDisposable</summary>
@ -91,6 +121,7 @@ namespace Nuclex.Windows.Forms {
this.rootWindowActivatedDelegate = rootWindowActivated;
this.rootWindowClosedDelegate = rootWindowClosed;
this.windowManagerAsScope = new WindowManagerScope(this);
this.viewTypesForViewModels = new ConcurrentDictionary<Type, Type>();
}
@ -193,8 +224,13 @@ namespace Nuclex.Windows.Forms {
public virtual Control CreateView<TViewModel>(
TViewModel viewModel = null
) where TViewModel : class {
Control viewControl;
{
Type viewType = LocateViewForViewModel(typeof(TViewModel));
Control viewControl = (Control)CreateInstance(viewType);
IWindowScope scope = CreateWindowScope();
using(var scopeDisposer = new CancellableDisposer(scope)) {
viewControl = (Control)scope.CreateInstance(viewType);
using(var viewDisposer = new CancellableDisposer(viewControl)) {
// Create a view model if none was provided, and in either case assign
@ -203,16 +239,16 @@ namespace Nuclex.Windows.Forms {
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));
viewModel = (TViewModel)scope.CreateInstance(typeof(TViewModel));
viewModelDisposer.Set(viewModel);
} else if(viewControlAsView.DataContext == null) { // View has no view model
viewModel = (TViewModel)CreateInstance(typeof(TViewModel));
viewModel = (TViewModel)scope.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));
viewModel = (TViewModel)scope.CreateInstance(typeof(TViewModel));
viewModelDisposer.Set(viewModel);
viewControlAsView.DataContext = viewModel;
}
@ -230,9 +266,14 @@ namespace Nuclex.Windows.Forms {
}
viewDisposer.Dismiss(); // Everything went well, we keep the view
}
scopeDisposer.Dismiss(); // Everything went well, we keep the scope
}
setupScopeDisposal(viewControl, scope);
} // beauty scope
return viewControl;
}
@ -322,8 +363,18 @@ namespace Nuclex.Windows.Forms {
return Activator.CreateInstance(type);
}
protected virtual object CreateServiceScope() {
return null;
/// <summary>Creates an instance of the specified type in a new scope</summary>
/// <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>
@ -338,6 +389,7 @@ namespace Nuclex.Windows.Forms {
ActiveWindow = null;
}
// Application.Run() already does this and it's the user's responsibility anyways
//disposeIfDisposable(closedWindow);
}
@ -392,7 +444,7 @@ namespace Nuclex.Windows.Forms {
/// <param name="control">
/// Control whose view model will be disposed when it is itself disposed
/// </param>
private void setupViewModelDisposal(Control control) {
private static void setupViewModelDisposal(Control control) {
IView controlAsView = control as IView;
if(controlAsView != null) {
IDisposable disposableViewModel = controlAsView.DataContext as IDisposable;
@ -408,6 +460,21 @@ namespace Nuclex.Windows.Forms {
//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>
/// <param name="exportedTypes">List of exported types that will be filtered</param>
/// <param name="filteredNamespace">
@ -432,6 +499,8 @@ namespace Nuclex.Windows.Forms {
private EventHandler rootWindowActivatedDelegate;
/// <summary>Invoked when a root window has been closed</summary>
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>
private IAutoBinder autoBinder;
/// <summary>Caches the view types to use for a view model</summary>