Mejores prácticas de ViewModel


238

A partir de esta pregunta , parece que tiene sentido que un controlador cree un ViewModel que refleje con mayor precisión el modelo que la vista está tratando de mostrar, pero tengo curiosidad por algunas de las convenciones (soy nuevo en el patrón MVC , si ya no fuera obvio).

Básicamente, tuve las siguientes preguntas:

  1. Normalmente me gusta tener una clase / archivo. ¿Tiene sentido esto con un ViewModel? si solo se está creando para transferir datos de un controlador a una vista?
  2. Si un ViewModel pertenece a su propio archivo y está utilizando una estructura de directorio / proyecto para mantener las cosas separadas, ¿a dónde pertenece el archivo ViewModel ? En el directorio de controladores ?

Eso es básicamente todo por ahora. Es posible que tenga algunas preguntas más por venir, pero esto me ha estado molestando durante la última hora más o menos, y parece que puedo encontrar una guía constante en otros lugares.

EDITAR: Al mirar la aplicación NerdDinner de muestra en CodePlex, parece que los ViewModels son parte de los Controladores , pero todavía me incomoda que no estén en sus propios archivos.


66
No llamaría exactamente a NerdDinner un ejemplo de "Mejores Prácticas". Tu intuición te sirve bien. :)
Ryan Montgomery

Respuestas:


211

Creo lo que llamo un "ViewModel" para cada vista. Los puse en una carpeta llamada ViewModels en mi proyecto web MVC. Los nombro después del controlador y la acción (o vista) que representan. Entonces, si necesito pasar datos a la vista Registrarse en el controlador de Membresía, creo una clase MembershipSignUpViewModel.cs y la pongo en la carpeta ViewModels.

Luego agrego las propiedades y métodos necesarios para facilitar la transferencia de datos desde el controlador a la vista. Utilizo el Automapper para pasar de mi ViewModel al Modelo de dominio y viceversa si es necesario.

Esto también funciona bien para ViewModels compuestos que contienen propiedades que son del tipo de otros ViewModels. Por ejemplo, si tiene 5 widgets en la página de índice en el controlador de membresía y creó un ViewModel para cada vista parcial, ¿cómo pasa los datos de la acción Index a los parciales? Agregue una propiedad al MembershipIndexViewModel de tipo MyPartialViewModel y al representar el parcial pasaría en Model.MyPartialViewModel.

Hacerlo de esta manera le permite ajustar las propiedades parciales de ViewModel sin tener que cambiar la vista de índice. Todavía pasa en Model.MyPartialViewModel, por lo que hay menos posibilidades de que tenga que pasar por toda la cadena de parciales para arreglar algo cuando todo lo que está haciendo es agregar una propiedad al ViewModel parcial.

También agregaré el espacio de nombres "MyProject.Web.ViewModels" al web.config para permitirme hacer referencia a ellos en cualquier vista sin tener que agregar una declaración de importación explícita en cada vista. Solo lo hace un poco más limpio.


3
¿Qué sucede si desea PUBLICAR desde una vista parcial y devolver la vista completa (en caso de error del modelo)? Dentro de la vista parcial no tiene acceso al modelo principal.
Cosmo

55
@ Cosmo: luego PUBLICAR en una acción que puede devolver la vista completa en caso de un error de modelo. En el lado del servidor, tiene suficiente para recrear el modelo principal.
Tomas Aschan

¿Qué pasa con las acciones de inicio de sesión [POST] e inicio de sesión [GET]? con diferentes modelos de vista?
Bart Calixto

Por lo general, el inicio de sesión [GET] no llama a ViewModel porque no necesita cargar ningún dato.
Andre Figueiredo

Buen consejo. ¿A dónde deben ir el acceso a los datos, el procesamiento y la configuración de las propiedades del modelo / VM? En mi caso, tendremos algunos datos provenientes de una base de datos local de CMS y algunos provenientes de servicios web, que deberán procesarse / manipularse antes de establecerse en un modelo. Poner todo eso en el controlador se vuelve bastante desordenado.
xr280xr

