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

je démarre une nouvelle application de bureau et je veux la construire en utilisant 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 modèle de vue ou une classe d'affaires...

public class SomeViewModel
{
    private IStorage _storage;

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

avec ceci je peux facilement écrire des tests unitaires pour s'assurer qu'ils fonctionnent correctement, en utilisant des moqueries et etc.

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

par exemple, comment serait-il si j'avais suivant xaml:

<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 de mon code cs , Comment dois-je le faire?

je me sens complètement perdu dans cela, je voudrais n'importe quel exemple ou orientation de comment est la meilleure façon de le gérer.

Je suis familier avec StructureMap, mais je ne suis pas un expert. Aussi, si il ya un mieux/plus facile/dehors-de-le-boîte-cadre, s'il vous plaît laissez-moi savoir.

Merci d'avance.

75
demandé sur Fedaykin 2014-08-18 18:56:52

8 réponses

J'ai utilisé Ninject, et j'ai trouvé que c'est un plaisir de travailler avec. Tout est configuré en code, la syntaxe est assez simple et il a une bonne documentation (et beaucoup de réponses sur SO).

donc fondamentalement, il va comme ceci:

créer le modèle de vue, et prendre l'interface D'IStorage comme paramètre de constructeur:

class UserControlViewModel
{
    public UserControlViewModel(IStorage storage)
    {

    }
}

créer un ViewModelLocator avec une propriété get pour le modèle view, qui charge le modèle de vue de Ninject:

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

Faire le ViewModelLocator une demande de ressources à l'échelle de l'App.xaml:

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

lie le DataContext du UserControl à la propriété correspondante dans le ViewModelLocator.

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

créer une classe héritant NinjectModule, qui mettra en place les fixations nécessaires (IStorage et le 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
    }
}

initialise le CIO noyau 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 iockernel statique pour maintenir l'application large instance du noyau du CIO, de sorte que je peux facilement y accéder si nécessaire:

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 fait appel à un Localisateur de service statique (le IocKernel), qui est généralement considéré comme un anti-pattern, parce qu'il cache les dépendances de la classe. Cependant, il est très difficile pour éviter une sorte de recherche manuelle de service pour les classes UI, car elles doivent avoir un constructeur sans paramètres, et vous ne pouvez pas contrôler l'instanciation de toute façon, donc vous ne pouvez pas injecter la VM. Au moins de cette façon vous permet de tester la VM dans l'isolement, qui est où toute la logique commerciale est.

si quelqu'un a une meilleure façon, s'il vous plaît partagez.

EDIT: Lucky Likey a fourni une réponse pour se débarrasser du Localisateur de service statique, en laissant Ninject instantiate Classes de l'INTERFACE utilisateur. Les détails de la réponse peuvent être consultés ici

66
répondu sondergard 2017-05-23 12:26:38

dans votre question vous définissez la valeur de la propriété DataContext de la vue dans XAML. Cela nécessite que votre vue-modèle possède un constructeur par défaut. Cependant, comme vous l'avez remarqué, cela ne fonctionne pas bien avec l'injection de dépendance où vous voulez injecter des dépendances dans le constructeur.

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

si votre demande est basé sur un modèle de vue hiérarchique simple, vous pouvez construire la totalité de la hiérarchie de la vue-Modèle lorsque l'application démarre (vous devrez supprimer la propriété StartupUri du fichier 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();
  }

}

cela est basé sur un graphique d'objet de la vue-modèles enracinés à la RootViewModel , mais vous pouvez injecter quelques usines de vue-modèle dans la vue parent-modèles leur permettant de créer une nouvelle vue enfant-modèles de sorte que le graphique d'objet n'a pas à être fixé. Cela aussi j'espère répondre à votre question supposons que j'ai besoin d'une instance de SomeViewModel 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 plus dynamique dans la nature et peut-être est basée sur la navigation, vous aurez à accrocher dans le code qui effectue la navigation. Chaque fois que vous naviguez vers une nouvelle vue, vous devez créer une vue-Modèle (à partir du conteneur DI), la vue elle-même et de définir le DataContext de la vue à le view-model. Vous pouvez faire cela première vue où vous choisissez une vue (modèle basé sur une vue ou vous pouvez le faire view-model first d'où la vue-modèle détermine vue de leur utilisation. Un cadre MVVM fournit cette fonctionnalité clé avec un moyen pour vous d'accrocher votre conteneur DI dans la création de vues-modèles, mais vous pouvez également l'implémenter vous-même. Je suis un peu vague ici parce que selon vos besoins cette fonctionnalité peut devenir très complexe. C'est l'une des fonctions de base que vous obtenez à partir d'un cadre MVVM, mais rouler votre propre dans une application simple vous donnera une bonne compréhension de ce que les cadres MVVM fournissent sous le capot.

en n'étant pas en mesure de déclarer le DataContext dans XAML vous perdez un certain support de temps de conception. Si votre view-model contient des données, elles apparaîtront pendant le design-time, ce qui peut être très utile. Heureusement, vous pouvez utiliser les attributs de conception , également en WPF. Un pour ce faire, vous pouvez ajouter les attributs suivants à l'élément <Window> ou <UserControl> dans 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 view-model doit avoir deux constructeurs, la valeur par défaut pour les données de temps de calcul et une 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 de temps de conception.

38
répondu Martin Liversage 2017-06-08 19:47:58

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

en fait, je suis en train d'introduire une solution soignée, qui évite le besoin d'un ServiceLocator et un enveloppeur pour le StandardKernel - Instance, qui dans la Solution de sondergard est appelé IocContainer . Pourquoi? Comme mentionné, ce sont des anti-modèles.

rendant disponible le StandardKernel partout

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

alternativement au IocContainer de sondergard, vous pouvez créer le StandardKernel dans la classe App .

il suffit de supprimer StartUpUri de votre application.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 CodeBehind intérieur de L'application.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();
    }
}

