WPF/C#:在WPF中如何实现依赖注入
前言
本文通过 WPF Gallery 这个项目学习依赖注入的相关概念与如何在WPF中进行依赖注入。
什么是依赖注入
依赖注入(Dependency Injection,简称DI)是一种设计模式,用于实现控制反转(Inversion of Control,简称IoC)原则。依赖注入的主要目的是将对象的创建和对象之间的依赖关系的管理从对象内部转移到外部容器或框架中,从而提高代码的可维护性、可测试性和灵活性。
依赖注入的核心概念
- 依赖:一个对象需要另一个对象来完成其工作,那么前者就依赖于后者。例如,一个OrderService类可能依赖于一个ProductRepository类来获取产品信息。
- 注入:将依赖的对象传递给需要它的对象,而不是让需要它的对象自己去创建依赖的对象。注入可以通过构造函数、属性或方法参数来实现。
- 容器:一个管理对象创建和依赖关系的框架或库。容器负责实例化对象,解析依赖关系,并将依赖的对象注入到需要它们的对象中。
依赖注入的类型
构造函数注入:依赖的对象通过类的构造函数传递。
public class OrderService
{
private readonly IProductRepository _productRepository;
public OrderService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
}
属性注入:依赖的对象通过类的公共属性传递。
public class OrderService
{
public IProductRepository ProductRepository { get; set; }
}
方法注入:依赖的对象通过类的方法参数传递。
public class OrderService
{
public void ProcessOrder(IProductRepository productRepository)
{
// 使用 productRepository 处理订单
}
}
为什么要进行依赖注入
依赖注入(Dependency Injection,简称DI)是一种设计模式,通过它可以将对象的创建和对象之间的依赖关系的管理从对象内部转移到外部容器或框架中。进行依赖注入有以下几个重要的原因和优点:
- 降低耦合度: 依赖注入通过将依赖关系的管理从对象内部转移到外部容器,使得对象不需要知道如何创建其依赖的对象,只需要知道依赖对象的接口。这样可以显著降低对象之间的耦合度,使得代码更加模块化和灵活。
- 提高可测试性: 依赖注入使得单元测试变得更加容易和高效。通过使用模拟对象(Mock Object)或存根(Stub)来替代实际的依赖对象,开发者可以在不依赖于实际实现的情况下进行单元测试。这有助于确保测试的独立性和可靠性。
- 提高可维护性: 由于依赖注入降低了对象之间的耦合度,代码变得更加模块化和清晰。这使得代码更容易理解和维护。当需要修改或替换某个依赖对象时,只需要修改配置或注册信息,而不需要修改使用该对象的代码。
- 提高灵活性: 依赖注入使得系统更加灵活,能够轻松地替换依赖的对象,从而实现不同的功能或行为。例如,可以通过配置文件或代码来切换不同的数据库访问层实现,而不需要修改业务逻辑代码。
- 促进关注点分离: 依赖注入有助于实现关注点分离(Separation of Concerns),使得每个对象只需要关注自己的职责,而不需要关心如何创建和获取其依赖的对象。这有助于提高代码的清晰度和可维护性。
- 支持设计模式和最佳实践: 依赖注入是许多设计模式和最佳实践的基础,如控制反转(Inversion of Control,简称IoC)、服务定位器模式(Service Locator Pattern)等。通过使用依赖注入,开发者可以更容易地实现这些模式和实践,从而提高代码的质量和可扩展性。
如何实现依赖注入
本文通过 WPF Gallery 项目学习在WPF中如何使用依赖注入,代码地址:
[WPF-Samples/Sample Applications/WPFGallery/App.xaml.cs at main · microsoft/WPF-Samples (github.com)](https://github.com/microsoft/WPF-Samples/blob/main/Sample Applications/WPFGallery/App.xaml.cs)
这个项目中实现依赖注入,使用到了这两个包:

首先查看App.xaml.cs中的内容:
public partial class App : Application
{
private static readonly IHost _host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<MainWindow>();
services.AddSingleton<MainWindowViewModel>();
services.AddTransient<DashboardPage>();
services.AddTransient<DashboardPageViewModel>();
services.AddTransient<ButtonPage>();
services.AddTransient<ButtonPageViewModel>();
services.AddTransient<CheckBoxPage>();
services.AddTransient<CheckBoxPageViewModel>();
services.AddTransient<ComboBoxPage>();
services.AddTransient<ComboBoxPageViewModel>();
services.AddTransient<RadioButtonPage>();
services.AddTransient<RadioButtonPageViewModel>();
services.AddTransient<SliderPage>();
services.AddTransient<SliderPageViewModel>();
services.AddTransient<CalendarPage>();
services.AddTransient<CalendarPageViewModel>();
services.AddTransient<DatePickerPage>();
services.AddTransient<DatePickerPageViewModel>();
services.AddTransient<TabControlPage>();
services.AddTransient<TabControlPageViewModel>();
services.AddTransient<ProgressBarPage>();
services.AddTransient<ProgressBarPageViewModel>();
services.AddTransient<MenuPage>();
services.AddTransient<MenuPageViewModel>();
services.AddTransient<ToolTipPage>();
services.AddTransient<ToolTipPageViewModel>();
services.AddTransient<CanvasPage>();
services.AddTransient<CanvasPageViewModel>();
services.AddTransient<ExpanderPage>();
services.AddTransient<ExpanderPageViewModel>();
services.AddTransient<ImagePage>();
services.AddTransient<ImagePageViewModel>();
services.AddTransient<DataGridPage>();
services.AddTransient<DataGridPageViewModel>();
services.AddTransient<ListBoxPage>();
services.AddTransient<ListBoxPageViewModel>();
services.AddTransient<ListViewPage>();
services.AddTransient<ListViewPageViewModel>();
services.AddTransient<TreeViewPage>();
services.AddTransient<TreeViewPageViewModel>();
services.AddTransient<LabelPage>();
services.AddTransient<LabelPageViewModel>();
services.AddTransient<TextBoxPage>();
services.AddTransient<TextBoxPageViewModel>();
services.AddTransient<TextBlockPage>();
services.AddTransient<TextBlockPageViewModel>();
services.AddTransient<RichTextEditPage>();
services.AddTransient<RichTextEditPageViewModel>();
services.AddTransient<PasswordBoxPage>();
services.AddTransient<PasswordBoxPageViewModel>();
services.AddTransient<ColorsPage>();
services.AddTransient<ColorsPageViewModel>();
services.AddTransient<LayoutPage>();
services.AddTransient<LayoutPageViewModel>();
services.AddTransient<AllSamplesPage>();
services.AddTransient<AllSamplesPageViewModel>();
services.AddTransient<BasicInputPage>();
services.AddTransient<BasicInputPageViewModel>();
services.AddTransient<CollectionsPage>();
services.AddTransient<CollectionsPageViewModel>();
services.AddTransient<MediaPage>();
services.AddTransient<MediaPageViewModel>();
services.AddTransient<NavigationPage>();
services.AddTransient<NavigationPageViewModel>();
services.AddTransient<TextPage>();
services.AddTransient<TextPageViewModel>();
services.AddTransient<DateAndTimePage>();
services.AddTransient<DateAndTimePageViewModel>();
services.AddTransient<StatusAndInfoPage>();
services.AddTransient<StatusAndInfoPageViewModel>();
services.AddTransient<SamplesPage>();
services.AddTransient<SamplesPageViewModel>();
services.AddTransient<DesignGuidancePage>();
services.AddTransient<DesignGuidancePageViewModel>();
services.AddTransient<UserDashboardPage>();
services.AddTransient<UserDashboardPageViewModel>();
services.AddTransient<TypographyPage>();
services.AddTransient<TypographyPageViewModel>();
services.AddSingleton<IconsPage>();
services.AddSingleton<IconsPageViewModel>();
services.AddSingleton<SettingsPage>();
services.AddSingleton<SettingsPageViewModel>();
services.AddSingleton<AboutPage>();
services.AddSingleton<AboutPageViewModel>();
}).Build();
[STAThread]
public static void Main()
{
_host.Start();
App app = new();
app.InitializeComponent();
app.MainWindow = _host.Services.GetRequiredService<MainWindow>();
app.MainWindow.Visibility = Visibility.Visible;
app.Run();
}
}

