web-dev-qa-db-fra.com

Comment gérer l'injection de dépendance dans une application WPF / MVVM

Je commence une nouvelle application de bureau et je veux la construire à l'aide de MVVM et WPF.

J'ai également l'intention d'utiliser TDD.

Le problème est que je ne sais pas comment utiliser un conteneur IoC pour injecter mes dépendances sur mon code de production.

Supposons que j'ai la classe et l'interface suivantes:

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

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

Et puis j'ai une autre classe qui a IStorage comme dépendance, supposons aussi que cette classe est un ViewModel ou une classe métier ...

public class SomeViewModel
{
    private IStorage _storage;

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

Avec cela, je peux facilement écrire des tests unitaires pour s’assurer qu’ils fonctionnent correctement, en utilisant des simulacres, etc.

Le problème, c'est quand il s'agit de l'utiliser dans l'application réelle. Je sais que je dois avoir un conteneur IoC qui lie une implémentation par défaut pour l'interface IStorage, mais comment puis-je le faire?

Par exemple, comment cela se passerait-il si j'avais le xaml suivant:

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

Comment puis-je correctement "dire" à WPF d'injecter des dépendances dans ce cas?

Aussi, supposons que j'ai besoin d'une instance de SomeViewModel à partir de mon code cs, comment dois-je le faire?

Je sens que je suis complètement perdu dans cette situation. J'apprécierais tout exemple ou toute indication de la meilleure façon de le gérer.

Je connais bien StructureMap, mais je ne suis pas un expert. De plus, s'il existe un cadre meilleur/plus simple/prêt à l'emploi, veuillez me le faire savoir.

Merci d'avance.

88
Fedaykin

J'utilise Ninject et je trouve que c'est un plaisir de travailler avec. Tout est mis en place dans le code, la syntaxe est assez simple et il a une bonne documentation (et beaucoup de réponses sur SO).

Donc, fondamentalement, cela ressemble à ceci:

Créez le modèle de vue et prenez l'interface IStorage en tant que paramètre constructeur:

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

Créez un ViewModelLocator avec une propriété get pour le modèle de vue, qui charge le modèle de vue à partir de Ninject:

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

Faites du ViewModelLocator une ressource d'application étendue dans App.xaml:

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

Liez le DataContext de UserControl à la propriété correspondante dans ViewModelLocator.

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

Créez une classe héritant de NinjectModule, qui configurera les liaisons nécessaires (IStorage et le modèle de vue):

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

Initialisez le noyau IoC au démarrage de l'application avec les modules Ninject nécessaires (celui ci-dessus pour le moment):

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

        base.OnStartup(e);
    }
}

J'ai utilisé une classe statique IocKernel pour contenir l'instance d'application du noyau IoC, afin de pouvoir y accéder facilement en cas de besoin:

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

Cette solution utilise un ServiceLocator statique (IocKernel), généralement considéré comme un anti-modèle, car il masque les dépendances de la classe. Cependant, il est très difficile d'éviter une sorte de recherche de service manuelle pour les classes d'interface utilisateur, car elles doivent avoir un constructeur sans paramètre, et vous ne pouvez pas contrôler l'instanciation de toute façon, vous ne pouvez donc pas injecter la machine virtuelle. Au moins, cette méthode vous permet de tester le VM de manière isolée, où se trouve toute la logique métier.

Si quelqu'un a un meilleur moyen, partagez-le s'il vous plaît.

EDIT: Lucky Likey a fourni une réponse pour se débarrasser du localisateur de services statique, en laissant Ninject instancier des classes d’UI. Les détails de la réponse peuvent être vus ici

78
sondergard

Dans votre question, vous définissez la valeur de la propriété DataContext de la vue en XAML. Cela nécessite que votre modèle de vue ait un constructeur par défaut. Cependant, comme vous l'avez noté, cela ne fonctionne pas bien avec l'injection de dépendance lorsque vous souhaitez injecter des dépendances dans le constructeur.

Donc vous ne pouvez pas définir la propriété DataContext dans XAML . Au lieu de cela, vous avez d'autres alternatives.

Si votre application est basée sur un modèle de vue hiérarchique simple, vous pouvez construire la hiérarchie complète du modèle de vue au démarrage de l'application (vous devez supprimer la propriété StartupUri de la propriété App.xaml fichier):

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

}

Ceci est basé sur un graphe d’objets de modèles de vues s’appuyant sur le RootViewModel, mais vous pouvez injecter des fabriques de modèles de vues dans les modèles de vues parent en leur permettant de créer de nouveaux modèles de vues enfants, de sorte que le graphe d’objets n’a pas à réparer. Cela répond aussi, espérons-le, à votre question supposons qu'il me faut une instance de SomeViewModel à partir de mon code cs, comment dois-je le faire?

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

}

