Technology Sharing

WPF/C#: How to implement dependency injection in WPF

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

Preface

This article uses the WPF Gallery project to learn the concepts of dependency injection and how to perform dependency injection in WPF.

What is Dependency Injection

Dependency Injection (DI) is a design pattern used to implement the Inversion of Control (IoC) principle. The main purpose of dependency injection is to transfer the creation of objects and the management of dependencies between objects from within the object to an external container or framework, thereby improving the maintainability, testability and flexibility of the code.

Core Concepts of Dependency Injection

  1. rely: An object depends on another object if it needs it to do its job. For example, an OrderService class might depend on a ProductRepository class to get product information.
  2. injection: Pass the dependent object to the object that needs it, rather than letting the object that needs it create the dependent object itself. Injection can be achieved through constructors, properties, or method parameters.
  3. container: A framework or library that manages object creation and dependencies. The container is responsible for instantiating objects, resolving dependencies, and injecting dependent objects into objects that need them.

Types of Dependency Injection

Constructor Injection: Dependent objects are passed through the class constructor.

public class OrderService
{
    private readonly IProductRepository _productRepository;

    public OrderService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Property Injection: Dependent objects are passed through the public properties of the class.

public class OrderService
{
    public IProductRepository ProductRepository { get; set; }
}
  • 1
  • 2
  • 3
  • 4

Method Injection: The dependent objects are passed through the class method parameters.

public class OrderService
{
    public void ProcessOrder(IProductRepository productRepository)
    {
        // 使用 productRepository 处理订单
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Why do we need dependency injection?

Dependency Injection (DI) is a design pattern that allows objects to be created and managed from dependencies between objects to external containers or frameworks. There are several important reasons and advantages for performing dependency injection:

  1. Reduce coupling: Dependency injection transfers the management of dependency relationships from the object to the external container, so that the object does not need to know how to create the objects it depends on, but only needs to know the interface of the dependent object. This can significantly reduce the coupling between objects and make the code more modular and flexible.
  2. Improve testability: Dependency injection makes unit testing easier and more efficient. By using mock objects or stubs to replace actual dependent objects, developers can perform unit testing without relying on actual implementations. This helps ensure the independence and reliability of tests.
  3. Improve maintainability: Because dependency injection reduces the coupling between objects, the code becomes more modular and clear. This makes the code easier to understand and maintain. When you need to modify or replace a dependent object, you only need to modify the configuration or registration information without modifying the code that uses the object.
  4. Increased flexibility: Dependency injection makes the system more flexible and can easily replace dependent objects to achieve different functions or behaviors. For example, you can switch different database access layer implementations through configuration files or code without modifying the business logic code.
  5. Promotes separation of concerns: Dependency injection helps achieve separation of concerns, so that each object only needs to focus on its own responsibilities without worrying about how to create and obtain its dependent objects. This helps improve code clarity and maintainability.
  6. Support design patterns and best practices: Dependency injection is the basis of many design patterns and best practices, such as Inversion of Control (IoC), Service Locator Pattern, etc. By using dependency injection, developers can more easily implement these patterns and practices, thereby improving the quality and scalability of the code.

How to implement dependency injection

This article uses the WPF Gallery project to learn how to use dependency injection in WPF. The code address is:

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

This project implements dependency injection using these two packages:

image-20240711100435001

First, check the content in 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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114

image-20240711083011393

What is IHost?

In C#,IHost Is an interface that is used in .NET to build and configure applicationsHostConcept of abstraction.IHostThe interface defines a collection of services and components required to start, run, and manage an application. It is commonly used in ASP.NET Core applications, but it also applies to other types of .NET applications, such as console applications or WPF programs.

image-20240711082817268

IHostInterface byHostBuilderClass implementation that provides the creation and configurationIHostInstance method.HostBuilderAllows you to add various services such as logging, configuration, dependency injection container, etc., and configure the start and stop behavior of the application.

image-20240711083048854

image-20240711083156306

Provides convenience methods for creating Microsoft.Extensions.Hosting.IHostBuilder instances using preconfigured defaults.

image-20240711083713204

Returns an IHostBuilder.

image-20240711084145756

image-20240711084211035

Adds a service to the container. This operation can be called multiple times and the results are cumulative.

The parameter configureDelegate means to configure the delegate of Microsoft.Extensions.DependencyInjection.IServiceCollection.
This collection will be used to construct a System.IServiceProvider.

The delegate requires two parameter types, HostBuilderContext and IServiceCollection, and has no return value.

image-20240711084853971

A Lambda expression that satisfies the delegate type is passed in here.

In C#,() => {}Is a syntax for Lambda expressions. Lambda expressions are lightweight delegate wrappers that allow you to define an anonymous method and pass it as a parameter to a method that supports delegates or expression trees.

Lambda expressions provide a concise way to define methods, and they are particularly useful when you need to pass methods as parameters to other methods.

image-20240711085344696

When adding services, there are two life cycles, AddSingleton, AddTransient and AddScoped.

These methods define the lifecycle of a service, that is, how service instances are created and managed in your application.

AddSingleton

  • life cycle: Singleton
  • meaning: Only one service instance is created throughout the application lifecycle. No matter how many times it is requested from the container, the same instance is returned.
  • Applicable scene: Suitable for stateless services, or resources shared across the entire application, such as configuration, loggers, etc.

AddTransient

  • life cycle: Transient
  • meaning: Each time a service is requested from the container, a new instance is created.
  • Applicable scene: Suitable for stateful services, or scenarios where a new instance is required for each request, such as pages, view models, etc.

AddScoped

  • life cycle:Scoped
  • meaning: Within each scope, a service instance is unique. Scopes are usually associated with the lifecycle of a request, for example, in a web application, each HTTP request creates a new scope.
  • Applicable scene: Suitable for services that need to share instances within the scope of a request, such as a database context.

Using these services

In the Main function:

image-20240711095100016

start up_host,pass_host.Services.GetRequiredService<MainWindow>();Get the MainWindow instance.

Take the MainWindow class as an example and look at the MainWindow constructor in MainWindow.xaml.cs:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

After removing the content not related to this topic, it looks like this:

public MainWindow(MainWindowViewModel viewModel, IServiceProvider serviceProvider, INavigationService navigationService)
{
    _serviceProvider = serviceProvider;
    ViewModel = viewModel; 
    _navigationService = navigationService;  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Have you noticed that you don’t need to create these objects yourself? The creation of these objects is managed by the dependency injection container. When these objects are needed, they can be injected through the constructor as we do now.

Without dependency injection, it might look like this:

public MainWindow()
{
    _serviceProvider = new IServiceProvider();
    ViewModel = new MainWindowViewModel(); 
    _navigationService = new INavigationService();  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Summarize

This article first introduces the concept of dependency injection, then explains why dependency injection is needed, and finally learns how to use dependency injection in WPF through the WPF Gallery project.

refer to

1、[WPF-Samples/Sample Applications/WPFGallery at main · microsoft/WPF-Samples (github.com)](https://github.com/microsoft/WPF-Samples/tree/main/Sample Applications/WPFGallery)