Come gestire l’integrazione delle dipendenze in un’applicazione WPF / MVVM

Sto avviando una nuova applicazione desktop e voglio costruirla usando MVVM e WPF.

Intendo anche usare TDD.

Il problema è che non so come usare un contenitore IoC per iniettare le mie dipendenze sul mio codice di produzione.

Supponiamo che io abbia la class e l’interfaccia seguenti:

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

E poi ho un’altra class che ha IStorage come dipendenza, supponiamo anche che questa class sia un ViewModel o una class business …

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

Con questo posso facilmente scrivere test unitari per garantire che funzionino correttamente, usando mock e così via

Il problema è quando si tratta di usarlo nella vera applicazione. So che devo avere un contenitore IoC che collega un’implementazione predefinita per l’interfaccia IStorage , ma come posso farlo?

Ad esempio, come sarebbe se avessi il seguente xaml:

      

Come posso “dire” a WPF in modo corretto di iniettare dipendenze in quel caso?

Inoltre, supponiamo di aver bisogno di un’istanza di SomeViewModel dal mio codice cs , come dovrei farlo?

Mi sento completamente perso in questo, apprezzerei qualsiasi esempio o guida su come è il modo migliore per gestirlo.

Conosco StructureMap, ma non sono un esperto. Inoltre, se esiste un framework migliore / più facile / immediato, per favore fatemelo sapere.

Grazie in anticipo.

Ho usato Ninject e ho scoperto che è un piacere lavorare con. Tutto è impostato in codice, la syntax è abbastanza semplice e ha una buona documentazione (e molte risposte su SO).

Quindi in pratica va così:

Creare il modello di visualizzazione e utilizzare l’interfaccia IStorage come parametro del costruttore:

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

Creare un ViewModelLocator con una proprietà get per il modello di vista, che carica il modello di visualizzazione da Ninject:

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

Rendi ViewModelLocator una risorsa a livello di applicazione in App.xaml:

      

Associare il DataContext di UserControl alla proprietà corrispondente in ViewModelLocator.

     

Creare una class che eredita NinjectModule, che imposterà i binding necessari (IStorage e viewmodel):

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

Inizializza il kernel IoC all’avvio dell’applicazione con i moduli necessari di Ninject (quello sopra per ora):

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

Ho usato una class IocKernel statica per contenere l’istanza dell’applicazione estesa del kernel IoC, così posso accedere facilmente quando necessario:

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

Questa soluzione fa uso di un ServiceLocator statico (il IocKernel), che è generalmente considerato un anti-pattern, perché nasconde le dipendenze della class. Tuttavia è molto difficile evitare una sorta di ricerca del servizio manuale per le classi UI, poiché devono avere un costruttore senza parametri e non è ansible controllare l’istanza in ogni caso, quindi non è ansible iniettare la VM. Almeno in questo modo è ansible testare la VM in isolamento, che è dove si trova tutta la logica di business.

Se qualcuno ha un modo migliore, per favore condividilo.

EDIT: Lucky Likey ha fornito una risposta per sbarazzarsi del localizzatore di servizi statici, permettendo a Ninject di istanziare le classi UI. I dettagli della risposta possono essere visti qui

Nella tua domanda imposti il ​​valore della proprietà DataContext della vista in XAML. Ciò richiede che il tuo modello di vista abbia un costruttore predefinito. Tuttavia, come avete notato, questo non funziona bene con l’iniezione di dipendenza in cui si vogliono iniettare dipendenze nel costruttore.

Quindi non è ansible impostare la proprietà DataContext in XAML . Invece hai altre alternative.

Se l’applicazione si basa su un semplice modello di vista gerarchico, è ansible build l’intera gerarchia del modello di vista all’avvio dell’applicazione (sarà necessario rimuovere la proprietà StartupUri dal file StartupUri ):

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

Questo si basa su un grafico a oggetti di modelli di vista radicati su RootViewModel ma è ansible iniettare alcune fabbriche di modelli di vista in modelli di visualizzazione genitore che consentono loro di creare nuovi modelli di visualizzazione figlio in modo che il grafico dell’object non debba essere corretto. Anche questo spero che risponda alla tua domanda supponiamo di aver bisogno di un’istanza di SomeViewModel dal mio codice cs , come dovrei farlo?

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

Se la tua applicazione è di natura più dynamic e forse è basata sulla navigazione, dovrai agganciare il codice che esegue la navigazione. Ogni volta che si passa a una nuova vista è necessario creare un modello di visualizzazione (dal contenitore DI), la vista stessa e impostare il DataContext della vista sul modello di vista. È ansible eseguire prima questa vista in cui si seleziona un modello di vista basato su una vista oppure è ansible farlo prima di tutto nel modello di visualizzazione in cui il modello di vista determina quale vista utilizzare. Un framework MVVM fornisce questa funzionalità chiave in qualche modo per colbind il contenitore DI alla creazione di modelli di visualizzazione, ma è anche ansible implementarlo da soli. Sono un po ‘vago perché, a seconda delle esigenze, questa funzionalità potrebbe diventare piuttosto complessa. Questa è una delle funzioni principali che si ottiene da un framework MVVM, ma il rollover proprio in una semplice applicazione ti darà una buona comprensione di quali framework MVVM forniscono sotto il cofano.

