Wie man die Abhängigkeitsinjektion in einer WPF / MVVM-Anwendung handhabt

Ich beginne eine neue Desktop-Anwendung und möchte sie mit MVVM und WPF erstellen.

Ich beabsichtige auch, TDD zu verwenden.

Das Problem ist, dass ich nicht weiß, wie ich einen IoC-Container verwenden sollte, um meine Abhängigkeiten von meinem Produktionscode zu injizieren.

Angenommen, ich habe die folgende class und Schnittstelle:

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

Und dann habe ich eine andere class, die IStorage als Abhängigkeit hat, nehmen wir auch an, dass diese class ein ViewModel oder eine Business Class ist …

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

Damit kann ich problemlos Unit-Tests schreiben, um sicherzustellen, dass sie ordnungsgemäß funktionieren, Mocks usw. verwenden.

Das Problem ist, wenn es in der realen Anwendung verwendet wird. Ich weiß, dass ich einen IoC-Container haben muss, der eine Standardimplementierung für die IStorage Schnittstelle verbindet, aber wie kann ich das tun?

Zum Beispiel, wie wäre es, wenn ich das folgende xaml hätte:

      

Wie kann ich WPF sagen, Abhängigkeiten in diesem Fall zu injizieren?

Angenommen, ich benötige eine Instanz von SomeViewModel aus meinem SomeViewModel Code, wie soll ich das machen?

Ich fühle mich völlig verloren in diesem, ich würde jedes Beispiel oder Anleitung schätzen, wie der beste Weg ist, damit umzugehen.

Ich bin mit StructureMap vertraut, aber ich bin kein Experte. Wenn es ein besseres / einfacheres / out-of-the-box-Framework gibt, lassen Sie es mich wissen.

Danke im Voraus.

Ich habe Ninject benutzt und festgestellt, dass es eine Freude ist, mit ihm zu arbeiten. Alles ist im Code eingerichtet, die Syntax ist ziemlich einfach und es hat eine gute Dokumentation (und viele Antworten auf SO).

Also im Grunde geht es so:

Erstellen Sie das Ansichtsmodell, und nehmen Sie die IStorage-Schnittstelle als Konstruktorparameter:

 class UserControlViewModel { public UserControlViewModel(IStorage storage) { } } 

Erstellen Sie einen ViewModelLocator mit einer get-Eigenschaft für das Ansichtsmodell, das das Ansichtsmodell aus Ninject lädt:

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

Machen Sie den ViewModelLocator zu einer anwendungsweiten Ressource in App.xaml:

      

Binden Sie den DataContext des UserControl an die entsprechende Eigenschaft im ViewModelLocator.

     

