c# - Cómo usar la inyección de dependencia con conductores en Caliburn.Micro
dependency-injection (3)
El enfoque más simple y directo sería seguir el principio de dependencia explícita.
Asumiendo
public MySecondViewModel(MyParamClass input) {
//do the work
}
Y que ella y sus dependencias están registradas en el contenedor,
simpleContainer.PerRequest<MyParamClass>();
simpleContainer.PerRequest<MySecondViewModel>();
MainViewModel
conductor de MainViewModel
puede depender de un delegado (fábrica) que puede usarse para resolver la dependencia cuando sea necesario.
public class MainViewModel : Conductor<object> {
//...
private readonly Func<MySecondViewModel> mySecondViewModelFactory;
public MyMainViewModel(IWindowManager windowManager, Func<MySecondViewModel> mySecondViewModelFactory) {
this.mySecondViewModelFactory = mySecondViewModelFactory;
//...do the init
}
public void ShowPageOne() {
var item = mySecondViewModelFactory(); //invoke factory
ActivateItem(item);
}
}
Si bien no está documentado adecuadamente, SimpleContainer
permite la inyección de delegados de fábrica ( Código fuente ) en forma de Func<TDependency>
para la resolución / instanciación diferida de dependencias inyectadas. Puede aprovechar esa característica para resolver sus dependencias solo cuando en realidad son necesarias.
A veces uso Caliburn.Micro para crear aplicaciones.
Usando el BootStrapper más simple, puedo usar el contenedor IoC (SimpleContainer) de esta manera:
private SimpleContainer _container = new SimpleContainer();
protected override object GetInstance(Type serviceType, string key) {
return _container.GetInstance(serviceType, key);
}
protected override IEnumerable<object> GetAllInstances(Type serviceType) {
return _container.GetAllInstances(serviceType);
}
protected override void BuildUp(object instance) {
_container.BuildUp(instance);
}
Así que en el método Configure
puedo agregar y registrar mis ViewModels de esta manera:
container.PerRequest<MyMainViewModel>();
El constructor de My ViewModel puede tener un parámetro que es inyectado por el contenedor IoC cuando se solicita:
public MyMainViewModel(IWindowManager windowManager)
{
//do the init
}
Funciona como se esperaba, cuando llamo a DisplayRootViewFor<MyMainViewModel>()
Pero, ¿qué sucede si tengo la intención de crear algo más de lógica y usar un Conductor ?
En los ejemplos, los autores utilizan una implementación simple y libre de IoC para "conveniencia":
Para mantener esta muestra lo más simple posible, ni siquiera estoy usando un contenedor IoC con el Bootstrapper. Veamos primero el ShellViewModel. Hereda de Conductor y se implementa de la siguiente manera:
public class ShellViewModel : Conductor<object> { public ShellViewModel() { ShowPageOne(); } public void ShowPageOne() { ActivateItem(new PageOneViewModel()); } public void ShowPageTwo() { ActivateItem(new PageTwoViewModel()); } }
Así que instancian los ViewModels, en lugar de solicitar una instancia del contenedor IoC .
¿Cuál sería el uso adecuado de la inyección de dependencia en este caso?
Tengo otro ViewModel que tiene un constructor como este:
public MySecondViewModel(MyParamClass input)
{
//do the work
}
¿Debo modificar el código de esta manera?
En el método de Configure
:
simpleContainer.PerRequest<MyParamClass>(); //How could it be different every time?
En el conductor:
public void ShowPageOne()
{
ActivateItem(IoC.Get<MySecondViewModel>());
}
Además, está esto permitido o viola las reglas de DI:
protected override object GetInstance(Type serviceType, string key)
{
if(serviceType==typeof(MySecondViewModel))
return new MySecondViewModel(new MyParamClass(2));
return _container.GetInstance(serviceType, key);
}
Puedo ver que al usar DI, los contenedores de IoC deben proporcionar los modelos de visualización y no deben crearse manualmente (sin mencionar el parámetro requerido, que está dentro del contenedor).
Entonces, ¿puede dar una pista sobre cómo implementar el patrón IoC con conductores?
La forma en que normalmente hago esto es presentar un Navigator
y acoplarlo con un shellView de singleton (que será nuestro conductor) y la instancia del container
IOC. Una simple interfaz de navegación podría parecer:
Implementación simple:
public interface INavigator
{
void Navigate<T>();
}
public class Navigator : INavigator
{
private ShellViewModel _shellview;
public Navigator(ShellViewModel shellview) //where ShellViewModel:IConductor
{
_shellview = shellview;
}
public void Navigate<T>()
{
//you can inject the IOC container or a wrapper for the same from constructor
//and use that to resolve the vm instead of this
var screen = IoC.Get<T>();
_shellview.ActivateItem(screen);
}
}
Para una alternativa más flexible, puede mejorar este patrón para introducir el concepto de una solicitud de navegación, encapsulando todos los detalles relacionados con la inicialización de la pantalla y la propia pantalla y activarlo según sea necesario.
Una pequeña implementación extendida
Para tal patrón, diseñe una NavigationRequest
como, por ejemplo,
public interface INavigationRequest<out T>
{
T Screen { get; }
void Go();
}
Actualice el INavigator
para devolver esta solicitud.
public interface INavigator
{
INavigationRequest<T> To<T>();
}
Proporcione un contrato para su ShellViewModel similar a
public interface IShell : IConductActiveItem
{
}
Implementar el INavigator
:
public class MyApplicationNavigator : INavigator
{
private readonly IShell _shell;
public MyApplicationNavigator(IShell shell)
{
_shell = shell;
}
public INavigationRequest<T> To<T>()
{
return new MyAppNavigationRequest<T>(() => IoC.Get<T>(), _shell);
}
/// <summary>
/// <see cref="MyApplicationNavigator"/> specific implementation of <see cref="INavigationRequest{T}"/>
/// </summary>
/// <typeparam name="T">Type of view model</typeparam>
private class MyAppNavigationRequest<T> : INavigationRequest<T>
{
private readonly Lazy<T> _viemodel;
private readonly IShell _shell;
public MyAppNavigationRequest(Func<T> viemodelFactory, IShell shell)
{
_viemodel = new Lazy<T>(viemodelFactory);
_shell = shell;
}
public T Screen { get { return _viemodel.Value; } }
public void Go()
{
_shell.ActivateItem(_viemodel.Value);
}
}
}
Una vez que esta infraestructura esté en su lugar, puede consumirla inyectando INavigator
en los modelos de vista según sea necesario.
Esta arquitectura básica se puede ampliar mediante métodos de extensión para proporcionar funciones de utilidad adicionales, suponiendo que desea pasar argumentos a los modelos de vista mientras navega hacia ellos. Puede introducir servicios adicionales de la siguiente manera,
/// <summary>
/// Defines a contract for View models that accept parameters
/// </summary>
/// <typeparam name="T">Type of argument expected</typeparam>
public interface IAcceptArguments<in T>
{
void Accept(T args);
}
Proporcionar métodos de utilidad para el mismo,
public static class NavigationExtensions
{
public static INavigationRequest<T> WithArguments<T, TArgs>(this INavigationRequest<T> request, TArgs args) where T : IAcceptArguments<TArgs>
{
return new NavigationRequestRequestWithArguments<T, TArgs>(request, args);
}
}
internal class NavigationRequestRequestWithArguments<T, TArgs> : INavigationRequest<T> where T : IAcceptArguments<TArgs>
{
private readonly INavigationRequest<T> _request;
private readonly TArgs _args;
public NavigationRequestRequestWithArguments(INavigationRequest<T> request, TArgs args)
{
_request = request;
_args = args;
}
public T Screen { get { return _request.Screen; } }
public void Go()
{
_request.Screen.Accept(_args);
_request.Go();
}
}
Uso:
Esto se puede consumir usando una api fluida concisa:
public void GoToProfile()
{
//Say, this.CurrentUser is UserProfile
//and UserDetailsViewModel implements IAcceptArguments<UserProfile>
_navigator.To<UserDetailsViewModel>().WithArguments(this.CurrentUser).Go();
}
Esto puede extenderse tanto como se requiera según sus necesidades. Las principales ventajas de una arquitectura como esta son,
- Está desacoplando la resolución, navegación e inicialización de los modelos de vista (pantallas) del solicitante (otros modelos de vista o servicios).
- Unidad comprobable, puede burlarse de todo lo que no concierne a sus modelos de visualización, la navegación se puede probar por separado.
- Extensible. Los requisitos de navegación adicionales, como la gestión del ciclo de vida, la navegación de ida y vuelta entre diferentes vistas, se pueden implementar fácilmente extendiendo el navegador.
- Adaptabilidad: se puede adaptar a diferentes
IoC
o incluso sin una, sin alterar ninguno de sus modelos de vista.
Solución
Creo que la mejor solución es pasar una fábrica que sepa cómo crear modelos de vista de mi hijo. Y el modelo de vista principal llamará a la fábrica.
Logros:
- Usted crea una instancia del modelo de vista infantil solo cuando es necesario (perezoso)
- Puede pasar parámetros desde el modelo de vista principal y / o desde la inyección
- Puede escribir pruebas unitarias para su modelo de vista principal con una fábrica simulada. Eso le permite probar que el modelo de vista principal creó sus modelos de vista secundarios sin crearlos realmente.
EDITAR : Gracias a la respuesta de @Nkosi, hay una forma sencilla de inyectar modelos de visualización perezosa (como de fábrica) con Caliburn.Micro :) Use mi respuesta con esta inyección para obtener mejores resultados.