Cómo manejar la dependency injection en una aplicación WPF / MVVM

Estoy comenzando una nueva aplicación de escritorio y quiero construirla usando MVVM y WPF.

También tengo la intención de usar TDD.

El problema es que no sé cómo debo usar un contenedor IoC para inyectar mis dependencias en mi código de producción.

Supongamos que tengo la siguiente clase e interfaz:

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

Y luego tengo otra clase que tiene IStorage como dependencia, supongamos también que esta clase es un ViewModel o una clase de negocios …

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

Con esto puedo escribir fácilmente pruebas unitarias para asegurarme de que están funcionando correctamente, usando burlas, etc.

El problema es cuando se trata de usarlo en la aplicación real. Sé que debo tener un contenedor IoC que vincule una implementación predeterminada para la interfaz IStorage , pero ¿cómo puedo hacerlo?

Por ejemplo, ¿cómo sería si tuviera el siguiente xaml:

      

¿Cómo puedo ‘decirle’ a WPF que inserte dependencias en ese caso?

Además, supongamos que necesito una instancia de SomeViewModel de mi código cs , ¿cómo debería hacerlo?

Siento que estoy completamente perdido en esto, agradecería cualquier ejemplo u orientación sobre cómo es la mejor manera de manejarlo.

Estoy familiarizado con StructureMap, pero no soy un experto. Además, si hay un marco mejor / más fácil / fuera de la caja, házmelo saber.

Gracias por adelantado.

He estado usando Ninject y he descubierto que es un placer trabajar con él. Todo está configurado en código, la syntax es bastante sencilla y tiene una buena documentación (y muchas respuestas en SO).

Así que básicamente es así:

Cree el modelo de vista y tome la interfaz IStorage como parámetro de constructor:

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

Cree un ViewModelLocator con una propiedad get para el modelo de vista, que carga el modelo de vista desde Ninject:

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

Convierta ViewModelLocator en un recurso de aplicación en App.xaml:

      

Enlace el DataContext del UserControl a la propiedad correspondiente en ViewModelLocator.

     

Cree una clase heredando NinjectModule, que configurará los enlaces necesarios (IStorage y el modelo de vista):

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

Inicialice el kernel IoC en el inicio de la aplicación con los módulos Ninject necesarios (el anterior por ahora):

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

He utilizado una clase IocKernel estática para mantener la instancia de toda la aplicación del kernel IoC, por lo que puedo acceder fácilmente a ella cuando sea necesario:

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

Esta solución hace uso de un ServiceLocator estático (el IocKernel), que generalmente se considera como un antipatrón, porque oculta las dependencias de la clase. Sin embargo, es muy difícil evitar algún tipo de búsqueda de servicio manual para las clases de UI, ya que deben tener un constructor sin parámetros, y no se puede controlar la creación de instancias de todos modos, por lo que no se puede inyectar la máquina virtual. Al menos de esta manera, le permite probar la máquina virtual aisladamente, que es donde está toda la lógica comercial.

Si alguien tiene una mejor manera, por favor comparte.

EDITAR: Lucky Likey proporcionó una respuesta para deshacerse del localizador de servicio estático, permitiendo que Ninject creara instancias de clases de UI. Los detalles de la respuesta se pueden ver aquí

En su pregunta, establece el valor de la propiedad DataContext de la vista en XAML. Esto requiere que su modelo de vista tenga un constructor predeterminado. Sin embargo, como ha notado, esto no funciona bien con la dependency injections en la que desea inyectar dependencias en el constructor.

Por lo tanto, no puede establecer la propiedad DataContext en XAML . En cambio, tienes otras alternativas.

Si su aplicación se basa en un modelo de vista jerárquico simple, puede construir toda la jerarquía del modelo de vista cuando se inicia la aplicación (deberá eliminar la propiedad App.xaml archivo App.xaml ):

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

Esto se basa en un gráfico de objetos de modelos vista enraizados en RootViewModel pero puede inyectar algunas fábricas de modelos de vista en modelos de vista padres, lo que les permite crear nuevos modelos de vista secundarios para que el gráfico de objetos no tenga que ser reparado. También espero que esto responda a su pregunta, supongamos que necesito una instancia de SomeViewModel de mi código cs , ¿cómo debo hacerlo?

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

