[c#] WPF / MVVM 애플리케이션에서 종속성 주입을 처리하는 방법

새 데스크톱 응용 프로그램을 시작 중이며 MVVM 및 WPF를 사용하여 빌드하고 싶습니다.

TDD도 사용할 계획입니다.

문제는 생산 코드에 의존성을 주입하기 위해 IoC 컨테이너를 어떻게 사용해야할지 모르겠다는 것입니다.

다음과 같은 클래스와 인터페이스가 있다고 가정합니다.

public interface IStorage
{
    bool SaveFile(string content);
}

public class Storage : IStorage
{
    public bool SaveFile(string content){
        // Saves the file using StreamWriter
    }
}

그리고 IStorage종속성으로있는 다른 클래스가 있습니다.이 클래스가 ViewModel 또는 비즈니스 클래스라고 가정합니다.

public class SomeViewModel
{
    private IStorage _storage;

    public SomeViewModel(IStorage storage){
        _storage = storage;
    }
}

이를 통해 모의 등을 사용하여 제대로 작동하는지 확인하는 단위 테스트를 쉽게 작성할 수 있습니다.

문제는 실제 응용 프로그램에서 사용할 때입니다. IStorage인터페이스에 대한 기본 구현을 연결하는 IoC 컨테이너가 있어야한다는 것을 알고 있지만 어떻게해야합니까?

예를 들어 다음 xaml이 있으면 어떻게 될까요?

<Window
    ... xmlns definitions ...
>
   <Window.DataContext>
        <local:SomeViewModel />
   </Window.DataContext>
</Window>

이 경우 종속성을 주입하도록 WPF에 올바르게 ‘알려’하려면 어떻게해야합니까?

또한 SomeViewModelC # 코드 의 인스턴스가 필요하다고 가정 하면 어떻게해야합니까?

나는 이것에서 완전히 길을 잃었다 고 느낍니다. 그것을 처리하는 가장 좋은 방법에 대한 예 또는 지침에 감사드립니다.

StructureMap에 익숙하지만 전문가는 아닙니다. 또한 더 나은 / 쉬운 / 기본 프레임 워크가 있으면 알려주세요.



답변

저는 Ninject를 사용해 왔는데 함께 일하는 것이 즐겁다는 것을 알았습니다. 모든 것이 코드로 설정되고 구문은 매우 간단하며 좋은 문서 (SO에 대한 많은 답변)가 있습니다.

따라서 기본적으로 다음과 같이 진행됩니다.

뷰 모델을 만들고 IStorage인터페이스를 생성자 매개 변수로 사용합니다.

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

ViewModelLocatorNinject에서 뷰 모델을로드하는 뷰 모델에 대한 get 속성을 사용하여을 만듭니다 .

class ViewModelLocator
{
    public UserControlViewModel UserControlViewModel
    {
        get { return IocKernel.Get<UserControlViewModel>();} // Loading UserControlViewModel will automatically load the binding for IStorage
    }
}

ViewModelLocatorApp.xaml에서 애플리케이션 전체 리소스를 만듭니다 .

<Application ...>
    <Application.Resources>
        <local:ViewModelLocator x:Key="ViewModelLocator"/>
    </Application.Resources>
</Application>

인드 DataContextUserControlViewModelLocator에 대응하는 속성.

<UserControl ...
             DataContext="{Binding UserControlViewModel, Source={StaticResource ViewModelLocator}}">
    <Grid>
    </Grid>
</UserControl>

NinjectModule을 상속하는 클래스를 만들어 필요한 바인딩 ( IStorage및 뷰 모델)을 설정합니다.

class IocConfiguration : NinjectModule
{
    public override void Load()
    {
        Bind<IStorage>().To<Storage>().InSingletonScope(); // Reuse same storage every time

        Bind<UserControlViewModel>().ToSelf().InTransientScope(); // Create new instance every time
    }
}

필요한 Ninject 모듈 (현재는 위의 모듈)을 사용하여 애플리케이션 시작시 IoC 커널을 초기화합니다.

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        IocKernel.Initialize(new IocConfiguration());

        base.OnStartup(e);
    }
}

IocKernelIoC 커널의 애플리케이션 전체 인스턴스를 보유하기 위해 정적 클래스를 사용 했으므로 필요할 때 쉽게 액세스 할 수 있습니다.

