Cómo hacer que crear modelos de vista en tiempo de ejecución sea menos doloroso

17

Pido disculpas por la larga pregunta, se lee un poco como una queja, ¡pero prometo que no lo es! He resumido mis preguntas a continuación

En el mundo MVC, las cosas son sencillas. El Modelo tiene estado, la Vista muestra el Modelo y el Controlador hace cosas con / con el Modelo (básicamente), un controlador no tiene estado. Para hacer cosas, el controlador tiene algunas dependencias en los servicios web, el repositorio, el lote. Cuando crea una instancia de un controlador, le importa suministrar esas dependencias, nada más. Cuando ejecuta una acción (método en el Controlador), usa esas dependencias para recuperar o actualizar el Modelo o llamar a algún otro servicio de dominio. Si hay algún contexto, digamos que si algún usuario quiere ver los detalles de un elemento en particular, pasa el Id de ese elemento como parámetro a la Acción. En ninguna parte del controlador hay alguna referencia a ningún estado. Hasta aquí todo bien.

Ingrese MVVM. Me encanta WPF, me encanta el enlace de datos. Me encantan los marcos que hacen que el enlace de datos a ViewModels sea aún más fácil (usando Caliburn Micro atm). Sin embargo, siento que las cosas son menos sencillas en este mundo. Vamos a hacer de nuevo el ejercicio: el modelo ha estado, la vista muestra el ViewModel, y el modelo de vista hace cosas para / con el Modelo (básicamente), un modelo de vista hace que el estado! (para aclarar, tal vez delega todas las propiedades a uno o más modelos, pero eso significa que deben tener una referencia al modelo de un modo u otro, que es el estado en sí mismo) Para hacerEl ViewModel tiene algunas dependencias en los servicios web, el repositorio, el lote. Cuando crea una instancia de un ViewModel, le importa suministrar esas dependencias, pero también el estado. Y esto, damas y caballeros, me molesta sin fin.

Siempre que necesite crear una instancia ProductDetailsViewModelde ProductSearchViewModel(de la cual llamó a la ProductSearchWebServiceque a su vez regresó IEnumerable<ProductDTO>, ¿todos siguen conmigo?), Puede hacer una de estas cosas:

  • llame new ProductDetailsViewModel(productDTO, _shoppingCartWebService /* dependcy */);, esto es malo, imagine 3 dependencias más, esto significa que también ProductSearchViewModelnecesita asumir esas dependencias. También cambiar el constructor es doloroso.
  • llame _myInjectedProductDetailsViewModelFactory.Create().Initialize(productDTO);, la fábrica es solo un Func, son generados fácilmente por la mayoría de los marcos de IoC. Creo que esto es malo porque los métodos Init son una abstracción permeable. Tampoco puede usar la palabra clave de solo lectura para los campos que se establecen en el método Init. Estoy seguro de que hay algunas razones más.
  • llame _myInjectedProductDetailsViewModelAbstractFactory.Create(productDTO);Entonces ... este es el patrón (fábrica abstracta) que generalmente se recomienda para este tipo de problema. Pensé que era genial, ya que satisface mis ansias de escritura estática, hasta que realmente comencé a usarlo. La cantidad de código repetitivo es demasiado (creo, aparte de los ridículos nombres de variables que uso). Por cada ViewModel que necesite parámetros de tiempo de ejecución, obtendrá dos archivos adicionales (interfaz de fábrica e implementación), y deberá escribir las dependencias que no sean de tiempo de ejecución como 4 veces adicionales. Y cada vez que cambian las dependencias, también puedes cambiarlo en la fábrica. Parece que ya ni siquiera uso un contenedor DI. (Creo que Castle Windsor tiene algún tipo de solución para esto [con sus propios inconvenientes, corrígeme si me equivoco]).
  • hacer algo con tipos anónimos o diccionario. Me gusta mi escritura estática.

