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 MainWindowViewModel2 {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 : Application2 {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 MainWindowViewModel2 {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