Utilizar ViewData e Implementar Classes ViewModel

pela Microsoft

Baixar PDF

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 você siga os tutoriais Introdução ao MVC 3 ou MVC Music Store.

Etapa 6 do NerdDinner: ViewData e ViewModel

Abordamos vários cenários de envio de formulários e discutimos como implementar o suporte a CRUD (Criar, Atualizar e Excluir). Agora, levaremos nossa implementação dinnersController mais adiante 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 ajuda a impor entre os diferentes componentes de um aplicativo. Modelos, controladores e visõ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 templates de visualização nunca devem realizar recuperação de dados ou lógica de aplicação e, em vez disso, devem limitar-se a ter apenas o código de renderização que é baseado no modelo/dados passados para ele pelo controlador.

No momento, os dados de modelo que estão sendo passados pela 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" nas nossas visualizações de Edição e Criação de uma caixa de texto HTML para um menu suspenso. Em vez de codificar a lista suspensa de nomes de país e região no modelo de exibição, convém gerá-la de uma lista de países e regiões com suporte que populamos dinamicamente. Precisaremos de uma maneira de passar o objeto Dinner e a lista de países e regiões com suporte de 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 Views.

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 Editar() 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 do SelectList acima está aceitando uma lista de países e regiões para preencher a lista suspensa, 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 para saída. O segundo é o modelo "SelectList" que passamos por meio do dicionário ViewData. Estamos usando a palavra-chave "as" em C# para converter o tipo dentro do dicionário como um 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:

Captura de tela da interface de edição do usuário com a lista suspensa de países e regiões realçada com uma seta vermelha.

Como também renderizamos o modelo de visualização Editar a partir do método HTTP-POST Editar (em cenários de ocorrência de erros), queremos garantir que também atualizemos esse método para adicionar o SelectList ao ViewData quando o modelo de visualizaçã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, no entanto, uma vez que 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, quando se utiliza 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 otimizadas para exibição para nosso modelo de visualização para uso. Isso permite tipagem segura, verificação em tempo de compilação e 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 a mostrada abaixo, que expõe duas propriedades fortemente tipadas: 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 Editar() para criar o DinnerFormViewModel usando o objeto Dinner que recuperamos de nosso repositório e, em seguida, passá-lo para o 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 template de exibição para que ele espere um "DinnerFormViewModel" em vez de um objeto "Dinner", alterando o atributo "inherits" na parte superior da edit.aspx como no exemplo a seguir:

Inherits="System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerFormViewModel>

Assim que fizermos 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:

Captura de tela da janela do editor de código com uma lista suspensa e o item 'Jantar' da lista destacado com um retângulo azul.

Captura de tela da janela do editor de código com uma lista suspensa e o item de lista de endereços realçado com um retângulo pontilhado cinza.

Em seguida, podemos atualizar nosso código de visualização para trabalhar a partir 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 chamados de "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 postagem 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 a mesma classe DinnerFormViewModel para habilitar o DropDownList "Países" dentro deles também. Veja abaixo a implementação de HTTP-GET:

//
// GET: /Dinners/Create

public ActionResult Create() {

    Dinner dinner = new Dinner() {
        EventDate = DateTime.Now.AddDays(7)
    };

    return View(new DinnerFormViewModel(dinner));
}

Veja abaixo 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 com formato personalizado

No cenário acima, nossa classe DinnerFormViewModel expõe diretamente o objeto de modelo Dinner como uma propriedade, juntamente com uma propriedade auxiliar de modelo SelectList. 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 de perto aos 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 de forma personalizada, cujo modelo de objeto seja mais otimizado para consumo pela visualização, podendo parecer completamente diferente do objeto de modelo de domínio subjacente. Por exemplo, ele poderia potencialmente expor nomes de propriedades diferentes e/ou propriedades de agregação coletadas de vários objetos de modelo.

As classes ViewModel personalizadas podem ser usadas tanto para passar dados de controladores para exibições para renderização, quanto para ajudar a lidar com os dados de formulário enviados de volta ao método de ação de um controlador. Para este 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 use a instância ViewModel para mapear ou recuperar um objeto de modelo de domínio real.

Classes ViewModel personalizadas podem oferecer uma grande flexibilidade e são algo que vale a pena considerar sempre que perceber que o código de renderização nos seus modelos de visualização ou o código de postagem de formulário nos seus métodos de ação estão ficando muito complicados. Isso geralmente é um sinal de que seus modelos de domínio não correspondem de forma limpa à interface do usuário que você está gerando e que uma classe ViewModel de forma personalizada intermediária pode ajudar.

Próxima Etapa

Agora, vamos examinar como podemos usar componentes parciais e páginas mestras para reutilizar e compartilhar a interface do usuário em nosso aplicativo.