2009-09-23 18 views
12

Para aquellos que crean ViewModels (para uso de vistas tipadas) en ASP.NET MVC, ¿prefieres recuperar los datos de un servicio/repositorio desde el ViewModel o las clases de controlador?Obteniendo datos dentro de una clase ASP.NET MVC ViewModel?

Por ejemplo, empezamos por tener ViewModels esencialmente siendo dtos y permitiendo que nuestros controladores para ir a buscar los datos (ejemplo groseramente simplificada asume que el usuario sólo puede cambiar el nombre del empleado):

public class EmployeeViewModel 
{ 
    public String Name; //posted back 
    public int Num; //posted back 
    public IEnumerable<Dependent> Dependents; //static 
    public IEnumerable<Spouse> Spouses; //static 
} 

public class EmployeeController() 
{ 
    ... 
    public ActionResult Employee(int empNum) 
    { 
     Models.EmployeeViewModel model = new Models.EmployeeViewModel(); 
     model.Name = _empSvc.FetchEmployee(empNum).Name; 
     model.Num = empNum; 
     model.Dependents = _peopleSvc.FetchDependentsForView(empNum); 
     model.Spouses = _peopleSvc.FetchDependentsForView(empNum); 
     return View(model); 
    } 

    [AcceptVerbs(HttpVerbs.Post)] 
    public ActionResult Employee(Models.EmployeeViewModel model) 
    { 
     if (!_empSvc.ValidateAndSaveName(model.Num, model.Name)) 
     { 
      model.Dependents = _peopleSvc.FetchDependentsForView(model.Num); 
      model.Spouses = _peopleSvc.FetchDependentsForView(model.Num); 
      return View(model); 
     } 
     this.RedirectToAction(c => c.Index()); 
    } 
} 
Este

todo parecía estar bien hasta comenzamos a crear vistas grandes (más de 40 campos) con muchos menús desplegables y tal. Como las pantallas tendrían una acción GET y POST (con POST devolviendo una vista si había un error de validación), estaríamos duplicando el código y haciendo ViewModels más grande de lo que probablemente debería ser.

Estoy pensando que la alternativa sería Obtener datos a través del Servicio dentro de ViewModel. Me preocupa que tengamos algunos datos poblados del ViewModel y algunos del Controlador (por ejemplo, en el ejemplo anterior, Nombre se llenaría del Controlador ya que es un valor publicado, mientras que Dependientes y Cónyuges se llenarían a través de algunos tipo de función GetStaticData() en ViewModel).

¿Pensamientos?

+11

IEnumerable ? ¿Qué pasa con el patrón polígamo? :-D –

Respuesta

7

Tuve el mismo problema. Empecé a crear clases para cada acción cuando el código era demasiado grande para los métodos de acción. Sí, tendrá cierta recuperación de datos en clases y algunos en los métodos de controlador. La alternativa es tener toda la recuperación de datos en clases, pero la mitad de las clases que realmente no necesitarás, se crearán por coherencia o tendrán toda la recuperación de datos en los métodos del controlador, pero de nuevo, algunos de esos métodos ser demasiado complejo y necesario haber sido abstraído en clases ... así que elige tu veneno. Prefiero tener un poco de inconsistencia y tener la solución adecuada para el trabajo.

En cuanto a poner comportamiento en el modelo de vista, no es así, el objetivo de ViewModel es ser una clase delgada para establecer y extraer valores de la vista.

Ha habido casos en los que he puesto métodos de conversión en ViewModel. Por ejemplo, necesito convertir ViewModel a la entidad correspondiente o necesito cargar ViewModel con datos de la Entity.

Para responder a su pregunta, prefiero recuperar los datos de en el controlador/métodos de acción.

Normalmente, con DropDowns, creo un servicio desplegable. DropDowns tiende a ser la misma información que abarca las vistas. Con los menús desplegables en un servicio, puedo usarlos en otras vistas y/o guardarlos en caché.

Según el diseño, más de 40 campos podrían crear una vista desordenada. Dependiendo del tipo de datos, trataría de abarcar esos muchos campos en múltiples vistas con algún tipo de interfaz con pestañas o asistente.

+0

Gracias por los comentarios. Esto es para una aplicación de cliente segura, no para una aplicación pública, por lo que 40 campos generalmente no es ridículo (aunque la mayoría de nuestras más de 100 vistas planificadas están más cerca de 10-15 campos). Cuando dices que creas un servicio desplegable, ¿te refieres a la misma manera que hice anteriormente, o el servicio rellena un objeto SelectList y lo pasa al ViewModel? –

+0

Parece que sus datos dependen del empleado actual. Se ve bien, siempre y cuando te funcione. Cuando me refería a un servicio desplegable, estaba pensando en un menú desplegable de zonas horarias o un menú desplegable de países. Este tipo de datos no cambia a menudo y puede almacenarse fácilmente en caché. –

+0

En realidad, también tenemos ese tipo de datos, que almacenamos en caché, pero llamamos de la misma manera (el caché se gestiona en la capa de servicio). ¿Cómo manejarías esta información en caché de forma diferente, llamándola dentro de tu ViewModel? –

