WPF/C#:在WPF中如何实现依赖注入

wpf · 浏览次数 : 125

小编点评

本文通过WPF Gallery项目学习依赖注入的相关概念及其在WPF中的应用。依赖注入是一种设计模式,通过将对象的创建和依赖关系的管理从对象内部转移到外部容器或框架中,以提高代码的可维护性、可测试性和灵活性。 依赖注入的核心概念包括依赖、注入和容器。依赖是指一个对象需要另一个对象来完成其工作,而注入是将依赖的对象传递给需要它的对象的方式。容器是一个管理对象创建和依赖关系的框架或库,负责实例化对象、解析依赖关系,并将依赖的对象注入到需要它们的对象中。 依赖注入有以下优点:降低耦合度、提高可测试性、提高可维护性和提高灵活性。通过使用依赖注入,可以实现控制反转原则,使得对象不需要知道如何创建其依赖的对象,只需要知道依赖对象的接口。 本文通过WPF Gallery项目展示了如何在WPF中使用依赖注入。示例中使用了依赖注入容器Microsoft.Extensions.DependencyInjection,通过不同的服务接口和生命周期方法,实现了对象的创建和管理。这种做法避免了手动创建和初始化对象,使得代码更加模块化和灵活。

正文

前言

本文通过 WPF Gallery 这个项目学习依赖注入的相关概念与如何在WPF中进行依赖注入。

什么是依赖注入

依赖注入(Dependency Injection,简称DI)是一种设计模式,用于实现控制反转(Inversion of Control,简称IoC)原则。依赖注入的主要目的是将对象的创建和对象之间的依赖关系的管理从对象内部转移到外部容器或框架中,从而提高代码的可维护性、可测试性和灵活性。

依赖注入的核心概念

  1. 依赖:一个对象需要另一个对象来完成其工作,那么前者就依赖于后者。例如,一个OrderService类可能依赖于一个ProductRepository类来获取产品信息。
  2. 注入:将依赖的对象传递给需要它的对象,而不是让需要它的对象自己去创建依赖的对象。注入可以通过构造函数、属性或方法参数来实现。
  3. 容器:一个管理对象创建和依赖关系的框架或库。容器负责实例化对象,解析依赖关系,并将依赖的对象注入到需要它们的对象中。

依赖注入的类型

构造函数注入:依赖的对象通过类的构造函数传递。

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)是一种设计模式,通过它可以将对象的创建和对象之间的依赖关系的管理从对象内部转移到外部容器或框架中。进行依赖注入有以下几个重要的原因和优点:

  1. 降低耦合度: 依赖注入通过将依赖关系的管理从对象内部转移到外部容器,使得对象不需要知道如何创建其依赖的对象,只需要知道依赖对象的接口。这样可以显著降低对象之间的耦合度,使得代码更加模块化和灵活。
  2. 提高可测试性: 依赖注入使得单元测试变得更加容易和高效。通过使用模拟对象(Mock Object)或存根(Stub)来替代实际的依赖对象,开发者可以在不依赖于实际实现的情况下进行单元测试。这有助于确保测试的独立性和可靠性。
  3. 提高可维护性: 由于依赖注入降低了对象之间的耦合度,代码变得更加模块化和清晰。这使得代码更容易理解和维护。当需要修改或替换某个依赖对象时,只需要修改配置或注册信息,而不需要修改使用该对象的代码。
  4. 提高灵活性: 依赖注入使得系统更加灵活,能够轻松地替换依赖的对象,从而实现不同的功能或行为。例如,可以通过配置文件或代码来切换不同的数据库访问层实现,而不需要修改业务逻辑代码。
  5. 促进关注点分离: 依赖注入有助于实现关注点分离(Separation of Concerns),使得每个对象只需要关注自己的职责,而不需要关心如何创建和获取其依赖的对象。这有助于提高代码的清晰度和可维护性。
  6. 支持设计模式和最佳实践: 依赖注入是许多设计模式和最佳实践的基础,如控制反转(Inversion of Control,简称IoC)、服务定位器模式(Service Locator Pattern)等。通过使用依赖注入,开发者可以更容易地实现这些模式和实践,从而提高代码的质量和可扩展性。

如何实现依赖注入

本文通过 WPF Gallery 项目学习在WPF中如何使用依赖注入,代码地址:

https://github.com/microsoft/WPF-Samples/blob/main/SampleApplications/WPFGallery

这个项目中实现依赖注入,使用到了这两个包:

image-20240711100435001

首先查看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();
    }
}

image-20240711083011393

IHost是什么?

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

image-20240711082817268

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

image-20240711083048854

image-20240711083156306

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

image-20240711083713204

返回一个IHostBuilder。

image-20240711084145756

image-20240711084211035

向容器中添加服务。此操作可以调用多次,其结果是累加的。

参数configureDelegate的含义是配置Microsoft.Extensions.DependencyInjection.IServiceCollection的委托,
该集合将用于构造System.IServiceProvider。

