web-dev-qa-db-ja.com

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を正しく「伝える」にはどうすればよいですか?

また、SomeViewModelコードからcsのインスタンスが必要だとしたら、どうすればいいですか?

私はこれで完全に失われたと感じています。どのようにそれを処理するのが最善の方法であるかの例やガイダンスをいただければ幸いです。

私はStructureMapに精通していますが、私は専門家ではありません。また、より優れた、より簡単な、すぐに使えるフレームワークがある場合はお知らせください。

前もって感謝します。

88
Fedaykin

私はNinjectを使用してきましたが、一緒に仕事をするのは楽しいことです。すべてがコードで設定されており、構文はかなり単純であり、優れたドキュメント(およびSOに関する多くの回答)があります。

したがって、基本的には次のようになります。

ビューモデルを作成し、IStorageインターフェイスをコンストラクターパラメーターとして使用します。

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

ビューモデルのgetプロパティを使用してViewModelLocatorを作成します。これにより、Ninjectからビューモデルがロードされます。

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

ViewModelLocatorをApp.xamlのアプリケーション全体のリソースにします。

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

UserControlのDataContextをViewModelLocatorの対応するプロパティにバインドします。

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

NinjectModuleを継承するクラスを作成し、必要なバインディング(IStorageおよびviewmodel)をセットアップします。

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);
    }
}

静的なIocKernelクラスを使用してIoCカーネルのアプリケーション全体のインスタンスを保持しているため、必要なときに簡単にアクセスできます。

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クラスには何らかのパラメーターのないコンストラクターが必要であり、インスタンス化を制御できないため、VMを挿入できないため、UIクラスの何らかの種類の手動サービス検索を回避することは非常に困難です。少なくともこの方法では、VMを単独でテストできます。これは、すべてのビジネスロジックがある場所です。

誰かがより良い方法を持っているなら、共有してください。

編集:Lucky Likeyは、NinjectにUIクラスをインスタンス化させることにより、静的サービスロケーターを取り除くための回答を提供しました。答えの詳細を見ることができます こちら

78
sondergard

質問では、XAMLのビューのDataContextプロパティの値を設定します。これには、ビューモデルにデフォルトのコンストラクターが必要です。ただし、既に述べたように、これは、コンストラクターに依存関係を注入する依存関係注入ではうまく機能しません。

XAMLでDataContextプロパティを設定することはできません。代わりに、他の選択肢があります。

アプリケーションが単純な階層ビューモデルに基づいている場合、アプリケーションの起動時にビューモデル階層全体を構築できます(App.xamlファイルからStartupUriプロパティを削除する必要があります)。

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をルートとするビューモデルのオブジェクトグラフに基づいていますが、一部のビューモデルファクトリを親ビューモデルに挿入して、新しい子ビューモデルを作成できるため、オブジェクトグラフは修正されます。これもあなたの質問に答えることを望みます私はSomeViewModelコードからcsのインスタンスが必要だと仮定します、どうすればいいですか?

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フレームワークから得られるコア機能の1つですが、単純なアプリケーションで独自のものを使用すると、MVVMフレームワークが内部で提供するものをよく理解できます。

XAMLでDataContextを宣言できないと、設計時のサポートが失われます。ビューモデルにデータが含まれている場合、デザイン時に表示され、非常に役立ちます。さいわい、WPFでも design-time attributes を使用できます。これを行う1つの方法は、XAMLで<Window>要素または<UserControl>に次の属性を追加することです。

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}"

View-modelタイプには、設計時データのデフォルトと依存性注入用の2つのコンストラクターが必要です。

class MyViewModel : INotifyPropertyChanged {

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

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

}

これを行うことにより、依存性注入を使用して、設計時の適切なサポートを維持できます。

46

私がここに投稿しているのは、sondergardの回答の改善です。なぜなら、私が伝えようとしていることはコメントに合わないからです:)

ファクトでは、ServiceLocatorとsondergardのStandardKernel- Instanceのラッパーの必要性を回避するきちんとしたソリューションを紹介しています。ソリューションはIocContainerと呼ばれます。どうして?前述のように、これらはアンチパターンです。

StandardKernelをどこでも利用可能にする

Ninjectの魔法の鍵は、.Get<T>()- Methodを使用するために必要なStandardKernel- Instanceです。

SondergardのIocContainerの代わりに、StandardKernelクラス内にAppを作成できます。

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など、あらゆる種類の注入を実行できます注入