Non essendo in grado di dichiarare il DataContext in XAML, si perde qualche supporto in fase di progettazione. Se il tuo modello di vista contiene alcuni dati, questo apparirà durante la fase di progettazione che può essere molto utile. Fortunatamente, è ansible utilizzare gli attributi in fase di progettazione anche in WPF. Un modo per farlo è aggiungere i seguenti attributi all’elemento o in 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}" 

Il tipo di modello di vista dovrebbe avere due costruttori, il predefinito per i dati di progettazione e un altro per l’iniezione di dipendenza:

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

In questo modo è ansible utilizzare l’iniezione di dipendenza e mantenere un buon supporto in fase di progettazione.

Quello che sto postando qui è un miglioramento della risposta di Sondergard, perché quello che sto per dire non rientra in un commento 🙂

In realtà sto introducendo una soluzione pulita, che evita la necessità di un ServiceLocator e un wrapper per l’istanza di StandardKernel , che nella soluzione di sondergard si chiama IocContainer . Perché? Come accennato, quelli sono anti-schemi.

Rendendo disponibile StandardKernel ovunque

La chiave per la magia di Ninject è StandardKernel -Instance che è necessario per utilizzare. .Get() -Method.

In alternativa a IocContainer di IocContainer è ansible creare il kernel StandardKernel all’interno App class.

Basta rimuovere StartUpUri dalla tua App.xaml

  ...  

Questo è il CodeBehind dell’app all’interno di 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(); Current.MainWindow.Show(); } } 

D’ora in poi, Ninject è vivo e pronto a combattere 🙂

Iniezione di DataContext

Dato che Ninject è vivo, è ansible eseguire tutti i tipi di iniezioni, ad esempio l’ Iniezione del setter proprietà o il più comune Iniezione del Costruttore .

Questo è il modo in cui iniettate ViewModel nel DataContext vostra Window

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

Ovviamente puoi anche IViewModel un IViewModel se fai i binding giusti, ma non fa parte di questa risposta.

Accedere direttamente al kernel

Se è necessario chiamare direttamente i metodi sul kernel (ad es. .Get() -Method), è ansible lasciare che il kernel si inietti.

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

Se aveste bisogno di un’istanza locale del kernel, potreste iniettarla come proprietà.

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

Sebbene questo possa essere abbastanza utile, non ti consiglierei di farlo. Basta notare che gli oggetti iniettati in questo modo, non saranno disponibili all’interno del Costruttore, perché è iniettato in seguito.

In base a questo collegamento, è necessario utilizzare l’estensione di fabbrica anziché iniettare l’ IKernel (contenitore DI).

L’approccio consigliato per l’utilizzo di un contenitore DI in un sistema software consiste nel fatto che la radice di composizione dell’applicazione sia il singolo punto in cui il contenitore viene toccato direttamente.

Qui può essere anche mostrato il colore di Ninject.Extensions.Factory.

Vado per un approccio “view first”, dove passo il view-model al costruttore della vista (nel suo code-behind), che viene assegnato al contesto dati, ad es.

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

Questo sostituisce il tuo approccio basato su XAML.

Uso il framework Prism per gestire la navigazione – quando alcuni codici richiedono la visualizzazione di una determinata vista (“navigando” su di essa), Prism risolverà quella vista (internamente, utilizzando il framework DI dell’app); il framework DI a sua volta risolverà tutte le dipendenze della vista (il modello di visualizzazione nel mio esempio), quindi risolverà le sue dipendenze e così via.

La scelta del framework DI è pressoché irrilevante dal momento che tutti fanno essenzialmente la stessa cosa, cioè si registra un’interfaccia (o un tipo) insieme al tipo concreto che si desidera che il framework istanzia quando trova una dipendenza da tale interfaccia. Per la cronaca io uso Castle Windsor.

La navigazione a Prisma richiede un po ‘di tempo per abituarsi, ma è abbastanza buona una volta che hai capito, permettendoti di comporre la tua applicazione usando viste diverse. Ad esempio, potresti creare una “regione” di Prisma nella tua finestra principale, quindi usare la navigazione Prism per passare da una vista all’altra all’interno di questa regione, ad esempio quando l’utente seleziona le voci di menu o qualsiasi altra cosa.

In alternativa, dare un’occhiata a uno dei framework MVVM come MVVM Light. Non ho esperienza di questi, quindi non posso commentare ciò che vogliono usare.

Installa MVVM Light.