Si su aplicación es de naturaleza más dinámica y tal vez se basa en la navegación, tendrá que enganchar el código que realiza la navegación. Cada vez que navega hacia una nueva vista, necesita crear un modelo de vista (desde el contenedor DI), la vista misma y establecer el DataContext de la vista en el modelo de vista. Primero puede hacer esta vista donde elige un modelo de vista basado en una vista o puede hacerlo primero, donde el modelo de vista determina qué vista usar. Un marco de MVVM proporciona esta funcionalidad clave de alguna manera para conectar su contenedor DI a la creación de modelos de vista, pero también puede implementarlo usted mismo. Estoy un poco vago porque dependiendo de sus necesidades, esta funcionalidad puede volverse bastante compleja. Esta es una de las funciones principales que obtiene de un marco de MVVM, pero desarrollar la suya en una aplicación simple le dará una buena comprensión de lo que ofrecen los marcos de MVVM bajo el capó.

Al no poder declarar el DataContext en XAML, pierdes algo de soporte de tiempo de diseño. Si su modelo de vista contiene algunos datos, aparecerá durante el tiempo de diseño, lo que puede ser muy útil. Afortunadamente, puede usar atributos de tiempo de diseño también en WPF. Una forma de hacerlo es agregar los siguientes atributos al elemento o 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}" 

El tipo de modelo de vista debe tener dos constructores, el predeterminado para los datos de tiempo de diseño y otro para la dependency injection:

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

Al hacer esto, puede usar la dependency injection y conservar un buen soporte en tiempo de diseño.

Lo que estoy publicando aquí es una mejora a la respuesta de sondergard, porque lo que voy a contar no cabe en un comentario 🙂

De hecho, estoy presentando una solución ordenada, que evita la necesidad de un ServiceLocator y un contenedor para StandardKernel -Instance, que en la solución de sondergard se llama IocContainer . ¿Por qué? Como se mencionó, esos son anti-patrones.

Haciendo el StandardKernel disponible en todas partes

La magia de Key to Ninject es el StandardKernel -Instance que se necesita para usar el .Get() .

De forma alternativa al IocContainer de IocContainer , puede crear el StandardKernel dentro de App Class.

Simplemente elimine StartUpUri de su App.xaml

  ...  

Este es el código de la aplicación detrás de 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(); } } 

A partir de ahora, Ninject está vivo y listo para luchar 🙂

Inyectando su DataContext

Como Ninject está vivo, puede realizar todo tipo de inyecciones, por ejemplo, Inyección de Property Setter o la inyección de constructor más común.

Así es como se inyecta su ViewModel en el DataContext su Window

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

Por supuesto, también puedes Inyectar un IViewModel si haces los enlaces correctos, pero eso no forma parte de esta respuesta.

Accediendo al Kernel directamente

Si necesita llamar directamente a los Métodos en el Kernel (por ejemplo, .Get() -Método), puede dejar que el Kernel se inyecte.

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

Si necesita una instancia local del kernel, puede inyectarla como propiedad.

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

Aunque esto puede ser bastante útil, no te recomendaría que lo hicieras. Solo tenga en cuenta que los objetos inyectados de esta forma no estarán disponibles dentro del Constructor porque se inyectarán más tarde.

De acuerdo con este enlace , debe usar IKernel -Extension en lugar de inyectar IKernel (DI Container).

El enfoque recomendado para emplear un contenedor DI en un sistema de software es que la raíz de composición de la aplicación sea el único lugar donde se toca el contenedor directamente.

La forma en que se utilizará Ninject.Extensions.Factory también puede ser roja aquí .

Voy por un enfoque de “ver primero”, donde paso el modelo de vista al constructor de la vista (en su código subyacente), que se asigna al contexto de datos, por ejemplo

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

Esto reemplaza su enfoque basado en XAML.

Utilizo el marco de Prism para manejar la navegación: cuando algún código solicita que se muestre una vista en particular (“navegando” hacia ella), Prism resolverá esa vista (internamente, utilizando el marco DI de la aplicación); el marco DI resolverá a su vez cualquier dependencia que tenga la vista (el modelo de vista en mi ejemplo), luego resuelve sus dependencias, y así sucesivamente.

La elección del marco DI es bastante irrelevante, ya que todos hacen esencialmente lo mismo, es decir, registra una interfaz (o un tipo) junto con el tipo concreto que desea que el marco cree una instancia cuando encuentra una dependencia en esa interfaz. Para el registro utilizo Castle Windsor.

La navegación por prismas requiere de un tiempo para acostumbrarse, pero es bastante buena una vez que se familiariza con ella, lo que le permite componer su aplicación con diferentes vistas. Por ejemplo, puede crear una “región” de prismas en su ventana principal, y luego usar la navegación de Prisma para cambiar de una vista a otra dentro de esta región, por ejemplo, cuando el usuario selecciona elementos de menú o lo que sea.

Alternativamente, eche un vistazo a uno de los frameworks MVVM como MVVM Light. No tengo experiencia de estos, así que no puedo comentar cómo son.

Instalar MVVM Light.