Erstellen Sie eine class, die NinjectModule erbt, die die erforderlichen Bindungen (IStorage und das Viewmodel) einrichten:

 class IocConfiguration : NinjectModule { public override void Load() { Bind().To().InSingletonScope(); // Reuse same storage every time Bind().ToSelf().InTransientScope(); // Create new instance every time } } 

Initialisiere den IoC-coreel beim Start der Anwendung mit den notwendigen Ninject-Modulen (das oben genannte):

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

Ich habe eine statische Ioccoreel-class verwendet, um die anwendungsweite Instanz des IoC-coreels zu speichern, sodass ich bei Bedarf einfach darauf zugreifen kann:

 public static class Ioccoreel { private static Standardcoreel _kernel; public static T Get() { return _kernel.Get(); } public static void Initialize(params INinjectModule[] modules) { if (_kernel == null) { _kernel = new Standardcoreel(modules); } } } 

Diese Lösung verwendet einen statischen ServiceLocator (den Ioccoreel), der allgemein als ein Anti-Pattern betrachtet wird, da er die Abhängigkeiten der class versteckt. Es ist jedoch sehr schwierig, eine Art manueller Service-Lookup für UI-classn zu vermeiden, da sie einen parameterlosen Konstruktor haben müssen und Sie die Instantiierung trotzdem nicht steuern können, sodass Sie die VM nicht injizieren können. Zumindest können Sie auf diese Weise die VM isoliert testen, wo sich die gesamte Geschäftslogik befindet.

Wenn jemand einen besseren Weg hat, bitte teilen.

BEARBEITEN: Lucky Likey lieferte eine Antwort, um den statischen Service-Locator loszuwerden, indem Ninject die UI-classn instanziiert. Die Details der Antwort können hier gesehen werden

In Ihrer Frage haben Sie den Wert der DataContext Eigenschaft der Sicht in XAML festgelegt. Dies erfordert, dass Ihr Ansichtsmodell über einen Standardkonstruktor verfügt. Wie Sie jedoch festgestellt haben, funktioniert dies nicht gut mit der Abhängigkeitsinjektion, bei der Abhängigkeiten in den Konstruktor eingefügt werden sollen.

Daher können Sie die DataContext Eigenschaft nicht in XAML festlegen . Stattdessen haben Sie andere Alternativen.

Wenn Ihre Anwendung auf einem einfachen hierarchischen Ansichtsmodell basiert, können Sie beim StartupUri der Anwendung die gesamte Ansichtsmodellhierarchie erstellen (Sie müssen die StartupUri Eigenschaft aus der App.xaml Datei entfernen):

 public partial class App { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var container = CreateContainer(); var viewModel = container.Resolve(); var window = new MainWindow { DataContext = viewModel }; window.Show(); } } 

Dies basiert auf einem Objektdiagramm von View-Modellen, die im RootViewModel verwurzelt sind. Sie können jedoch einige View-Model-Factories in übergeordnete View-Modelle RootViewModel , um neue Child-View-Modelle zu erstellen, sodass das Objektdiagramm nicht fixiert werden muss. Dies beantwortet hoffentlich auch deine Frage. Angenommen, ich brauche eine Instanz von SomeViewModel aus meinem SomeViewModel Code, wie soll ich das machen?

 class ParentViewModel { public ParentViewModel(ChildViewModelFactory childViewModelFactory) { _childViewModelFactory = childViewModelFactory; } public void AddChild() { Children.Add(_childViewModelFactory.Create()); } ObservableCollection Children { get; private set; } } class ChildViewModelFactory { public ChildViewModelFactory(/* ChildViewModel dependencies */) { // Store dependencies. } public ChildViewModel Create() { return new ChildViewModel(/* Use stored dependencies */); } } 

Wenn Ihre Anwendung dynamischer ist und vielleicht auf der Navigation basiert, müssen Sie sich in den Code einklinken, der die Navigation durchführt. Jedes Mal, wenn Sie zu einer neuen Ansicht navigieren, müssen Sie ein Ansichtsmodell (aus dem DI-Container), die Ansicht selbst erstellen und den DataContext der Ansicht auf das Ansichtsmodell setzen. Sie können diese Ansicht zuerst ausführen, wenn Sie ein Ansichtsmodell basierend auf einer Ansicht auswählen, oder Sie können zuerst das Ansichtsmodell ausführen, wobei das Ansichtsmodell bestimmt, welche Ansicht verwendet werden soll. Ein MVVM-Framework stellt diese Schlüsselfunktionalität mit einer Möglichkeit zur Verfügung, Ihren DI-Container in die Erstellung von Ansichtsmodellen einzubinden, Sie können sie jedoch auch selbst implementieren. Ich bin hier ein bisschen vage, denn abhängig von Ihren Bedürfnissen kann diese functionalität ziemlich komplex werden. Dies ist eine der corefunktionen, die Sie von einem MVVM-Framework erhalten, aber wenn Sie Ihre eigenen Anwendungen in einer einfachen Anwendung ausführen, werden Sie verstehen, was MVVM-Frameworks unter der Haube bieten.

Indem Sie den DataContext in XAML nicht deklarieren können, verlieren Sie einige Entwurfszeitunterstützung. Wenn Ihr Ansichtsmodell einige Daten enthält, wird es während der Entwurfszeit angezeigt, was sehr nützlich sein kann. Glücklicherweise können Sie Entwurfszeitattribute auch in WPF verwenden. Eine Möglichkeit, dies zu tun, besteht darin, dem -Element oder in XAML die folgenden Attribute hinzuzufügen:

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

Der View-Model-Typ sollte zwei Konstruktoren haben, den Standard für Entwurfszeitdaten und einen weiteren für die Abhängigkeitsinjektion:

 class MyViewModel : INotifyPropertyChanged { public MyViewModel() { // Create some design-time data. } public MyViewModel(/* Dependencies */) { // Store dependencies. } } 

Auf diese Weise können Sie die Abhängigkeitsinjektion nutzen und eine gute Unterstützung für die Entwurfszeit beibehalten.

Was ich hier poste, ist eine Verbesserung von Sondergards Antwort, weil das, was ich erzählen werde, nicht in einen Kommentar passt 🙂

In der Tat führe ich eine saubere Lösung ein, die die Notwendigkeit eines ServiceLocators und eines Wrappers für die Standardcoreel -Instanz vermeidet, die in der Lösung von IocContainer heißt. Warum? Wie erwähnt, sind dies Anti-Patterns.

Den Standardcoreel überall verfügbar machen

Der Schlüssel zu Ninjects Magie ist die Standardcoreel -Instanz, die benötigt wird, um die .Get() -Methode zu verwenden.

Alternativ zu sondergards IocContainer Sie den Standardcoreel in der App class erstellen.

Entfernen Sie StartUpUri aus Ihrer App.xaml

  ...  

Dies ist der CodeBehind der App in App.xaml.cs

 public partial class App { private Icoreel _ioccoreel; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); _ioccoreel = new Standardcoreel(); _ioccoreel.Load(new YourModule()); Current.MainWindow = _ioccoreel.Get(); Current.MainWindow.Show(); } } 

Ab jetzt ist Ninject am Leben und bereit zu kämpfen 🙂

Injizieren Ihres DataContext

Wenn Ninject am Leben ist, können Sie alle Arten von Injektionen durchführen, zB Property Setter Injection oder die gängigste Constructor Injection .

So injizieren Sie Ihr ViewModel in den DataContext Ihres DataContext

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

Natürlich können Sie auch ein IViewModel wenn Sie die richtigen Bindungen machen, aber das ist kein Teil dieser Antwort.

Direkter Zugriff auf den coreel

Wenn Sie Methoden auf dem coreel direkt .Get() (zB. .Get() -Methode), können Sie den coreel sich selbst injizieren lassen.

  private void DoStuffWithcoreel(Icoreel kernel) { kernel.Get(); kernel.Whatever(); } 

Wenn Sie eine lokale Instanz des coreels benötigen, können Sie diese als Property injizieren.

  [Inject] public Icoreel coreel { private get; set; } 

Obwohl dies sehr nützlich sein kann, würde ich Ihnen nicht empfehlen, dies zu tun. Beachten Sie, dass Objekte, die auf diese Weise injiziert werden, nicht im Konstruktor verfügbar sind, da sie später injiziert werden.

Gemäß diesem Link sollten Sie die Factory-Extension verwenden, anstatt den Icoreel (DI Container) zu injizieren.

Der empfohlene Ansatz zum Verwenden eines DI-Containers in einem Softwaresystem besteht darin, dass der Kompositionsstamm der Anwendung der einzelne Ort ist, an dem der Container direkt berührt wird.

Wie die Ninject.Extensions.Factory verwendet werden soll, kann hier auch rot sein.

Ich gehe nach einem “view first” -Ansatz, bei dem ich das View-Modell an den Konstruktor der View (in seinem Code-Behind) weitergebe, der dem Datenkontext zugewiesen wird, z

 public class SomeView { public SomeView(SomeViewModel viewModel) { InitializeComponent(); DataContext = viewModel; } } 

Dies ersetzt Ihren XAML-basierten Ansatz.

Ich verwende das Prism-Framework für die Navigation – wenn ein Code eine bestimmte Ansicht anfordert (durch “Navigieren” zu dieser), wird Prism diese Ansicht auflösen (intern, unter Verwendung des DI-Frameworks der App); Das DI-Framework triggers dann alle Abhängigkeiten auf, die die View hat (das View-Modell in meinem Beispiel), triggers dann ihre Abhängigkeiten auf und so weiter.

Die Wahl des DI-Frameworks ist ziemlich irrelevant, da alle im Wesentlichen dasselbe tun, dh Sie registrieren eine Schnittstelle (oder einen Typ) zusammen mit dem konkreten Typ, den das Framework instanziieren soll, wenn es eine Abhängigkeit von dieser Schnittstelle findet. Für den Rekord benutze ich Castle Windsor.

Die Navigation mit Prismen ist gewöhnungsbedürftig, aber wenn Sie sich erst einmal damit beschäftigt haben, können Sie Ihre Anwendung mit verschiedenen Ansichten zusammenstellen. ZB könnten Sie eine Prism “Region” in Ihrem Hauptfenster erstellen und dann Prism Navigation benutzen, würden Sie innerhalb dieser Region von einer Ansicht zu einer anderen wechseln, zB wenn der Benutzer Menüpunkte oder was auch immer wählt.

Alternativ können Sie sich auch eines der MVVM-Frameworks wie MVVM Light ansehen. Ich habe keine Erfahrung mit diesen, kann also nichts dazu sagen.

Installieren Sie MVVM Light.

Teil der Installation ist das Erstellen eines View Model Locators. Dies ist eine class, die Ihre Viewmodels als Eigenschaften verfügbar macht. Der Getter dieser Eigenschaften kann dann Instanzen von Ihrer IOC-Engine zurückgegeben werden. Glücklicherweise enthält MVVM light auch das SimpleIOC-Framework, aber Sie können bei Bedarf auch andere anschließen.

Mit einfachen IOC registrieren Sie eine Implementierung gegen einen Typ …

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

In diesem Beispiel wird Ihr Ansichtsmodell erstellt und ein Dienstanbieterobjekt gemäß seinem Konstruktor übergeben.

Sie erstellen dann eine Eigenschaft, die eine Instanz von IOC zurückgibt.

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

Der clevere Teil ist, dass der View Model Locator dann in app.xaml oder äquivalent als Datenquelle erstellt wird.

  

Sie können jetzt an die Eigenschaft ‘MyViewModel’ binden, um Ihr Ansichtsmodell mit einem injizierten Service zu erhalten.

Ich hoffe, das hilft. Wir entschuldigen uns für etwaige Ungenauigkeiten des Codes, die auf einem iPad gespeichert wurden.

Verwenden Sie das verwaltete Erweiterbarkeits-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(); //Assert that MEF did as advertised Debug.Assert(viewModel is SomViewModel); Debug.Assert(viewModel.ProperlyInitialized); } 

Im Allgemeinen haben Sie eine statische class und verwenden das Factory-Muster, um Ihnen einen globalen Container zur Verfügung zu stellen (Cache, Natch).

Um die Ansichtsmodelle zu injizieren, injizierst du sie genauso, wie du alles andere injizierst. Erstellen Sie einen Importkonstruktor (oder legen Sie eine Importanweisung für eine Eigenschaft / ein Feld fest) im Code-Behind der XAML-Datei und weisen Sie sie an, das Ansichtsmodell zu importieren. Dann binden Sie den Window Ihres DataContext an diese Eigenschaft. Ihre Wurzelobjekte, die Sie selbst aus dem Container herausziehen, sind normalerweise zusammengesetzte Window . Fügen Sie einfach Schnittstellen zu den Fensterklassen hinzu und exportieren Sie sie. Greifen Sie dann wie oben beschrieben auf den Katalog zu (in App.xaml.cs … das ist die WPF-Bootstrap-Datei).

Ich würde vorschlagen, das ViewModel – First Ansatz https://github.com/Caliburn-Micro/Caliburn.Micro zu verwenden

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

Verwende Castle Windsor als IOC-Container.

Alles über Konventionen

Eines der Hauptmerkmale von Caliburn.Micro zeigt sich in seiner Fähigkeit, die Notwendigkeit eines Kesselblechcodes zu beseitigen, indem es auf einer Reihe von Konventionen einwirkt. Manche Leute lieben Konventionen und manche hassen sie. Aus diesem Grund sind die Konventionen von CM vollständig anpassbar und können sogar komplett deaktiviert werden, wenn dies nicht erwünscht ist. Wenn Sie Konventionen verwenden und standardmäßig aktiviert sind, ist es gut zu wissen, was diese Konventionen sind und wie sie funktionieren. Das ist das Thema dieses Artikels. Ansichtsauflösung (ViewModel-First)

Grundlagen

Die erste Konvention, der Sie bei der Verwendung von CM begegnen werden, hängt mit der Anzeigeauflösung zusammen. Diese Konvention wirkt sich auf alle ViewModel-First-Bereiche Ihrer Anwendung aus. In ViewModel-First haben wir ein vorhandenes ViewModel, das wir auf dem Bildschirm rendern müssen. Dazu verwendet CM ein einfaches Benennungsmuster, um ein UserControl1 zu finden, das an das ViewModel gebunden und angezeigt werden soll. Also, was ist das für ein Muster? Werfen wir einen Blick auf ViewLocator.LocateForModelType, um Folgendes herauszufinden:

 public static Func 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); }; 

Lassen Sie uns zunächst die Variable “context” ignorieren. Um die Ansicht abzuleiten, nehmen wir an, dass Sie den Text “ViewModel” in der Benennung Ihrer VMs verwenden, also ändern wir das einfach zu “View”, wo immer wir es finden, indem wir das Wort “Model” entfernen. Dies hat den Effekt, dass sowohl Typnamen als auch Namespaces geändert werden. So würde ViewModels.CustomerViewModel zu Views.CustomerView werden. Oder wenn Sie Ihre Anwendung nach functionen organisieren: CustomerManagement.CustomerViewModel wird zu CustomerManagement.CustomerView. Hoffentlich ist das ziemlich einfach. Sobald wir den Namen haben, suchen wir nach Typen mit diesem Namen. Wir durchsuchen jede Assembly, die Sie CM ausgesetzt haben, als durch AssemblySource.Instance2 durchsuchbar. Wenn wir den Typ finden, erstellen wir eine Instanz (oder erhalten eine aus dem IoC-Container, wenn sie registriert ist) und geben sie an den Aufrufer zurück. Wenn wir den Typ nicht finden, generieren wir eine Ansicht mit einer entsprechenden Nachricht “nicht gefunden”.

Nun zurück zu diesem “Kontext” -Wert. Auf diese Weise unterstützt CM mehrere Views über dasselbe ViewModel. Wenn ein Kontext (in der Regel eine Zeichenfolge oder eine Enumeration) bereitgestellt wird, führen wir eine weitere Transformation des Namens basierend auf diesem Wert durch. Diese Umwandlung geht davon aus, dass Sie einen Ordner (Namespace) für die verschiedenen Ansichten haben, indem Sie das Wort “View” vom Ende entfernen und stattdessen den Kontext anhängen. In einem Kontext von “Master” würde unser ViewModels.CustomerViewModel zu Views.Customer.Master werden.

Entfernen Sie das Startup-URI von Ihrer app.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); } } 

Jetzt können Sie Ihre IoC-class zum Erstellen der Instanzen verwenden.

MainWindowView.xaml.cs

 public partial class MainWindowView { public MainWindowView() { var mainWindowViewModel = IoC.GetInstance(); //Do other configuration DataContext = mainWindowViewModel; InitializeComponent(); } }