该委托需要两个参数类型分别为HostBuilderContext、IServiceCollection没有返回值。

image-20240711084853971

这里传入了一个满足该委托类型的Lambda表达式。

在C#中,() => {}是一种Lambda表达式的语法。Lambda表达式是一种轻量级的委托包装器,它可以让你定义一个匿名方法,并将其作为参数传递给支持委托或表达式树的方法。

Lambda表达式提供了一种简洁的方式来定义方法,特别是在需要将方法作为参数传递给其他方法时,它们非常有用。

image-20240711085344696

在添加服务,这里出现了两种生命周期,除了AddSingleton、AddTransient外还有AddScoped。

这些方法定义了服务的生命周期,即服务实例在应用程序中的创建和管理方式。

AddSingleton

  • 生命周期:单例(Singleton)
  • 含义:在整个应用程序生命周期内,只创建一个服务实例。无论从容器中请求多少次,都会返回同一个实例。
  • 适用场景:适用于无状态服务,或者在整个应用程序中共享的资源,如配置、日志记录器等。

AddTransient

  • 生命周期:瞬时(Transient)
  • 含义:每次从容器中请求服务时,都会创建一个新的实例。
  • 适用场景:适用于有状态的服务,或者每次请求都需要一个新的实例的场景,如页面、视图模型等。

AddScoped

  • 生命周期:作用域(Scoped)
  • 含义:在每个作用域内,服务实例是唯一的。作用域通常与请求的生命周期相关联,例如在Web应用程序中,每个HTTP请求会创建一个新的作用域。
  • 适用场景:适用于需要在请求范围内共享实例的服务,如数据库上下文。

使用这些服务

在Main函数中:

image-20240711095100016

启动_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中如何实现依赖注入相似的内容:

WPF/C#:在WPF中如何实现依赖注入

本文先介绍依赖注入的概念,再解释为什么要进行依赖注入,最后通过 WPF Gallery 这个项目学习如何在WPF中使用依赖注入。

WPF/C#:实现导航功能

前言 在WPF中使用导航功能可以使用Frame控件,这是比较基础的一种方法。前几天分享了wpfui中NavigationView的基本用法,但是如果真正在项目中使用起来,基础的用法是无法满足的。今天通过wpfui中的mvvm例子来说明在wpfui中如何通过依赖注入与MVVM模式使用导航功能。实践起来

WPF/C#:在DataGrid中显示选择框

前言 在使用WPF的过程中可能会经常遇到在DataGrid的最前或者最后添加一列选择框的需求,今天跟大家分享一下,在自己的项目中是如何实现的。 整体实现效果如下: 如果对此感兴趣,可以接下来看具体实现部分。 实践 假设数据库中的模型如下: public class Person { public i

WPF/C#:如何实现拖拉元素

前言 在Canvas中放置了一些元素,需要能够拖拉这些元素,在WPF Samples中的DragDropObjects项目中告诉了我们如何实现这种效果。 效果如下所示: 拖拉过程中的效果如下所示: 具体实现 xaml页面 我们先来看看xaml:

WPF/C#:数据绑定到方法

在WPF Samples中有一个关于数据绑定到方法的Demo,该Demo结构如下: 运行效果如下所示: 来看看是如何实现的。 先来看下MainWindow.xaml中的内容:

WPF/C#:如何将数据分组显示

WPF Samples中的示例 在WPF Samples中有一个关于Grouping的Demo。 该Demo结构如下: MainWindow.xaml如下:

在WPF中使用着色器

概念类比 范畴 CPU GPU 二进制文件 .exe .cso / .ps 二进制指令 机器码 CSO(shader指令) 助记符 汇编 SL 高级语言 C# HLSL 高级语言文件 .cs .hlsl / .fx 高级语言编译器 csc.exe fxc.exe API .NET API Direc

WPF/C#:BusinessLayerValidation

BusinessLayerValidation介绍 BusinessLayerValidation,即业务层验证,是指在软件应用程序的业务逻辑层(Business Layer)中执行的验证过程。业务逻辑层是应用程序架构中的一个关键部分,负责处理与业务规则和逻辑相关的操作。业务层验证的主要目的是确保数

.NET周刊【5月第2期 2024-05-12】

国内文章 C#在工业数字孪生中的开发路线实践 https://mp.weixin.qq.com/s/b_Pjt2oii0Xa_sZp_9wYWg 这篇文章探讨了C#在工业数字孪生技术中的应用,介绍了三种基于C#的数字孪生系统实现方案: WPF + Unity:结合WPF技术和Unity引擎,实现客户

.NET C# 程序自动更新组件

引言 本来博主想偷懒使用AutoUpdater.NET组件,但由于博主项目有些特殊性和它的功能过于多,于是博主自己实现一个轻量级独立自动更新组件,可稍作修改集成到大家自己项目中,比如:WPF/Winform/Windows服务。大致思路:发现更新后,从网络上下载更新包并进行解压,同时在 WinFor