기술나눔

WPF/C#: WPF에서 종속성 주입을 구현하는 방법

2024-07-12

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

머리말

이 문서에서는 WPF 갤러리 프로젝트를 사용하여 종속성 주입과 관련된 개념과 WPF에서 종속성 주입을 수행하는 방법을 알아봅니다.

의존성 주입이란 무엇입니까?

DI(종속성 주입)는 IoC(제어 반전) 원칙을 구현하는 데 사용되는 디자인 패턴입니다. 종속성 주입의 주요 목적은 개체 생성 및 개체 간 종속성 관리를 개체 내에서 외부 컨테이너 또는 프레임워크로 전송하여 코드의 유지 관리 가능성, 테스트 가능성 및 유연성을 향상시키는 것입니다.

의존성 주입의 핵심 개념

  1. 의존하다 : 객체는 작업을 완료하기 위해 다른 객체를 필요로 하며, 전자는 후자에 의존합니다. 예를 들어 OrderService 클래스는 ProductRepository 클래스를 사용하여 제품 정보를 얻을 수 있습니다.
  2. 주입 : 필요한 개체가 종속 개체 자체를 생성하도록 하는 대신 종속 개체를 필요한 개체에 전달합니다. 주입은 생성자, 속성 또는 메서드 매개변수를 통해 이루어질 수 있습니다.
  3. 컨테이너 : 객체 생성 및 종속성을 관리하는 프레임워크 또는 라이브러리입니다. 컨테이너는 객체를 인스턴스화하고, 종속성을 해결하고, 종속 객체를 필요로 하는 객체에 종속 객체를 주입하는 일을 담당합니다.

의존성 주입의 유형

생성자 주입: 종속 객체는 클래스 생성자를 통해 전달됩니다.

public class OrderService
{
    private readonly IProductRepository _productRepository;

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

속성 주입: 종속 개체는 클래스의 공용 속성을 통해 전달됩니다.

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

메소드 주입: 종속 개체는 클래스의 메서드 매개 변수를 통해 전달됩니다.

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

의존성 주입이 필요한 이유

DI(종속성 주입)는 개체 생성 및 개체 간 종속성 관리를 개체 내에서 외부 컨테이너 또는 프레임워크로 전송할 수 있는 디자인 패턴입니다. 종속성 주입에는 몇 가지 중요한 이유와 이점이 있습니다.

  1. 결합 감소 : 종속성 주입은 종속성 관리를 개체 내부에서 외부 컨테이너로 전송하므로 개체는 자신이 종속된 개체를 만드는 방법을 알 필요가 없고 종속 개체의 인터페이스만 알면 됩니다. 이를 통해 객체 간의 결합을 크게 줄이고 코드를 더욱 모듈화하고 유연하게 만들 수 있습니다.
  2. 테스트 용이성 향상 : 의존성 주입을 통해 단위 테스트를 더 쉽고 효율적으로 수행할 수 있습니다. 실제 종속 객체 대신 모의 객체나 스텁을 사용함으로써 개발자는 실제 구현에 의존하지 않고 단위 테스트를 수행할 수 있습니다. 이는 테스트의 독립성과 신뢰성을 보장하는 데 도움이 됩니다.
  3. 유지관리성 향상 : 종속성 주입은 객체 간의 결합을 줄이므로 코드가 더욱 모듈화되고 명확해집니다. 이렇게 하면 코드를 더 쉽게 이해하고 유지 관리할 수 있습니다. 종속 개체를 수정하거나 교체해야 하는 경우 해당 개체를 사용하는 코드를 수정하지 않고 구성 또는 등록 정보만 수정하면 됩니다.
  4. 유연성 향상 : 종속성 주입은 시스템을 더욱 유연하게 만들고 종속 개체를 쉽게 대체하여 다양한 기능이나 동작을 달성할 수 있습니다. 예를 들어, 비즈니스 로직 코드를 수정하지 않고도 구성 파일이나 코드를 통해 다양한 데이터베이스 액세스 계층 구현을 전환할 수 있습니다.
  5. 우려사항 분리 촉진 : 종속성 주입은 우려 사항 분리(Separation of Concerns)를 달성하는 데 도움이 되므로 각 객체는 자신이 의존하는 객체를 생성하고 얻는 방법에 신경 쓰지 않고 자신의 책임에만 집중하면 됩니다. 이는 코드 명확성과 유지 관리성을 향상시키는 데 도움이 됩니다.
  6. 설계 패턴 및 모범 사례 지원 : 종속성 주입은 IoC(Inversion of Control), 서비스 로케이터 패턴 등과 같은 많은 디자인 패턴과 모범 사례의 기초입니다. 종속성 주입을 사용하면 개발자가 이러한 패턴과 방식을 더 쉽게 구현할 수 있으므로 코드 품질과 확장성이 향상됩니다.

종속성 주입을 구현하는 방법

이 문서에서는 WPF 갤러리 프로젝트를 사용하여 WPF 코드 주소에서 종속성 주입을 사용하는 방법을 알아봅니다.

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

이 프로젝트에서 종속성 주입을 구현하려면 다음 두 패키지가 사용됩니다.

이미지-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();
    }
}
  • 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