3

Hay más que eso ;-) Puedes buscar en la carpeta de modelo o en el filtro de acción. Para la segunda opción, consulte el blog de Jimmy Bogard en algún lugar alrededor de here. Personalmente lo hago en carpetas modelo. Yo uso ViewModel así: My custom ASP.NET MVC entity binding: is it a good solution?. Es procesado por mi carpeta modelo personalizado:

public object BindModel(ControllerContext c, BindingContext b) 
{ 
    var id = b.ValueProvider[b.ModelName]; // don't remember exact syntax 
    var repository = ServiceLocator.GetInstance(GetRepositoryType(b.ModelType)); 
    var obj = repository.Get(id); 
    if (obj == null) 
    b.ModelState.AddModelError(b.ModelName, "Not found in database"); 
    return obj; 
} 

public ActionResult Action(EntityViewModel<Order> order) 
{ 
    if (!ModelState.IsValid) 
     ...; 
} 

También puede ver un ejemplo de ligante modelo de acceso al repositorio haciendo en S#arp Architecture.

En cuanto a los datos estáticos en los modelos de vista, todavía estoy explorando enfoques.Por ejemplo, usted puede tener sus modelos de vista recuerdan las entidades en lugar de las listas, y

MyViewModel clase pública { MyViewModel pública (orden de pedido, IEmployeesSvc _svc) { }

public IList<Employee> GetEmployeesList() 
    { 
     return _svc.GetEmployeesFor(order.Number); 
    } 

}

Usted decide cómo inyectar _svc en ViewModel, pero básicamente es lo mismo que lo hace para el controlador. Solo tenga en cuenta que ViewModel también es creado por MVC a través de un constructor sin parámetros, por lo que puede usar ServiceLocator o extender MVC para la creación de ViewModel, por ejemplo, dentro de su carpeta de modelo personalizada. O puede usar el enfoque de Jimmy Bogard con AutoMapper, que también admite contenedores IoC.

El enfoque común aquí es que cada vez que veo código repetitivo, busco eliminarlo. 100 acciones de controlador haciendo marshalling domain-viewmodel más búsqueda de repositorio es un mal caso. La carpeta de modelo único que lo hace de manera genérica es buena.

1

Aquí hay otra solución: http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/06/29/how-we-do-mvc-view-models.aspx

puntos principales allí:

  1. la transformación se realiza por un mediador - en este caso, es AutoMapper pero puede ser su propia clase (aunque más de código). Esto mantiene tanto el dominio como el modelo de vista concentrados en la lógica del dominio/presentación. El mediador (asignador) contendrá la lógica (principalmente automática) para el mapeo, incluidos los servicios inyectados.
  2. La asignación se aplica automáticamente, todo lo que debe hacer es indicar al filtro de acción los tipos de fuente/destino, muy limpio.
  3. (Parece ser importante para usted) AutoMapper admite asignaciones/tipos anidados, por lo que puede tener su ViewModel combinado de varios modelos de vista independientes, para que su "DTO de pantalla" no sea desordenada.

Al igual que en este modelo:

public class WholeViewModel 
{ 
    public Part1ViewModel ModelPart1 { get; set; } 
    public Part2ViewModel ModelPart2 { get; set; } 
} 

vuelve a utilizar asignaciones para determinadas partes de su vista, y no se escribe ninguna nueva línea de código, puesto que ya estamos asignaciones para el modelos de vista parcial.

Si no desea AutoMapper, hay que tener interfaces IViewModelMapper, y luego su contenedor IoC ayudará a su filtro de la acción apropiada para encontrar

container.Resolve(typeof(IViewModelMapper<>).MakeGenericType(mysourcetype, mydesttype)) 

y también proporcionará ninguna servicios externos necesarios para que el asignador (Esto también es posible con AutoMapper). Pero, por supuesto, AutoMapper puede hacer recursiones y, de todos modos, ¿por qué escribir AutoMapper adicional ;-)

3

No obtendría datos de la base de datos en su ViewModel. ViewModel existe para promover la separación de preocupaciones (entre su Vista y su Modelo). Enredarse en la lógica de la persistencia ahí derrota el propósito.

Afortunadamente, el marco ASP.NET MVC nos brinda más puntos de integración, específicamente el ModelBinder.

Tengo una implementación de un ModelBinder genérica tirando de la información de la capa de servicios en: -

http://www.iaingalloway.com/going-further-a-generic-servicelayer-modelbinder

No utiliza un modelo de vista, pero eso se puede arreglar fácilmente. De ninguna manera es la única implementación. Para un proyecto del mundo real, probablemente esté mejor con una solución menos genérica y más personalizada.

Si es diligente, sus métodos GET ni siquiera necesitan saber que existe la capa de servicio.

La solución probablemente se ve algo como: -

método de acción del controlador: -

public ActionResult Details(MyTypeIndexViewModel model) 
{ 
    if(ModelState.IsValid) 
    { 
    return View(model); 
    } 
    else 
    { 
    // Handle the case where the ModelState is invalid 
    // usually because they've requested MyType/Details/x 
    // and there's no matching MyType in the repository 
    // e.g. return RedirectToAction("Index") 
    } 
} 