de maintenant, Ninject est vivant et prêt à se battre :)

injection de votre DataContext

Comme Ninject est vivant, vous pouvez effectuer toutes sortes d'injections, de l'e.g Propriété Injection par Mutateur ou le plus commun Constructeur d'Injection .

voilà comment vous injectez votre modèle de vue dans votre Window 's DataContext

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

bien sûr vous peut également injecter un IViewModel si vous faites les bonnes fixations, mais cela ne fait pas partie de cette réponse.

accéder directement au noyau

si vous devez appeler directement des méthodes sur le noyau (par exemple .Get<T>() - méthode), vous pouvez laisser le Noyau injecter lui-même.

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

si vous avez besoin d'une instance locale du noyau, vous pouvez l'injecter comme propriété.

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

bien que cela puisse soyez très utile, Je ne vous le recommande pas. Il suffit de noter que les objets injectés de cette façon, ne seront pas disponibles à l'intérieur du constructeur, car il est injecté plus tard.

selon ce lien vous devez utiliser l'extension de l'usine au lieu d'injecter le IKernel (récipient DI).

l'approche recommandée pour employer un conteneur DI dans un système logiciel est que la racine de la Composition la demande doit être le seul endroit où le conteneur est touché directement.

Comment le Ninject.Extension.L'usine doit être utilisée peut aussi être rouge ici .

16
répondu LuckyLikey 2017-04-26 12:51:10

j'opte pour une approche "view first", où je passe le modèle de la vue au constructeur de la vue( dans son code-derrière), qui est affecté au contexte des 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 cadre Prism pour gérer la navigation - lorsque certains codes demandent qu'une vue particulière soit affichée (en" navigant " vers elle), Prism résoudra cette vue (à l'interne, en utilisant le cadre DI de l'application); le cadre DI va à son tour résoudre toutes les dépendances que la vue a (le modèle de vue dans mon exemple), puis résout ses dépendances, et ainsi de suite.

le choix du framework DI est assez peu pertinent car ils font tous essentiellement la même chose, i.e. vous enregistrez une interface (ou un type) avec le type concret que vous voulez que le framework instanciate quand il trouve une dépendance sur cette interface. Pour l'enregistrement, j'ai utiliser du Château de Windsor.

Prism navigation prend un peu de s'habituer à, mais est assez bon une fois que vous obtenez votre tête autour, vous permettant de composer votre application en utilisant des vues différentes. Par exemple: vous pouvez créer une "région" de Prism sur votre fenêtre principale, puis en utilisant la navigation de Prism vous changerez d'une vue à l'autre dans cette région, par exemple comme l'utilisateur sélectionne des éléments de menu ou quoi que ce soit.

alternativement, jetez un oeil à l'un des cadres MVVM tels que la lumière MVVM. J'ai pas d'expérience de ces ne peut donc pas commenter sur ce qu'ils aiment utiliser.

11
répondu Andrew Stephens 2014-08-19 09:47:45

installer la lumière MVVM.

une partie de l'installation est de créer un localisateur de modèle de vue. C'est une classe qui expose votre viewmodel en tant que propriétés. Le getter de ces propriétés peut alors être retourné des instances de votre moteur de CIO. Heureusement, MVVM light inclut également le cadre SimpleIOC, mais vous pouvez en brancher 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 passé un objet de fournisseur de services selon son constructeur.

vous créez alors une propriété qui renvoie une instance du CIO.

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

la partie intelligente est que le Localisateur de modèle de vue est alors créé dans app.xaml ou équivalent comme source de données.

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

vous pouvez maintenant vous lier à sa propriété 'MyViewModel' pour obtenir votre modèle de vue avec un service injecté.

Espère que ça aide. Mes excuses pour toute inexactitude de code, codée de mémoire sur un iPad.

11
répondu kidshaw 2014-08-22 13:26:08

utiliser 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 est d'avoir une classe statique et d'utiliser le modèle D'usine pour vous fournir un conteneur global (Caché, natch).

