Práticas recomendadas do ViewModel


238

A partir dessa pergunta , parece que faz sentido que um controlador crie um ViewModel que reflita com mais precisão o modelo que a visualização está tentando exibir, mas estou curioso sobre algumas das convenções (sou novo no padrão MVC , se já não era óbvio).

Basicamente, eu tinha as seguintes perguntas:

  1. Eu normalmente gosto de ter uma classe / arquivo. Isso faz sentido com um ViewModel se ele estiver sendo criado apenas para transferir dados de um controlador para uma exibição?
  2. Se um ViewModel pertence ao seu próprio arquivo e você está usando uma estrutura de diretório / projeto para manter as coisas separadas, a que lugar pertence o arquivo ViewModel ? No diretório Controladores ?

É basicamente isso por enquanto. Talvez eu tenha mais algumas perguntas, mas isso está me incomodando há mais ou menos uma hora, e consigo encontrar orientações consistentes em outros lugares.

EDIT: Olhando para o exemplo de aplicativo NerdDinner no CodePlex, parece que os ViewModels fazem parte dos Controladores , mas ainda me sinto desconfortável por não estarem em seus próprios arquivos.


66
Eu não chamaria exatamente o NerdDinner de exemplo de "Melhores práticas". Sua intuição lhe serve bem. :)
Ryan Montgomery

Respostas:


211

Eu crio o que chamo de "ViewModel" para cada exibição. Coloquei-os em uma pasta chamada ViewModels no meu projeto da Web MVC. Eu os nomeio após o controlador e a ação (ou exibição) que eles representam. Portanto, se eu precisar passar dados para a visualização SignUp no controlador Membership, crio uma classe MembershipSignUpViewModel.cs e a coloco na pasta ViewModels.

Em seguida, adiciono as propriedades e métodos necessários para facilitar a transferência de dados do controlador para a visualização. Eu uso o Automapper para ir do meu ViewModel para o modelo de domínio e voltar, se necessário.

Isso também funciona bem para os ViewModels compostos que contêm propriedades do tipo de outros ViewModels. Por exemplo, se você possui 5 widgets na página de índice no controlador de associação e criou um ViewModel para cada visualização parcial - como você passa os dados da ação Index para as parciais? Você adiciona uma propriedade ao MembershipIndexViewModel do tipo MyPartialViewModel e ao renderizar o parcial você passaria em Model.MyPartialViewModel.

Fazer isso dessa maneira permite ajustar as propriedades parciais do ViewModel sem precisar alterar a exibição do Índice. Ele ainda passa no Model.MyPartialViewModel, então há menos chances de você ter que passar por toda a cadeia de parciais para corrigir algo quando tudo o que você está fazendo é adicionar uma propriedade ao ViewModel parcial.

Também adicionarei o espaço de nome "MyProject.Web.ViewModels" ao web.config para permitir que eu os faça referência em qualquer modo de exibição sem nunca adicionar uma declaração de importação explícita em cada modo de exibição. Apenas torna um pouco mais limpo.


3
E se você deseja POSTAR a partir de uma vista parcial e retornar a vista inteira (no caso de erro do modelo)? Na visão parcial, você não tem acesso ao modelo pai.
Cosmo

5
@ Cosmo: Em seguida, POST para uma ação que pode retornar a exibição inteira no caso de um erro de modelo. No lado do servidor, você tem o suficiente para recriar o modelo pai.
Tomas Aschan

Que tal ações de login [POST] e login [GET]? com diferentes modelos de visualização?
Bart Calixto

Normalmente, o login [GET] não chama o ViewModel porque não precisa carregar nenhum dado.
Andre Figueiredo

Ótimo conselho. Para onde devem ir o acesso, o processamento e a configuração das propriedades do modelo / VM? No meu caso, teremos alguns dados provenientes de um banco de dados CMS local e outros provenientes de serviços da Web, que precisarão ser processados ​​/ manipulados antes de serem configurados em um modelo. Colocar tudo isso no controlador fica muito confuso.
Xr280xr

124

Separar classes por categoria (controladores, modelos de exibição, filtros etc.) não faz sentido.

Se você deseja escrever um código para a seção Inicial do seu site (/), crie uma pasta chamada Home e coloque lá o HomeController, IndexViewModel, AboutViewModel, etc. e todas as classes relacionadas usadas pelas ações da Página inicial.

Se você compartilhou classes, como um ApplicationController, pode colocá-lo na raiz do seu projeto.

Por que separar as coisas que estão relacionadas (HomeController, IndexViewModel) e manter as coisas juntas que não têm nenhuma relação (HomeController, AccountController)?


Eu escrevi um post sobre este tópico.


13
As coisas vão ficar bem confusas rapidamente, se você fizer isso.
UpTheCreek 9/11/2009

14
Não, bagunçado é colocar todos os controladores em um diretório / namespace. Se você possui 5 controladores, cada um usando 5 modelos de visualização, você tem 25 modelos de visualização. Namespaces é o mecanismo para organizar o código e não deve ser diferente aqui.
Max Toro

41
@ Max Toro: surpreendeu você ter sido tão votado. Depois de algum tempo trabalhando no ASP.Net MVC, sinto muita dor por ter todos os ViewModels em um local, todos os controladores em outro e todos os Views em outro. MVC é um trio de peças relacionadas, elas são acopladas - elas se apoiam. Sinto que uma solução pode ser muito mais organizada se o Controller, ViewModels e Views de uma determinada seção morarem juntos no mesmo diretório. MyApp / Contas / Controller.cs, MyApp / Contas / Criar / ViewModel.cs, MyApp / Contas / Criar / View.cshtml, etc.
Quentin-starin