ModelBinder: -

public object BindModel 
(
    ControllerContext controllerContext, 
    BindingContext bindingContext 
) 
{ 
    // Get the Primary Key from the requestValueProvider. 
    // e.g. bindingContext.ValueProvider["id"] 
    int id = ...; 

    // Get an instance of your service layer via your 
    // favourite dependancy injection framework. 
    // Or grab the controller's copy e.g. 
    // (controllerContext.Controller as MyController).Service 
    IMyTypeService service = ...; 

    MyType myType = service.GetMyTypeById(id) 

    if (myType == null) 
    { 
    // handle the case where the PK has no matching MyType in the repository 
    // e.g. bindingContext.ModelState.AddModelError(...) 
    } 


    MyTypeIndexViewModel model = new MyTypeIndexViewModel(myType); 

    // If you've got more repository calls to make 
    // (e.g. populating extra fields on the model) 
    // you can do that here. 

    return model; 
} 

modelo de vista: -

public class MyTypeIndexViewModel 
{ 
    public MyTypeIndexViewModel(MyType source) 
    { 
    // Bind all the properties of the ViewModel in here, or better 
    // inherit from e.g. MyTypeViewModel, bind all the properties 
    // shared between views in there and chain up base(source) 
    } 
} 

Construir su capa de servicio, y registre su ModelBinder de forma normal.

+1

Sus ideas son intrigantes para mí y deseo suscribirme a su boletín informativo. – anewcomer

0

Considere pasar sus servicios en el ViewModel personalizado en su constructor (ala Inyección de Dependencia). Eso elimina el código de población modelo de su controlador y le permite concentrarse en controlar el flujo lógico de la aplicación. Los Custom ViewModels son un lugar ideal para abstraer la preparación de elementos como SelectLists de los que dependerán sus droplists.

Un montón de código en el controlador para cosas como la recuperación de datos no se considera una mejor práctica. La principal responsabilidad del controlador es "controlar" el flujo de la aplicación.

0

Enviar esto tarde ... Bounty casi ha terminado. Pero ...

Otro asignador a tener en cuenta es AutoMapper: http://www.codeplex.com/AutoMapper

y una visión general sobre cómo usarlo: http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/01/22/automapper-the-object-object-mapper.aspx

me gusta mucho es la sintaxis.

// place this somewhere in your globals, or base controller constructor 
Mapper.CreateMap<Employee, EmployeeViewModel>(); 

Ahora, en su controlador, usaría varios modelos de vista. Esto aplica DRY al permitirle reutilizar esos modelos de vista en otra parte de su aplicación. No los vincularía a todos a 1 modelo de vista. Me gustaría refactorizar a algo como:

public class EmployeeController() 
{ 
    private IEmployeeService _empSvc; 
    private ISpouseService _peopleSvc; 

    public EmployeeController(
     IEmployeeService empSvc, ISpouseService peopleSvc) 
    { 
    // D.I. hard at work! Auto-wiring up our services. :) 
    _empSvc = empSvc; 
    _peopleSvc = peopleSvc; 

    // setup all ViewModels here that the controller would use 
    Mapper.CreateMap<Employee, EmployeeViewModel>(); 
    Mapper.CreateMap<Spouse, SpouseViewModel>(); 
    } 

    public ActionResult Employee(int empNum) 
    { 
    // really should have some validation here that reaches into the domain 
    // 

    var employeeViewModel = 
     Mapper.Map<Employee, EmployeeViewModel>(
      _empSvc.FetchEmployee(empNum) 
     ); 

    var spouseViewModel = 
     Mapper.Map<Spouses, SpousesViewModel>(
      _peopleSvc.FetchSpouseByEmployeeID(empNum) 
     ); 

    employeeViewModel.SpouseViewModel = spouseViewModel; 

    return View(employeeViewModel);  
    } 

    [AcceptVerbs(HttpVerbs.Post)] 
    public ActionResult Employee(int id, FormCollection values)  
    { 
    try 
    { 
     // always post to an ID, which is the employeeID 
     var employee = _empSvc.FetchEmployee(id); 

     // and bind using the built-in UpdateModel helpers. 
     // this will throw an exception if someone is posting something 
     // they shouldn't be posting. :) 
     UpdateModel(employee); 

     // save employee here 

     this.RedirectToAction(c => c.Index()); 
    } 
    catch 
    { 
     // check your domain model for any errors. 
     // check for any other type of exception. 
     // fail back to the employee screen 
     RedirectToAction(c => c.Employee(id)); 
    } 
    } 
} 

Por lo general, trato de evitar almacenar varias entidades en una acción de controlador. En cambio, refactorizaría el objeto de dominio empleado para tener los métodos AddSpouse() y SaveSpouse(), que tomarían un objeto de Cónyuge. Este concepto se conoce como AggregateRoots, que controla todas las dependencias desde la raíz, que es el objeto Employee(). Pero, ese soy solo yo.

Cuestiones relacionadas