IHost是什么?
在C#中,IHost 是一个接口,它是.NET 中用于构建和配置应用程序的Host的概念的抽象。IHost接口定义了启动、运行和管理应用程序所需的服务和组件的集合。它通常用于ASP.NET Core应用程序,但也适用于其他类型的.NET 应用程序,如控制台应用程序或WPF程序。

IHost接口由HostBuilder类实现,它提供了创建和配置IHost实例的方法。HostBuilder允许你添加各种服务,如日志记录、配置、依赖注入容器等,并配置应用程序的启动和停止行为。


提供了用于使用预配置默认值创建Microsoft.Extensions.Hosting.IHostBuilder实例的方便方法。

返回一个IHostBuilder。


向容器中添加服务。此操作可以调用多次,其结果是累加的。
参数configureDelegate的含义是配置Microsoft.Extensions.DependencyInjection.IServiceCollection的委托,
该集合将用于构造System.IServiceProvider。
该委托需要两个参数类型分别为HostBuilderContext、IServiceCollection没有返回值。

这里传入了一个满足该委托类型的Lambda表达式。
在C#中,() => {}是一种Lambda表达式的语法。Lambda表达式是一种轻量级的委托包装器,它可以让你定义一个匿名方法,并将其作为参数传递给支持委托或表达式树的方法。
Lambda表达式提供了一种简洁的方式来定义方法,特别是在需要将方法作为参数传递给其他方法时,它们非常有用。

