WPF/C#:在WPF中如何實現依賴注入

mingupupup發表於2024-07-11

前言

本文透過 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、https://github.com/microsoft/WPF-Samples/tree/main/SampleApplications/WPFGallery

相關文章