¿Cómo mapear el modelo de vista de nuevo al modelo de dominio en una acción POST?

87

Cada artículo que se encuentra en Internet sobre el uso de ViewModels y el uso de Automapper proporciona las pautas del mapeo de dirección "Controlador -> Vista". Toma un modelo de dominio junto con todas las listas de selección en un ViewModel especializado y lo pasa a la vista. Eso es claro y está bien.
La vista tiene una forma y finalmente estamos en la acción POST. Aquí todos los Model Binders entran en escena junto con [obviamente] otro View Model que está [obviamente] relacionado con el ViewModel original al menos en la parte de las convenciones de nomenclatura por motivos de vinculación y validación.

¿Cómo se asigna a su modelo de dominio?

Que sea una acción de inserción, podríamos usar el mismo Automapper. Pero, ¿y si fuera una acción de actualización? Tenemos que recuperar nuestra entidad de dominio del repositorio, actualizar sus propiedades de acuerdo con los valores en ViewModel y guardar en el repositorio.

ADENDA 1 (9 de febrero de 2010): A veces, asignar las propiedades del Modelo no es suficiente. Debería tomarse alguna acción contra el Modelo de dominio de acuerdo con los valores de Modelo de vista. Es decir, algunos métodos deben llamarse en el modelo de dominio. Probablemente, debería haber una especie de capa de servicio de aplicación que se encuentre entre el controlador y el dominio para procesar los modelos de vista ...


¿Cómo organizar este código y dónde colocarlo para lograr los siguientes objetivos?

  • mantener los controladores delgados
  • honrar la práctica de SoC
  • seguir los principios del diseño basado en dominios
  • estar seco
  • continuará ...
Anthony Serdyukov
fuente

Respuestas:

37

Utilizo una interfaz IBuilder y la implemento usando ValueInjecter

public interface IBuilder<TEntity, TViewModel>
{
      TEntity BuildEntity(TViewModel viewModel);
      TViewModel BuildViewModel(TEntity entity);
      TViewModel RebuildViewModel(TViewModel viewModel); 
}

... (implementación) RebuildViewModel solo llamaBuildViewModel(BuilEntity(viewModel))

[HttpPost]
public ActionResult Update(ViewModel model)
{
   if(!ModelState.IsValid)
    {
       return View(builder.RebuildViewModel(model);
    }

   service.SaveOrUpdate(builder.BuildEntity(model));
   return RedirectToAction("Index");
}

Por cierto, no escribo ViewModel, escribo Input porque es mucho más corto, pero eso no es realmente importante
espero que ayude

Actualización: estoy usando este enfoque ahora en la aplicación de demostración ProDinner ASP.net MVC , ahora se llama IMapper, también se proporciona un pdf donde se explica este enfoque en detalle

Omu
fuente
Me gusta este enfoque. Sin embargo, una cosa que no tengo clara es la implementación de IBuilder, especialmente a la luz de una aplicación por niveles. Por ejemplo, mi ViewModel tiene 3 SelectLists. ¿Cómo recupera la implementación del constructor los valores de la lista de selección del repositorio?
Matt Murrell
@Matt Murrell mira prodinner.codeplex.com Hago esto allí, y lo llamo IMapper allí en lugar de IBuilder
Omu
6
Me gusta este enfoque, implementé una muestra aquí: gist.github.com/2379583
Paul Stovell
En mi opinión, no cumple con el enfoque del modelo de dominio. Parece un enfoque CRUD para requisitos poco claros. ¿No deberíamos usar Factories (DDD) y métodos relacionados en Domain Model para transmitir alguna acción razonable? De esta manera, será mejor que carguemos una entidad desde la base de datos y la actualicemos según sea necesario, ¿verdad? Entonces parece que no es del todo correcto.
Artyom
7

Se pueden utilizar herramientas como AutoMapper para actualizar un objeto existente con datos del objeto de origen. La acción del controlador para la actualización podría verse así:

[HttpPost]
public ActionResult Update(MyViewModel viewModel)
{
    MyDataModel dataModel = this.DataRepository.GetMyData(viewModel.Id);
    Mapper<MyViewModel, MyDataModel>(viewModel, dataModel);
    this.Repostitory.SaveMyData(dataModel);
    return View(viewModel);
}

Aparte de lo que se ve en el fragmento de arriba:

  • Los datos POST para ver el modelo + la validación se realiza en ModelBinder (se pueden ampliar con enlaces personalizados)
  • El manejo de errores (es decir, la captura de excepciones de acceso a datos por parte del repositorio) se puede realizar mediante el filtro [HandleError]

La acción del controlador es bastante delgada y las preocupaciones están separadas: los problemas de mapeo se abordan en la configuración de AutoMapper, la validación la realiza ModelBinder y el acceso a los datos a través del repositorio.

PanJanek
fuente
6
No estoy seguro de que Automapper sea útil aquí, ya que no puede revertir el aplanamiento. Después de todo, el modelo de dominio no es un DTO simple como el modelo de vista, por lo que puede que no sea suficiente asignarle algunas propiedades. Probablemente, se deberían realizar algunas acciones contra el modelo de dominio de acuerdo con el contenido del modelo de vista. Sin embargo, +1 por compartir un enfoque bastante bueno.
Anthony Serdyukov
@Anton ValueInjecter puede revertir el aplanamiento;)
Omu
con este enfoque, no mantiene el controlador delgado, viola SoC y DRY ... como Omu mencionó, debe tener una capa separada que se preocupe por el mapeo.
Rookian
5

Me gustaría decir que reutiliza el término ViewModel para ambas direcciones de la interacción del cliente. Si ha leído suficiente código ASP.NET MVC en la naturaleza, probablemente haya visto la distinción entre un ViewModel y un EditModel. Pienso que es importante.