Parte dell’installazione consiste nel creare un localizzatore del modello di vista. Questa è una class che espone i tuoi modelmodelli come proprietà. Il getter di queste proprietà può quindi essere restituito dal tuo motore IOC. Fortunatamente, MVVM light include anche il framework SimpleIOC, ma se lo desideri puoi colbind altri utenti.

Con il semplice IOC si registra un’implementazione rispetto a un tipo …

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

In questo esempio, il tuo modello di vista viene creato e passato un object fornitore di servizi come dal suo costruttore.

Quindi si crea una proprietà che restituisce un’istanza da IOC.

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

La parte più intelligente è che il localizzatore del modello di vista viene quindi creato in app.xaml o equivalente come origine dati.

  

Ora puoi associare la proprietà “MyViewModel” per ottenere il tuo viewmodel con un servizio iniettato.

Spero possa aiutare. Chiede scusa per eventuali inesattezze del codice, codificate dalla memoria su un iPad.

Utilizzare il quadro di estensibilità gestita .

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

In generale, quello che dovresti fare è avere una class statica e usare il modello di fabbrica per fornire un contenitore globale (cache, natch).

Per quanto riguarda l’iniezione dei modelli di vista, li si inietta nello stesso modo in cui si inietta tutto il resto. Creare un costruttore di importazione (o inserire un’istruzione import su una proprietà / campo) nel code-behind del file XAML e dirgli di importare il modello di visualizzazione. Quindi associa DataContext tua Window a quella proprietà. Gli oggetti radice effettivamente estratti dal contenitore sono di solito composti Window oggetti Window . Basta aggiungere interfacce alle classi della finestra ed esportarle, quindi prelevare dal catalogo come sopra (in App.xaml.cs … è il file bootstrap di WPF).

Vorrei suggerire di utilizzare ViewModel – Primo approccio https://github.com/Caliburn-Micro/Caliburn.Micro

vedere: https://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Convention

usa Castle Windsor come contenitore IOC.

All About Conventions

Una delle caratteristiche principali di Caliburn.Micro è evidente nella sua capacità di rimuovere la necessità del codice della piastra della caldaia agendo su una serie di convenzioni. Alcune persone amano le convenzioni e alcune li odiano. Ecco perché le convenzioni CM sono completamente personalizzabili e possono persino essere distriggerste completamente se non desiderato. Se si utilizzano le convenzioni e poiché esse sono attive per impostazione predefinita, è opportuno sapere quali sono queste convenzioni e come funzionano. Questo è l’argomento di questo articolo. Visualizza risoluzione (ViewModel-First)

Nozioni di base

La prima convenzione che è probabile incontrare quando si utilizza CM è correlata alla risoluzione della vista. Questa convenzione ha effetto su tutte le aree ViewModel-First della tua applicazione. In ViewModel-First, abbiamo un ViewModel esistente che dobbiamo renderizzare sullo schermo. Per fare ciò, CM usa un semplice schema di denominazione per trovare un UserControl1 che dovrebbe associare al ViewModel e visualizzarlo. Quindi, qual è quel modello? Diamo un’occhiata a ViewLocator.LocateForModelType per scoprire:

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

Ignoriamo la variabile “contesto” all’inizio. Per ricavare il punto di vista, supponiamo che tu stia utilizzando il testo “ViewModel” nella denominazione delle tue macchine virtuali, quindi lo cambiamo in “Visualizza” ovunque che troviamo rimuovendo la parola “Modello”. Ciò ha l’effetto di modificare sia i nomi dei tipi che i namespace. Quindi ViewModels.CustomerViewModel diventerebbe Views.CustomerView. Oppure se stai organizzando la tua applicazione per funzione: CustomerManagement.CustomerViewModel diventa CustomerManagement.CustomerView. Spero che sia abbastanza semplice. Una volta ottenuto il nome, cerchiamo i tipi con quel nome. Cerchiamo qualsiasi assembly esposto a CM come ricercabile tramite AssemblySource.Instance.2 Se troviamo il tipo, creiamo un’istanza (o ne prendiamo una dal contenitore IoC se è registrata) e la restituiamo al chiamante. Se non troviamo il tipo, generiamo una vista con un messaggio appropriato “non trovato”.

Ora, torniamo a quel valore di “contesto”. In questo modo CM supporta più viste sullo stesso ViewModel. Se viene fornito un contesto (in genere una stringa o un enum), eseguiamo un’ulteriore trasformazione del nome, in base a tale valore. Questa trasformazione presuppone effettivamente di avere una cartella (spazio dei nomi) per le diverse viste rimuovendo la parola “Visualizza” dalla fine e aggiungendo il contesto. Quindi, dato un contesto di “Master”, il nostro ViewModels.CustomerViewModel diventerebbe Views.Customer.Master.

Rimuovi l’uri di avvio dalla tua 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); } } 

Ora puoi usare la tua class IoC per build le istanze.

MainWindowView.xaml.cs

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