深入浅出 WPF/C# 依赖注入中的委托工厂模式(Delegate Factory)【AI生成】
在现代 WPF MVVM 架构中,随着页面导航、向导流程、复杂弹窗和动态业务模块的增加,ViewModel 的创建方式逐渐成为一个值得认真考虑的设计问题。
假设一个导入窗口包含多个功能模块:文件上传、格式解析、数据预览和导入结果展示。父级 ImportViewModel 负责协调整个导入流程,而 ParserViewModel 负责具体的解析逻辑。当用户更换文件、重新解析或者重新打开窗口时,系统可能需要创建一个全新的 ParserViewModel,同时保留其他模块已有的状态。
如果所有子 ViewModel 都通过构造函数直接注入,代码虽然简单,却可能使对象的创建时机、状态归属和生命周期管理变得不够清晰。
委托工厂(Delegate Factory)正是解决这类问题的一种常用设计方案。本文以 C# 的 Func<T> 和微软的 Microsoft.Extensions.DependencyInjection 为例,说明其工作原理、适用场景和需要注意的边界。
一、传统构造函数注入的局限
先看一个常见的设计。
public class ImportViewModel { private readonly UploadViewModel _uploadViewModel; private readonly ParserViewModel _parserViewModel; public ImportViewModel( UploadViewModel uploadViewModel, ParserViewModel parserViewModel) { _uploadViewModel = uploadViewModel; _parserViewModel = parserViewModel; } }
这里使用构造函数注入,将 UploadViewModel 和 ParserViewModel 作为 ImportViewModel 的依赖项。
这种写法本身没有问题。对于那些需要在父级 ViewModel 的整个工作期间持续存在、并且应当共享状态的子对象,直接注入通常是最简单、最清晰的方案。
问题出现在另一类场景中:父级 ViewModel 需要的不是一个固定的子对象,而是在某个业务动作发生时,按需创建一个新的子对象。
例如:
-
用户选择另一个文件,需要重新创建解析器。
-
用户重新开始导入,需要创建一组全新的流程对象。
-
用户连续打开多个弹窗,每个弹窗都需要独立的编辑状态。
-
用户创建多个相互独立的页面实例,每个实例都需要自己的查询条件和操作记录。
如果直接注入 ParserViewModel,父级得到的是一个已经创建好的实例,而不是创建实例的能力。
当业务要求创建第二个、第三个解析器时,父级就必须自行处理对象替换、状态重置或者其他创建逻辑。随着业务复杂度提高,创建职责可能逐渐混入 ViewModel 的业务代码。
因此,真正需要解决的问题不是构造函数注入本身,而是:
当依赖对象需要动态创建时,如何将对象创建能力纳入依赖注入架构,同时避免让业务层直接操作 DI 容器?
二、委托工厂:从注入对象到注入创建能力
1. 什么是委托工厂?
委托工厂是一种利用委托封装对象创建操作的设计方式。
在 C# 中,Func<T> 表示一个无参数、返回类型为 T 的函数:
Func<ParserViewModel> parserFactory;
它可以理解为一个返回 ParserViewModel 的可调用对象。
与直接注入实例相比,两者的区别如下:
// 直接注入:获得一个对象
ParserViewModel parserViewModel;
// 委托工厂:获得一个创建对象的入口
Func<ParserViewModel> parserViewModelFactory;
前者提供的是对象,后者提供的是获取对象的能力。
调用工厂时:
ParserViewModel parserViewModel =
parserViewModelFactory();
才会执行委托内部定义的逻辑。
这里有一个重要前提:Func<T> 本身并不保证每次调用都会产生新实例。它究竟返回新对象还是已有对象,取决于委托的具体实现。
只有当工厂内部每次都执行一次新的对象解析,并且目标服务配置为AddTransient瞬态生命周期时,才能实现通常所说的“每次调用创建新实例”。
2. 在 ViewModel 中使用委托工厂
我们可以将 ImportViewModel 改写为以下形式:
using System; public class ImportViewModel { private readonly UploadViewModel _uploadViewModel; private readonly Func<ParserViewModel> _parserViewModelFactory; public ImportViewModel( UploadViewModel uploadViewModel, Func<ParserViewModel> parserViewModelFactory) { _uploadViewModel = uploadViewModel ?? throw new ArgumentNullException( nameof(uploadViewModel)); _parserViewModelFactory = parserViewModelFactory ?? throw new ArgumentNullException( nameof(parserViewModelFactory)); } private void OpenParserPage() { ParserViewModel parserViewModel = _parserViewModelFactory(); // 将新创建的解析器交给相应的页面宿主。 // 后续由宿主负责使用、替换和释放相关资源。 } }
现在,ImportViewModel 不再要求 DI 容器提前提供一个固定的 ParserViewModel 实例,而是接收一个创建委托。
当业务需要新的解析器时,调用 _parserViewModelFactory() 即可。
这种设计保留了构造函数注入的优点,同时将依赖对象的创建时机交给业务流程控制。
需要强调的是,示例中的 OpenParserPage() 只是说明创建方式。实际项目还需要将新实例交给页面宿主,并明确旧实例的处理策略。
三、如何在微软 DI 容器中注册委托工厂?
使用 Microsoft.Extensions.DependencyInjection 时,有一个容易忽略的细节:微软默认 DI 容器不会自动为任意服务生成 Func<T> 工厂。
因此,不能只注册 ParserViewModel,还需要显式注册对应的工厂委托。
using Microsoft.Extensions.DependencyInjection; services.AddTransient<ParserViewModel>(); services.AddTransient<Func<ParserViewModel>>( provider => () => provider.GetRequiredService<ParserViewModel>());
第一条注册:
services.AddTransient<ParserViewModel>();
表示每次通过容器解析 ParserViewModel 时,容器都会创建一个新的实例,并解析其构造函数所需的依赖项。
第二条注册:
services.AddTransient<Func<ParserViewModel>>( provider => () => provider.GetRequiredService<ParserViewModel>());
则告诉容器如何构造一个 Func<ParserViewModel>。
这里需要区分两个时间点。
第一,DI 容器解析 Func<ParserViewModel> 时,会创建一个委托。此时,委托内部的表达式尚未执行。
第二,业务代码调用该委托时:
var parser1 = parserFactory();
var parser2 = parserFactory();
委托才会分别调用 GetRequiredService<ParserViewModel>()。
由于 ParserViewModel 注册为 Transient,且每次调用都会重新向容器解析,因此在正常情况下:
ReferenceEquals(parser1, parser2); // false
两个对象具有独立的实例状态。
如果 ParserViewModel 内部也注入了其他服务,那么这些服务仍然按照各自的 DI 注册规则进行解析。例如,瞬态解析器可以依赖一个单例日志服务,但不能因此假设它的所有依赖也都是全新的。
四、委托工厂为什么比直接注入 IServiceProvider 更合适?
在动态创建对象时,另一种常见写法是直接注入 IServiceProvider。
public class ImportViewModel { private readonly IServiceProvider _serviceProvider; public ImportViewModel( IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } private void OpenParserPage() { var parserViewModel = _serviceProvider .GetRequiredService<ParserViewModel>(); } }
这种方式确实能够创建对象,但它也引入了一个架构问题。
ImportViewModel 原本只需要创建 ParserViewModel,现在却获得了访问整个服务容器的能力。它可以继续解析其他服务,甚至逐渐承担起本应属于组合根的依赖组装职责。
这种将服务解析逻辑放入业务对象的方式,通常被称为服务定位器模式(Service Locator),也是依赖注入设计中需要谨慎使用的做法。
两种方式的区别在于:
| 比较维度 | 注入 IServiceProvider | 注入 Func<T> |
|---|---|---|
| 依赖表达 | 依赖整个服务容器 | 明确依赖某种对象的创建能力 |
| 可见性 | 具体依赖可能隐藏在方法内部 | 工厂类型直接出现在构造函数中 |
| 创建范围 | 理论上可以解析多个已注册服务 | 只能通过该委托获取指定类型 |
| 单元测试 | 通常需要模拟容器解析行为 | 可以直接传入创建委托 |
| 适用场景 | 组合根、基础设施或确有需要的动态解析场景 | 业务层需要按需创建特定类型的对象 |
需要注意,委托工厂并不会让业务层与 DI 容器完全没有间接关系。工厂本身通常仍然由容器构建,只是它将依赖边界收窄到了一个明确的创建操作。
这种设计的价值在于:业务对象只依赖自己真正需要的能力,而不必获得整个容器的访问权。
五、委托工厂与 Transient:创建和生命周期是两个问题
在讨论委托工厂时,最容易产生的误解是将 Func<T>、Transient 和对象释放视为同一件事。
实际上,它们分别解决不同的问题。
1. Func<T> 解决创建时机
Func<T> 让业务代码决定什么时候执行对象创建或获取逻辑。
var parser = parserFactory();
在调用之前,工厂内部的创建逻辑不会因为委托本身被注入而自动执行。
2. Transient 解决容器解析时的实例策略
对于瞬态注册:
services.AddTransient<ParserViewModel>();
每次通过容器解析 ParserViewModel 时,都会创建新实例。
但这并不意味着对象会在方法结束时自动销毁,也不意味着其引用会自动消失。
如果父级 ViewModel 将创建出来的实例保存在字段中,那么只要父级仍然引用它,该对象就可能继续存活。
3. 对象释放需要单独设计
假设 ParserViewModel 实现了 IDisposable:
public class ParserViewModel : IDisposable { public void Dispose() { // 释放该对象自行拥有的资源。 } }
当页面关闭时,不能仅仅因为它注册为 Transient,就认为容器会立即调用 Dispose()。
微软默认 DI 容器会跟踪由容器创建的可释放服务,并通常在相应容器或作用域释放时统一释放这些服务,而不是在瞬态对象不再被页面使用时立即释放。
这意味着,如果一个长期存活的根容器反复创建大量可释放的瞬态对象,这些对象可能一直被容器跟踪到根容器释放时。
对于 WPF 桌面程序,这可能造成资源长期占用。
因此,使用委托工厂时,应当同时考虑:
-
谁持有新创建的 ViewModel。
-
页面关闭后,谁清除相关引用。
-
事件订阅是否需要取消。
-
定时器、文件句柄等资源是否需要释放。
-
对象是否属于某个明确的 DI 作用域。
-
是否需要由窗口或页面宿主统一管理对象生命周期。
如果解析器只负责普通的数据转换,并不拥有需要释放的资源,那么可能根本不需要实现 IDisposable。不要为了追求形式上的完整而增加不必要的生命周期复杂度。
六、结合 WPF MVVM:什么时候应该使用委托工厂?
委托工厂不是用来替代所有构造函数注入的。它更适合那些创建时机由业务操作决定、并且需要独立实例的对象。
场景一:重新解析文件
在题库导入模块中,用户可能先导入 Excel,再切换到 Markdown,或者修改解析规则后重新解析。
如果解析过程包含独立的状态,例如解析选项、错误列表、预览数据和当前文件信息,那么创建新的 ParserViewModel 可以避免不必要的状态残留。
private void Reparse() { var parser = _parserViewModelFactory(); // 将本次解析任务所需的数据交给 parser。 // 由当前导入流程负责管理该实例。 }
这里是否每次都需要创建新实例,仍取决于业务需求。如果解析器能够安全地重置全部状态,复用实例也可能是合理方案。委托工厂提供的是一种选择,而不是强制要求。
场景二:多窗口或多弹窗
当用户同时打开两个导入窗口时,每个窗口通常需要独立的编辑状态、当前文件和错误信息。
此时,为每个窗口创建一个新的窗口 ViewModel,往往比让多个窗口共享同一个 ViewModel 更符合业务语义。
需要特别注意的是,如果要求每个窗口拥有独立的整个对象图,只将最外层 ViewModel 注册为 Transient 并不总是充分的。
如果新创建的窗口 ViewModel 依赖某个 Singleton 子 ViewModel,那么多个窗口仍然可能共享该子对象。
因此,独立实例不仅取决于顶层对象,还取决于依赖图中各个对象的注册策略。
场景三:带参数的动态创建
如果新建的解析器需要在创建时获得文件路径、解析模式或任务编号,那么无参数的 Func<T> 可能就不够用了。
此时可以定义显式工厂接口:
public interface IParserViewModelFactory { ParserViewModel Create( string filePath, ImportMode mode); }
父级 ViewModel 依赖这个接口,而不是直接依赖 DI 容器。
public class ImportViewModel { private readonly IParserViewModelFactory _factory; public ImportViewModel( IParserViewModelFactory factory) { _factory = factory; } private void OpenParser( string filePath, ImportMode mode) { var parser = _factory.Create( filePath, mode); } }
工厂接口能够清晰表达创建参数,也方便在单元测试中替换实现。
它的代价是需要额外编写接口和实现类。因此,对于无参数、简单的创建需求,Func<T> 往往更轻量;对于参数复杂、创建规则多或者需要集中管理对象所有权的场景,显式工厂通常更容易维护。
七、设计原则:让创建职责与业务职责保持清晰
委托工厂真正有价值的地方,并不是减少了多少行代码,而是让对象的创建时机成为一个明确的设计决策。
传统构造函数注入表达的是:
这个对象需要另一个对象才能正常工作。
委托工厂表达的是:
这个对象需要在业务执行过程中,按需获取某种新对象的创建能力。
两者的语义不同,因此应当根据实际需求选择。
在 WPF MVVM 架构中,可以遵循以下原则:
-
对于稳定、持续使用的依赖,优先采用直接构造函数注入。
-
对于需要按需创建的独立实例,可以采用
Func<T>委托工厂。 -
使用微软默认 DI 容器时,显式注册相应的
Func<T>委托。 -
根据业务状态的归属决定生命周期,不要机械地将所有 ViewModel 都设置为 Transient。
-
对于需要创建参数、复杂初始化或明确所有权的对象,考虑使用显式工厂接口。
-
将对象释放、事件解绑和资源清理作为独立的生命周期设计问题处理。
八、总结
委托工厂(Delegate Factory)是一种将对象创建能力封装为委托的设计方式。在 C# 中,Func<T> 可以用较少的代码实现这一思想,并与依赖注入容器协同工作。
在合适的场景下,Func<T> + Transient 能够使父级 ViewModel 按需创建独立的子 ViewModel,降低对预先创建实例的依赖,并避免为了动态解析对象而将整个 IServiceProvider 暴露给业务层。
但它并不能自动解决所有状态隔离、生命周期和内存管理问题。实例是否独立,取决于委托的实际实现和整个依赖图的注册方式;资源是否得到释放,则取决于对象所有权和释放机制。
对于复杂的 WPF MVVM 应用,合理的设计不是一味追求工厂化,而是明确区分三个问题:什么时候创建对象、创建什么样的对象,以及由谁负责对象的生命周期。
当这三个问题都有清晰答案时,委托工厂才能真正发挥作用。

浙公网安备 33010602011771号