云计算百科
云计算领域专业知识百科平台

从零搭建灌装监控系统(二):依赖注入与服务架构,告别手动 new

依赖注入与服务架构

这是「从零搭建灌装监控系统」系列第2篇。上一篇搭好了 MVVM 骨架,但 ViewModel 还是手动 new 的。这篇引入 DI 容器,把对象创建和依赖管理交给容器,为后续接入 PLC、数据库、报警服务打好架构底座。


当手动 new 变成噩梦

上篇结尾留了个坑:DataContext = new DashboardViewModel() 这种手动 new 的写法,项目小的时候没问题,一旦功能多了就遭殃。

我朋友后来真遭殃了。

他按上篇的骨架把项目搭起来,Dashboard 能跑了,开始往里加功能。先加日志查看页面,又加报警页面,再加数据查询页面。每个页面一个 ViewModel,每个 ViewModel 都在构造函数里 new 一堆依赖:

// 他写的 LogsViewModel
public LogsViewModel()
{
_logService = new LogService("logs\\\\log-.txt");
_dbProvider = new DbProvider("Data Source=FillTrack.db");
_alarmService = new AlarmService(_dbProvider);
}

// 他写的 AlarmsViewModel
public AlarmsViewModel()
{
_dbProvider = new DbProvider("Data Source=FillTrack.db"); // 又 new 一次
_alarmService = new AlarmService(_dbProvider); // 又 new 一次
}

问题来了——每个 ViewModel 都自己 new 依赖,DbProvider 被 new 了三回,三个不同的数据库连接实例。AlarmService 也被 new 了三回,报警事件三个地方各订阅一遍,一个报警弹三次窗。

他想"那把 DbProvider 改成单例吧",于是写了个 static Instance。改完发现 AlarmService 也得单例,LogService 也得单例……改到最后全是静态类,跟没封装一样。

更恶心的是测试。他想给 LogsViewModel 写单元测试,但构造函数里直接 new 了 LogService,测试的时候真去创建日志文件了。没法 mock。

手动 new 的本质问题是:对象自己负责创建依赖,导致对象之间强耦合,没法替换、没法测试、没法管理生命周期。

他需要的是——别人负责创建依赖,对象只管用。 这个"别人"就是 DI 容器。


DI 容器到底干了啥

DI 容器(Dependency Injection Container)干的事很简单:替你 new 对象,替你管理依赖关系,替你控制生命周期。

你告诉它"我需要这几个服务,它们分别是什么类型、什么生命周期",它就给你建个"对象工厂"。你需要哪个对象,问它要就行,它自动把依赖都注入好。

DI 容器工作原理