Así que sí. La combinación de estado y comportamiento de esta manera crea un problema que no existe en absoluto en MVC. Y siento que actualmente no hay una solución realmente adecuada para este problema. Ahora me gustaría observar algunas cosas:

  • La gente realmente usa MVVM. Entonces, o no les importa todo lo anterior, o tienen alguna otra solución brillante.
  • No he encontrado un ejemplo en profundidad de MVVM con WPF. Por ejemplo, el proyecto de muestra NDDD me ayudó inmensamente a comprender algunos conceptos DDD. Realmente me gustaría que alguien pudiera señalarme en la dirección de algo similar para MVVM / WPF.
  • Tal vez estoy haciendo MVVM todo mal y debería poner mi diseño al revés. Quizás no debería tener este problema en absoluto. Bueno, sé que otras personas han hecho la misma pregunta, así que creo que no soy el único.

Para resumir

  • ¿Estoy en lo cierto al concluir que tener el ViewModel como un punto de integración tanto para el estado como para el comportamiento es la razón de algunas dificultades con el patrón MVVM en su conjunto?
  • ¿Usar el patrón de fábrica abstracto es la única / mejor manera de crear una instancia de ViewModel de forma estática?
  • ¿Hay algo como una implementación de referencia en profundidad disponible?
  • ¿Tener muchos ViewModels con estado / comportamiento es un olor de diseño?
dvdvorle
fuente
10
Esto es demasiado largo para leer, considere revisar, hay muchas cosas irrelevantes allí. Puede perder buenas respuestas porque la gente no se molestará en leer todo eso.
Yannis
Dijiste que amas Caliburn.Micro, ¿pero no sabes cómo este marco puede ayudar a crear instancias de nuevos modelos de vista? Mira algunos ejemplos de ello.
Eufórico el
@Euphoric ¿Podría ser un poco más específico? Google no parece ayudarme aquí. ¿Tengo algunas palabras clave que podría buscar?
dvdvorle el
3
Creo que estás simplificando un poco MVC. Claro que la Vista muestra el Modelo al principio, pero durante la operación está cambiando de estado. Este estado cambiante es, en mi opinión, un "Editar modelo". Es decir, una versión aplanada del Modelo con restricciones de consistencia reducidas. De hecho, lo que yo llamo un modelo de edición es el MVVM ViewModel. Mantiene el estado durante la transición, que anteriormente tenía la Vista en MVC, o se devolvió a una versión no comprometida del Modelo, donde no creo que pertenezca. Entonces tenías un estado "in flux" antes. Ahora todo está en ViewModel.
Scott Whitlock
@ScottWhitlock Estoy simplificando MVC de hecho. Pero no digo que esté mal que el estado "in flux" esté en el ViewModel, estoy diciendo que también el comportamiento abarrotado hace que sea más difícil inicializar el ViewModel a un estado utilizable, por ejemplo, otro ViewModel. Su "Editar modelo" en MVC no sabe cómo salvarse a sí mismo (no tiene un método de guardar). Pero el controlador sí lo sabe y tiene todas las dependencias necesarias para hacerlo.
dvdvorle el

Respuestas:

2

El problema de las dependencias al iniciar un nuevo modelo de vista se puede manejar con IOC.

public class MyCustomViewModel{
  private readonly IShoppingCartWebService _cartService;

  private readonly ITimeService _timeService;

  public ProductDTO ProductDTO { get; set; }

  public ProductDetailsViewModel(IShoppingCartWebService cartService, ITimeService timeService){
    _cartService = cartService;
    _timeService = timeService;
  }
}

Al configurar el contenedor ...

Container.Register<IShoppingCartWebService,ShoppingCartWebSerivce>().As.Singleton();
Container.Register<ITimeService,TimeService>().As.Singleton();
Container.Register<ProductDetailsViewModel>();

Cuando necesite su modelo de vista:

var viewmodel = Container.Resolve<ProductDetailsViewModel>();
viewmodel.ProductDTO = myProductDTO;

Cuando se utiliza un marco como Caliburn Micro, a menudo ya existe alguna forma de contenedor de COI.

SomeCompositionView view = new SomeCompositionView();
ISomeCompositionViewModel viewModel = IoC.Get<ISomeCompositionViewModel>();
ViewModelBinder.Bind(viewModel, view, null);
Miguel
fuente
1

Trabajo diariamente con ASP.NET MVC y he trabajado en un WPF durante más de un año y así es como lo veo:

MVC

Se supone que el controlador orquesta acciones (busque esto, agregue eso).

La vista es responsable de mostrar el modelo.