124

Separar clases por categoría (Controladores, ViewModels, Filtros, etc.) no tiene sentido.

Si desea escribir código para la sección Inicio de su sitio web (/), cree una carpeta llamada Inicio y coloque el HomeController, IndexViewModel, AboutViewModel, etc. y todas las clases relacionadas utilizadas por las acciones de Inicio.

Si ha compartido clases, como un ApplicationController, puede ponerlo en la raíz de su proyecto.

¿Por qué separar las cosas que están relacionadas (HomeController, IndexViewModel) y mantener las cosas juntas que no tienen ninguna relación (HomeController, AccountController)?


Escribí una publicación de blog sobre este tema.


13
Las cosas se pondrán bastante desordenadas bastante rápido si haces esto.
UpTheCreek

14
No, desordenado es poner todos los controladores en un directorio / espacio de nombres. Si tiene 5 controladores, cada uno con 5 modelos de vista, entonces tiene 25 modelos de vista. Los espacios de nombres son el mecanismo para organizar el código, y no deberían ser diferentes aquí.
Max Toro el

41
@Max Toro: sorprendido de que te hayan votado tanto. Después de un tiempo trabajando en ASP.Net MVC, siento mucho dolor al tener todos los ViewModels en un lugar, todos los controladores en otro y todas las Vistas en otro. MVC es un trío de piezas relacionadas, están unidas, se apoyan mutuamente. Siento que una solución puede ser mucho más organizada si el Controlador, ViewModels y Views para una sección determinada viven juntos en el mismo directorio. MiApl / Cuentas / Controller.cs, MiApl / Cuentas / Crear / ViewModel.cs, MiApl / Cuentas / Crear / View.cshtml, etc.
Quentin-starin

13
La separación de preocupaciones de @RyanJMcGowan no es la separación de clases.
Max Toro

12
@RyanJMcGowan no importa cómo abordes el desarrollo, el problema es con lo que terminas, especialmente para aplicaciones grandes. Una vez en el modo de mantenimiento, no piensa en todos los modelos y luego en todos los controladores, agrega una función a la vez.
Max Toro

21

Mantengo mis clases de aplicación en una subcarpeta llamada "Core" (o una biblioteca de clases separada) y utilizo los mismos métodos que el aplicación de muestra KIGG pero con algunos pequeños cambios para que mis aplicaciones sean más SECAS.

Creo una clase BaseViewData en / Core / ViewData / donde almaceno propiedades comunes de todo el sitio.

Después de esto, también creo todas mis clases ViewData de vista en la misma carpeta que luego derivan de BaseViewData y tienen propiedades específicas de vista.

Luego creo un ApplicationController del que derivan todos mis controladores. ApplicationController tiene un método genérico GetViewData de la siguiente manera:

protected T GetViewData<T>() where T : BaseViewData, new()
    {
        var viewData = new T
        {
           Property1 = "value1",
           Property2 = this.Method() // in the ApplicationController
        };
        return viewData;
    }

Finalmente, en mi acción Controlador, hago lo siguiente para construir mi Modelo ViewData

public ActionResult Index(int? id)
    {
        var viewData = this.GetViewData<PageViewData>();
        viewData.Page = this.DataContext.getPage(id); // ApplicationController
        ViewData.Model = viewData;
        return View();
    }

Creo que esto funciona muy bien y mantiene sus vistas ordenadas y sus controladores delgados.


13

Una clase ViewModel está ahí para encapsular múltiples piezas de datos representados por instancias de clases en un objeto fácil de administrar que puede pasar a su Vista.

Tendría sentido tener sus clases ViewModel en sus propios archivos, en el propio directorio. En mis proyectos tengo una subcarpeta de la carpeta Modelos llamada ViewModels. Ahí es donde viven mis ViewModels (por ejemplo ProductViewModel.cs).


13