Parte de la instalación es crear un localizador de modelos de vista. Esta es una clase que expone tus viewmodels como propiedades. El captador de estas propiedades puede ser instancias devueltas desde su motor IOC. Afortunadamente, MVVM light también incluye el marco SimpleIOC, pero puede conectar otros si lo desea.

Con el IOC simple, registra una implementación contra un tipo …

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

En este ejemplo, su modelo de vista se crea y pasa un objeto de proveedor de servicio según su constructor.

A continuación, crea una propiedad que devuelve una instancia de IOC.

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

La parte inteligente es que el localizador de modelos de vista se crea en app.xaml o equivalente como fuente de datos.

  

Ahora puede enlazar a su propiedad ‘MyViewModel’ para obtener su modelo de vista con un servicio inyectado.

Espero que ayude. Disculpas por las inexactitudes de los códigos, codificados desde la memoria en un iPad.

Use el Marco de Extensibilidad Administrada .

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

En general, lo que haría sería tener una clase estática y usar Factory Pattern para proporcionarle un contenedor global (en caché, natch).

En cuanto a cómo inyectar los modelos de vista, los inyecta de la misma manera que inyecta todo lo demás. Cree un constructor de importación (o coloque una statement de importación en una propiedad / campo) en el código subyacente del archivo XAML y dígale que importe el modelo de vista. A continuación, enlace el DataContext su Window a esa propiedad. Los objetos raíz que de hecho sacas del contenedor son objetos de Window compuestos. Simplemente agregue interfaces a las clases de ventana, y expórtelas, luego tome el catálogo como se muestra arriba (en App.xaml.cs … ese es el archivo WPF bootstrap).

Sugeriría usar ViewModel – Primer acercamiento https://github.com/Caliburn-Micro/Caliburn.Micro

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

use Castle Windsor como contenedor de IOC.

Todo sobre convenciones

Una de las principales características de Caliburn.Micro se manifiesta en su capacidad para eliminar la necesidad de código de placa de caldera actuando sobre una serie de convenciones. Algunas personas aman las convenciones y otras las odian. Es por eso que las convenciones de CM son totalmente personalizables e incluso se pueden desactivar por completo si no se desea. Si va a usar convenciones y dado que están activadas por defecto, es bueno saber cuáles son esas convenciones y cómo funcionan. Ese es el tema de este artículo. Ver resolución (ViewModel-First)

Lo esencial

La primera convención que es probable que encuentre al usar CM está relacionada con la resolución de la vista. Esta convención afecta a cualquier área ViewModel-First de su aplicación. En ViewModel-First, tenemos un ViewModel existente que debemos representar en la pantalla. Para hacer esto, CM usa un patrón de nomenclatura simple para encontrar un UserControl1 que debe vincular al ViewModel y mostrar. Entonces, ¿qué es ese patrón? Echemos un vistazo a ViewLocator.LocateForModelType para descubrir:

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

Vamos a ignorar la variable “contexto” al principio. Para derivar la vista, suponemos que está utilizando el texto “ViewModel” en el nombre de sus máquinas virtuales, por lo que simplemente lo cambiamos a “Ver” en cualquier lugar que lo encontremos eliminando la palabra “Modelo”. Esto tiene el efecto de cambiar ambos nombres de tipos y espacios de nombres. Entonces ViewModels.CustomerViewModel se convertiría en Views.CustomerView. O bien, si está organizando su aplicación por función: CustomerManagement.CustomerViewModel se convierte en CustomerManagement.CustomerView. Con suerte, eso es bastante directo. Una vez que tenemos el nombre, buscamos los tipos con ese nombre. Buscamos en cualquier conjunto que haya expuesto a CM como buscable a través de AssemblySource.Instance.2 Si encontramos el tipo, creamos una instancia (u obtenemos una del contenedor IoC si está registrada) y la devolvemos a la persona que llama. Si no encontramos el tipo, generamos una vista con un mensaje “no encontrado” apropiado.

Ahora, volvamos a ese valor de “contexto”. Así es como CM admite múltiples Vistas sobre el mismo Modelo de Vista. Si se proporciona un contexto (generalmente una cadena o una enumeración), hacemos una transformación adicional del nombre, en función de ese valor. Esta transformación supone efectivamente que tiene una carpeta (espacio de nombres) para las diferentes vistas eliminando la palabra “Ver” del final y añadiendo el contexto en su lugar. Entonces, dado el contexto de “Master”, nuestro ViewModels.CustomerViewModel se convertiría en Views.Customer.Master.

Elimina el uri de inicio de tu 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); } } 

Ahora puede usar su clase de IoC para construir las instancias.

MainWindowView.xaml.cs

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