Usar ViewData e implementar classes ViewModel
pela Microsoft
Esta é a etapa 6 de um tutorial gratuito de aplicativo "NerdDinner" que explica como criar um aplicativo Web pequeno, mas completo, usando ASP.NET MVC 1.
A etapa 6 mostra como habilitar o suporte para cenários de edição de formulários mais avançados e também discute duas abordagens que podem ser usadas para passar dados de controladores para exibições: ViewData e ViewModel.
Se você estiver usando ASP.NET MVC 3, recomendamos que siga os tutoriais do Introdução With MVC 3 ou MVC Music Store.
Etapa 6 do NerdDinner: ViewData e ViewModel
Abordamos vários cenários de postagem de formulário e discutimos como implementar o suporte crud (criar, atualizar e excluir). Agora, levaremos nossa implementação dinnersController ainda mais longe e habilitaremos o suporte para cenários mais avançados de edição de formulários. Ao fazer isso, discutiremos duas abordagens que podem ser usadas para passar dados de controladores para exibições: ViewData e ViewModel.
Passando dados de controladores para View-Templates
Uma das características definidoras do padrão MVC é a estrita "separação de preocupações" que ele ajuda a impor entre os diferentes componentes de um aplicativo. Modelos, Controladores e Exibições têm funções e responsabilidades bem definidas e se comunicam entre si de maneiras bem definidas. Isso ajuda a promover a testabilidade e a reutilização de código.
Quando uma classe Controller decide renderizar uma resposta HTML de volta para um cliente, ela é responsável por passar explicitamente para o modelo de exibição todos os dados necessários para renderizar a resposta. Os modelos de exibição nunca devem executar nenhuma recuperação de dados ou lógica de aplicativo e, em vez disso, devem limitar-se a ter apenas o código de renderização que é expulso do modelo/dados passados para ele pelo controlador.
No momento, os dados de modelo que estão sendo passados por nossa classe DinnersController para nossos modelos de exibição são simples e diretos – uma lista de objetos Dinner no caso de Index() e um único objeto Dinner no caso de Details(), Edit(), Create() e Delete(). À medida que adicionamos mais recursos de interface do usuário ao nosso aplicativo, muitas vezes precisaremos passar mais do que apenas esses dados para renderizar respostas HTML em nossos modelos de exibição. Por exemplo, talvez queiramos alterar o campo "País" em nossos modos de exibição Editar e Criar de ser uma caixa de texto HTML para uma lista suspensa. Em vez de codificar a lista suspensa de nomes de país e região no modelo de exibição, talvez queiramos gerá-la de uma lista de países e regiões com suporte que preenchemos dinamicamente. Precisaremos de uma maneira de passar o objeto Dinner e a lista de países e regiões com suporte do nosso controlador para nossos modelos de exibição.
Vamos examinar duas maneiras de fazer isso.
Usando o dicionário ViewData
A classe base Controller expõe uma propriedade de dicionário "ViewData" que pode ser usada para passar itens de dados adicionais de Controladores para Exibições.
Por exemplo, para dar suporte ao cenário em que queremos alterar a caixa de texto "País" em nosso modo de exibição Editar de ser uma caixa de texto HTML para uma lista suspensa, podemos atualizar nosso método de ação Edit() para passar (além de um objeto Dinner) um objeto SelectList que pode ser usado como o modelo de uma lista suspensa "Países".
//
// GET: /Dinners/Edit/5
[Authorize]
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
ViewData["Countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);
return View(dinner);
}
O construtor da SelectList acima está aceitando uma lista de países e regiões para preencher a lista suspensa com, bem como o valor selecionado no momento.
Em seguida, podemos atualizar nosso modelo de exibição Edit.aspx para usar o método auxiliar Html.DropDownList() em vez do método auxiliar Html.TextBox() que usamos anteriormente:
<%= Html.DropDownList("Country", ViewData["Countries"] as SelectList) %>
O método auxiliar Html.DropDownList() acima usa dois parâmetros. O primeiro é o nome do elemento de formulário HTML a ser gerado. O segundo é o modelo "SelectList" que passamos por meio do dicionário ViewData. Estamos usando o palavra-chave "as" do C# para converter o tipo no dicionário como selectlist.
E agora, quando executarmos nosso aplicativo e acessarmos a URL /Dinners/Edit/1 em nosso navegador, veremos que nossa interface do usuário de edição foi atualizada para exibir uma lista suspensa de países e regiões em vez de uma caixa de texto:
Como também renderizamos o modelo editar modo de exibição do método Editar HTTP-POST (em cenários em que ocorrem erros), queremos ter certeza de que também atualizamos esse método para adicionar SelectList a ViewData quando o modelo de exibição for renderizado em cenários de erro:
//
// POST: /Dinners/Edit/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
ViewData["countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);
return View(dinner);
}
}
E agora nosso cenário de edição DinnersController dá suporte a um DropDownList.
Usando um padrão ViewModel
A abordagem do dicionário ViewData tem o benefício de ser bastante rápida e fácil de implementar. Alguns desenvolvedores não gostam de usar dicionários baseados em cadeia de caracteres, pois erros de digitação podem levar a erros que não serão capturados em tempo de compilação. O dicionário ViewData não tipado também requer o uso do operador "as" ou a conversão ao usar uma linguagem fortemente tipada como C# em um modelo de exibição.
Uma abordagem alternativa que poderíamos usar é geralmente conhecida como o padrão "ViewModel". Ao usar esse padrão, criamos classes fortemente tipadas que são otimizadas para nossos cenários de exibição específicos e que expõem propriedades para os valores/conteúdo dinâmicos necessários para nossos modelos de exibição. Nossas classes de controlador podem preencher e passar essas classes com otimização de exibição para nosso modelo de exibição a ser usado. Isso permite a segurança de tipo, a verificação em tempo de compilação e o intelliSense do editor nos modelos de exibição.
Por exemplo, para habilitar cenários de edição de formulário de jantar, podemos criar uma classe "DinnerFormViewModel", como abaixo, que expõe duas propriedades fortemente tipados: um objeto Dinner e o modelo SelectList necessário para preencher a lista suspensa "Países":
public class DinnerFormViewModel {
// Properties
public Dinner Dinner { get; private set; }
public SelectList Countries { get; private set; }
// Constructor
public DinnerFormViewModel(Dinner dinner) {
Dinner = dinner;
Countries = new SelectList(PhoneValidator.AllCountries, dinner.Country);
}
}
Em seguida, podemos atualizar nosso método de ação Edit() para criar o DinnerFormViewModel usando o objeto Dinner que recuperamos do nosso repositório e, em seguida, passá-lo para nosso modelo de exibição:
//
// GET: /Dinners/Edit/5
[Authorize]
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
return View(new DinnerFormViewModel(dinner));
}
Em seguida, atualizaremos nosso modelo de exibição para que ele espere um "DinnerFormViewModel" em vez de um objeto "Dinner" alterando o atributo "herda" na parte superior da página edit.aspx da seguinte maneira:
Inherits="System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerFormViewModel>
Depois de fazermos isso, o intelliSense da propriedade "Model" em nosso modelo de exibição será atualizado para refletir o modelo de objeto do tipo DinnerFormViewModel que estamos passando:
Em seguida, podemos atualizar nosso código de exibição para trabalhar fora dele. Observe abaixo como não estamos alterando os nomes dos elementos de entrada que estamos criando (os elementos de formulário ainda serão nomeados "Title", "Country") – mas estamos atualizando os métodos auxiliares HTML para recuperar os valores usando a classe DinnerFormViewModel:
<p>
<label for="Title">Dinner Title:</label>
<%= Html.TextBox("Title", Model.Dinner.Title) %>
<%=Html.ValidationMessage("Title", "*") %>
</p>
<p>
<label for="Country">Country:</label>
<%= Html.DropDownList("Country", Model.Countries) %>
<%=Html.ValidationMessage("Country", "*") %>
</p>
Também atualizaremos nosso método editar post para usar a classe DinnerFormViewModel ao renderizar erros:
//
// POST: /Dinners/Edit/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
return View(new DinnerFormViewModel(dinner));
}
}
Também podemos atualizar nossos métodos de ação Create() para reutilizar exatamente a mesma classe DinnerFormViewModel para habilitar o DropDownList "Countries" dentro deles também. Abaixo está a implementação HTTP-GET:
//
// GET: /Dinners/Create
public ActionResult Create() {
Dinner dinner = new Dinner() {
EventDate = DateTime.Now.AddDays(7)
};
return View(new DinnerFormViewModel(dinner));
}
Abaixo está a implementação do método HTTP-POST Create:
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Dinner dinner) {
if (ModelState.IsValid) {
try {
dinner.HostedBy = "SomeUser";
dinnerRepository.Add(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
}
}
return View(new DinnerFormViewModel(dinner));
}
E agora nossas telas Editar e Criar dão suporte a listas suspensas para escolher o país ou a região.
Classes ViewModel em forma personalizada
No cenário acima, nossa classe DinnerFormViewModel expõe diretamente o objeto de modelo Dinner como uma propriedade, juntamente com uma propriedade de modelo SelectList de suporte. Essa abordagem funciona bem para cenários em que a interface do usuário HTML que queremos criar em nosso modelo de exibição corresponde relativamente perto de nossos objetos de modelo de domínio.
Para cenários em que esse não é o caso, uma opção que você pode usar é criar uma classe ViewModel em forma personalizada cujo modelo de objeto é mais otimizado para consumo pela exibição – e que pode parecer completamente diferente do objeto de modelo de domínio subjacente. Por exemplo, ele poderia potencialmente expor nomes de propriedade diferentes e/ou propriedades de agregação coletadas de vários objetos de modelo.
Classes ViewModel em forma personalizada podem ser usadas para passar dados de controladores para exibições a serem renderizadas, bem como para ajudar a lidar com dados de formulário postados novamente no método de ação de um controlador. Para esse cenário posterior, você pode fazer com que o método de ação atualize um objeto ViewModel com os dados postados no formulário e, em seguida, use a instância ViewModel para mapear ou recuperar um objeto de modelo de domínio real.
As classes ViewModel em forma personalizada podem fornecer muita flexibilidade e são algo para investigar sempre que você encontrar o código de renderização em seus modelos de exibição ou o código de postagem de formulário dentro de seus métodos de ação começando a ficar muito complicado. Isso geralmente é um sinal de que seus modelos de domínio não correspondem corretamente à interface do usuário que você está gerando e que uma classe ViewModel intermediária em forma personalizada pode ajudar.
Próxima etapa
Agora vamos examinar como podemos usar parciais e master páginas para reutilização e compartilhamento da interface do usuário em nosso aplicativo.