13
@RyanJMcGowan separação de preocupações não é separação de classes.
Max Toro

12
@RyanJMcGowan, não importa como você aborda o desenvolvimento, o problema é o que você acaba enfrentando, especialmente para aplicativos grandes. Uma vez no modo de manutenção, você não pensa em todos os modelos e em todos os controladores, adiciona uma função por vez.
Max Toro

21

Eu mantenho minhas classes de aplicativos em uma subpasta chamada "Core" (ou uma biblioteca de classes separada) e uso os mesmos métodos que o aplicativo de amostra KIGG , mas com algumas pequenas alterações para tornar meus aplicativos mais SECA.

Crio uma classe BaseViewData em / Core / ViewData / onde armazeno propriedades comuns em todo o site.

Depois disso, também crio todas as minhas classes de ViewData de exibição na mesma pasta que derivam de BaseViewData e têm propriedades específicas de exibição.

Em seguida, crio um ApplicationController do qual todos os meus controladores derivam. O ApplicationController possui um método GetViewData genérico da seguinte maneira:

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

Finalmente, na minha ação do Controller, faço o seguinte para criar meu modelo ViewData

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

Eu acho que isso funciona muito bem e mantém suas opiniões organizadas e seus controladores magros.


13

Uma classe ViewModel existe para encapsular várias partes de dados representadas por instâncias de classes em um objeto fácil de gerenciar que você pode passar para o seu View.

Seria bom ter suas classes ViewModel em seus próprios arquivos, no próprio diretório. Nos meus projetos, tenho uma subpasta da pasta Modelos chamada ViewModels. É aí que meus ViewModels (por exemplo ProductViewModel.cs) moram.


13

Não há um bom lugar para manter seus modelos. Você pode mantê-los em montagem separada se o projeto for grande e houver muitos ViewModels (Data Transfer Objects). Além disso, você pode mantê-los em uma pasta separada do projeto do site. Por exemplo, no Oxite, eles são colocados no projeto Oxite, que também contém várias classes. Os controladores da Oxite são movidos para um projeto separado e as visualizações também estão em um projeto separado.
No CodeCampServer, os modelos de vista são nomeados * Form e são colocados no projeto de interface do usuário na pasta Modelos.
No projeto MvcPress , eles são colocados no projeto Data, que também contém todo o código para trabalhar com o banco de dados e um pouco mais (mas eu não recomendei essa abordagem, é apenas uma amostra)
Então você pode ver que existem muitos pontos de vista. Normalmente mantenho meus ViewModels (objetos DTO) no projeto do site. Mas quando tenho mais de 10 modelos, prefiro movê-los para montagem separada. Normalmente, neste caso, também estou movendo os controladores para a montagem separada.
Outra questão é como mapear facilmente todos os dados do modelo para o seu ViewModel. Sugiro dar uma olhada na biblioteca do AutoMapper . Eu gosto muito, faz todo o trabalho sujo para mim.
E também sugiro olhar para o projeto SharpArchitecture . Ele fornece uma arquitetura muito boa para projetos e contém muitos frameworks e orientações interessantes e ótima comunidade.


8
ViewModels! = DTO
Bart Calixto

6

aqui está um trecho de código das minhas práticas recomendadas:

    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

Lançamos todos os nossos ViewModels na pasta Modelos (toda a nossa lógica de negócios está em um projeto separado do ServiceLayer)


4

Pessoalmente, sugiro que, se o ViewModel não for trivial, use uma classe separada.

Se você tiver mais de um modelo de visualização, sugiro que faça sentido particioná-lo em pelo menos um diretório. se o modelo de vista for compartilhado posteriormente, o espaço para nome implícito no diretório facilita a mudança para um novo assembly.


2

No nosso caso, temos os modelos junto com os controladores em um projeto separado das vistas.

Como regra geral, tentamos mover e evitar a maioria das coisas do ViewData ["..."] para o ViewModel, para evitar castings e seqüências de caracteres mágicas, o que é uma coisa boa.

O ViewModel também possui algumas propriedades comuns, como informações de paginação para listas ou informações de cabeçalho da página para desenhar migalhas de pão e títulos. Nesse momento, a classe base contém muitas informações em minha opinião e podemos dividi-las em três partes, as informações mais básicas e necessárias para 99% das páginas em um modelo de vista base e, em seguida, um modelo para as listas e um modelo para os formulários que contêm dados específicos para esses cenários e herdam do base.

Por fim, implementamos um modelo de visualização para cada entidade para lidar com as informações específicas.


0

código no 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 no modelo de exibição:

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       
    }

projetos:

  • DevJet.Web (o projeto da Web ASP.NET MVC)

  • DevJet.Web.App.Dictionary (um projeto de biblioteca de classes separado)

    Neste projeto, criei algumas pastas como: DAL, BLL, BO, VM (pasta para modelos de exibição)


Olá, você pode compartilhar qual é a estrutura da classe Entry?
Dinis Cruz

0

Crie uma classe base de modelo de exibição que possua propriedades normalmente necessárias, como resultado da operação e dados contextuais, você também pode colocar dados e funções atuais do usuário

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

Na classe do controlador base, existe um método como PopulateViewModelBase (), esse método preencherá os dados contextuais e as funções do usuário. O HasError e o ErrorMessage, defina essas propriedades se houver exceção ao extrair dados do service / db. Ligue essas propriedades na exibição para mostrar erro. As funções de usuário podem ser usadas para mostrar a seção ocultar na exibição com base nas funções.

Para preencher modelos de exibição em diferentes ações get, é possível torná-lo consistente com o controlador base com o método abstrato FillModel

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

Nos 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;
}
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.