在添加服务,这里出现了两种生命周期,除了AddSingleton、AddTransient外还有AddScoped。
这些方法定义了服务的生命周期,即服务实例在应用程序中的创建和管理方式。
AddSingleton
- 生命周期:单例(Singleton)
- 含义:在整个应用程序生命周期内,只创建一个服务实例。无论从容器中请求多少次,都会返回同一个实例。
- 适用场景:适用于无状态服务,或者在整个应用程序中共享的资源,如配置、日志记录器等。
AddTransient
- 生命周期:瞬时(Transient)
- 含义:每次从容器中请求服务时,都会创建一个新的实例。
- 适用场景:适用于有状态的服务,或者每次请求都需要一个新的实例的场景,如页面、视图模型等。
AddScoped
- 生命周期:作用域(Scoped)
- 含义:在每个作用域内,服务实例是唯一的。作用域通常与请求的生命周期相关联,例如在Web应用程序中,每个HTTP请求会创建一个新的作用域。
- 适用场景:适用于需要在请求范围内共享实例的服务,如数据库上下文。
使用这些服务
在Main函数中:

启动_host,通过_host.Services.GetRequiredService<MainWindow>();获取MainWindow实例。
以MainWindow类为例,查看MainWindow.xaml.cs中MainWindow的构造函数:
public MainWindow(MainWindowViewModel viewModel, IServiceProvider serviceProvider, INavigationService navigationService)
{
_serviceProvider = serviceProvider;
ViewModel = viewModel;
DataContext = this;
InitializeComponent();
Toggle_TitleButtonVisibility();
_navigationService = navigationService;
_navigationService.Navigating += OnNavigating;
_navigationService.SetFrame(this.RootContentFrame);
_navigationService.Navigate(typeof(DashboardPage));
WindowChrome.SetWindowChrome(
this,
new WindowChrome
{
CaptionHeight = 50,
CornerRadius = default,
GlassFrameThickness = new Thickness(-1),
ResizeBorderThickness = ResizeMode == ResizeMode.NoResize ? default : new Thickness(4),
UseAeroCaptionButtons = true
}
);
this.StateChanged += MainWindow_StateChanged;
}
去掉与本主题无关的内容之后,如下所示:
public MainWindow(MainWindowViewModel viewModel, IServiceProvider serviceProvider, INavigationService navigationService)
{
_serviceProvider = serviceProvider;
ViewModel = viewModel;
_navigationService = navigationService;
}
有没有发现不用自己new这些对象了,这些对象的创建由依赖注入容器来管理,在需要这些对象的时候,像现在这样通过构造函数中注入即可。
如果没有用依赖注入,可能就是这样子的:
public MainWindow()
{
_serviceProvider = new IServiceProvider();
ViewModel = new MainWindowViewModel();
_navigationService = new INavigationService();
}
总结
本文先介绍依赖注入的概念,再解释为什么要进行依赖注入,最后通过 WPF Gallery 这个项目学习如何在WPF中使用依赖注入。
参考
1、[WPF-Samples/Sample Applications/WPFGallery at main · microsoft/WPF-Samples (github.com)](https://github.com/microsoft/WPF-Samples/tree/main/Sample Applications/WPFGallery)
WPF/C#:在WPF中如何实现依赖注入的更多相关文章
- 为什么多线程、junit 中无法使用spring 依赖注入?
为什么多线程.junit 中无法使用spring 依赖注入? 这个问题,其实体现了,我们对spring已依赖太深,以至于不想自己写实例了. 那么到底是为什么在多线程和junit单元测试中不能使用依赖注 ...
- 深入浅出spring IOC中三种依赖注入方式
深入浅出spring IOC中三种依赖注入方式 spring的核心思想是IOC和AOP,IOC-控制反转,是一个重要的面向对象编程的法则来消减计算机程序的耦合问题,控制反转一般分为两种类型,依赖注入和 ...
- 转:深入浅出spring IOC中四种依赖注入方式
转:https://blog.csdn.net/u010800201/article/details/72674420 深入浅出spring IOC中四种依赖注入方式 PS:前三种是我转载的,第四种是 ...
- 【17MKH】我在框架中对.Net依赖注入的扩展
说明 依赖注入(DI)是控制反转(IoC)的一种技术实现,它应该算是.Net中最核心,也是最基本的一个功能.但是官方只是实现了基本的功能和扩展方法,而我呢,在自己的框架 https://github. ...
- ASP.NET Core中如影随形的”依赖注入”[下]: 历数依赖注入的N种玩法
在对ASP.NET Core管道中关于依赖注入的两个核心对象(ServiceCollection和ServiceProvider)有了足够的认识之后,我们将关注的目光转移到编程层面.在ASP.NET ...
- ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起
我们一致在说 ASP.NET Core广泛地使用到了依赖注入,通过前面两个系列的介绍,相信读者朋友已经体会到了这一点.由于前面两章已经涵盖了依赖注入在管道构建过程中以及管道在处理请求过程的应用,但是内 ...
- 【SSH系列】深入浅出spring IOC中三种依赖注入方式
spring的核心思想是IOC和AOP,IOC-控制反转,是一个重要的面向对象编程的法则来消减计算机程序的耦合问题,控制反转一般分为两种类型,依赖注入和依赖查找,依赖什么?为什么需要依赖?注入什么?控 ...
- WebApi中使用Ninject 依赖注入
之前Ninject依赖注入是在MVC中使用,最近在WebApi中使用,用之前的MVC方式发现使用接口注入,一直是Null错误,网上查询了一些资源,总结一下,以后备用. 主要分为以下几步骤: 在NuGe ...
- spring IOC中三种依赖注入方式
Spring的核心思想是IOC和AOP,IOC-控制反转,是一个重要的面向对象编程的法则,用来消减计算机程序之间的耦合问题,控制反转一般分为两种类型,依赖注入和依赖查找,依赖什么?为什么需要依赖?注入 ...
- spring IOC中四种依赖注入方式
在spring ioc中有三种依赖注入,分别是:https://blog.csdn.net/u010800201/article/details/72674420 a.接口注入:b.setter方法注 ...
随机推荐
- MegaCli64查看磁盘损坏,错误个数统计情况
如下,两个命令,是磁盘濒临崩坏,比如存在扇区损坏之类的事情发生.咨询的浪潮热线,报sn.他们的临界值是500,我们监控脚本是200告警.Predictive Failure Count 这个的数字比M ...
- 【漏洞复现】用友NC-Cloud PMCloudDriveProjectStateServlet接口存在JNDI注入漏洞
阅读须知 花果山的技术文章仅供参考,此文所提供的信息只为网络安全人员对自己所负责的网站.服务器等(包括但不限于)进行检测或维护参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作.利用此 ...
- visualstudio着色器设计器shadergraph使用
第一次使用着色器设计器. vs的着色器设计器是hlsl的着色器设计器.不得不说里面节点得翻译是一坨屎. 附一个光线于法向量夹角渲染的设计图
- Android OpenMAX(六)OMXStore
在前面两节的学习中我们知道了OMX Core是用来管理(查询/创建/销毁)Android平台上的硬件编解码组件的.这一节我们再向上一层,Android平台除了提供有硬件编解码组件支持,还内置了一些软件 ...
- Yarp 让系统内调度更灵活 http、https、websocket 反向代理
简介 Yarp 是微软团队开发的一个反向代理组件, 除了常规的 http 和 https 转换通讯,它最大的特点是可定制化,很容易根据特定场景开发出需要的定制代理通道. 详细介绍:https://de ...
- MySQL配置主从同步过程记录
今天由于工作需要,配置了一下主从同步,这里记录一下配置过程,以备查阅. 事先度娘了一番,主从同步需要保证主从服务器MySQL版本一致(我的略有差别,主服务器版本5.5.31,从服务器版本5.5.19) ...
- numpy cumsum()函数简介
函数原型:numpy.cumsum(a, axis=None, dtype=None, out=None) 可参考链接:https://docs.scipy.org/doc/numpy-1.10.1/ ...
- gRPC入门学习之旅(十)
gRPC入门学习之旅目录 gRPC入门学习之旅(一) gRPC入门学习之旅(二) gRPC入门学习之旅(三) gRPC入门学习之旅(四) gRPC入门学习之旅(七) gRPC入门学习之旅(九) 3. ...
- C#异步编程是怎么回事(番外)
在上一篇通信协议碰到了多线程,阻塞.非阻塞.锁.信号量...,会碰到很多问题.因此我感觉很有必要研究多线程与异步编程. 首先以一个例子开始 我说明一下这个例子. 这是一个演示异步编程的例子. 输入jo ...
- ABC326
上次说我的写法low的人的AT号在这里!!( 我又来提供 low 算法了. 从 D 开始. T4 我们把 \(\text{A}\) 看成 \(1\),把 \(\text{B}\) 看成 \(2\),把 ...