El modelo generalmente abarca datos (ej. UserId, FirstName), así como el estado (ej. Titles) y generalmente es específico de la vista.

MVVM

El modelo generalmente solo contiene datos (ej. UserId, FirstName) y generalmente se pasa

El modelo de vista abarca el comportamiento de la vista (métodos), sus datos (modelo) e interacciones (comandos), similar al patrón MVP activo en el que el presentador conoce el modelo. El modelo de vista es específico de vista (1 vista = 1 modelo de vista).

La vista es responsable de mostrar los datos y el enlace de datos al modelo de vista. Cuando se crea una vista, generalmente se crea su modelo de vista asociado.


Lo que debe recordar es que el patrón de presentación MVVM es específico de WPF / Silverlight debido a su naturaleza de enlace de datos.

La vista generalmente sabe con qué modelo de vista está asociada (o una abstracción de uno).

Le aconsejaría que trate el modelo de vista como un singleton, a pesar de que se instancia por vista. En otras palabras, debe ser capaz de crearlo a través de DI a través de un contenedor IOC y llamar a los métodos apropiados para decirlo; cargar su modelo basado en parámetros. Algo como esto:

public partial class EditUserView
{
    public EditUserView(IContainer container, int userId) : this() {
        var viewModel = container.Resolve<EditUserViewModel>();
        viewModel.LoadModel(userId);
        DataContext = viewModel;
    }
}

Como ejemplo en este caso, no crearía un modelo de vista específico para el usuario que se está actualizando; en cambio, el modelo contendría datos específicos del usuario que se cargan a través de alguna llamada en el modelo de vista.

Shelakel
fuente
Si mi nombre es "Peter" y mis títulos son {"Rev", "Dr"} *, ¿por qué considera los datos de nombre y el estado del título? ¿O puedes aclarar tu ejemplo? * no realmente
Pete Kirkham
@PeteKirkham: el ejemplo de 'títulos' al que me refería en el contexto de decir un cuadro combinado. En general, cuando envía información para que sea persistente, no enviará el estado (por ejemplo, una lista de estados / provincias / títulos) que se utilizó para hacer selecciones. Cualquier estado que valga la pena transferir con los datos (por ejemplo, el nombre de usuario en uso) debe verificarse en el punto de procesamiento porque el estado podría haberse quedado obsoleto (si estaba utilizando algún patrón asincrónico como la cola de mensajes).
Shelakel
Aunque han pasado dos años desde esta publicación, debo hacer un comentario a favor de los futuros espectadores: dos cosas me perturbaron con su respuesta. Una vista puede corresponder a un ViewModel, pero un ViewModel puede estar representado por varias vistas. En segundo lugar, lo que está describiendo es el antipatrón del Localizador de servicios. En mi humilde opinión, no debe resolver directamente los modelos de vista en todas partes. Para eso está la DI. Haga su resolución en menos puntos como pueda. Deje que Caliburn haga este trabajo por usted, por ejemplo.
Jony Adamit
1

Respuesta corta a sus preguntas:

  1. Sí Estado + Comportamiento conduce a esos problemas, pero esto es cierto para todos los OO. El verdadero culpable es el acoplamiento de ViewModels, que es una especie de violación de SRP.
  2. Mecanografiado estáticamente, probablemente. Pero debe reducir / eliminar su necesidad de crear instancias de ViewModels de otros ViewModels.
  3. No es que yo sepa.
  4. No, pero tener ViewModels con estado y comportamiento no relacionados (como algunas referencias de modelo y algunas referencias de ViewModel)

La versión larga:

Estamos enfrentando el mismo problema y encontramos algunas cosas que pueden ayudarlo. Aunque no conozco la solución "mágica", esas cosas alivian un poco el dolor.

  1. Implemente modelos vinculables de DTO para el seguimiento y la validación de cambios. Esos "Data" -ViewModels no deben depender de los servicios y no provienen del contenedor. Pueden ser simplemente "nuevos" editados, pasados ​​e incluso pueden derivarse del DTO. La conclusión es implementar un modelo específico para su aplicación (como MVC).

  2. Desacoplar sus ViewModels. Caliburn facilita el acoplamiento de los ViewModels juntos. Incluso lo sugiere a través de su modelo de pantalla / conductor. Pero este acoplamiento dificulta la prueba de unidad de ViewModels, crea muchas dependencias y lo más importante: impone la carga de administrar el ciclo de vida de ViewModel en sus ViewModels. Una forma de desacoplarlos es usar algo como un servicio de navegación o un controlador ViewModel. P.ej

    interfaz pública IShowViewModels {void Show (objeto inlineArgumentsAsAnonymousType, string regionId); }