Si votre application est de nature plus dynamique et peut-être basée sur la navigation, vous devrez vous connecter au code qui effectue la navigation. Chaque fois que vous accédez à une nouvelle vue, vous devez créer un modèle de vue (à partir du conteneur DI), la vue elle-même et définir le DataContext de la vue sur le modèle de vue. Vous pouvez faire cette vue en premier lorsque vous choisissez un modèle de vue basé sur une vue ou vous pouvez le faire -model first où le modèle de vue détermine la vue à utiliser. Une infrastructure MVVM fournit à cette fonctionnalité clé un moyen de relier votre conteneur DI à la création de modèles de vue, mais vous pouvez également l'implémenter vous-même. Je suis un peu vague parce que, selon vos besoins, cette fonctionnalité peut devenir assez complexe. C’est l’une des fonctions essentielles d’un framework MVVM, mais si vous le faites dans une application simple, vous comprendrez bien ce que les frameworks MVVM fournissent sous le capot.

En ne pouvant pas déclarer le DataContext en XAML, vous perdez du support au moment de la conception. Si votre modèle de vue contient des données, elles apparaîtront au moment de la conception, ce qui peut s'avérer très utile. Heureusement, vous pouvez utiliser attributs de conception également dans WPF. Une façon de faire est d’ajouter les attributs suivants au <Window> élément ou <UserControl> en 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}"

Le type de modèle de vue doit avoir deux constructeurs, le par défaut pour les données de conception et un autre pour l'injection de dépendance:

class MyViewModel : INotifyPropertyChanged {

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

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

}

En faisant cela, vous pouvez utiliser l’injection de dépendance et conserver un bon support au moment de la conception.

46
Martin Liversage

Ce que je publie ici est une amélioration de la réponse de sondergard, car ce que je vais dire ne rentre pas dans un commentaire :)

En fait, je vous présente une solution soignée, qui évite le recours à un ServiceLocator et à un wrapper pour la StandardKernel- Instance, qui La solution de sondergard s'appelle IocContainer. Pourquoi? Comme mentionné, ce sont des anti-modèles.

Rendre le StandardKernel disponible partout

La clé de la magie de Ninject est l'instance StandardKernel- qui est nécessaire pour utiliser la méthode .Get<T>()-.

Vous pouvez également créer le IocContainer de sondergard StandardKernel à l'intérieur de la classe App-.

Supprimez simplement StartUpUri de votre App.xaml

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

C'est le code derrière l'application dans App.xaml.cs

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 est désormais en vie et prêt à se battre :)

Injecter votre DataContext

Comme Ninject est en vie, vous pouvez effectuer toutes sortes d’injections, par exemple , propriété Setter Injection ou la plus courante . Constructeur Injection .

Voici comment vous injectez votre ViewModel dans votre WindowDataContext

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

Bien sûr, vous pouvez aussi Injecter un IViewModel si vous faites les bonnes liaisons, mais cela ne fait pas partie de cette réponse.

Accéder directement au noyau

Si vous devez appeler des méthodes directement sur le noyau (par exemple, .Get<T>()- Method), vous pouvez laisser le noyau s’injecter.

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

Si vous avez besoin d’une instance locale du noyau, vous pouvez l’injecter en tant que propriété.

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

Bien que cela puisse être très utile, je ne vous recommanderais pas de le faire. Notez simplement que les objets injectés de cette manière ne seront pas disponibles dans le constructeur, car ils seront injectés plus tard.

Selon ce lien vous devriez utiliser l’extension-usine au lieu d’injecter le IKernel (conteneur DI).

L'approche recommandée pour utiliser un conteneur DI dans un système logiciel est que la racine de composition de l'application soit le seul endroit où le conteneur est directement touché.

La manière dont Ninject.Extensions.Factory doit être utilisé peut aussi être rouge ici .

23
LuckyLikey

Je choisis une approche "vue en premier", dans laquelle je passe le modèle de vue au constructeur de la vue (dans son code-behind), qui est affecté au contexte de données, par exemple.

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

        DataContext = viewModel;
    }
}

Ceci remplace votre approche basée sur XAML.

J'utilise le framework Prism pour gérer la navigation - lorsque du code demande l'affichage d'une vue particulière (en "y naviguant"), Prism résoud cette vue (en interne, à l'aide du framework DI de l'application); Le framework DI va à son tour résoudre les dépendances de la vue (le modèle de vue dans mon exemple), puis résout les dépendances ses, etc.

