From d057f704497b63faa5e57c6330afb436a0d9708c Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Thu, 19 Jun 2025 15:12:51 +0200 Subject: [PATCH] Added option to create service scopes per window or dialog by overriding the WindowManager.CreateScope() method --- ReadMe.md | 2 +- Source/IWindowScope.cs | 45 +++++++++++++ Source/WindowManager.cs | 137 ++++++++++++++++++++++++++++++---------- 3 files changed, 149 insertions(+), 35 deletions(-) create mode 100644 Source/IWindowScope.cs diff --git a/ReadMe.md b/ReadMe.md index 7cd8cce..99b9927 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -118,7 +118,7 @@ you have set up in your Ninject kernel. class MainViewModel { public MainViewModel(IMyService myService, IMySettings mySettings) { - // ... + // ... } } diff --git a/Source/IWindowScope.cs b/Source/IWindowScope.cs new file mode 100644 index 0000000..43c55c3 --- /dev/null +++ b/Source/IWindowScope.cs @@ -0,0 +1,45 @@ +using System; + +namespace Nuclex.Windows.Forms { + + /// Constructs views and view model in a scope + /// + /// + /// By default the uses its own + /// method to construct views + /// and view models via , + /// 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. + /// + /// + /// To support dependency injection via constructor parameters, you can + /// inherit from the and provide your own override + /// of 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. + /// + /// + /// In this final case, "scopes" 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 + /// method, you can make + /// the window manager set up an implicit scope per window or dialog. + /// + /// + public interface IWindowScope : IDisposable { + + /// Creates an instance of the specified type in the scope + /// 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. + /// + object CreateInstance(Type type); + + } + +} // namespace Nuclex.Windows.Forms diff --git a/Source/WindowManager.cs b/Source/WindowManager.cs index 4d0fea8..b83c448 100644 --- a/Source/WindowManager.cs +++ b/Source/WindowManager.cs @@ -39,6 +39,36 @@ namespace Nuclex.Windows.Forms { #endif public class WindowManager : Observable, IWindowManager { + #region class WindowManagerScope + + /// Global scope that uses the WindowManager's CreateInstance() + public class WindowManagerScope : IWindowScope { + + /// Initializes a new global window scope + /// + /// Window manager whose CreateInstance() method will be used + /// + public WindowManagerScope(WindowManager windowManager) { + this.windowManager = windowManager; + } + + /// Creates an instance of the specified type in the scope + /// Type an instance will be created of + /// The created instance + object IWindowScope.CreateInstance(Type type) { + return this.windowManager.CreateInstance(type); + } + + /// Does nothing because this is the global fallback scope + void IDisposable.Dispose() {} + + /// WindowManager whose CreateInstance() method will be wrapped + private WindowManager windowManager; + + } + + #endregion // class WindwoManagerScope + #region class CancellableDisposer /// Disposes an object that potentially implements IDisposable @@ -91,6 +121,7 @@ namespace Nuclex.Windows.Forms { this.rootWindowActivatedDelegate = rootWindowActivated; this.rootWindowClosedDelegate = rootWindowClosed; + this.windowManagerAsScope = new WindowManagerScope(this); this.viewTypesForViewModels = new ConcurrentDictionary(); } @@ -193,45 +224,55 @@ namespace Nuclex.Windows.Forms { 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)) { + Control viewControl; + { + Type viewType = LocateViewForViewModel(typeof(TViewModel)); - // 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); + 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 + // 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)scope.CreateInstance(typeof(TViewModel)); + viewModelDisposer.Set(viewModel); + } else if(viewControlAsView.DataContext == null) { // View has no view model + 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)scope.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 } - } 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 - if(this.autoBinder != null) { - this.autoBinder.Bind(viewControl, viewModel); - } - - viewModelDisposer.Dismiss(); // Everything went well, we keep the view model + scopeDisposer.Dismiss(); // Everything went well, we keep the scope } - viewDisposer.Dismiss(); // Everything went well, we keep the view - - } + 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; + /// Creates an instance of the specified type in a new scope + /// Type an instance will be created of + /// The created instance and the scope in which it lives + /// + /// This is identical to 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. + /// + protected virtual IWindowScope CreateWindowScope() { + return this.windowManagerAsScope; } /// Called when one of the application's root windows is closed @@ -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 { /// /// Control whose view model will be disposed when it is itself disposed /// - 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); } + /// Attaches a scope disposer to a control + /// + /// Control that will dispose a scope when it is itself disposed + /// + 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(); + }; + } + } + } + /// Filters a list of types to contain only those in a specific namespace /// List of exported types that will be filtered /// @@ -432,6 +499,8 @@ namespace Nuclex.Windows.Forms { private EventHandler rootWindowActivatedDelegate; /// Invoked when a root window has been closed private EventHandler rootWindowClosedDelegate; + /// Scope that uses the WindowManager's global CreateInstance() method + private WindowManagerScope windowManagerAsScope; /// View model binder that will be used on all created views private IAutoBinder autoBinder; /// Caches the view types to use for a view model