public static class IocKernel
{
    private static StandardKernel _kernel;

    public static T Get<T>()
    {
        return _kernel.Get<T>();
    }

    public static void Initialize(params INinjectModule[] modules)
    {
        if (_kernel == null)
        {
            _kernel = new StandardKernel(modules);
        }
    }
}

이 솔루션은 클래스의 종속성을 숨기기 때문에 일반적으로 안티 패턴으로 간주되는 정적 ServiceLocator( IocKernel) 을 사용 합니다. 그러나 UI 클래스에 대한 일종의 수동 서비스 조회를 피하는 것은 매우 어렵습니다. UI 클래스에는 매개 변수없는 생성자가 있어야하고 어쨌든 인스턴스화를 제어 할 수 없으므로 VM을 삽입 할 수 없습니다. 최소한이 방법을 사용하면 모든 비즈니스 논리가있는 VM을 격리 상태로 테스트 할 수 있습니다.

더 나은 방법이 있다면 공유 해주세요.

편집 : Lucky Likey는 Ninject가 UI 클래스를 인스턴스화하도록하여 정적 서비스 로케이터를 제거하기위한 답변을 제공했습니다. 답변에 대한 자세한 내용은 여기에서 볼 수 있습니다.


답변

귀하의 질문 DataContext에서 XAML에서 뷰 의 속성 값을 설정했습니다 . 이를 위해서는 뷰 모델에 기본 생성자가 있어야합니다. 그러나 앞서 언급했듯이 이것은 생성자에 종속성을 주입하려는 종속성 주입에서는 제대로 작동하지 않습니다.

따라서 XAML 에서 DataContext속성을 설정할 수 없습니다 . 대신 다른 대안이 있습니다.

응용 프로그램이 간단한 계층 적 뷰 모델을 기반으로하는 경우 응용 프로그램이 시작될 때 전체 뷰 모델 계층을 구성 할 수 있습니다 ( 파일 에서 StartupUri속성 을 제거해야 함 App.xaml).

public partial class App {

  protected override void OnStartup(StartupEventArgs e) {
    base.OnStartup(e);
    var container = CreateContainer();
    var viewModel = container.Resolve<RootViewModel>();
    var window = new MainWindow { DataContext = viewModel };
    window.Show();
  }

}

이것은에 뿌리를 둔 뷰 모델의 객체 그래프를 기반으로 RootViewModel하지만 일부 뷰 모델 팩토리를 부모 뷰 모델에 삽입하여 새로운 자식 뷰 모델을 생성 할 수 있으므로 객체 그래프를 수정할 필요가 없습니다. 이것은 또한 코드 의 인스턴스가 필요하다고 가정하는 귀하의 질문에 대한 답변을 희망합니다 . 어떻게해야합니까?SomeViewModelcs

class ParentViewModel {

  public ParentViewModel(ChildViewModelFactory childViewModelFactory) {
    _childViewModelFactory = childViewModelFactory;
  }

  public void AddChild() {
    Children.Add(_childViewModelFactory.Create());
  }

  ObservableCollection<ChildViewModel> Children { get; private set; }

 }

class ChildViewModelFactory {

  public ChildViewModelFactory(/* ChildViewModel dependencies */) {
    // Store dependencies.
  }

  public ChildViewModel Create() {
    return new ChildViewModel(/* Use stored dependencies */);
  }

}

애플리케이션이 본질적으로 더 동적이고 탐색을 기반으로하는 경우 탐색을 수행하는 코드에 연결해야합니다. 새 뷰로 이동할 때마다 뷰 모델 (DI 컨테이너에서)을 생성하고 뷰 자체 DataContext를 뷰 모델로 설정해야합니다. 당신이 할 수있는 첫 번째보기를 당신이보기에 따라 뷰 – 모델을 선택할 경우 또는 당신이 그것을 할 수 있습니다 뷰 – 모델을 처음뷰 모델이 사용할 뷰를 결정합니다. MVVM 프레임 워크는 DI 컨테이너를 뷰 모델 생성에 연결할 수있는 방법과 함께이 주요 기능을 제공하지만 직접 구현할 수도 있습니다. 필요에 따라이 기능이 매우 복잡해질 수 있기 때문에 여기서는 약간 모호합니다. 이것은 MVVM 프레임 워크에서 얻을 수있는 핵심 기능 중 하나이지만 간단한 애플리케이션에서 직접 롤링하면 MVVM 프레임 워크가 내부에서 제공하는 내용을 잘 이해할 수 있습니다.