三步走:

  • 注册 — 在启动时告诉容器:“DashboardViewModel 用单例,LoginViewModel 用瞬态,DbProvider 用单例”
  • 构建 — 调用 BuildServiceProvider() 生成容器实例
  • 解析 — 需要某个对象时调 GetRequiredService<T>(),容器自动创建并注入所有依赖
  • .NET 生态里 DI 容器不少——Unity、Autofac、DryIoc、Ninject。但微软官方的 Microsoft.Extensions.DependencyInjection(简称 MEDI)够用了,而且 ASP.NET Core 同款,文档最全。这个系列就用它。


    第一步:装包 + 建容器

    1.1 安装 DI 包

    程序包管理器控制台:

    Install-Package Microsoft.Extensions.DependencyInjection

    就这一个包,包含 ServiceCollection、IServiceProvider、IServiceCollection 这些核心接口。

    1.2 改造 App.xaml.cs

    上篇的 App.xaml.cs 里 OnStartup 直接 new MainWindow。现在改成用 DI 容器:

    using Microsoft.Extensions.DependencyInjection;
    using Serilog;
    using FillTrack.Models;
    using FillTrack.Services;
    using FillTrack.Services.Logs;
    using FillTrack.ViewModels;
    using FillTrack.Views;

    namespace FillTrack
    {
    public partial class App : Application
    {
    // 保存构建好的 DI 容器,让其他类可以解析依赖
    public IServiceProvider ServiceProvider { get; private set; }

    protected override async void OnStartup(StartupEventArgs e)
    {
    base.OnStartup(e);
    SetExceptionHandling();

    // 先初始化数据库(建表),再启动日志,避免写入冲突
    await InitializeCoreService();
    ConfigLogging();

    try
    {
    // 1. 创建服务集合
    var services = new ServiceCollection();
    // 2. 注册所有服务和 ViewModel
    ConfigureServices(services);
    // 3. 构建容器
    ServiceProvider = services.BuildServiceProvider();

    // 4. 走登录流程,成功后显示主窗口
    await InitialLoginFlowAsync();
    LogService.Debug("Initializing PLC Service…");
    // … 后续初始化 PLC
    }
    catch (Exception ex)
    {
    LogService.Fatal("应用程序启动失败:{0}", ex);
    MessageBox.Show($"应用程序启动失败:{ex.Message}", "错误",
    MessageBoxButton.OK, MessageBoxImage.Error);
    Shutdown(1);
    }
    }

    private void ConfigureServices(IServiceCollection services)
    {
    // ViewModel 注册
    services.AddSingleton<MainWindowViewModel>();
    services.AddSingleton<DashboardViewModel>();
    services.AddSingleton<DataQueryViewModel>();
    services.AddSingleton<LogsViewModel>();
    services.AddSingleton<AlarmsViewModel>();
    services.AddSingleton<SettingViewModel>();
    services.AddTransient<LoginViewModel>(); // 登录窗口每次 new 一个
    }
    }
    }

    注意看 ConfigureServices 方法——所有 ViewModel 都在这里注册。AddSingleton<T>() 表示容器里只保留一个实例,谁要都给同一个;AddTransient<T>() 表示每次要都 new 一个新的。

    登录窗口用 Transient 是有讲究的——每次登录都该是个全新的窗口,不该复用上次的(上次可能已经关闭了,复用会报异常)。


    服务生命周期:Singleton vs Transient vs Scoped

    MEDI 提供三种生命周期,搞不清楚会出大问题。

    生命周期方法行为什么时候用
    单例 AddSingleton<T>() 全局唯一实例,第一次请求时创建,之后都返回同一个 数据库连接、配置服务、全局状态服务
    瞬态 AddTransient<T>() 每次请求都 new 一个新的 轻量无状态服务、ViewModel(如登录窗口)
    作用域 AddScoped<T>() 同一个作用域内单例,不同作用域不同实例 ASP.NET Core 请求作用域;WPF 用得少

    服务生命周期对比

    WPF 项目里基本只用前两种。Scoped 是给 Web 请求那种"一次请求一个作用域"的场景用的,桌面应用很少需要。

    一个容易踩的坑:生命周期不一致

    // ❌ 错误:单例依赖瞬态
    services.AddSingleton<MainWindowViewModel>(); // 单例
    services.AddTransient<ILogService, LogService>(); // 瞬态

    public MainWindowViewModel(ILogService logService) { ... }
    // MainWindowViewModel 是单例,构造时注入的 logService 会被一直持有
    // 即使 ILogService 注册成 Transient,实际效果也是单例——因为容器只构造一次 MainWindowViewModel

    这不是 bug,但会让人困惑。规则记一条:依赖的生命周期要 ≥ 使用者的生命周期。 单例只能依赖单例,瞬态可以依赖任何东西。


    第二步:构造函数注入

    容器注册了,怎么用?两种方式。

    2.1 构造函数注入(推荐)

    容器在创建对象时,会看它的构造函数需要哪些参数,自动从容器里解析对应类型注入进去。

    public partial class MainWindowViewModel : ObservableObject
    {
    private readonly IServiceProvider _serviceProvider;
    private readonly DispatcherTimer _timer;

    // 构造函数声明需要 IServiceProvider,容器会自动注入
    public MainWindowViewModel(IServiceProvider serviceProvider)
    {
    _serviceProvider = serviceProvider;
    // … 其他初始化
    }
    }

    容器看到 MainWindowViewModel 的构造函数要 IServiceProvider,它自己就是 IServiceProvider,直接把自己传进去。你不用手动传。

    如果构造函数要 DashboardViewModel,容器也会自动去注册表里找,找到了就 new 一个传进去。层层递归,自动解析整个依赖树。

    2.2 服务定位器(Service Locator)

    另一种方式是直接从容器里 GetRequiredService<T>():

    public MainWindowViewModel(IServiceProvider serviceProvider)
    {
    _serviceProvider = serviceProvider;
    // 需要哪个 ViewModel,问容器要
    var dashboard = _serviceProvider.GetRequiredService<DashboardViewModel>();
    MainContent = dashboard;
    }

    这种方式叫服务定位器模式。它和构造函数注入的区别是:构造函数注入在创建时就明确依赖,服务定位器是运行时按需解析。

    服务定位器被一些人骂是"反模式",因为依赖关系不明确——看构造函数不知道这个类用了哪些服务。但在导航场景下它有合理性:MainWindowViewModel 需要在用户点击不同菜单时动态切换 ViewModel,你不可能在构造函数里把所有 ViewModel 都注入进来(用户可能永远不点某个页面)。

    这个项目里两种方式都用了:

    • 构造函数注入用于明确、固定的依赖(如 IServiceProvider 本身)
    • 服务定位器用于动态、按需的依赖(如导航时切换的 ViewModel)

    务实一点,别教条。


    第三步:用 DI 实现导航

    现在把上篇的手动 new 改成 DI 容器解析。MainWindowViewModel 的导航逻辑:

    public partial class MainWindowViewModel : ObservableObject
    {
    [ObservableProperty]
    private object mainContent; // 当前显示的 ViewModel,绑定到 UI

    private readonly IServiceProvider _serviceProvider;

    public MainWindowViewModel(IServiceProvider serviceProvider)
    {
    _serviceProvider = serviceProvider;

    // 订阅 PLC 连接状态事件
    PlcService.ConnectionChanged += (s, connected) => IsPlcConnected = connected;
    // 订阅 PLC 数据事件
    PlcService.DataReceived += PlcService_DataReceived;

    // 启动时显示 Dashboard
    MainContent = _serviceProvider.GetRequiredService<DashboardViewModel>();

    // 时钟
    _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
    _timer.Tick += (s, e) => CurrentTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
    _timer.Start();
    }

    [RelayCommand]
    private void Navigate(string? destination)
    {
    if (destination == null) return;
    switch (destination)
    {
    case "Dashboard":
    MainContent = _serviceProvider.GetRequiredService<DashboardViewModel>();
    break;
    case "DataQuery":
    MainContent = _serviceProvider.GetRequiredService<DataQueryViewModel>();
    break;
    case "Logs":
    MainContent = _serviceProvider.GetRequiredService<LogsViewModel>();
    break;
    case "Alarms":
    MainContent = _serviceProvider.GetRequiredService<AlarmsViewModel>();
    break;
    case "Settings":
    MainContent = _serviceProvider.GetRequiredService<SettingViewModel>();
    break;
    }
    }
    }

    Navigate 命令接收一个字符串参数(菜单项的 CommandParameter),根据参数从容器里解析对应的 ViewModel,赋值给 MainContent。界面上的 ContentControl 绑定 MainContent,自动切换显示。

    注意——所有 ViewModel 都注册成 Singleton,所以每次导航返回的是同一个实例。Dashboard 切走再切回来,之前的状态还在(滚动位置、筛选条件都保留)。这是单例的好处。

    如果某个页面需要每次进入都重置状态,就把它注册成 Transient,每次导航都 new 一个新的。


    第四步:改造登录流程

    登录窗口比较特殊——它在主窗口之前显示,登录成功后才创建主窗口。看 App.xaml.cs 怎么处理:

    private async Task InitialLoginFlowAsync()
    {
    // 临时改成手动关闭模式,否则登录窗口关闭会直接退出程序
    ShutdownMode = ShutdownMode.OnExplicitShutdown;

    var loginWindow = new LoginWindow
    {
    WindowStartupLocation = WindowStartupLocation.CenterScreen
    };

    bool? result = loginWindow.ShowDialog();
    if (result == true)
    {
    LogService.Info("登录成功,启动主窗口");
    // 从容器解析 MainWindowViewModel(构造函数会自动注入 IServiceProvider)
    var mainVM = ServiceProvider.GetRequiredService<MainWindowViewModel>();
    var mainWindow = new MainWindow { DataContext = mainVM };
    Current.MainWindow = mainWindow;
    // 登录成功后改回主窗口关闭模式
    ShutdownMode = ShutdownMode.OnMainWindowClose;
    mainWindow.Show();
    }
    else
    {
    Shutdown(); // 登录取消,退出程序
    }
    }

    关键点:

    • ShutdownMode.OnExplicitShutdown — 登录窗口关闭时不要退出程序(默认是 OnLastWindowClose,登录窗口一关程序就没了)
    • ServiceProvider.GetRequiredService<MainWindowViewModel>() — 从容器解析主 ViewModel,容器自动注入 IServiceProvider
    • ShutdownMode.OnMainWindowClose — 登录成功后改回正常模式,主窗口关闭时退出

    LoginViewModel 注册成 Transient,每次登录都是新实例。如果用户登录失败重试,不会残留上次输入的用户名密码。


    这个项目的混合模式

    到这里你可能发现一个问题——上面注册的全是 ViewModel,那些 Service(PlcService、DbProvider、UserService)怎么没注册?

    因为这个项目用了混合 DI 模式:

    #mermaid-svg-qsjl203BtrdoljVx{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qsjl203BtrdoljVx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qsjl203BtrdoljVx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qsjl203BtrdoljVx .error-icon{fill:#552222;}#mermaid-svg-qsjl203BtrdoljVx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qsjl203BtrdoljVx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qsjl203BtrdoljVx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qsjl203BtrdoljVx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qsjl203BtrdoljVx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qsjl203BtrdoljVx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qsjl203BtrdoljVx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qsjl203BtrdoljVx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qsjl203BtrdoljVx .marker.cross{stroke:#333333;}#mermaid-svg-qsjl203BtrdoljVx svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qsjl203BtrdoljVx p{margin:0;}#mermaid-svg-qsjl203BtrdoljVx .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-qsjl203BtrdoljVx .cluster-label text{fill:#333;}#mermaid-svg-qsjl203BtrdoljVx .cluster-label span{color:#333;}#mermaid-svg-qsjl203BtrdoljVx .cluster-label span p{background-color:transparent;}#mermaid-svg-qsjl203BtrdoljVx .label text,#mermaid-svg-qsjl203BtrdoljVx span{fill:#333;color:#333;}#mermaid-svg-qsjl203BtrdoljVx .node rect,#mermaid-svg-qsjl203BtrdoljVx .node circle,#mermaid-svg-qsjl203BtrdoljVx .node ellipse,#mermaid-svg-qsjl203BtrdoljVx .node polygon,#mermaid-svg-qsjl203BtrdoljVx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qsjl203BtrdoljVx .rough-node .label text,#mermaid-svg-qsjl203BtrdoljVx .node .label text,#mermaid-svg-qsjl203BtrdoljVx .image-shape .label,#mermaid-svg-qsjl203BtrdoljVx .icon-shape .label{text-anchor:middle;}#mermaid-svg-qsjl203BtrdoljVx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qsjl203BtrdoljVx .rough-node .label,#mermaid-svg-qsjl203BtrdoljVx .node .label,#mermaid-svg-qsjl203BtrdoljVx .image-shape .label,#mermaid-svg-qsjl203BtrdoljVx .icon-shape .label{text-align:center;}#mermaid-svg-qsjl203BtrdoljVx .node.clickable{cursor:pointer;}#mermaid-svg-qsjl203BtrdoljVx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qsjl203BtrdoljVx .arrowheadPath{fill:#333333;}#mermaid-svg-qsjl203BtrdoljVx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qsjl203BtrdoljVx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qsjl203BtrdoljVx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qsjl203BtrdoljVx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qsjl203BtrdoljVx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qsjl203BtrdoljVx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qsjl203BtrdoljVx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qsjl203BtrdoljVx .cluster text{fill:#333;}#mermaid-svg-qsjl203BtrdoljVx .cluster span{color:#333;}#mermaid-svg-qsjl203BtrdoljVx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qsjl203BtrdoljVx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qsjl203BtrdoljVx rect.text{fill:none;stroke-width:0;}#mermaid-svg-qsjl203BtrdoljVx .icon-shape,#mermaid-svg-qsjl203BtrdoljVx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qsjl203BtrdoljVx .icon-shape p,#mermaid-svg-qsjl203BtrdoljVx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qsjl203BtrdoljVx .icon-shape rect,#mermaid-svg-qsjl203BtrdoljVx .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qsjl203BtrdoljVx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qsjl203BtrdoljVx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qsjl203BtrdoljVx :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    静态服务(自行管理)

    DI 容器管理

    直接调用

    直接调用

    直接调用

    直接调用

    直接调用

    MainWindowViewModel\\n单例

    DashboardViewModel\\n单例

    LogsViewModel\\n单例

    其他 ViewModel\\n单例/瞬态

    PlcService\\n静态类

    DbProvider\\n静态类

    UserService\\n静态类

    ConfigServices\\n静态类

    ViewModel 由 DI 容器管理生命周期,但 Service 是静态类,直接通过类名调用:

    // DashboardViewModel 里直接用静态服务,不走构造函数注入
    public DashBoardViewModel()
    {
    PlcService.DataReceived += PlcService_DataReceived;
    PlcService.ConnectionChanged += (s, connected) => IsPlcConnected = connected;
    }

    为什么这么设计?

    说实话,这是项目演进的结果,不是一开始就这么规划的。早期写 PlcService 的时候没想那么多,直接写成静态类方便全局调用。后来引入 DI 容器是为了管理 ViewModel 的生命周期(导航需要单例),Service 已经是静态类了,没必要再改。

    这种混合模式有它的好处:

    • Service 全局可用,任何 ViewModel 都能直接调,不用层层传参
    • ViewModel 由容器管理,导航时单例复用,状态保留
    • 改动最小,不用把所有静态类改成实例类再注册

    但也有代价:

    • Service 没法 mock,单元测试只能测 ViewModel,Service 是真跑
    • Service 依赖关系不透明,看构造函数不知道用了哪些 Service
    • Service 初始化顺序敏感,DbProvider.Initialize() 必须在 UserService.InitializeAsync() 之前

    这个系列后面讲单元测试那篇会专门讲怎么处理这个问题——用反射测试私有方法,或者把关键 Service 抽接口。现在先跑起来,架构演进是渐进的,不是一步到位的。


    异常处理:别让程序悄悄崩

    App.xaml.cs 里有个 SetExceptionHandling 方法,跟 DI 没直接关系,但既然在改造启动流程,顺手讲一下。WPF 程序有三种异常需要兜住:

    private void SetExceptionHandling()
    {
    // 1. UI 线程异常——界面上抛的未捕获异常
    DispatcherUnhandledException += (s, e) =>
    {
    LogService.Error("UI线程未处理异常:{0}", e.Exception);
    e.Handled = true; // 标记已处理,不退出程序
    MessageBox.Show($"UI异常: {e.Exception.Message}", "错误",
    MessageBoxButton.OK, MessageBoxImage.Error);
    };

    // 2. 非UI线程异常——后台线程抛的,UI 线程捕获不到
    AppDomain.CurrentDomain.UnhandledException += (s, e) =>
    {
    var ex = e.ExceptionObject as Exception;
    LogService.Fatal("非UI线程未处理异常: {0}", ex?.Message);
    };

    // 3. Task 未观察异常——async Task 里抛了异常但没人 await
    TaskScheduler.UnobservedTaskException += (s, e) =>
    {
    var ex = e.Exception?.InnerException ?? e.Exception;
    LogService.Error($"Task.UnobservedTaskException: {ex?.Message}", ex);
    e.SetObserved(); // 标记已观察,不触发崩溃
    };
    }

    三种异常处理方式不一样:

    • UI 线程:e.Handled = true 可以吞掉异常不退出
    • 非UI 线程:没法阻止崩溃,只能记日志
    • Task:e.SetObserved() 标记已观察,避免触发 UnobservedTaskException 事件导致崩溃

    工业上位机 7×24 小时跑,一个未捕获异常把程序崩了,整条产线停工。这三个兜底必须有。


    踩坑记录

    坑现象原因解决
    忘了注册 ViewModel GetRequiredService<T>() 抛 InvalidOperationException 容器里没注册这个类型 在 ConfigureServices 里加上 services.AddSingleton<T>()
    单例依赖瞬态 瞬态服务实际变成单例 单例只构造一次,构造时注入的瞬态服务也被一直持有 依赖生命周期 ≥ 使用者生命周期,或把瞬态改成单例
    循环依赖 启动时抛 InvalidOperationException: A circular dependency A 构造需要 B,B 构造需要 A,容器不知道先 new 谁 重构,把循环依赖拆开(通常是把共享逻辑抽到第三个类)
    在构造函数里干重活 启动卡顿,界面半天不出来 容器在解析时调构造函数,构造函数里干 IO 操作会阻塞 构造函数只做赋值,重活放 InitializeAsync 方法,启动后异步调

    第四个坑值得展开。我朋友在 DashboardViewModel 构造函数里去读数据库初始化数据,结果登录后主窗口卡了 2 秒才出来——因为 GetRequiredService<MainWindowViewModel>() 触发构造,构造里又同步读数据库。

    正确做法是构造函数只做轻量初始化(赋值、订阅事件),耗时操作放异步方法,界面显示后再调:

    public DashboardViewModel()
    {
    // 构造函数:只做轻量初始化
    PlcService.DataReceived += PlcService_DataReceived;
    }

    // 异步初始化,界面显示后调用
    public async Task InitializeAsync()
    {
    var records = await DbProvider.GetProductionRecordsAsync();
    // … 填充数据
    }


    本篇小结

    知识点关键代码
    创建 DI 容器 var services = new ServiceCollection();
    注册服务 services.AddSingleton<MainWindowViewModel>();
    构建容器 ServiceProvider = services.BuildServiceProvider();
    构造函数注入 public MainWindowViewModel(IServiceProvider sp)
    服务定位器 _serviceProvider.GetRequiredService<DashboardViewModel>()
    单例 vs 瞬态 AddSingleton<T>() / AddTransient<T>()
    异常兜底 DispatcherUnhandledException + AppDomain.UnhandledException + TaskScheduler.UnobservedTaskException

    从手动 new 到 DI 容器,本质是把"对象创建"这件事从业务代码里剥离出来。你只管声明依赖,容器负责创建和注入。ViewModel 之间不再互相 new,Service 通过静态类全局可用,导航通过容器解析单例 ViewModel。

    但有个问题还没解决——MainContent 是个 object 类型,界面上的 ContentControl 怎么知道该用哪个 View 来显示这个 ViewModel?下一篇讲主窗口与导航系统,用 ViewModel-first 导航 + DataTemplate 映射,让界面自动根据 ViewModel 类型选择对应的 View。


    下期预告

    第3篇:主窗口与导航系统

    我们将设计主窗口的布局(侧边栏菜单 + 顶部状态栏 + 内容区),用 ViewModel-first 导航模式配合 DataTemplate 映射,让 ContentControl 根据绑定的 ViewModel 自动切换 View。告别手动写 View 切换逻辑,导航只管改 ViewModel,界面自己跟。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从零搭建灌装监控系统(二):依赖注入与服务架构,告别手动 new
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!