Fornecer suporte de CRUD (criar, ler, atualizar e excluir) ao formulário de entrada de dados
pela Microsoft
Esta é a etapa 5 de um tutorial gratuito de aplicativo "NerdDinner" que explica como criar um aplicativo Web pequeno, mas completo, usando ASP.NET MVC 1.
A etapa 5 mostra como levar nossa classe DinnersController ainda mais longe, habilitando o suporte para edição, criação e exclusão de jantares com ela também.
Se você estiver usando ASP.NET MVC 3, recomendamos que siga os tutoriais Introdução With MVC 3 ou MVC Music Store.
Etapa 5 do NerdDinner: criar, atualizar, excluir cenários de formulário
Apresentamos controladores e exibições e abordamos como usá-los para implementar uma experiência de listagem/detalhes para Jantares no site. Nossa próxima etapa será levar nossa classe DinnersController ainda mais longe e habilitar o suporte para editar, criar e excluir jantares com ele também.
URLs manipuladas por DinnersController
Anteriormente, adicionamos métodos de ação ao DinnersController que implementavam suporte para duas URLs: /Dinners e /Dinners/Details/[id].
URL | VERBO | Finalidade |
---|---|---|
/Jantares/ | GET | Exibir uma lista HTML dos próximos jantares. |
/Dinners/Details/[id] | GET | Exiba detalhes sobre um jantar específico. |
Agora adicionaremos métodos de ação para implementar três URLs adicionais: /Dinners/Edit/[id], /Dinners/Create e /Dinners/Delete/[id]. Essas URLs habilitarão o suporte para editar jantares existentes, criar novos Jantares e excluir Jantares.
Daremos suporte a interações de verbo HTTP GET e HTTP POST com essas novas URLs. As solicitações HTTP GET para essas URLs exibirão a exibição HTML inicial dos dados (um formulário preenchido com os dados do Dinner no caso de "editar", um formulário em branco no caso de "criar" e uma tela de confirmação de exclusão no caso de "excluir"). As solicitações HTTP POST para essas URLs salvarão/atualizarão/excluirão os dados do Dinner em nosso DinnerRepository (e de lá para o banco de dados).
URL | VERBO | Finalidade |
---|---|---|
/Dinners/Edit/[id] | GET | Exiba um formulário HTML editável preenchido com dados do Jantar. |
POST | Salve as alterações de formulário para um Jantar específico no banco de dados. | |
/Dinners/Create | GET | Exiba um formulário HTML vazio que permite que os usuários definam novos Jantares. |
POST | Crie um jantar e salve-o no banco de dados. | |
/Dinners/Delete/[id] | GET | Exibir tela de confirmação de exclusão. |
POST | Exclui o jantar especificado do banco de dados. |
Editar Suporte
Vamos começar implementando o cenário de "editar".
O método de ação de edição HTTP-GET
Começaremos implementando o comportamento HTTP "GET" do nosso método de ação de edição. Esse método será invocado quando a URL /Dinners/Edit/[id] for solicitada. Nossa implementação terá a seguinte aparência:
//
// GET: /Dinners/Edit/2
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
return View(dinner);
}
O código acima usa o DinnerRepository para recuperar um objeto Dinner. Em seguida, ele renderiza um modelo view usando o objeto Dinner. Como não passamos explicitamente um nome de modelo para o método auxiliar View(), ele usará o caminho padrão baseado em convenção para resolve o modelo de exibição: /Views/Dinners/Edit.aspx.
Agora vamos criar esse modelo de exibição. Faremos isso clicando com o botão direito do mouse no método Editar e selecionando o comando de menu de contexto "Adicionar Exibição":
Na caixa de diálogo "Adicionar Exibição", indicaremos que estamos passando um objeto Dinner para nosso modelo de exibição como modelo e optaremos por realizar o scaffold automático de um modelo "Editar":
Quando clicarmos no botão "Adicionar", o Visual Studio adicionará um novo arquivo de modelo de exibição "Edit.aspx" para nós no diretório "\Views\Dinners". Ele também abrirá o novo modelo de exibição "Edit.aspx" no editor de código – preenchido com uma implementação inicial de scaffold "Editar", como abaixo:
Vamos fazer algumas alterações no scaffold padrão "editar" gerado e atualizar o modelo de exibição de edição para ter o conteúdo abaixo (que remove algumas das propriedades que não queremos expor):
<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
Edit: <%=Html.Encode(Model.Title)%>
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">
<h2>Edit Dinner</h2>
<%=Html.ValidationSummary("Please correct the errors and try again.") %>
<% using (Html.BeginForm()) { %>
<fieldset>
<p>
<label for="Title">Dinner Title:</label>
<%=Html.TextBox("Title") %>
<%=Html.ValidationMessage("Title", "*") %>
</p>
<p>
<label for="EventDate">EventDate:</label>
<%=Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate))%>
<%=Html.ValidationMessage("EventDate", "*") %>
</p>
<p>
<label for="Description">Description:</label>
<%=Html.TextArea("Description") %>
<%=Html.ValidationMessage("Description", "*")%>
</p>
<p>
<label for="Address">Address:</label>
<%=Html.TextBox("Address") %>
<%=Html.ValidationMessage("Address", "*") %>
</p>
<p>
<label for="Country">Country:</label>
<%=Html.TextBox("Country") %>
<%=Html.ValidationMessage("Country", "*") %>
</p>
<p>
<label for="ContactPhone">ContactPhone #:</label>
<%=Html.TextBox("ContactPhone") %>
<%=Html.ValidationMessage("ContactPhone", "*") %>
</p>
<p>
<label for="Latitude">Latitude:</label>
<%=Html.TextBox("Latitude") %>
<%=Html.ValidationMessage("Latitude", "*") %>
</p>
<p>
<label for="Longitude">Longitude:</label>
<%=Html.TextBox("Longitude") %>
<%=Html.ValidationMessage("Longitude", "*") %>
</p>
<p>
<input type="submit" value="Save"/>
</p>
</fieldset>
<% } %>
</asp:Content>
Quando executarmos o aplicativo e solicitarmos a URL "/Dinners/Edit/1", veremos a seguinte página:
A marcação HTML gerada por nossa exibição é semelhante a abaixo. É HTML padrão – com um <elemento de formulário> que executa um HTTP POST para a URL /Dinners/Edit/1 quando o botão de entrada "Salvar" <type="submit"/> é pressionado. Um elemento de entrada HTML <type="text"/> foi gerado para cada propriedade editável:
Métodos auxiliares Html.BeginForm() e Html.TextBox() Html
Nosso modelo de exibição "Edit.aspx" está usando vários métodos "Auxiliar html": Html.ValidationSummary(), Html.BeginForm(), Html.TextBox() e Html.ValidationMessage(). Além de gerar marcação HTML para nós, esses métodos auxiliares fornecem suporte interno para tratamento de erros e validação.
Método auxiliar Html.BeginForm()
O método auxiliar Html.BeginForm() é o que gera o elemento de formulário> HTML <em nossa marcação. Em nosso modelo de exibição Edit.aspx, você observará que estamos aplicando uma instrução "using" em C# ao usar esse método. A chave aberta indica o início do conteúdo do <formulário> e a chave de fechamento é o que indica o final do <elemento /form> :
<% using (Html.BeginForm()) { %>
<fieldset>
<!-- Fields Omitted for Brevity -->
<p>
<input type="submit" value="Save"/>
</p>
</fieldset>
<% } %>
Como alternativa, se você achar a abordagem de instrução "using" não natural para um cenário como este, poderá usar uma combinação Html.BeginForm() e Html.EndForm() (que faz a mesma coisa):
<% Html.BeginForm(); %>
<fieldset>
<!-- Fields Omitted for Brevity -->
<p>
<input type="submit" value="Save"/>
</p>
</fieldset>
<% Html.EndForm(); %>
Chamar Html.BeginForm() sem parâmetros fará com que ele gere um elemento de formulário que faz um HTTP-POST para a URL da solicitação atual. É por isso que nosso modo de exibição Editar gera um <elemento form action="/Dinners/Edit/1" method="post> ". Como alternativa, poderíamos ter passado parâmetros explícitos para Html.BeginForm() se quiséssemos postar em uma URL diferente.
Método auxiliar Html.TextBox()
Nossa exibição Edit.aspx usa o método auxiliar Html.TextBox() para gerar <elementos de entrada type="text"/> :
<%= Html.TextBox("Title") %>
O método Html.TextBox() acima usa um único parâmetro – que está sendo usado para especificar os atributos id/name do <elemento type="text"/> de entrada para a saída, bem como a propriedade de modelo da qual preencher o valor da caixa de texto. Por exemplo, o objeto Dinner que passamos para o modo de exibição Editar tinha um valor de propriedade "Title" de ".NET Futures" e, portanto, nossa saída de chamada de método Html.TextBox("Title") : <input id="Title" name="Title" type="text" value=".NET Futures" />.
Como alternativa, podemos usar o primeiro parâmetro Html.TextBox() para especificar a id/nome do elemento e, em seguida, passar explicitamente o valor a ser usado como um segundo parâmetro:
<%= Html.TextBox("Title", Model.Title)%>
Muitas vezes, queremos executar a formatação personalizada no valor que é a saída. O método estático String.Format() interno do .NET é útil para esses cenários. Nosso modelo de exibição Edit.aspx está usando isso para formatar o valor EventDate (que é do tipo DateTime) para que ele não mostre segundos para o tempo:
<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>
Um terceiro parâmetro para Html.TextBox() pode, opcionalmente, ser usado para gerar atributos HTML adicionais. O snippet de código abaixo demonstra como renderizar um atributo size="30" adicional e um atributo class="mycssclass" no <elemento type="text"/> de entrada. Observe como estamos fugindo do nome do atributo de classe usando um caractere "@" porque "class" é um palavra-chave reservado em C#:
<%= Html.TextBox("Title", Model.Title, new { size=30, @class="myclass" } )%>
Implementando o método de ação de edição HTTP-POST
Agora temos a versão HTTP-GET do nosso método de ação Editar implementada. Quando um usuário solicita a URL /Dinners/Edit/1 , ele recebe uma página HTML como a seguinte:
Pressionar o botão "Salvar" causa uma postagem de formulário na URL /Dinners/Edit/1 e envia os valores de formulário de entrada> HTML <usando o verbo HTTP POST. Agora, vamos implementar o comportamento HTTP POST do nosso método de ação de edição – que tratará de salvar o Jantar.
Começaremos adicionando um método de ação "Editar" sobrecarregado ao nosso DinnersController que tem um atributo "AcceptVerbs" que indica que ele manipula cenários HTTP POST:
//
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
...
}
Quando o atributo [AcceptVerbs] é aplicado a métodos de ação sobrecarregados, ASP.NET MVC manipula automaticamente as solicitações de expedição para o método de ação apropriado, dependendo do verbo HTTP de entrada. As solicitações HTTP POST para URLs /Dinners/Edit/[id] irão para o método Editar acima, enquanto todas as outras solicitações de verbo HTTP para URLs /Dinners/Edit/[id] irão para o primeiro método Editar implementado (que não tinha um [AcceptVerbs]
atributo).
Tópico Lateral: Por que diferenciar por meio de verbos HTTP? |
---|
Você pode perguntar : por que estamos usando uma única URL e diferenciando seu comportamento por meio do verbo HTTP? Por que não apenas duas URLs separadas para lidar com o carregamento e salvamento de alterações de edição? Por exemplo: /Dinners/Edit/[id] para exibir o formulário inicial e /Dinners/Save/[id] para manipular a postagem de formulário para salvá-lo? A desvantagem com a publicação de duas URLs separadas é que, nos casos em que postamos em /Dinners/Save/2 e, em seguida, precisamos reexibir o formulário HTML devido a um erro de entrada, o usuário final acabará tendo a URL /Dinners/Save/2 na barra de endereços do navegador (uma vez que essa foi a URL na qual o formulário foi postado). Se o usuário final marcar essa página reproduzida na lista de favoritos do navegador ou copiar/colar a URL e enviar por email para um amigo, ele acabará salvando uma URL que não funcionará no futuro (já que essa URL depende de valores de postagem). Ao expor uma única URL (como: /Dinners/Edit/[id]) e diferenciar o processamento dela pelo verbo HTTP, é seguro que os usuários finais marquem a página de edição e/ou enviem a URL para outras pessoas. |
Recuperando valores de postagem de formulário
Há várias maneiras de acessar parâmetros de formulário postados em nosso método HTTP POST "Edit". Uma abordagem simples é usar apenas a propriedade Request na classe base Controller para acessar a coleção de formulários e recuperar os valores postados diretamente:
//
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
// Retrieve existing dinner
Dinner dinner = dinnerRepository.GetDinner(id);
// Update dinner with form posted values
dinner.Title = Request.Form["Title"];
dinner.Description = Request.Form["Description"];
dinner.EventDate = DateTime.Parse(Request.Form["EventDate"]);
dinner.Address = Request.Form["Address"];
dinner.Country = Request.Form["Country"];
dinner.ContactPhone = Request.Form["ContactPhone"];
// Persist changes back to database
dinnerRepository.Save();
// Perform HTTP redirect to details page for the saved Dinner
return RedirectToAction("Details", new { id = dinner.DinnerID });
}
No entanto, a abordagem acima é um pouco detalhada, especialmente quando adicionamos lógica de tratamento de erros.
Uma abordagem melhor para esse cenário é aproveitar o método auxiliar interno UpdateModel() na classe base Controller. Ele dá suporte à atualização das propriedades de um objeto que passamos usando os parâmetros de formulário de entrada. Ele usa reflexão para determinar os nomes de propriedade no objeto e, em seguida, converte e atribui automaticamente valores a eles com base nos valores de entrada enviados pelo cliente.
Poderíamos usar o método UpdateModel() para simplificar nossa Ação de Edição HTTP-POST usando este código:
//
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
Dinner dinner = dinnerRepository.GetDinner(id);
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id = dinner.DinnerID });
}
Agora podemos visitar a URL /Dinners/Edit/1 e alterar o título do nosso Jantar:
Quando clicarmos no botão "Salvar", executaremos uma postagem de formulário em nossa ação Editar e os valores atualizados serão persistidos no banco de dados. Em seguida, seremos redirecionados para a URL de Detalhes do Jantar (que exibirá os valores salvos recentemente):
Manipulando erros de edição
Nossa implementação HTTP-POST atual funciona bem – exceto quando há erros.
Quando um usuário comete um erro ao editar um formulário, precisamos garantir que o formulário seja reproduzido com uma mensagem de erro informativa que os guia para corrigi-lo. Isso inclui casos em que um usuário final posta entrada incorreta (por exemplo: uma cadeia de caracteres de data malformada), bem como casos em que o formato de entrada é válido, mas há uma violação de regra de negócios. Quando ocorrem erros, o formulário deve preservar os dados de entrada que o usuário inseriu originalmente para que ele não precise recarregar suas alterações manualmente. Esse processo deve repetir quantas vezes forem necessárias até que o formulário seja concluído com êxito.
ASP.NET MVC inclui alguns recursos internos interessantes que facilitam o tratamento de erros e o redisplay de formulário. Para ver esses recursos em ação, vamos atualizar nosso método de ação Editar com o seguinte código:
//
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
foreach (var issue in dinner.GetRuleViolations()) {
ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
}
return View(dinner);
}
}
O código acima é semelhante à nossa implementação anterior, exceto que agora estamos encapsulando um bloco de tratamento de erros try/catch em torno de nosso trabalho. Se ocorrer uma exceção ao chamar UpdateModel() ou quando tentarmos salvar o DinnerRepository (o que gerará uma exceção se o objeto Dinner que estamos tentando salvar for inválido devido a uma violação de regra em nosso modelo), nosso bloco de tratamento de erros catch será executado. Nele, fazemos um loop sobre quaisquer violações de regra que existem no objeto Dinner e as adicionamos a um objeto ModelState (que discutiremos em breve). Em seguida, reproduzemos a exibição.
Para ver esse trabalho, vamos executar novamente o aplicativo, editar um Jantar e alterá-lo para ter um Título vazio, um EventDate de "FALSE" e usar um número de telefone do Reino Unido com um valor de país/região dos EUA. Quando pressionarmos o botão "Salvar", nosso método HTTP POST Edit não poderá salvar o Jantar (porque há erros) e reproduzirá o formulário:
Nosso aplicativo tem uma experiência de erro decente. Os elementos de texto com a entrada inválida são realçados em vermelho e as mensagens de erro de validação são exibidas para o usuário final sobre eles. O formulário também está preservando os dados de entrada que o usuário inseriu originalmente para que ele não precise recarregar nada.
Como, você pode perguntar, isso ocorreu? Como as caixas de texto Título, EventDate e ContactPhone se destacaram em vermelho e souberam gerar os valores de usuário inseridos originalmente? E como as mensagens de erro foram exibidas na lista na parte superior? A boa notícia é que isso não ocorreu por magia - em vez disso, foi porque usamos alguns dos recursos internos ASP.NET MVC que facilitam a validação de entrada e os cenários de tratamento de erros.
Noções básicas sobre ModelState e os métodos auxiliares html de validação
As classes de controlador têm uma coleção de propriedades "ModelState" que fornece uma maneira de indicar que existem erros com um objeto de modelo sendo passado para um View. Entradas de erro na coleção ModelState identificam o nome da propriedade de modelo com o problema (por exemplo: "Title", "EventDate" ou "ContactPhone" e permitem que uma mensagem de erro amigável seja especificada (por exemplo: "O título é necessário").
O método auxiliar UpdateModel() preenche automaticamente a coleção ModelState quando encontra erros ao tentar atribuir valores de formulário a propriedades no objeto modelo. Por exemplo, a propriedade EventDate do objeto Dinner é do tipo DateTime. Quando o método UpdateModel() não pôde atribuir o valor de cadeia de caracteres "BOGUS" a ele no cenário acima, o método UpdateModel() adicionou uma entrada à coleção ModelState indicando que ocorreu um erro de atribuição com essa propriedade.
Os desenvolvedores também podem escrever código para adicionar explicitamente entradas de erro à coleção ModelState, como estamos fazendo abaixo em nosso bloco de tratamento de erros "catch", que está preenchendo a coleção ModelState com entradas baseadas nas Violações de Regra ativas no objeto Dinner:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
foreach (var issue in dinner.GetRuleViolations()) {
ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
}
return View(dinner);
}
}
Integração do Auxiliar html com ModelState
Métodos auxiliares HTML - como Html.TextBox() - marcar a coleção ModelState ao renderizar a saída. Se houver um erro para o item, eles renderizarão o valor inserido pelo usuário e uma classe de erro CSS.
Por exemplo, em nossa exibição "Editar", estamos usando o método auxiliar Html.TextBox() para renderizar o EventDate do nosso objeto Dinner:
<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>
Quando a exibição foi renderizada no cenário de erro, o método Html.TextBox() verificou a coleção ModelState para ver se houve erros associados à propriedade "EventDate" do nosso objeto Dinner. Quando determinou que havia um erro, ele renderizava a entrada do usuário enviada ("FALSE") como o valor e adicionava uma classe de erro css à <marcação type="textbox"/> de entrada gerada:
<input class="input-validation-error"id="EventDate" name="EventDate" type="text" value="BOGUS"/>
Você pode personalizar a aparência da classe de erro css para parecer como desejar. A classe de erro CSS padrão – "input-validation-error" – é definida na folha de estilos \content\site.css e tem a seguinte aparência:
.input-validation-error
{
border: 1px solid #ff0000;
background-color: #ffeeee;
}
Essa regra CSS foi o que fez com que nossos elementos de entrada inválidos fossem realçados como abaixo:
Método Auxiliar Html.ValidationMessage()
O método auxiliar Html.ValidationMessage() pode ser usado para gerar a mensagem de erro ModelState associada a uma propriedade de modelo específica:
<%= Html.ValidationMessage("EventDate")%>
As saídas de código acima: <span class="field-validation-error"> O valor 'FALSE' é inválido</span>
O método auxiliar Html.ValidationMessage() também dá suporte a um segundo parâmetro que permite aos desenvolvedores substituir a mensagem de texto de erro exibida:
<%= Html.ValidationMessage("EventDate","*") %>
O código acima gera: <span class="field-validation-error">*</span> em vez do texto de erro padrão quando um erro está presente para a propriedade EventDate.
Método Auxiliar Html.ValidationSummary()
O método auxiliar Html.ValidationSummary() pode ser usado para renderizar uma mensagem de erro de resumo, acompanhada de uma <lista ul><li/></ul> de todas as mensagens de erro detalhadas na coleção ModelState:
O método auxiliar Html.ValidationSummary() usa um parâmetro de cadeia de caracteres opcional – que define uma mensagem de erro de resumo a ser exibida acima da lista de erros detalhados:
<%= Html.ValidationSummary("Please correct the errors and try again.") %>
Opcionalmente, você pode usar o CSS para substituir a aparência da lista de erros.
Usando um método auxiliar AddRuleViolations
Nossa implementação inicial de Edição HTTP-POST usou uma instrução foreach em seu bloco catch para fazer loop sobre as Violações de Regra do objeto Dinner e adicioná-las à coleção ModelState do controlador:
catch {
foreach (var issue in dinner.GetRuleViolations()) {
ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
}
return View(dinner);
}
Podemos tornar esse código um pouco mais limpo adicionando uma classe "ControllerHelpers" ao projeto NerdDinner e implementando um método de extensão "AddRuleViolations" dentro dele que adiciona um método auxiliar à classe ASP.NET MVC ModelStateDictionary. Esse método de extensão pode encapsular a lógica necessária para preencher o ModelStateDictionary com uma lista de erros RuleViolation:
public static class ControllerHelpers {
public static void AddRuleViolations(this ModelStateDictionary modelState, IEnumerable<RuleViolation> errors) {
foreach (RuleViolation issue in errors) {
modelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
}
}
}
Em seguida, podemos atualizar nosso método de ação editar HTTP-POST para usar esse método de extensão para preencher a coleção ModelState com nossas Violações de Regra de Jantar.
Concluir implementações de método de ação de edição
O código a seguir implementa toda a lógica do controlador necessária para nosso cenário de Edição:
//
// GET: /Dinners/Edit/2
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
return View(dinner);
}
//
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddRuleViolations(dinner.GetRuleViolations());
return View(dinner);
}
}
O bom sobre nossa implementação de Edição é que nem nossa classe Controller nem nosso modelo de Exibição precisam saber nada sobre a validação específica ou as regras de negócios que estão sendo impostas pelo nosso modelo dinner. Podemos adicionar regras adicionais ao nosso modelo no futuro e não precisamos fazer nenhuma alteração de código em nosso controlador ou exibição para que elas tenham suporte. Isso nos fornece a flexibilidade para desenvolver facilmente nossos requisitos de aplicativo no futuro com um mínimo de alterações de código.
Criar suporte
Terminamos de implementar o comportamento "Editar" da nossa classe DinnersController. Agora vamos seguir em frente para implementar o suporte "Criar" nele , o que permitirá que os usuários adicionem novos Jantares.
O método de ação criar HTTP-GET
Começaremos implementando o comportamento HTTP "GET" do nosso método de ação create. Esse método será chamado quando alguém visitar a URL /Dinners/Create . Nossa implementação é semelhante a:
//
// GET: /Dinners/Create
public ActionResult Create() {
Dinner dinner = new Dinner() {
EventDate = DateTime.Now.AddDays(7)
};
return View(dinner);
}
O código acima cria um novo objeto Dinner e atribui sua propriedade EventDate para ser uma semana no futuro. Em seguida, ele renderiza um View baseado no novo objeto Dinner. Como não passamos explicitamente um nome para o método auxiliar View(), ele usará o caminho padrão baseado em convenção para resolve o modelo de exibição: /Views/Dinners/Create.aspx.
Agora vamos criar esse modelo de exibição. Podemos fazer isso clicando com o botão direito do mouse no método de ação Criar e selecionando o comando de menu de contexto "Adicionar Exibição". Na caixa de diálogo "Adicionar Exibição", indicaremos que estamos passando um objeto Dinner para o modelo de exibição e optaremos por realizar o scaffold automático de um modelo "Criar":
Quando clicarmos no botão "Adicionar", o Visual Studio salvará uma nova exibição "Create.aspx" baseada em scaffold no diretório "\Views\Dinners" e a abrirá no IDE:
Vamos fazer algumas alterações no arquivo de scaffold padrão "create" que foi gerado para nós e modificá-lo para parecer com o seguinte:
<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
Host a Dinner
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">
<h2>Host a Dinner</h2>
<%=Html.ValidationSummary("Please correct the errors and try again.") %>
<% using (Html.BeginForm()) {%>
<fieldset>
<p>
<label for="Title">Title:</label>
<%= Html.TextBox("Title") %>
<%= Html.ValidationMessage("Title", "*") %>
</p>
<p>
<label for="EventDate">EventDate:</label>
<%=Html.TextBox("EventDate") %>
<%=Html.ValidationMessage("EventDate", "*") %>
</p>
<p>
<label for="Description">Description:</label>
<%=Html.TextArea("Description") %>
<%=Html.ValidationMessage("Description", "*") %>
</p>
<p>
<label for="Address">Address:</label>
<%=Html.TextBox("Address") %>
<%=Html.ValidationMessage("Address", "*") %>
</p>
<p>
<label for="Country">Country:</label>
<%=Html.TextBox("Country") %>
<%=Html.ValidationMessage("Country", "*") %>
</p>
<p>
<label for="ContactPhone">ContactPhone:</label>
<%=Html.TextBox("ContactPhone") %>
<%=Html.ValidationMessage("ContactPhone", "*") %>
</p>
<p>
<label for="Latitude">Latitude:</label>
<%=Html.TextBox("Latitude") %>
<%=Html.ValidationMessage("Latitude", "*") %>
</p>
<p>
<label for="Longitude">Longitude:</label>
<%=Html.TextBox("Longitude") %>
<%=Html.ValidationMessage("Longitude", "*") %>
</p>
<p>
<input type="submit" value="Save"/>
</p>
</fieldset>
<% }
%>
</asp:Content>
E agora, quando executarmos nosso aplicativo e acessarmos a URL "/Dinners/Create" no navegador, ele renderizará a interface do usuário como abaixo em nossa implementação Criar ação:
Implementando o método de ação criar HTTP-POST
Temos a versão HTTP-GET do nosso método de ação Criar implementado. Quando um usuário clica no botão "Salvar", ele executa uma postagem de formulário na URL /Dinners/Create e envia os valores de formulário de entrada> HTML <usando o verbo HTTP POST.
Agora, vamos implementar o comportamento HTTP POST do nosso método de ação create. Começaremos adicionando um método de ação "Criar" sobrecarregado ao dinnersController que tem um atributo "AcceptVerbs" que indica que ele lida com cenários HTTP POST:
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {
...
}
Há várias maneiras de acessar os parâmetros de formulário postados em nosso método "Create" habilitado para HTTP-POST.
Uma abordagem é criar um novo objeto Dinner e, em seguida, usar o método auxiliar UpdateModel() (como fizemos com a ação Editar) para preenchê-lo com os valores de formulário postados. Em seguida, podemos adicioná-lo ao nosso DinnerRepository, mantê-lo no banco de dados e redirecionar o usuário para nossa ação Detalhes para mostrar o Jantar recém-criado usando o código abaixo:
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {
Dinner dinner = new Dinner();
try {
UpdateModel(dinner);
dinnerRepository.Add(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new {id=dinner.DinnerID});
}
catch {
ModelState.AddRuleViolations(dinner.GetRuleViolations());
return View(dinner);
}
}
Como alternativa, podemos usar uma abordagem em que temos nosso método de ação Create() tomar um objeto Dinner como um parâmetro de método. ASP.NET MVC criará automaticamente uma instância de um novo objeto Dinner para nós, preencherá suas propriedades usando as entradas do formulário e o passará para nosso método de ação:
//
//
// 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.AddRuleViolations(dinner.GetRuleViolations());
}
}
return View(dinner);
}
Nosso método de ação acima verifica se o objeto Dinner foi preenchido com êxito com os valores de postagem de formulário verificando a propriedade ModelState.IsValid. Isso retornará false se houver problemas de conversão de entrada (por exemplo: uma cadeia de caracteres de "FALSE" para a propriedade EventDate) e, se houver algum problema, nosso método de ação exibirá novamente o formulário.
Se os valores de entrada forem válidos, o método de ação tentará adicionar e salvar o novo Jantar no DinnerRepository. Ele encapsula esse trabalho em um bloco try/catch e reproduz o formulário se houver violações de regra de negócios (o que faria com que o método dinnerRepository.Save() gerasse uma exceção).
Para ver esse comportamento de tratamento de erros em ação, podemos solicitar a URL /Dinners/Create e preencher detalhes sobre um novo Jantar. A entrada ou os valores incorretos farão com que o formulário de criação seja exibido novamente com os erros realçados como abaixo:
Observe como nosso formulário Criar está respeitando exatamente as mesmas regras de validação e de negócios que nosso formulário editar. Isso ocorre porque nossas regras de validação e de negócios foram definidas no modelo e não foram inseridas na interface do usuário ou no controlador do aplicativo. Isso significa que, posteriormente, podemos alterar/evoluir nossas regras de validação ou de negócios em um único lugar e fazer com que elas se apliquem em todo o nosso aplicativo. Não precisaremos alterar nenhum código em nossos métodos de ação Editar ou Criar para respeitar automaticamente quaisquer novas regras ou modificações nas existentes.
Quando corrigirmos os valores de entrada e clicarmos no botão "Salvar" novamente, nossa adição ao DinnerRepository terá êxito e um novo Jantar será adicionado ao banco de dados. Em seguida, seremos redirecionados para a URL /Dinners/Details/[id] – em que teremos detalhes sobre o jantar recém-criado:
Excluir suporte
Agora, vamos adicionar o suporte a "Delete" ao dinnersController.
O método de ação de exclusão HTTP-GET
Começaremos implementando o comportamento HTTP GET do nosso método de ação de exclusão. Esse método será chamado quando alguém visitar a URL /Dinners/Delete/[id] . Abaixo está a implementação:
//
// HTTP GET: /Dinners/Delete/1
public ActionResult Delete(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (dinner == null)
return View("NotFound");
else
return View(dinner);
}
O método de ação tenta recuperar o Jantar a ser excluído. Se o Jantar existir, ele renderizará um View com base no objeto Dinner. Se o objeto não existir (ou já tiver sido excluído), ele retornará uma Exibição que renderiza o modelo de exibição "NotFound" que criamos anteriormente para nosso método de ação "Detalhes".
Podemos criar o modelo de exibição "Excluir" clicando com o botão direito do mouse no método de ação Excluir e selecionando o comando de menu de contexto "Adicionar Exibição". Na caixa de diálogo "Adicionar Exibição", indicaremos que estamos passando um objeto Dinner para nosso modelo de exibição como modelo e optaremos por criar um modelo vazio:
Quando clicarmos no botão "Adicionar", o Visual Studio adicionará um novo arquivo de modelo de exibição "Delete.aspx" para nós no diretório "\Views\Dinners". Adicionaremos um HTML e um código ao modelo para implementar uma tela de confirmação de exclusão, como abaixo:
<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
Delete Confirmation: <%=Html.Encode(Model.Title) %>
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">
<h2>
Delete Confirmation
</h2>
<div>
<p>Please confirm you want to cancel the dinner titled:
<i> <%=Html.Encode(Model.Title) %>? </i>
</p>
</div>
<% using (Html.BeginForm()) { %>
<input name="confirmButton" type="submit" value="Delete" />
<% } %>
</asp:Content>
O código acima exibe o título do Jantar a ser excluído e gera um <elemento de formulário> que faz um POST para a URL /Dinners/Delete/[id] se o usuário final clicar no botão "Excluir" dentro dele.
Quando executamos nosso aplicativo e acessamos a URL "/Dinners/Delete/[id]" para um objeto Dinner válido, ele renderiza a interface do usuário como abaixo:
Tópico lateral: por que estamos fazendo um POST? |
---|
Você pode perguntar – por que passamos pelo esforço de criar um <formulário> em nossa tela de confirmação Excluir? Por que não usar apenas um hiperlink padrão para vincular a um método de ação que faz a operação de exclusão real? O motivo é que queremos ter cuidado para proteger-se contra rastreadores da Web e mecanismos de pesquisa descobrindo nossas URLs e inadvertidamente fazendo com que os dados sejam excluídos quando eles seguem os links. AS URLs baseadas em HTTP-GET são consideradas "seguras" para que elas acessem/rastreiem e não devem seguir as HTTP-POST. Uma boa regra é garantir que você sempre coloque operações destrutivas ou de modificação de dados por trás de solicitações HTTP-POST. |
Implementando o método de ação de exclusão HTTP-POST
Agora temos a versão HTTP-GET do nosso método de ação Delete implementado, que exibe uma tela de confirmação de exclusão. Quando um usuário final clicar no botão "Excluir", ele executará uma postagem de formulário na URL /Dinners/Dinner/[id] .
Agora, vamos implementar o comportamento HTTP "POST" do método de ação de exclusão usando o código abaixo:
//
// HTTP POST: /Dinners/Delete/1
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmButton) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (dinner == null)
return View("NotFound");
dinnerRepository.Delete(dinner);
dinnerRepository.Save();
return View("Deleted");
}
A versão HTTP-POST do nosso método de ação Delete tenta recuperar o objeto dinner a ser excluído. Se ele não conseguir encontrá-lo (porque já foi excluído), ele renderizará nosso modelo "NotFound". Se encontrar o Jantar, ele o excluirá do DinnerRepository. Em seguida, ele renderiza um modelo "Excluído".
Para implementar o modelo "Excluído", clicaremos com o botão direito do mouse no método de ação e escolheremos o menu de contexto "Adicionar Exibição". Nomearemos nosso modo de exibição como "Excluído" e o faremos ser um modelo vazio (e não usaremos um objeto de modelo fortemente tipado). Em seguida, adicionaremos algum conteúdo HTML a ele:
<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
Dinner Deleted
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">
<h2>Dinner Deleted</h2>
<div>
<p>Your dinner was successfully deleted.</p>
</div>
<div>
<p><a href="/dinners">Click for Upcoming Dinners</a></p>
</div>
</asp:Content>
E agora, quando executarmos nosso aplicativo e acessarmos a URL "/Dinners/Delete/[id]" para um objeto Dinner válido, ele renderizará nossa tela de confirmação de exclusão do Jantar como abaixo:
Quando clicarmos no botão "Excluir", ele executará um HTTP-POST na URL /Dinners/Delete/[id] , que excluirá o Jantar do nosso banco de dados e exibirá nosso modelo de exibição "Excluído":
Segurança de associação de modelo
Discutimos duas maneiras diferentes de usar os recursos internos de model-binding do ASP.NET MVC. O primeiro usando o método UpdateModel() para atualizar propriedades em um objeto de modelo existente e o segundo usando ASP.NET suporte do MVC para passar objetos de modelo como parâmetros de método de ação. Ambas as técnicas são muito poderosas e extremamente úteis.
Esse poder também traz consigo a responsabilidade. É importante sempre ser paranóico com a segurança ao aceitar qualquer entrada do usuário, e isso também é verdadeiro ao associar objetos à entrada de formulário. Você deve ter cuidado para sempre codificar html quaisquer valores inseridos pelo usuário para evitar ataques de injeção html e JavaScript e ter cuidado com ataques de injeção de SQL (observação: estamos usando LINQ to SQL para nosso aplicativo, que codifica automaticamente parâmetros para evitar esses tipos de ataques). Você nunca deve confiar apenas na validação do lado do cliente e sempre empregar a validação do lado do servidor para se proteger contra hackers que tentam enviar valores falsos.
Um item de segurança adicional para garantir que você pense ao usar os recursos de associação do ASP.NET MVC é o escopo dos objetos que você está associando. Especificamente, você deseja ter certeza de entender as implicações de segurança das propriedades que você está permitindo que sejam associadas e certifique-se de permitir que apenas as propriedades que realmente devem ser atualizáveis por um usuário final sejam atualizadas.
Por padrão, o método UpdateModel() tentará atualizar todas as propriedades no objeto de modelo que correspondem aos valores de parâmetro de formulário de entrada. Da mesma forma, os objetos passados como parâmetros de método de ação também podem, por padrão, ter todas as suas propriedades definidas por meio de parâmetros de formulário.
Bloquear a associação por uso
Você pode bloquear a política de associação por uso fornecendo uma "lista de inclusão" explícita de propriedades que podem ser atualizadas. Isso pode ser feito passando um parâmetro de matriz de cadeia de caracteres extra para o método UpdateModel() como abaixo:
string[] allowedProperties = new[]{ "Title","Description",
"ContactPhone", "Address",
"EventDate", "Latitude",
"Longitude"};
UpdateModel(dinner, allowedProperties);
Os objetos passados como parâmetros de método de ação também dão suporte a um atributo [Bind] que permite que uma "lista de inclusão" de propriedades permitidas seja especificada como abaixo:
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create( [Bind(Include="Title,Address")] Dinner dinner ) {
...
}
Bloquear a associação por tipo
Você também pode bloquear as regras de associação por tipo. Isso permite que você especifique as regras de associação uma vez e as aplique em todos os cenários (incluindo updateModel e cenários de parâmetro de método de ação) em todos os controladores e métodos de ação.
Você pode personalizar as regras de associação por tipo adicionando um atributo [Bind] a um tipo ou registrando-o no arquivo Global.asax do aplicativo (útil para cenários em que você não possui o tipo). Em seguida, você pode usar as propriedades Include e Exclude do atributo Bind para controlar quais propriedades são associáveis para a classe ou interface específica.
Usaremos essa técnica para a classe Dinner em nosso aplicativo NerdDinner e adicionaremos um atributo [Bind] a ela que restringe a lista de propriedades associáveis ao seguinte:
[Bind(Include="Title,Description,EventDate,Address,Country,ContactPhone,Latitude,Longitude")]
public partial class Dinner {
...
}
Observe que não estamos permitindo que a coleção RSVPs seja manipulada por meio da associação, nem permitimos que as propriedades DinnerID ou HostedBy sejam definidas por meio da associação. Por motivos de segurança, manipularemos apenas essas propriedades específicas usando código explícito em nossos métodos de ação.
CRUD Wrap-Up
ASP.NET MVC inclui uma série de recursos internos que ajudam na implementação de cenários de postagem de formulário. Usamos uma variedade desses recursos para fornecer suporte à interface do usuário CRUD sobre nosso DinnerRepository.
Estamos usando uma abordagem focada em modelo para implementar nosso aplicativo. Isso significa que toda a lógica de validação e regra de negócios é definida em nossa camada de modelo e não em nossos controladores ou exibições. Nem nossa classe Controller nem nossos modelos de Exibição sabem nada sobre as regras de negócios específicas que estão sendo impostas por nossa classe de modelo Dinner.
Isso manterá nossa arquitetura de aplicativo limpo e facilitará o teste. Podemos adicionar regras de negócios adicionais à nossa camada de modelo no futuro e não precisamos fazer nenhuma alteração de código em nosso Controlador ou Exibição para que elas tenham suporte. Isso nos fornecerá muita agilidade para evoluir e mudar nosso aplicativo no futuro.
Nosso DinnersController agora habilita as listagens/detalhes do jantar, bem como o suporte para criar, editar e excluir. O código completo para a classe pode ser encontrado abaixo:
public class DinnersController : Controller {
DinnerRepository dinnerRepository = new DinnerRepository();
//
// GET: /Dinners/
public ActionResult Index() {
var dinners = dinnerRepository.FindUpcomingDinners().ToList();
return View(dinners);
}
//
// GET: /Dinners/Details/2
public ActionResult Details(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (dinner == null)
return View("NotFound");
else
return View(dinner);
}
//
// GET: /Dinners/Edit/2
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
return View(dinner);
}
//
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id= dinner.DinnerID });
}
catch {
ModelState.AddRuleViolations(dinner.GetRuleViolations());
return View(dinner);
}
}
//
// GET: /Dinners/Create
public ActionResult Create() {
Dinner dinner = new Dinner() {
EventDate = DateTime.Now.AddDays(7)
};
return View(dinner);
}
//
// 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.AddRuleViolations(dinner.GetRuleViolations());
}
}
return View(dinner);
}
//
// HTTP GET: /Dinners/Delete/1
public ActionResult Delete(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (dinner == null)
return View("NotFound");
else
return View(dinner);
}
//
// HTTP POST: /Dinners/Delete/1
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmButton) {
Dinner dinner = dinnerRepository.GetDinner(id);
if (dinner == null)
return View("NotFound");
dinnerRepository.Delete(dinner);
dinnerRepository.Save();
return View("Deleted");
}
}
Próxima etapa
Agora temos suporte básico para CRUD (Criar, Ler, Atualizar e Excluir) implementado em nossa classe DinnersController.
Agora vamos examinar como podemos usar as classes ViewData e ViewModel para habilitar uma interface do usuário ainda mais rica em nossos formulários.