DataContextXAML에서 를 선언 할 수 없으므로 일부 디자인 타임 지원이 손실됩니다. 뷰 모델에 일부 데이터가 포함되어 있으면 디자인 타임에 매우 유용 할 수 있습니다. 다행히도 WPF에서도 디자인 타임 특성을 사용할 수 있습니다 . 이를 수행하는 한 가지 방법은 <Window>요소 또는 <UserControl>XAML에 다음 특성을 추가하는 것입니다 .

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MyViewModel, IsDesignTimeCreatable=True}"

뷰 모델 유형에는 디자인 타임 데이터에 대한 기본값과 종속성 주입에 대한 다른 두 개의 생성자가 있어야합니다.

class MyViewModel : INotifyPropertyChanged {

  public MyViewModel() {
    // Create some design-time data.
  }

  public MyViewModel(/* Dependencies */) {
    // Store dependencies.
  }

}

이렇게하면 종속성 주입을 사용하고 좋은 디자인 타임 지원을 유지할 수 있습니다.


답변

내가 여기에 게시하는 것은 sondergard의 답변에 대한 개선 사항입니다. 왜냐하면 내가 말할 내용이 댓글에 맞지 않기 때문입니다. 🙂

사실 나는 ServiceLocatorStandardKernelsondergard의 Solution에서 IocContainer. 왜? 언급했듯이, 그것들은 안티 패턴입니다.

제작 StandardKernel어디서나 사용할 수를

Ninject의 마법의 핵심은 StandardKernel-Method를 사용하기 위해 필요한 .Get<T>()-Instance입니다.

Sondergard의 대안으로 -Class 내부를 IocContainer만들 수 있습니다 .StandardKernelApp

App.xaml에서 StartUpUri를 제거하기 만하면됩니다.

<Application x:Class="Namespace.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
             ...
</Application>

App.xaml.cs 내부의 앱 CodeBehind입니다.

public partial class App
{
    private IKernel _iocKernel;

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        _iocKernel = new StandardKernel();
        _iocKernel.Load(new YourModule());

        Current.MainWindow = _iocKernel.Get<MainWindow>();
        Current.MainWindow.Show();
    }
}

이제부터 Ninject는 살아 있고 싸울 준비가되었습니다. 🙂

당신의 DataContext

Ninject가 살아 있기 때문에 Property Setter Injection 이나 가장 일반적인 Constructor Injection 과 같은 모든 종류의 주입을 수행 할 수 있습니다 .

이것이 ViewModel을 WindowDataContext

public partial class MainWindow : Window
{
    public MainWindow(MainWindowViewModel vm)
    {
        DataContext = vm;
        InitializeComponent();
    }
}

물론 IViewModel올바른 바인딩을 수행하면 Inject를 사용할 수도 있지만 이것은이 답변의 일부가 아닙니다.

커널에 직접 액세스

커널에서 직접 메소드를 호출해야하는 경우 (예 : .Get<T>()-Method) 커널이 자체적으로 주입하도록 할 수 있습니다.

    private void DoStuffWithKernel(IKernel kernel)
    {
        kernel.Get<Something>();
        kernel.Whatever();
    }

커널의 로컬 인스턴스가 필요한 경우 속성으로 삽입 할 수 있습니다.

    [Inject]
    public IKernel Kernel { private get; set; }

이것은 매우 유용 할 수 있지만 그렇게하지 않는 것이 좋습니다. 이런 식으로 주입 된 개체는 나중에 주입되기 때문에 생성자 내부에서 사용할 수 없습니다.

링크 에 따르면 IKernel(DI Container) 를 주입하는 대신 factory-Extension을 사용해야합니다 .

소프트웨어 시스템에서 DI 컨테이너를 사용하는 데 권장되는 접근 방식은 애플리케이션의 컴포지션 루트가 컨테이너를 직접 터치하는 단일 위치라는 것입니다.

Ninject.Extensions.Factory를 사용하는 방법도 여기에서 빨간색으로 표시 할 수 있습니다 .


