依赖注入(Dependency Injection, DI)
关于依赖注入,可以参考我前面写的一篇文章(比较杂乱,还没整理,先凑合着看吧。)
一文了解C#中的依赖注入(Dependency Injection) - zhaotianff - 博客园
DI的作用是将依赖进行解耦,帮助我们尽可能的编写可维护的代码,以便能实现高效地交付可运行的软件。
使用依赖注入对于WPF MVVM程序来说不是必须的,正如我在第一篇文章结尾处说的,引入一个框架的目的是为了更好的实现需求。
Unity Container
Unity Container是WPF MVVM开发中,使用较多的一种DI容器。
项目地址:
unitycontainer/unity: This repository contains all relevant information about Unity Container suit
案例演示
假设我们有如下需求
1、创建一个服务类,服务类中有一个显示消息的功能
2、当界面点击按钮时,调用这个服务类中的显示消息的功能
不使用DI容器
首先我们创建一个界面,在界面上放置一个按钮,并绑定命令。
MainWindow.xaml
1 <Window x:Class="ShowMessageWithoutDI.MainWindow"
2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6 xmlns:local="clr-namespace:ShowMessageWithoutDI"
7 mc:Ignorable="d"
8 Title="MainWindow" Height="450" Width="800">
9 <Grid>
10 <Button Content="Show Message" HorizontalAlignment="Center" VerticalAlignment="Center" Command="{Binding ShowMessageCommand}"></Button>
11 </Grid>
12 </Window>
创建服务类
IMyService.cs
1 internal interface IMyService
2 {
3 void ShowMessage();
4 }
MyService.cs
1 internal class MyService : IMyService
2 {
3 public void ShowMessage()
4 {
5 System.Windows.MessageBox.Show("HelloWorld");
6 }
7 }
创建ViewModel
MainWindowViewModel.cs
可以看到我们在ViewModel中创建了服务类的实例并创建了一个命令。
1 public class MainWindowViewModel
2 {
3 public RelayCommand ShowMessageCommand { get; private set; }
4
5 private IMyService service;
6
7 public MainWindowViewModel()
8 {
9 service = new MyService();
10 ShowMessageCommand = new RelayCommand(ShowMessage);
11 }
12
13 private void ShowMessage()
14 {
15 this.service.ShowMessage();
16 }
17 }
然后绑定ViewModel
MainWindow.xaml.cs
1 public partial class MainWindow : Window
2 {
3 public MainWindow()
4 {
5 InitializeComponent();
6
7 this.DataContext = new MainWindowViewModel();
8 }
9 }
这里我们就会发现一个比较明显的问题,就是这个服务类需要在ViewModel中创建实例。
那么就会存在一个问题,其它ViewModel中需要使用时,应该怎么办?
那么就需要我们再次创建实例,或者将服务类定义成单例。
带着这个问题,我们看看使用DI后的情况
使用DI容器
下面我们将前面的代码转换为使用Unity Container的代码。
因为服务类和界面定义是一样的,这里就不再重复定义了。
首先我们引入Unity.Container
nuget包。
然后在App.xaml.cs
中,对容器进行初始化,并将服务类和ViewModel类注入到容器中。
1 public partial class App : Application
2 {
3 public static IUnityContainer Container { get; private set; }
4
5 protected override void OnStartup(StartupEventArgs e)
6 {
7 base.OnStartup(e);
8
9 Container = new UnityContainer();
10 RegisterTypes(Container);
11 }
12
13 private void RegisterTypes(IUnityContainer container)
14 {
15 //控制生命周期为单例
16 container.RegisterType<IMyService, MyService>(new ContainerControlledLifetimeManager());
17
18 //默认
19 //container.RegisterType<MainWindowViewModel>();
20
21 //指定参数注入
22 container.RegisterFactory<MainWindowViewModel>("ParameterViewModel", x => new MainWindowViewModel(x.Resolve<IMyService>()));
23 }
24 }
此时,我们就可以将MainWindowViewModel
改造成如下模式的调用
1 public class MainWindowViewModel
2 {
3 public RelayCommand ShowMessageCommand { get; private set; }
4
5 private IMyService service;
6
7 public MainWindowViewModel(IMyService myService)
8 {
9 this.service = myService;
10 ShowMessageCommand = new RelayCommand(ShowMessage);
11 }
12
13 private void ShowMessage()
14 {
15 this.service.ShowMessage();
16 }
17 }
然后在MainWindow.xaml.cs
中设置DataContext
,跟前面有点区别的就是ViewModel实例是从容器中获取的。
1 public partial class MainWindow : Window
2 {
3 public MainWindow()
4 {
5 InitializeComponent();
6
7 this.DataContext = App.Container.Resolve<MainWindowViewModel>();
8 }
9 }
刚接触的小伙伴可能会觉得奇怪,因为我们并没有创建MainWindowViewModel
实例,而且也没有将IMyService
以参数传入。
这里解释一下原因:
1、调用container.RegisterType();
时,就会将MainWindowViewModel
类型放到容器中。可以通过参数指定容器中对象的生命周期,可以参考Unity Container 对象生命周期的介绍。
2、调用Container.Resolve()
时,就会创建一个MainWindowViewModel
的实例。
3、容器在创建实例时,会自动判断构造函数中的类型是否在容器中,如果在容器中,就会创建一个实例(生命周期规则同上)并以参数形式传入。
当然也可以手动指定构造函数中的参数,手动指定代码会更清晰
1 //指定参数注入
2 container.RegisterFactory<MainWindowViewModel>(x => new MainWindowViewModel(x.Resolve<IMyService>()));
这样改造以后,我们就可以在任意ViewModel中使用服务类。前面的问题也就解决了。并且ViewModel代码也会更加清晰 。
注意:虽然有不少的优点,但是也会有一些缺点,那就是代码整体结构变得复杂了,所以是否需要使用DI容器要根据自身的情况,框架的引入,是为了更好的解决问题,而不是带来问题。
ViewModelLocator
在使用容器的基础上,我们就可以使用ViewModelLocator模式
将ViewModel的绑定进行简化。
首先我们建立一个ViewModelLocator
类
1 public class ViewModelLocator
2 {
3 public MainWindowViewModel MainWindowViewModel => App.Container.Resolve<MainWindowViewModel>();
4 }
然后在App.xaml
中创建ViewModelLocator
实例
1 <Application x:Class="UseViewModelLocator.App"
2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4 xmlns:local="clr-namespace:UseViewModelLocator"
5 xmlns:viewmodel="clr-namespace:UseViewModelLocator.ViewModels"
6 StartupUri="MainWindow.xaml">
7 <Application.Resources>
8 <viewmodel:ViewModelLocator x:Key="ViewModelLocator"></viewmodel:ViewModelLocator>
9 </Application.Resources>
10 </Application>
最后在MainWindow.xaml
中绑定ViewModel(下面代码中加粗的部分)
1 <Window x:Class="UseViewModelLocator.MainWindow"
2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6 xmlns:local="clr-namespace:UseViewModelLocator"
7 mc:Ignorable="d"
8 Title="MainWindow" Height="450" Width="800" DataContext="{Binding Source={StaticResource ViewModelLocator},Path=MainWindowViewModel}">
9 <Grid>
10 <Button Content="Show Message" HorizontalAlignment="Center" VerticalAlignment="Center" Command="{Binding ShowMessageCommand}"></Button>
11 </Grid>
12 </Window>
使用其它的DI容器
CommunityToolkit.Mvvm包中的DI容器使用,可以参考下面的链接
CommunityToolkit.Mvvm中的Ioc - zhaotianff - 博客园
示例代码
https://github.com/zhaotianff/WPF-MVVM-Beginner/tree/main/9_UseDI