これは、ViewModelをWindowDataContextに挿入する方法です。

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

もちろん、正しいバインディングを行う場合はIViewModelを挿入することもできますが、それはこの答えの一部ではありません。

カーネルに直接アクセスする

カーネルのメソッドを直接呼び出す必要がある場合(例:.Get<T>()- Method)、カーネルにそれ自身を注入させることができます。

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

カーネルのローカルインスタンスが必要な場合は、プロパティとしてインジェクトでき​​ます。

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

これは非常に便利ですが、そうすることはお勧めしません。この方法で注入されたオブジェクトは、後で注入されるため、Constructor内では使用できないことに注意してください。

これによると、 linkIKernel(DI Container)を注入する代わりにfactory-Extensionを使用する必要があります。

ソフトウェアシステムでDIコンテナを使用するための推奨されるアプローチは、アプリケーションの構成ルートが、コンテナに直接触れる単一の場所であることです。

Ninject.Extensions.Factoryの使用方法も赤 here になります。

23
LuckyLikey

私は「ビューファースト」アプローチに行きます。そこでは、ビューモデルをビューのコンストラクターに(コードビハインドで)渡します。コンストラクターは、データコンテキストに割り当てられます。

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

        DataContext = viewModel;
    }
}

これは、XAMLベースのアプローチを置き換えます。

Prismフレームワークを使用してナビゲーションを処理します。一部のコードが特定のビューの表示を要求すると(「ナビゲート」することで)、Prismはそのビューを解決します(内部的にはアプリのDIフレームワークを使用)。 DIフレームワークは、ビューにある依存関係(この例ではビューモデル)を順番に解決してから、its依存関係などを解決します。

DIフレームワークの選択は、本質的に同じことを行うため、ほとんど無関係です。つまり、インターフェイス(または型)を登録し、そのインターフェイスへの依存関係が見つかったときにフレームワークにインスタンス化させる具体的な型を指定します。記録には、Castle Windsorを使用しています。

Prismナビゲーションは慣れるのに多少時間がかかりますが、一度頭を動かせば、さまざまなビューを使用してアプリケーションを作成できます。例えば。メインウィンドウにPrismの「リージョン」を作成し、Prismナビゲーションを使用して、このリージョン内であるビューから別のビューに切り替えることができます。ユーザーがメニュー項目などを選択するとき。

または、MVVM LightなどのMVVMフレームワークの1つを見てください。私はこれらの経験がありませんので、それらがどのようなものであるかについてコメントすることはできません。

12
Andrew Stephens

MVVM Lightをインストールします。

インストールの一部は、ビューモデルロケーターを作成することです。これは、ビューモデルをプロパティとして公開するクラスです。これらのプロパティの取得メソッドは、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のメモリからコード化されたコードの不正確さをおologiesびします。

10
kidshaw

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);
}

一般的には、静的クラスを用意し、ファクトリパターンを使用してグローバルコンテナ(キャッシュ、ナッチ)を提供します。

ビューモデルを注入する方法については、他のすべてを注入するのと同じ方法でビューモデルを注入します。 XAMLファイルのコードビハインドでインポートコンストラクターを作成(またはプロパティ/フィールドにインポートステートメントを配置)し、ビューモデルをインポートするように指示します。次に、WindowDataContextをそのプロパティにバインドします。自分で実際にコンテナから取り出すルートオブジェクトは、通常Windowオブジェクトで構成されます。インターフェイスをウィンドウクラスに追加してエクスポートし、上記のようにカタログから取得します(App.xaml.cs ...ではWPF bootstrapファイルです)。

2

Canonic DryIocケース

古い投稿に答えますが、DryIocを使用してこれを行い、DIおよびインターフェイスを適切に使用すると思うことを実行します(具体的なクラスの最小限の使用)。

  1. WPFアプリの開始点はApp.xamlであり、使用する初期ビューが何であるかを示しています。デフォルトのxamlの代わりにコードビハインドでそれを行います。
  2. app.xamlのStartupUri="MainWindow.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ビューでのみ具象クラスを使用しました。
  • ViewModelに使用するコンストラクターを指定する必要がありました(DryIocで行う必要があります)。これは、XAMLデザイナーにデフォルトコンストラクターが存在する必要があり、インジェクションを使用するコンストラクターが実際にアプリケーションで使用されるためです。

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アプリケーションの非常にクリーンで最小限の実装を実現しました。

2
Soleil