WPF MVVM进阶系列教程(三、使用依赖注入)

依赖注入(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

相关推荐
FuckPatience1 天前
WPF 具有跨线程功能的UI元素
wpf
诗仙&李白1 天前
HEFrame.WpfUI :一个现代化的 开源 WPF UI库
ui·开源·wpf
He BianGu1 天前
【笔记】在WPF中Binding里的详细功能介绍
笔记·wpf
He BianGu1 天前
【笔记】在WPF中 BulletDecorator 的功能、使用方式并对比 HeaderedContentControl 与常见 Panel 布局的区别
笔记·wpf
123梦野2 天前
WPF——效果和可视化对象
wpf
He BianGu2 天前
【笔记】在WPF中Decorator是什么以及何时优先考虑 Decorator 派生类
笔记·wpf
时光追逐者3 天前
一款专门为 WPF 打造的开源 Office 风格用户界面控件库
ui·开源·c#·.net·wpf
He BianGu3 天前
【笔记】介绍 WPF XAML 中 Binding 的 StringFormat详细功能
笔记·wpf
Rotion_深4 天前
C# WPF使用线程池运行Action方法
c#·wpf·线程池
袁震4 天前
Android-kotlin MVVM框架搭建+Retrofit二次封装
android·kotlin·mvvm·retrofit