Un ViewModel representa toda la información necesaria para representar una vista. Esto podría incluir datos que se representan en lugares estáticos no interactivos y también datos puramente para realizar una verificación para decidir qué representar exactamente. Una acción Controller GET generalmente es responsable de empaquetar el ViewModel para su Vista.

Un modelo de edición (o quizás un modelo de acción) representa los datos necesarios para realizar la acción que el usuario deseaba realizar para ese POST. Entonces, un EditModel realmente está tratando de describir una acción. Esto probablemente excluirá algunos datos del ViewModel y, aunque está relacionado, creo que es importante darse cuenta de que son realmente diferentes.

Una idea

Dicho esto, podría tener muy fácilmente una configuración de AutoMapper para ir desde Modelo -> ViewModel y una diferente para ir desde EditModel -> Modelo. Luego, las diferentes acciones del controlador solo necesitan usar AutoMapper. Demonios, el EditModel podría tener funciones para validar sus propiedades contra el modelo y aplicar esos valores al modelo en sí. No está haciendo nada más y tiene ModelBinders en MVC para asignar la Solicitud al EditModel de todos modos.

Otra idea

Más allá de eso, algo en lo que he estado pensando recientemente y que funciona con la idea de un ActionModel es que lo que el cliente te está enviando es en realidad la descripción de varias acciones que realizó el usuario y no solo una gran cantidad de datos. Esto ciertamente requeriría algo de Javascript en el lado del cliente para administrar, pero creo que la idea es intrigante.

Básicamente, a medida que el usuario realiza acciones en la pantalla que le ha presentado, Javascript comenzaría a crear una lista de objetos de acción. Un ejemplo es posiblemente el usuario está en la pantalla de información de un empleado. Actualizan el apellido y agregan una nueva dirección porque el empleado se ha casado recientemente. Bajo las sábanas, esto produce un ChangeEmployeeNamey un AddEmployeeMailingAddressobjetos en una lista. El usuario hace clic en 'Guardar' para confirmar los cambios y usted envía la lista de dos objetos, cada uno de los cuales contiene solo la información necesaria para realizar cada acción.

Necesitaría un ModelBinder más inteligente que el predeterminado, pero un buen serializador JSON debería poder encargarse de la asignación de los objetos de acción del lado del cliente a los del lado del servidor. Los del lado del servidor (si se encuentra en un entorno de 2 niveles) podrían tener fácilmente métodos que completen la acción en el modelo con el que trabajan. Entonces, la acción del controlador termina obteniendo un Id para que la instancia del modelo extraiga y una lista de acciones para realizar en ella. O las acciones tienen la identificación para mantenerlas muy separadas.

Entonces, tal vez algo como esto se realice en el lado del servidor:

public interface IUserAction<TModel>
{
     long ModelId { get; set; }
     IEnumerable<string> Validate(TModel model);
     void Complete(TModel model);
}

[Transaction] //just assuming some sort of 2-tier with transactions handled by filter
public ActionResult Save(IEnumerable<IUserAction<Employee>> actions)
{
     var errors = new List<string>();
     foreach( var action in actions ) 
     {
         // relying on ORM's identity map to prevent multiple database hits
         var employee = _employeeRepository.Get(action.ModelId);
         errors.AddRange(action.Validate(employee));
     }

     // handle error cases possibly rendering view with them

     foreach( var action in editModel.UserActions )
     {
         var employee = _employeeRepository.Get(action.ModelId);
         action.Complete(employee);
         // against relying on ORMs ability to properly generate SQL and batch changes
         _employeeRepository.Update(employee);
     }

     // render the success view
}

Eso realmente hace que la acción de devolución sea bastante genérica, ya que confía en su ModelBinder para obtener la instancia de IUserAction correcta y su instancia de IUserAction para realizar la lógica correcta en sí misma o (más probablemente) llamar al Modelo con la información.

Si estuviera en un entorno de 3 niveles, IUserAction podría convertirse en simples DTO para disparar a través del límite y realizar con un método similar en la capa de la aplicación. Dependiendo de cómo haga esa capa, podría dividirse muy fácilmente y aún permanecer en una transacción (lo que me viene a la mente es la solicitud / respuesta de Agatha y aprovechar el mapa de identidad de DI y NHibernate).

De todos modos, estoy seguro de que no es una idea perfecta, requeriría algo de JS en el lado del cliente para administrar, y aún no he podido hacer un proyecto para ver cómo se desarrolla, pero la publicación estaba tratando de pensar en cómo ir y volver, así que pensé que daría mis pensamientos. Espero que sea de ayuda y me encantaría conocer otras formas de gestionar las interacciones.

Sean Copenhaver
fuente
Interesante. Con respecto a la distinción entre ViewModel y EditModel ... ¿está sugiriendo que para una función de edición usaría un ViewModel para crear el formulario y luego se uniría a un EditModel cuando el usuario lo publicó? Si es así, ¿cómo manejaría las situaciones en las que necesitaría volver a publicar el formulario debido a errores de validación (por ejemplo, cuando ViewModel contenía elementos para completar un menú desplegable)? ¿Incluiría también los elementos desplegables en el modelo de edición? En cuyo caso, ¿cuál sería la diferencia entre los dos?
UpTheCreek
Supongo que su preocupación es que si uso un EditModel y hay un error, entonces tengo que reconstruir mi ViewModel, lo que podría ser muy costoso. Yo diría que simplemente reconstruya el ViewModel y asegúrese de que tenga un lugar para colocar los mensajes de notificación del usuario (probablemente tanto positivos como negativos, como errores de validación). Si resulta ser un problema de rendimiento, siempre puede almacenar en caché el ViewModel hasta que finalice la próxima solicitud de esa sesión (probablemente sea la publicación del EditModel).
Sean Copenhaver