Le choix du cadre DI n’a pratiquement aucune importance, car ils font tous essentiellement la même chose, c’est-à-dire que vous enregistrez une interface (ou un type) avec le type concret que vous voulez que le cadre instancie quand il trouve une dépendance à cette interface. Pour mémoire, j'utilise Castle Windsor.

La navigation dans Prism prend un peu de temps pour s'y habituer, mais elle est plutôt bonne une fois que vous vous y êtes bien compris, vous permettant de composer votre application à l'aide de différentes vues. Par exemple. vous pouvez créer une "région" Prism sur votre fenêtre principale, puis utiliser la navigation Prism pour basculer d'une vue à une autre dans cette région, par exemple. lorsque l'utilisateur sélectionne des éléments de menu ou autre.

Vous pouvez également consulter l’un des frameworks MVVM tels que MVVM Light. Je n'ai aucune expérience en la matière, je ne peux donc pas commenter leur utilisation.

12
Andrew Stephens

Installez MVVM Light.

Une partie de l'installation consiste à créer un localisateur de modèle de vue. C'est une classe qui expose vos modèles de vue en tant que propriétés. Le getter de ces propriétés peut ensuite être renvoyé à des instances de votre moteur IOC. Heureusement, MVVM Light inclut également le framework SimpleIOC, mais vous pouvez en connecter d'autres si vous le souhaitez.

Avec simple IOC vous enregistrez une implémentation contre un type ...

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

Dans cet exemple, votre modèle de vue est créé et un objet fournisseur de services est transmis conformément à son constructeur.

Vous créez ensuite une propriété qui renvoie une instance d'IOC.

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

La partie intelligente est que le localisateur de modèle de vue est ensuite créé dans app.xaml ou équivalent en tant que source de données.

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

Vous pouvez maintenant vous connecter à sa propriété 'MyViewModel' pour obtenir votre viewmodel avec un service injecté.

J'espère que ça t'as aidé. Toutes mes excuses pour les inexactitudes de code, codées à partir de la mémoire sur un iPad.

10
kidshaw

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

En général, ce que vous feriez serait d'avoir une classe statique et d'utiliser le modèle d'usine pour vous fournir un conteneur global (mis en cache, natch).

Pour savoir comment injecter les modèles de vue, vous les injectez de la même manière que vous injectez tout le reste. Créez un constructeur d'importation (ou placez une instruction d'importation sur une propriété/un champ) dans le code-behind du fichier XAML et indiquez-lui d'importer le modèle de vue. Liez ensuite votre WindowDataContext à cette propriété. Vos objets racine que vous extrayez vous-même du conteneur sont généralement composés Window objets. Ajoutez simplement des interfaces aux classes de fenêtre et exportez-les, puis extrayez-vous du catalogue comme ci-dessus (dans App.xaml.cs ..., c'est le fichier WPF bootstrap).

2
Clever Neologism

Étui Canonic DryIoc

Répondre à un ancien message, mais avec DryIoc et faire ce que j'estime être un bon usage de DI et d'interfaces (utilisation minimale de classes concrètes).

  1. Le point de départ d'une application WPF est App.xaml, et là nous disons quelle est la vue initiale à utiliser; nous faisons cela avec du code derrière au lieu du xaml par défaut:
  2. retirer StartupUri="MainWindow.xaml" dans App.xaml
  3. dans codebehind (App.xaml.cs) ajoutez ceci override OnStartup:

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

c'est le point de départ; c'est aussi le seul endroit où resolve devrait être appelé.

  1. la racine de la configuration (selon le livre de Mark Seeman intitulé Dependency injection dans .NET; le seul endroit où des classes concrètes doivent être mentionnées) se trouvera dans le même code derrière, dans le constructeur:

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

Remarques et quelques détails supplémentaires

  • J'ai utilisé la classe concrète uniquement avec la vue MainWindow;
  • Je devais spécifier le constructeur à utiliser (nous devons le faire avec DryIoc) pour ViewModel, car le constructeur par défaut devait exister pour le concepteur XAML et le constructeur avec injection était celui utilisé pour l'application.

Le constructeur ViewModel avec DI:

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

ViewModel constructeur par défaut pour la conception:

public MainWindowViewModel()
{
}

Le code derrière la vue:

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

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

et ce qui est nécessaire dans la vue (MainWindow.xaml) pour obtenir une instance de conception avec ViewModel:

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

Conclusion

Nous avons donc obtenu une implémentation très propre et minimale d'une application WPF avec un conteneur DryIoc et une ID, tout en maintenant des instances de conception de vues et de modèles de vues possibles.

2
Soleil