답변

“뷰 우선”접근 방식을 사용합니다. 여기서 뷰 모델을 뷰의 생성자 (코드 숨김)에 전달하여 데이터 컨텍스트에 할당됩니다.

public class SomeView
{
    public SomeView(SomeViewModel viewModel)
    {
        InitializeComponent();

        DataContext = viewModel;
    }
}

이는 XAML 기반 접근 방식을 대체합니다.

저는 Prism 프레임 워크를 사용하여 탐색을 처리합니다. 일부 코드가 특정 뷰를 표시하도록 요청하면 ( “탐색”하여) Prism이 해당 뷰를 해결합니다 (내부적으로 앱의 DI 프레임 워크를 사용). DI 프레임 워크는 차례로 뷰에있는 모든 종속성 (내 예제의 뷰 모델)을 확인한 다음 해당 종속성 을 확인합니다 .

DI 프레임 워크의 선택은 본질적으로 동일한 작업을 수행하기 때문에 거의 관련이 없습니다. 즉, 해당 인터페이스에 대한 종속성을 발견 할 때 프레임 워크가 인스턴스화 할 구체적인 유형과 함께 인터페이스 (또는 유형)를 등록합니다. 기록을 위해 저는 Castle Windsor를 사용합니다.

Prism 탐색은 익숙해지는 데 약간의 시간이 걸리지 만 일단 머리를 숙이면 꽤 괜찮아서 다른보기를 사용하여 응용 프로그램을 구성 할 수 있습니다. 예를 들어, 메인 창에 Prism “영역”을 생성 한 다음 Prism 탐색을 사용하여이 영역 내에서 한보기에서 다른보기로 전환 할 수 있습니다. 예를 들어 사용자가 메뉴 항목을 선택하거나 무엇이든 선택할 수 있습니다.

또는 MVVM Light와 같은 MVVM 프레임 워크 중 하나를 살펴보십시오. 나는 이것들에 대한 경험이 없으므로 그들이 사용하는 것에 대해 언급 할 수 없습니다.


답변

MVVM Light를 설치합니다.

설치의 일부는 뷰 모델 로케이터를 만드는 것입니다. 뷰 모델을 속성으로 노출하는 클래스입니다. 이러한 속성의 getter는 IOC 엔진에서 반환 된 인스턴스가 될 수 있습니다. 다행히 MVVM 조명에는 SimpleIOC 프레임 워크도 포함되어 있지만 원하는 경우 다른 프레임 워크를 연결할 수 있습니다.

간단한 IOC를 사용하여 유형에 대한 구현을 등록합니다.

SimpleIOC.Default.Register<MyViewModel>(()=> new MyViewModel(new ServiceProvider()), true);

이 예에서는 뷰 모델이 생성되고 생성자에 따라 서비스 공급자 개체가 전달됩니다.

그런 다음 IOC에서 인스턴스를 반환하는 속성을 만듭니다.

public MyViewModel
{
    get { return SimpleIOC.Default.GetInstance<MyViewModel>; }
}

영리한 부분은 뷰 모델 로케이터가 app.xaml 또는 이에 상응하는 데이터 소스로 생성된다는 것입니다.

<local:ViewModelLocator x:key="Vml" />

이제 ‘MyViewModel’속성에 바인딩하여 삽입 된 서비스로 뷰 모델을 가져올 수 있습니다.

도움이 되었기를 바랍니다. iPad의 메모리에서 코딩 된 부정확 한 코드에 대해 사과드립니다.


답변

Canonic DryIoc 케이스

이전 게시물에 답변했지만 DryIocDI와 인터페이스를 사용하는 것이 좋습니다 (구체적인 클래스 사용을 최소화).

  1. WPF 앱의 시작점 App.xaml은입니다. 여기서 사용할 초기 뷰가 무엇인지 알려줍니다. 기본 xaml 대신 코드 숨김으로 수행합니다.
  2. StartupUri="MainWindow.xaml"App.xaml에서 제거
  3. 코드 숨김 (App.xaml.cs)에서 다음을 추가합니다 override OnStartup.

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        DryContainer.Resolve<MainWindow>().Show();
    }