No hay un buen lugar para guardar sus modelos. Puede mantenerlos en ensamblaje separado si el proyecto es grande y hay muchos ViewModels (Objetos de transferencia de datos). También puede guardarlos en una carpeta separada del proyecto del sitio. Por ejemplo, en Oxite se colocan en el proyecto Oxite que también contiene muchas clases diferentes. Los controladores en Oxite se mueven a proyectos separados y las vistas también están en proyectos separados.
En CodeCampServer ViewModels se denominan * Form y se colocan en el proyecto de IU en la carpeta Modelos.
En MvcPress proyecto , se colocan en el proyecto de datos, que también contiene todo el código para trabajar con la base de datos y un poco más (pero no recomendé este enfoque, es solo para una muestra)
Entonces puedes ver que hay muchos puntos de vista. Por lo general, mantengo mis ViewModels (objetos DTO) en el proyecto del sitio. Pero cuando tengo más de 10 modelos, prefiero moverlos para ensamblarlos por separado. Por lo general, en este caso, también estoy moviendo los controladores para separar el ensamblaje.
Otra pregunta es cómo asignar fácilmente todos los datos del modelo a su ViewModel. Sugiero echar un vistazo a la biblioteca AutoMapper . Me gusta mucho, hace todo el trabajo sucio para mí.
Y también sugiero mirar el proyecto SharpArchitecture . Proporciona muy buena arquitectura para proyectos y contiene muchos marcos y guías geniales y una gran comunidad.


8
ViewModels! = DTO
Bart Calixto

6