이미지-20240711083011393

IHost 란 무엇입니까?

C#에서는IHost .NET에서 애플리케이션을 구축하고 구성하는 데 사용되는 인터페이스입니다.Host개념 개요입니다.IHost 인터페이스는 애플리케이션을 시작, 실행 및 관리하는 데 필요한 서비스 및 구성 요소 모음을 정의합니다. 일반적으로 ASP.NET Core 애플리케이션과 함께 사용되지만 콘솔 애플리케이션이나 WPF 프로그램과 같은 다른 유형의 .NET 애플리케이션에서도 작동합니다.

이미지-20240711082817268

IHost인터페이스는 다음과 같이 구성됩니다.HostBuilder생성 및 구성을 제공하는 클래스 구현IHost인스턴스 메소드.HostBuilder로깅, 구성, 종속성 주입 컨테이너 등과 같은 다양한 서비스를 추가하고 애플리케이션의 시작 및 중지 동작을 구성할 수 있습니다.

이미지-20240711083048854

이미지-20240711083156306

미리 구성된 기본값을 사용하여 Microsoft.Extensions.Hosting.IHostBuilder 인스턴스를 만들기 위한 편리한 방법을 제공합니다.

이미지-20240711083713204

IHostBuilder를 반환합니다.

이미지-20240711084145756

이미지-20240711084211035

컨테이너에 서비스를 추가합니다. 이 작업은 여러 번 호출할 수 있으며 그 결과는 누적됩니다.

매개 변수configureDelegate의 의미는 Microsoft.Extensions.DependencyInjection.IServiceCollection의 대리자를 구성하는 것입니다.
이 컬렉션은 System.IServiceProvider를 구성하는 데 사용됩니다.

대리자에는 반환 값이 없는 HostBuilderContext 및 IServiceCollection의 두 가지 매개 변수 유형이 필요합니다.

이미지-20240711084853971

대리자 유형을 충족하는 Lambda 표현식이 여기에 전달됩니다.

C#에서는() => {} 람다 표현식의 구문입니다. 람다 식은 익명 메서드를 정의하고 이를 대리자 또는 식 트리를 지원하는 메서드에 매개 변수로 전달할 수 있는 경량 대리자 래퍼입니다.

람다 표현식은 메서드를 정의하는 간결한 방법을 제공하며 특히 메서드를 다른 메서드에 매개 변수로 전달해야 할 때 유용합니다.

이미지-20240711085344696

서비스를 추가할 때 여기에는 AddSingleton 및 AddTransient 외에도 AddScoped라는 두 가지 수명 주기가 있습니다.

이러한 메서드는 서비스의 수명 주기, 즉 애플리케이션에서 서비스 인스턴스가 생성되고 관리되는 방법을 정의합니다.

AddSingleton

  • 수명주기:하나씩 일어나는 것
  • 의미 : 전체 애플리케이션 수명주기 동안 하나의 서비스 인스턴스만 생성됩니다. 컨테이너에서 요청한 횟수에 관계없이 동일한 인스턴스가 반환됩니다.
  • 적용 가능한 장면: 상태 비저장 서비스 또는 구성, 로거 등과 같이 전체 애플리케이션에서 공유되는 리소스에 적합합니다.

추가과도

  • 수명주기: 일시적
  • 의미: 컨테이너에서 서비스를 요청할 때마다 새 인스턴스가 생성됩니다.
  • 적용 가능한 장면: 페이지, 뷰 모델 등과 같이 요청마다 새 인스턴스가 필요한 상태 저장 서비스 또는 시나리오에 적합합니다.

추가 범위

  • 수명주기: 범위 지정
  • 의미 : 각 범위 내에서 서비스 인스턴스는 고유합니다. 범위는 종종 요청의 수명 주기와 연관됩니다. 예를 들어 웹 애플리케이션에서는 각 HTTP 요청마다 새 범위가 생성됩니다.
  • 적용 가능한 장면: 데이터베이스 컨텍스트 등 요청 범위 내에서 인스턴스를 공유해야 하는 서비스에 적합합니다.

이 서비스를 이용하세요

주요 기능에서:

이미지-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;
}
  • 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

본 주제와 관련없는 내용을 삭제한 후 다음과 같습니다.

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

더 이상 이러한 개체를 직접 새로 만들 필요가 없다는 것을 알고 계셨습니까? 이러한 개체의 생성은 종속성 주입 컨테이너에 의해 관리됩니다. 이러한 개체가 필요할 때 지금처럼 생성자를 통해 주입할 수 있습니다.

종속성 주입을 사용하지 않으면 다음과 같을 수 있습니다.

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

요약하다

이 문서에서는 먼저 종속성 주입의 개념을 소개한 다음 종속성 주입이 필요한 이유를 설명하고 마지막으로 WPF 갤러리 프로젝트를 통해 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)