그것이 시작 지점입니다. 그것은 또한 resolve호출되어야 하는 유일한 장소 입니다.

  1. 구성 루트 (Mark Seeman의 책 .NET의 Dependency injection에 따르면 구체적인 클래스를 언급해야하는 유일한 위치)는 생성자에서 동일한 코드 숨김에 있습니다.

    public Container DryContainer { get; private set; }
    
    public App()
    {
        DryContainer = new Container(rules => rules.WithoutThrowOnRegisteringDisposableTransient());
        DryContainer.Register<IDatabaseManager, DatabaseManager>();
        DryContainer.Register<IJConfigReader, JConfigReader>();
        DryContainer.Register<IMainWindowViewModel, MainWindowViewModel>(
            Made.Of(() => new MainWindowViewModel(Arg.Of<IDatabaseManager>(), Arg.Of<IJConfigReader>())));
        DryContainer.Register<MainWindow>();
    }

비고 및 기타 세부 사항

  • 나는보기에만 구체적인 클래스를 사용했다 MainWindow.
  • 기본 생성자는 XAML 디자이너에 대해 존재해야하고 주입이있는 생성자가 응용 프로그램에 사용되는 실제 생성자이기 때문에 ViewModel에 대해 사용할 생성자를 지정해야했습니다 (DryIoc를 사용하여 수행해야 함).

DI가있는 ViewModel 생성자 :

public MainWindowViewModel(IDatabaseManager dbmgr, IJConfigReader jconfigReader)
{
    _dbMgr = dbmgr;
    _jconfigReader = jconfigReader;
}

디자인을위한 ViewModel 기본 생성자 :

public MainWindowViewModel()
{
}

뷰의 코드 비하인드 :

public partial class MainWindow
{
    public MainWindow(IMainWindowViewModel vm)
    {
        InitializeComponent();
        ViewModel = vm;
    }

    public IViewModel ViewModel
    {
        get { return (IViewModel)DataContext; }
        set { DataContext = value; }
    }
}

ViewModel을 사용하여 디자인 인스턴스를 가져 오기 위해 뷰 (MainWindow.xaml)에 필요한 것 :

d:DataContext="{d:DesignInstance local:MainWindowViewModel, IsDesignTimeCreatable=True}"

결론

따라서 뷰 및 뷰 모델의 디자인 인스턴스를 가능한 유지하면서 DryIoc 컨테이너와 DI를 사용하여 WPF 응용 프로그램을 매우 깨끗하고 최소한으로 구현했습니다.


답변

Managed Extensibility Framework를 사용합니다 .

[Export(typeof(IViewModel)]
public class SomeViewModel : IViewModel
{
    private IStorage _storage;

    [ImportingConstructor]
    public SomeViewModel(IStorage storage){
        _storage = storage;
    }

    public bool ProperlyInitialized { get { return _storage != null; } }
}

[Export(typeof(IStorage)]
public class Storage : IStorage
{
    public bool SaveFile(string content){
        // Saves the file using StreamWriter
    }
}

//Somewhere in your application bootstrapping...
public GetViewModel() {
     //Search all assemblies in the same directory where our dll/exe is
     string currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
     var catalog = new DirectoryCatalog(currentPath);
     var container = new CompositionContainer(catalog);
     var viewModel = container.GetExport<IViewModel>();
     //Assert that MEF did as advertised
     Debug.Assert(viewModel is SomViewModel);
     Debug.Assert(viewModel.ProperlyInitialized);
}

일반적으로 할 일은 정적 클래스를 가지고 팩토리 패턴을 사용하여 전역 컨테이너 (캐시 됨, natch)를 제공하는 것입니다.

뷰 모델을 주입하는 방법은 다른 모든 것을 주입하는 것과 같은 방식으로 주입합니다. XAML 파일의 코드 숨김에서 가져 오기 생성자를 만들고 (또는 속성 / 필드에 import 문을 배치하고) 뷰 모델을 가져 오도록 지시합니다. 그런 다음 귀하 Window의의 DataContext를 해당 속성에 바인딩하십시오 . 실제로 컨테이너에서 직접 꺼내는 루트 개체는 일반적으로 구성된 Window개체입니다. 창 클래스에 인터페이스를 추가하고 내 보낸 다음 위와 같이 카탈로그에서 가져옵니다 (App.xaml.cs에서 WPF 부트 스트랩 파일).