quant à la façon d'injecter les modèles view, vous les injectez de la même façon que vous injectez tout le reste. Créer un constructeur importateur (ou mettre une déclaration d'importation sur une propriété / champ) dans le code-derrière le XAML fichier, et de le dire à importer le modèle de vue. Puis liez votre Window 's DataContext à cette propriété. Vos objets racine que vous retirez du conteneur vous-même sont généralement composés Window des objets. Il suffit d'ajouter des interfaces aux classes de fenêtres, et de les exporter, puis de saisir à partir du catalogue comme ci-dessus (dans App.XAML.cs... c'est le fichier de démarrage WPF).

3
répondu Clever Neologism 2014-08-25 21:35:56

je suggère d'utiliser le modèle de vue-première approche https://github.com/Caliburn-Micro/Caliburn.Micro

voir: https://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Conventions

utiliser Castle Windsor comme conteneur CIO.

Tout Sur Les Conventions

une des principales caractéristiques de Caliburn.Micro est manifeste dans sa capacité à supprimer le besoin de code de la plaque chauffante en agissant sur une série de conventions. Certaines personnes aiment les conventions et certains les détestent. C'est pourquoi les conventions CM sont entièrement personnalisables et peuvent même être désactivées complètement si ce n'est pas souhaité. Si vous allez utiliser des conventions, et puisqu'elles sont activées par défaut, il est bon de savoir ce que sont ces conventions et comment elles fonctionnent. C'est le sujet de cet article. Vue De La Résolution (ViewModel-Première)

de base

le premier convention que vous êtes susceptible de rencontrer lorsque vous utilisez CM est liée à la résolution de vue. Cette convention affecte tous les Modèles de vue-premiers domaines de votre application. Dans ViewModel-tout d'abord, nous avons un ViewModel existant que nous devons rendre à l'écran. Pour ce faire, CM utilise un simple patron de nommage pour trouver un UserControl1 qu'il devrait lier au ViewModel et à l'affichage. Alors, quel est ce modèle? Regardons le ViewLocator.LocateForModelType pour savoir:

public static Func<Type, DependencyObject, object, UIElement> LocateForModelType = (modelType, displayLocation, context) =>{
    var viewTypeName = modelType.FullName.Replace("Model", string.Empty);
    if(context != null)
    {
        viewTypeName = viewTypeName.Remove(viewTypeName.Length - 4, 4);
        viewTypeName = viewTypeName + "." + context;
    }

    var viewType = (from assmebly in AssemblySource.Instance
                    from type in assmebly.GetExportedTypes()
                    where type.FullName == viewTypeName
                    select type).FirstOrDefault();

    return viewType == null
        ? new TextBlock { Text = string.Format("{0} not found.", viewTypeName) }
        : GetOrCreateViewType(viewType);
};

Ignorons d'abord la variable" context". Pour obtenir la vue, nous faisons l'hypothèse que vous utilisez le texte "ViewModel" dans le nom de votre VMs, donc nous changeons simplement cela en "View" partout où nous le trouvons en supprimant le mot "Model". Cela a pour effet de changer à la fois les noms de type et les espaces de noms. Donc Viewmodel.CustomerViewModel deviendrait Views.CustomerView. Ou si vous organisez votre application par fonction: CustomerManagement.CustomerViewModel devient CustomerManagement.CustomerView. J'espère que c'est assez simple. Une fois que nous avons le nom, nous cherchons les types avec ce nom. Nous recherchons n'importe quel assemblage que vous avez exposé à CM comme searchable via AssemblySource.Instance.2 Si nous trouvons le type, nous créons une instance (ou en obtenons une du conteneur du CIO si elle est enregistrée) et nous la retournons à l'appelant. Si nous ne trouvons pas le type, nous générons une vue avec un message approprié "not found".

maintenant, retour à ce" contexte" valeur. C'est ainsi que CM Supporte plusieurs vues sur le même modèle de vue. Si un contexte (typiquement une chaîne ou un enum) est fourni, nous faisons une autre transformation du nom, basée sur cette valeur. Cette transformation suppose effectivement que vous avez un dossier (namespace) pour les différentes vues en supprimant le mot "View" de la fin et en ajoutant le contexte à la place. Donc, étant donné le contexte de "maîtriser" nos modèles de vue.CustomerViewModel deviendrait Views.Client.Maître.

0
répondu Nahum 2014-08-26 07:54:29

Supprimer l'uri de démarrage de votre application.XAML.

App.XAML.cs

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        IoC.Configure(true);

        StartupUri = new Uri("Views/MainWindowView.xaml", UriKind.Relative);

        base.OnStartup(e);
    }
}

Maintenant vous pouvez utiliser votre classe IoC pour construire les instances.

MainWindowView.XAML.cs

public partial class MainWindowView
{
    public MainWindowView()
    {
        var mainWindowViewModel = IoC.GetInstance<IMainWindowViewModel>();

        //Do other configuration            

        DataContext = mainWindowViewModel;

        InitializeComponent();
    }

}
0
répondu C Bauer 2014-08-27 13:47:45