使用MVVM模式提高应用程序代码库中的模块化程度的最常用模式是使用某种形式的反转控制(Ioc)。其中最常见的解决方案是使用依赖关系注入,该解决方案存在于创建多个注入后端类的服务(即以参数的形式传递给 viewmodel 构造函数)的过程中,这允许使用这些服务的代码不依赖这些服务的实现详细信息,并且也可以轻松地交换这些服务的具体实现。 这种模式还可以通过服务将特定于平台的功能抽象出来,然后在需要的地方注入这些功能,从而使后端代码可以轻松使用这些功能。
MVVM工具包没提供内置的API来促进这种模式的使用,因为已经有专用库(Microsoft.Extensions.DependencyInjection
包),本文中示例都是参考此库。
什么是依赖注入
依赖注入又称为依赖项注入,那什么是依赖项呢?比如在一个类A中,实现某中功能,而此功能是另外一个类B实现的,那就说明A依赖B,B就是A的依赖项。或者是另一个对象A所依赖的对象B。
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
}
}
类可以创建 MyDependency
类的实例,以便利用其 WriteMessage
方法。 在以下示例中,MyDependency
类是 IndexModel
类的依赖项
public class IndexModel : PageModel
{
private readonly MyDependency _dependency = new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet");
}
}
注意:在上述示例中,MyDependency
类依赖于IndexModel
类,所以IndexModel
就是MyDependency
的依赖项。 硬编码的依赖项(如前面的示例)会产生问题,应避免使用。
强依赖关系具有以下几个问题:
- 如果要用不同的实现替换
MyDependency
,必须修改IndexModel
类。 - 如果
MyDependency
具有依赖项,则必须由IndexModel
类对其进行配置,且很难进行初始化。 - 这种实现很难进行单元测试。
那如何解决上述依赖关系所造成的弊端呢?答案就是依赖项注入。可通过如下几个步骤实现:
- 使用接口或基类将依赖关系实现抽象化。
- 在服务容器中注册依赖关系。
- 将服务注入到使用它的类的构造函数中。
.NET 提供了一个内置的服务容器 IServiceProvider。 服务通常在应用启动时注册,并追加到 IServiceCollection。 添加所有服务后,可以使用 BuildServiceProvider 创建服务容器。 框架负责创建依赖关系的实例,并在不再需要时将其释放。
简单一句话说:依赖注入(DI)将所依赖的对象参数化,接口化,并且将依赖对象的创建和释放剥离出来,这样就做到了解耦,并且实现了控制反转(IoC)。
控制反转(IoC)具有如下两个特点:
- 高等级的代码不能依赖低等级的代码;
- 抽象接口不能依赖具体实现;
控制反转解决代码的强耦合,增加了代码的可扩展性。依赖注入将依赖具体实现类和控制实现类的创建和释放,变成了依赖接口或抽象类,不再控制接口的创建和释放。两者之间相辅相成,互相成就。
WPF依赖注入示例
步骤 1: 设置项目和安装必要的NuGet包
Install-Package Microsoft.Extensions.DependencyInjection
或者
步骤 2: 创建依赖注入容器
创建一个静态类来构建和存储IServiceProvider
实例。这个类将负责配置服务和解析依赖。
创建 DependencyInjection.cs
using Microsoft.Extensions.DependencyInjection;
using System;
public static class DependencyInjection
{
private static IServiceProvider serviceProvider;
public static void ConfigureServices()
{
var services = new ServiceCollection();
// 注册应用中的服务和ViewModel
services.AddSingleton<MainWindow>();
services.AddTransient<IMyService, MyService>();
services.AddTransient<MainViewModel>();
serviceProvider = services.BuildServiceProvider();
}
public static T GetService<T>()
{
return serviceProvider.GetService<T>();
}
}
在这个类中,我们使用了Microsoft.Extensions.DependencyInjection
来创建服务集合,然后构建IServiceProvider
。
步骤 3: 配置主窗口和ViewModel
修改你的MainWindow
,使其可以接收依赖(比如MainViewModel
)
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
步骤 4: 配置 App.xaml.cs
重写App.xaml.cs
中的启动逻辑,使用依赖注入初始化MainWindow
。
App.xaml.cs
using System.Windows;
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
DependencyInjection.ConfigureServices();
var mainWindow = DependencyInjection.GetService<MainWindow>();
mainWindow.Show();
}
}
这里,应用启动时会配置服务,并从服务提供者中获取MainWindow
的实例。
注意:在此示例中,MainWindow通过服务注册的方式进行实例化,所以需要删除默认的App.xaml中StartUri属性设置,否则将提示默认构造函数不存在。
步骤 5: 创建服务和ViewModel
定义服务接口和实现,以及ViewModel。
IService 和 Service 实现
public interface IMyService
{
string GetData();
}
public class MyService : IMyService
{
public string GetData()
{
return "Hello from MyService!";
}
}
ViewModel 实现
public class MainViewModel
{
public string Data { get; }
public MainViewModel(IMyService myService)
{
Data = myService.GetData();
}
}
步骤 6: xaml视图
<Grid>
<TextBlock Text="{Binding Data}"></TextBlock>
<Frame x:Name="MainFrame" Source="Views/Pages/RegistrationForm.xaml" NavigationUIVisibility="Hidden" ></Frame>
</Grid>
经过上述步骤,就实现了WPF中依赖注入和控制反转,测试结果如下:
题外话:生命周期和存储方式小知识
在WPF应用程序中,通过依赖注入(DI)获取的对象(例如,通过重写OnStartup
方法并从IServiceProvider
获取的实例)通常不会“自动消失”或自动释放,除非你的实现或应用程序逻辑中有特定的处理使其被释放或被垃圾回收。
生命周期和存储方式
对象的持久性和存储方式主要取决于如何在依赖注入容器中注册这些对象:
-
单例(Singleton):只创建一个实例,该实例在应用程序的整个生命周期内持续存在。例如,注册为单例的
MainWindow
,在应用程序关闭前,其实例会一直存在。 -
瞬态(Transient):每次请求都创建一个新的实例。这意味着每次从
IServiceProvider
获取时都会创建一个新的对象。 -
作用域(Scoped):在.NET Core的Web应用中常用,每个请求创建一个新的实例,但在WPF中通常使用单例或瞬态替代。
在上述示例中,MainWindow
如果注册为单例,则在应用程序关闭之前始终存在。如果注册为瞬态,那么每次调用GetService<MainWindow>()
时都会创建一个新的MainWindow
实例。
生命周期管理
- 引用保持:为了确保通过依赖注入获取的对象不会“消失”,你需要保持对这些对象的引用。在WPF中,通常的做法是保持对主窗口或核心服务的引用直到应用程序关闭。
- 释放资源:对于使用了资源较多的服务或对象,如数据库连接或文件句柄,应确保适当地管理其生命周期。这可能需要实现
IDisposable
接口,并在适当的时候(如窗口关闭或对象不再需要时)调用Dispose
方法。