Estoy iniciando una nueva aplicación de escritorio y quiero construirla usando MVVM y WPF.
También tengo la intención de utilizar 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 empresarial ...
public class SomeViewModel
{
private IStorage _storage;
public SomeViewModel(IStorage storage){
_storage = storage;
}
}
Con esto puedo escribir fácilmente pruebas unitarias para asegurarme de que funcionan correctamente, usando simulacros, etc.
El problema es a la hora de utilizarlo en la aplicación real. Sé que debo tener un contenedor de IoC que vincule una implementación predeterminada para la IStorage
interfaz, pero ¿cómo lo haría?
Por ejemplo, ¿cómo sería si tuviera el siguiente xaml:
<Window
... xmlns definitions ...
>
<Window.DataContext>
<local:SomeViewModel />
</Window.DataContext>
</Window>
¿Cómo puedo 'decirle' correctamente a WPF que inyecte dependencias en ese caso?
Además, supongamos que necesito una instancia de SomeViewModel
mi código C #, ¿cómo debo hacerlo?
Siento que estoy completamente perdido en esto, agradecería cualquier ejemplo u orientación de 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 / listo para usar, hágamelo saber.
fuente
Respuestas:
He estado usando Ninject y descubrí que es un placer trabajar con él. Todo está configurado en código, la sintaxis es bastante sencilla y tiene una buena documentación (y muchas respuestas sobre SO).
Entonces, básicamente, es así:
Cree el modelo de vista y tome la
IStorage
interfaz como parámetro de constructor:Cree una
ViewModelLocator
propiedad con una obtención para el modelo de vista, que carga el modelo de vista desde Ninject:Convierta el
ViewModelLocator
recurso en una aplicación amplia en App.xaml:Enlace el
DataContext
deUserControl
a la propiedad correspondiente en ViewModelLocator.Cree una clase heredando NinjectModule, que configurará los enlaces necesarios (
IStorage
y el modelo de vista):Inicialice el kernel de IoC al inicio de la aplicación con los módulos Ninject necesarios (el de arriba por ahora):
He usado una
IocKernel
clase estática para contener la instancia de toda la aplicación del kernel de IoC, por lo que puedo acceder fácilmente cuando sea necesario:Esta solución hace uso de un static
ServiceLocator
(theIocKernel
), que generalmente se considera un anti-patró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 interfaz de usuario, ya que deben tener un constructor sin parámetros y, de todos modos, no puede controlar la instanciación, por lo que no puede inyectar la VM. Al menos de esta manera, le permite probar la máquina virtual de forma aislada, que es donde está toda la lógica empresarial.Si alguien tiene una mejor manera, por favor comparta.
EDITAR: Lucky Likey proporcionó una respuesta para deshacerse del localizador de servicios estáticos, al permitir que Ninject instancia clases de UI. Los detalles de la respuesta se pueden ver aquí.
fuente
DataContext="{Binding [...]}"
. Esto hace que VS-Designer ejecute todo el código de programa en el constructor de ViewModel. En mi caso, la ventana se está ejecutando y bloquea modalmente cualquier interacción con VS. Quizás uno debería modificar el ViewModelLocator para no ubicar los ViewModels "reales" en tiempo de diseño. - Otra solución es "Desactivar el código del proyecto", que también evitará que se muestre todo lo demás. Quizás ya hayas encontrado una buena solución para esto. En este caso, le agradecería que lo mostrara.En su pregunta, establece el valor de la
DataContext
propiedad de la vista en XAML. Esto requiere que su modelo de vista tenga un constructor predeterminado. Sin embargo, como ha señalado, esto no funciona bien con la inyección de dependencias donde desea inyectar dependencias en el constructor.Por lo
DataContext
tanto, no puede establecer la propiedad en XAML . En su lugar, tiene 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 (tendrá que eliminar la
StartupUri
propiedad delApp.xaml
archivo):Esto se basa en un gráfico de objeto de modelos de vista arraigados en el,
RootViewModel
pero puede inyectar algunas fábricas de modelos de vista en modelos de vista principales, lo que les permite crear nuevos modelos de vista secundarios para que el gráfico de objetos no tenga que ser arreglado. Con suerte, esto también responde a su pregunta, supongamos que necesito una instancia deSomeViewModel
mics
código, ¿cómo debo hacerlo?Si su aplicación es de naturaleza más dinámica y quizás se basa en la navegación, tendrá que conectarse al código que realiza la navegación. Cada vez que navega a una nueva vista, necesita crear un modelo de vista (desde el contenedor DI), la vista en sí y establecer
DataContext
la vista en el modelo de vista. Puede hacer esta vista primero donde elige un modelo de vista basado en una vista o puede hacerlo primero el modelo de vistadonde el modelo de vista determina qué vista usar. Un marco MVVM proporciona esta funcionalidad clave con alguna forma de conectar su contenedor DI en la creación de modelos de vista, pero también puede implementarlo usted mismo. Soy un poco vago aquí porque, dependiendo de sus necesidades, esta funcionalidad puede volverse bastante compleja. Esta es una de las funciones principales que obtiene de un marco MVVM, pero implementar la suya propia en una aplicación simple le dará una buena comprensión de lo que proporcionan los marcos MVVM bajo el capó.Al no poder declarar
DataContext
en XAML, pierde algo de compatibilidad en 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, también puede usar atributos en tiempo de diseño en WPF. Una forma de hacerlo es agregar los siguientes atributos al<Window>
elemento o<UserControl>
en XAML:El tipo de modelo de vista debe tener dos constructores, el predeterminado para datos en tiempo de diseño y otro para la inyección de dependencia:
Al hacer esto, puede usar la inyección de dependencia y mantener un buen soporte en tiempo de diseño.
fuente
Lo que estoy publicando aquí es una mejora de 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 la
StandardKernel
-Instance, que en sondergard's Solution se llamaIocContainer
. ¿Por qué? Como se mencionó, esos son anti-patrones.Haciendo lo
StandardKernel
disponible en todas partesLa clave de la magia de Ninject es la
StandardKernel
-Instancia que se necesita para usar el.Get<T>()
-Método.Alternativamente a sondergard's
IocContainer
, puede crear elStandardKernel
interior deApp
-Class.Simplemente elimine StartUpUri de su App.xaml
Este es el CodeBehind de la aplicación dentro de App.xaml.cs
A partir de ahora, Ninject está vivo y listo para luchar :)
Inyectando tu
DataContext
Como Ninject está vivo, puede realizar todo tipo de inyecciones, por ejemplo, Property Setter Injection o la más común de Constructor Injection .
Esta es la forma en que se inyecte el modelo de vista en sus
Window
'sDataContext
Por supuesto, también puede inyectar un
IViewModel
si realiza las vinculaciones correctas, pero eso no es parte de esta respuesta.Accediendo al Kernel directamente
Si necesita llamar a Methods en el Kernel directamente (por ejemplo,
.Get<T>()
-Method), puede dejar que el Kernel se inyecte a sí mismo.Si necesita una instancia local del Kernel, puede inyectarla como Propiedad.
Aunque esto puede ser bastante útil, no te recomendaría que lo hicieras. Solo tenga en cuenta que los objetos inyectados de esta manera, no estarán disponibles dentro del Constructor, porque se inyecta más tarde.
De acuerdo con este enlace , debe usar la extensión de fábrica en lugar de inyectar el
IKernel
(Contenedor DI).Cómo el Ninject.Extensions.Factory se va a utilizar se puede también leer aquí .
fuente
Ninject.Extensions.Factory
esto, indíquelo aquí en los comentarios y agregaré más información.DependencyProperty
campo de respaldo como sus métodos Get y Set.Opto por un enfoque de "vista 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
Esto reemplaza su enfoque basado en XAML.
Utilizo el marco 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, usando el marco DI de la aplicación); el marco de DI, a su vez, resolverá cualquier dependencia que tenga la vista (el modelo de vista en mi ejemplo), luego resolverá 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 instancia cuando encuentre una dependencia en esa interfaz. Para que conste, utilizo Castle Windsor.
La navegación por prisma requiere algo de tiempo para acostumbrarse, pero es bastante buena una vez que la entiendes, lo que te permite componer tu aplicación usando diferentes vistas. Por ejemplo, puede crear una "región" de Prisma en su ventana principal, luego, utilizando la navegación de Prisma, cambiaría de una vista a otra dentro de esta región, por ejemplo, cuando el usuario selecciona elementos del menú o lo que sea.
Alternativamente, eche un vistazo a uno de los marcos MVVM como MVVM Light. No tengo experiencia con estos, así que no puedo comentar sobre cómo se usan.
fuente
Instale MVVM Light.
Parte de la instalación consiste en crear un localizador de modelos de vista. Esta es una clase que expone sus modelos de vista como propiedades. El getter de estas propiedades luego puede devolver instancias de su motor IOC. Afortunadamente, MVVM light también incluye el marco SimpleIOC, pero puede conectar otros si lo desea.
Con IOC simple, registra una implementación contra un tipo ...
En este ejemplo, se crea su modelo de vista y se le pasa un objeto de proveedor de servicios según su constructor.
Luego, crea una propiedad que devuelve una instancia de IOC.
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 cualquier inexactitud en el código, codificado desde la memoria en un iPad.
fuente
GetInstance
oresolve
fuera del programa de arranque de la aplicación. ¡Ese es el punto de DI!Estuche Canonic DryIoc
Respondiendo a una publicación anterior, pero haciendo esto
DryIoc
y haciendo lo que creo que es un buen uso de DI e interfaces (uso mínimo de clases concretas).App.xaml
, y allí le contamos cuál es la vista inicial a utilizar; lo hacemos con código detrás en lugar del xaml predeterminado:StartupUri="MainWindow.xaml"
en App.xamlen codebehind (App.xaml.cs) agregue esto
override OnStartup
:ese es el punto de inicio; también es el único lugar al que se
resolve
debe llamar.la raíz de la configuración (según el libro de Mark Seeman Inyección de dependencia en .NET; el único lugar donde se deben mencionar las clases concretas) estará en el mismo código subyacente, en el constructor:
Comentarios y algunos detalles más
MainWindow
;El constructor ViewModel con DI:
Constructor predeterminado de ViewModel para el diseño:
El código detrás de la vista:
y lo que se necesita en la vista (MainWindow.xaml) para obtener una instancia de diseño con ViewModel:
Conclusión
Por lo tanto, obtuvimos una implementación muy limpia y mínima de una aplicación WPF con un contenedor DryIoc y DI, al tiempo que mantuvimos posibles las instancias de diseño de vistas y modelos de vista.
fuente
Utilice el marco de extensibilidad administrado .
En general, lo que haría es tener una clase estática y usar el patrón de fábrica 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 declaración 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, se unen a su
Window
'sDataContext
de esa propiedad. Los objetos raíz que usted mismo saca del contenedor suelen serWindow
objetos compuestos . Simplemente agregue interfaces a las clases de ventana y expórtelas, luego tómelas del catálogo como se indicó anteriormente (en App.xaml.cs ... ese es el archivo de arranque de WPF).fuente
new
.Sugeriría usar ViewModel - Primer enfoque https://github.com/Caliburn-Micro/Caliburn.Micro
ver: https://caliburnmicro.codeplex.com/wikipage?title=All%20About%20Conventions
utilizar
Castle Windsor
como contenedor IOC.Todo sobre las convenciones
Una de las principales características de Caliburn.Micro se manifiesta en su capacidad para eliminar la necesidad de un 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 utilizar convenciones, y dado que están activadas de forma predeterminada, 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 probablemente encontrará al utilizar CM está relacionada con la resolución de la vista. Esta convención afecta a cualquier área de ViewModel-First de su aplicación. En ViewModel-First, tenemos un ViewModel existente que necesitamos renderizar en la pantalla. Para hacer esto, CM usa un patrón de nomenclatura simple para encontrar un UserControl1 que debería enlazar con ViewModel y mostrar. Entonces, ¿cuál es ese patrón? Echemos un vistazo a ViewLocator.LocateForModelType para descubrir:
Ignoremos la variable de "contexto" al principio. Para derivar la vista, asumimos que está utilizando el texto "ViewModel" en el nombre de sus VM, por lo que simplemente lo cambiamos a "Ver" en todos los lugares donde lo encontremos eliminando la palabra "Modelo". Esto tiene el efecto de cambiar tanto los nombres de tipos como los espacios de nombres. Entonces ViewModels.CustomerViewModel se convertiría en Views.CustomerView. O si está organizando su aplicación por función: CustomerManagement.CustomerViewModel se convierte en CustomerManagement.CustomerView. Con suerte, eso es bastante sencillo. Una vez que tenemos el nombre, buscamos tipos con ese nombre. Buscamos cualquier ensamblado 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 de IoC si está registrado) y se la devolvemos al llamador. Si no encontramos el tipo,
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 (normalmente una cadena o una enumeración), hacemos una transformación adicional del nombre, en función de ese valor. Esta transformación asume efectivamente que tiene una carpeta (espacio de nombres) para las diferentes vistas al eliminar la palabra "Vista" del final y agregar el contexto en su lugar. Entonces, dado un contexto de "Maestro", nuestros ViewModels.CustomerViewModel se convertirían en Views.Customer.Master.
fuente
Elimina la uri de inicio de tu app.xaml.
App.xaml.cs
Ahora puede usar su clase de IoC para construir las instancias.
MainWindowView.xaml.cs
fuente
GetInstance
deresolve
app.xaml.cs externo, está perdiendo el punto de DI. Además, mencionar la vista xaml en el código subyacente de la vista es algo complicado. Simplemente llame a la vista en c # puro y haga esto con el contenedor.