Aquí hay un fragmento de código de mis mejores prácticas:

    public class UserController : Controller
    {
        private readonly IUserService userService;
        private readonly IBuilder<User, UserCreateInput> createBuilder;
        private readonly IBuilder<User, UserEditInput> editBuilder;

        public UserController(IUserService userService, IBuilder<User, UserCreateInput> createBuilder, IBuilder<User, UserEditInput> editBuilder)
        {
            this.userService = userService;
            this.editBuilder = editBuilder;
            this.createBuilder = createBuilder;
        }

        public ActionResult Index(int? page)
        {
            return View(userService.GetPage(page ?? 1, 5));
        }

        public ActionResult Create()
        {
            return View(createBuilder.BuildInput(new User()));
        }

        [HttpPost]
        public ActionResult Create(UserCreateInput input)
        {
            if (input.Roles == null) ModelState.AddModelError("roles", "selectati macar un rol");

            if (!ModelState.IsValid)
                return View(createBuilder.RebuildInput(input));

            userService.Create(createBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }

        public ActionResult Edit(long id)
        {
            return View(editBuilder.BuildInput(userService.GetFull(id)));
        }

        [HttpPost]
        public ActionResult Edit(UserEditInput input)
        {           
            if (!ModelState.IsValid)
                return View(editBuilder.RebuildInput(input));

            userService.Save(editBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }
}

5

Lanzamos todos nuestros ViewModels en la carpeta Modelos (toda nuestra lógica de negocios está en un proyecto separado de ServiceLayer)


4

Personalmente, sugeriría si ViewModel es cualquier cosa menos trivial, luego use una clase separada.

Si tiene más de un modelo de vista, le sugiero que tenga sentido dividirlo en al menos un directorio. si el modelo de vista se comparte más tarde, el espacio de nombre implícito en el directorio facilita el traslado a un nuevo ensamblado.


2

En nuestro caso, tenemos los Modelos junto con los Controladores en un proyecto separado de las Vistas.

Como regla general, hemos tratado de mover y evitar la mayoría de las cosas de ViewData ["..."] al ViewModel, por lo tanto, evitamos los castings y las cadenas mágicas, lo cual es algo bueno.

ViewModel también contiene algunas propiedades comunes, como información de paginación para listas o información de encabezado de la página para dibujar migas de pan y títulos. En este momento, la clase base contiene demasiada información en mi opinión y podemos dividirla en tres partes, la información más básica y necesaria para el 99% de las páginas en un modelo de vista base, y luego un modelo para las listas y un modelo para los formularios que contienen datos específicos para esos escenarios y heredan de la base.

Finalmente, implementamos un modelo de vista para cada entidad para manejar la información específica.


0

código en el controlador:

    [HttpGet]
        public ActionResult EntryEdit(int? entryId)
        {
            ViewData["BodyClass"] = "page-entryEdit";
            EntryEditViewModel viewMode = new EntryEditViewModel(entryId);
            return View(viewMode);
        }

    [HttpPost]
    public ActionResult EntryEdit(Entry entry)
    {
        ViewData["BodyClass"] = "page-entryEdit";            

        #region save

        if (ModelState.IsValid)
        {
            if (EntryManager.Update(entry) == 1)
            {
                return RedirectToAction("EntryEditSuccess", "Dictionary");
            }
            else
            {
                return RedirectToAction("EntryEditFailed", "Dictionary");
            }
        }
        else
        {
            EntryEditViewModel viewModel = new EntryEditViewModel(entry);
            return View(viewModel);
        }

        #endregion
    }

código en el modelo de vista:

public class EntryEditViewModel
    {
        #region Private Variables for Properties

        private Entry _entry = new Entry();
        private StatusList _statusList = new StatusList();        

        #endregion

        #region Public Properties

        public Entry Entry
        {
            get { return _entry; }
            set { _entry = value; }
        }

        public StatusList StatusList
        {
            get { return _statusList; }
        }

        #endregion

        #region constructor(s)

        /// <summary>
        /// for Get action
        /// </summary>
        /// <param name="entryId"></param>
        public EntryEditViewModel(int? entryId)
        {
            this.Entry = EntryManager.GetDetail(entryId.Value);                 
        }

        /// <summary>
        /// for Post action
        /// </summary>
        /// <param name="entry"></param>
        public EntryEditViewModel(Entry entry)
        {
            this.Entry = entry;
        }

        #endregion       
    }

proyectos:

  • DevJet.Web (el proyecto web ASP.NET MVC)

  • DevJet.Web.App.Dictionary (un proyecto separado de la Biblioteca de clases)

    en este proyecto, hice algunas carpetas como: DAL, BLL, BO, VM (carpeta para ver modelos)


Hola, ¿puedes compartir cuál es la estructura de la clase Entry?
Dinis Cruz

0

Cree una clase base de modelo de vista que comúnmente requiera propiedades como el resultado de la operación y los datos contextuales, también puede colocar datos y roles de usuario actuales

class ViewModelBase 
{
  public bool HasError {get;set;} 
  public string ErrorMessage {get;set;}
  public List<string> UserRoles{get;set;}
}

En la clase de controlador base, tenga un método como PopulateViewModelBase (), este método llenará los datos contextuales y los roles de usuario. HasError y ErrorMessage, configure estas propiedades si hay una excepción al extraer datos del servicio / db. Enlace estas propiedades a la vista para mostrar el error. Los roles de usuario se pueden usar para mostrar la sección oculta en la vista en función de los roles.

Para rellenar modelos de vista en diferentes acciones de obtención, puede hacerse coherente al tener un controlador base con el método abstracto FillModel

class BaseController :BaseController 
{
   public PopulateViewModelBase(ViewModelBase model) 
{
   //fill up common data. 
}
abstract ViewModelBase FillModel();
}

En controladores

class MyController :Controller 
{

 public ActionResult Index() 
{
   return View(FillModel()); 
}

ViewModelBase FillModel() 
{ 
    ViewModelBase  model=;
    string currentAction = HttpContext.Current.Request.RequestContext.RouteData.Values["action"].ToString(); 
 try 
{ 
   switch(currentAction) 
{  
   case "Index": 
   model= GetCustomerData(); 
   break;
   // fill model logic for other actions 
}
}
catch(Exception ex) 
{
   model.HasError=true;
   model.ErrorMessage=ex.Message;
}
//fill common properties 
base.PopulateViewModelBase(model);
return model;
}
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.