Aún mejor es hacerlo mediante alguna forma de mensajería. Pero lo importante es no manejar el ciclo de vida de ViewModel desde otros ViewModels. En MVC los controladores no dependen el uno del otro, y en MVVM ViewModels no deberían depender el uno del otro. Intégrelos a través de otras formas.

  1. Use sus contenedores de tipo "en cadena" / características dinámicas. Aunque podría ser posible crear algo así INeedData<T1,T2,...>como forzar parámetros de creación de tipo seguro, no vale la pena. Tampoco vale la pena crear fábricas para cada tipo de ViewModel. La mayoría de los contenedores IoC brindan soluciones a esto. Obtendrá errores en tiempo de ejecución, pero la desconexión y la capacidad de prueba de la unidad lo valen. Todavía realiza algún tipo de prueba de integración y esos errores se detectan fácilmente.
sanosdole
fuente
0

La forma en que generalmente hago esto (usando PRISM) es que cada ensamblaje contiene un módulo de inicialización de contenedor, donde todas las interfaces e instancias se registran en el inicio.

private void RegisterResources()
{
    Container.RegisterType<IDataService, DataService>();
    Container.RegisterType<IProductSearchViewModel, ProductSearchViewModel>();
    Container.RegisterType<IProductDetailsViewModel, ProductDetailsViewModel>();
}

Y dadas sus clases de ejemplo, se implementaría de esta manera, con el contenedor pasando completamente. De esta manera, cualquier dependencia nueva se puede agregar fácilmente ya que ya tiene acceso al contenedor.

/// <summary>
/// IDataService Interface
/// </summary>
public interface IDataService
{
    DataTable GetSomeData();
}

public class DataService : IDataService
{
    public DataTable GetSomeData()
    {
        MessageBox.Show("This is a call to the GetSomeData() method.");

        var someData = new DataTable("SomeData");
        return someData;
    }
}

public interface IProductSearchViewModel
{
}

public class ProductSearchViewModel : IProductSearchViewModel
{
    private readonly IUnityContainer _container;

    /// <summary>
    /// This will get resolved if it's been added to the container.
    /// Or alternately you could use constructor resolution. 
    /// </summary>
    [Dependency]
    public IDataService DataService { get; set; }

    public ProductSearchViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void SearchAndDisplay()
    {
        DataTable results = DataService.GetSomeData();

        var detailsViewModel = _container.Resolve<IProductDetailsViewModel>();
        detailsViewModel.DisplaySomeDataInView(results);

        // Create the view, usually resolve using region manager etc.
        var detailsView = new DetailsView() { DataContext = detailsViewModel };
    }
}

public interface IProductDetailsViewModel
{
    void DisplaySomeDataInView(DataTable dataTable);
}

public class ProductDetailsViewModel : IProductDetailsViewModel
{
    private readonly IUnityContainer _container;

    public ProductDetailsViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void DisplaySomeDataInView(DataTable dataTable)
    {
    }
}

Es bastante común tener una clase ViewModelBase, de la que derivan todos sus modelos de vista, que contiene una referencia al contenedor. Siempre que tenga el hábito de resolver todos los modelos de vista en lugar de new()'ingellos, debería hacer que toda resolución de dependencia sea mucho más simple.

Martin Cooper
fuente
0

A veces es bueno ir a la definición más simple en lugar de un ejemplo completo: http://en.wikipedia.org/wiki/Model_View_ViewModel tal vez leer el ejemplo ZK Java es más esclarecedor que el C # one.

Otras veces escucha tu instinto ...

¿Tener muchos ViewModels con estado / comportamiento es un olor de diseño?

¿Sus modelos son asignaciones de objetos por tabla? Quizás un ORM ayudaría a mapear objetos de dominio mientras maneja el negocio o actualiza varias tablas.

Gerry King
fuente