V následujících dvou dílech nás čeká dodělání implementace CRUDu – Create, Read, Update, Delete - tedy implementace podpory pro vytváření, čtení, upravování a mazání večeří. Čtení už máme z minula, dnes se zaměříme na upravování existujících večeří.
Momentálně umí DinnersController zachytávat dvě URL: „/Dinners/“ a „/Dinners/Details/{id}“. Dnes přidáme jednu další akční metodu pro podporu upravování existujících večeří. Pojďme rovnou na to.
Akční metoda Edit
Nová akční metoda se bude jmenovat Edit a zpočátku bude podobná metodě Details, také zatím jen bude vracet jednu konkrétní večeři, podle zadaného ID.
// GET: /Dinners/Edit/2
public ActionResult Edit(int id)
{
Dinner dinner = dinnerRepository.GetDinner(id);
return View(dinner);
}
Hned přidáme view šablonu Edit kliknutím dovnitř metody Edit a zvolením položky Add View. Objevivší se dialog nastavíme podle obrázku, proč co děláme už byste měli vědět z minulého dílu:
Visual Studio opět vygeneruje kód, tentokrát pro upravovací dialog. Kód trochu upravíme, některé vlastnosti nechceme zobrazit a chceme, aby s námi mluvil česky:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<NerdDinner.Models.Dinner>" %>
<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
Upravit: <%=Html.Encode(Model.Title)%>
</asp:Content>
<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">
<h2>Upravit večeři</h2>
<%=Html.ValidationSummary("Opravte prosím všechny chyby a zkuste to znovu.") %>
<% using (Html.BeginForm()) { %>
<fieldset>
<p>
<label for="Title">Název večeře:</label>
<%=Html.TextBox("Title") %>
<%=Html.ValidationMessage("Title", "*") %>
</p>
<p>
<label for="EventDate">Datum:</label>
<%=Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate))%>
<%=Html.ValidationMessage("EventDate", "*") %>
</p>
<p>
<label for="Description">Popis:</label>
<%=Html.TextArea("Description") %>
<%=Html.ValidationMessage("Description", "*")%>
</p>
<p>
<label for="Address">Adresa:</label>
<%=Html.TextBox("Address") %>
<%=Html.ValidationMessage("Address", "*") %>
</p>
<p>
<label for="Country">Země:</label>
<%=Html.TextBox("Country") %>
<%=Html.ValidationMessage("Country", "*") %>
</p>
<p>
<label for="ContactPhone">Telefon:</label>
<%=Html.TextBox("ContactPhone") %>
<%=Html.ValidationMessage("ContactPhone", "*") %>
</p>
<p>
<label for="Latitude">Zem. šířka:</label>
<%=Html.TextBox("Latitude") %>
<%=Html.ValidationMessage("Latitude", "*") %>
</p>
<p>
<label for="Longitude">Zem. délka:</label>
<%=Html.TextBox("Longitude") %>
<%=Html.ValidationMessage("Longitude", "*") %>
</p>
<p>
<input type="submit" value="Uložit"/>
</p>
</fieldset>
<% } %>
</asp:Content>
Aplikaci teď můžeme spustit a zadat URL např. „/Dinners/Edit/1“, zobrazí se nám podobná věc jako na obrázku:
Metody Html.BeginForm a Html.TextBox
V kódu výše používáme dvě pomocné metody pro generování HTML kódu – Html.BeginForm a Html.TextBox (ještě používáme metody Html.ValidationMessage a Html.ValidationSummary, ale jejich význam je asi jasný - validace). Teď trochu podrobněji probereme první dvě metody.
Html.BeginForm
Tato metoda vygeneruje formulářový element <form>
. Všimněte si, že v kódu používáme klíčové slovo using a složenou závorku „{“, která je místo otevíracího tagu. Druhá složená závorka na konci vygeneruje uzavírací tag </form>
.
<% using (Html.BeginForm()) { %>
<fieldset>
<p>
<input type="submit" value="Uložit"/>
</p>
</fieldset>
<% } %>
Existuje ještě druhá možnost – nepoužít using a závorky, ale místo toho metody Html.BeginForm a Html.EndForm pro otevírací, resp. zavírací tag.
<% Html.BeginForm(); %>
<fieldset>
<p>
<input type="submit" value="Uložit"/>
</p>
</fieldset>
<% Html.EndForm(); %>
Volání Html.BeginForm bez parametrů vytvoří formulář, který dělá POST na současnou URL. Proto také vygenerované HTML je ve tvaru <form action="/Dinners/Edit/1" method="post">
.
Html.TextBox
Většinu polí v kódu výše generujeme tímto kódem:
<%= Html.TextBox("Title") %>
Tato metoda bere jeden parametr, který je použit pro specifikování atributů „id“ a „name“ výsledného TextBoxu a také je to název vlastnosti, kterou se TextBox naplní. Příklad: Objekt, který jsme předali šabloně Edit měl vlastnost Title a v ní hodnotu „.NET Futures“, takže volání metody Html.TextBox vygeneruje takovéto HTML:
<input id="Title" name="Title" type="text" value=".NET Futures" />
Název vlastnosti bychom ale také mohli dosadit jako druhý parametr metody:
<%= Html.TextBox("Title", Model.Title)%>
Často také chceme nějak naformátovat výstupní text, například kvůli formátu data. K tomu slouží dobře známá metoda String.Format. Tímto způsobem formátujeme právě TextBox EventDate tak, aby se v datu neukazovaly vteřiny:
<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>
Třetí parametr metody Html.TextBox můžeme použít pro specifikování dalších HTML atributů. V ukázce níže je nutné před „class“ napsat zavináč, protože „class“ je klíčové slovo jazyka C#:
<%= Html.TextBox("Title", Model.Title, new { size=30, @class="myclass" } )%>
Implementace HTTP-POST
HTTP-GET verzi naší akční metody Edit už máme implementovanou – když uživatel zadá URL „/Dinners/Edit/1“, tak dostane HTML stránku, kde může upravit zadanou večeři.
Kliknutí na tlačítku „Save“ způsobí, že se data z formuláře pošlou POST požadavkem. Na nás teď je, abychom vytvořili upravenou metodu Edit tak, aby uměla tento požadavek přijmout a uložit změny ve večeři.
Přidáme do DinnersControlleru přetíženou verzi metody Edit, která bude označená atributem AcceptVerbs a bude přijímat jeden parametr navíc:
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues)
{
//...
}
Když přetíženou verzi metody označíme atributem AcceptVerbs, tak ASP.NET MVC automaticky přesměruje požadavek na příslušnou metodu. Všechny POST požadavky tak bude zpracovávat naše nová metoda a všechny ostatní požadavky dostane její neoznačená sestřička.
Získání dat z formuláře
Je několik způsobů, jak přistupovat k datům z formuláře poslaných POSTem. Jde to jednoduše přes vlastnost Request v bázové třídě Controller:
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues)
{
// získat existující večeři
Dinner dinner = dinnerRepository.GetDinner(id);
// upravit ji pomocí hodnot z formuláře
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"];
// uložit změny
dinnerRepository.Save();
// přesměrovat uživatele na detaily o upravené večeři
return RedirectToAction("Details", new { id = dinner.DinnerID });
}
Tento kód je sice jednoduchý na porozumění, ale není úplně ideální. Je přece jen trochu víc „ukecaný“, což se projeví ve chvíli, kdy bychom chtěli zachytávat validační chyby.
Lepším způsobem je využití vestavěné metody UpdateModel. Umí totiž upravit vlastnosti objektu, který jí předáme, podle příchozích parametrů z POSTu. Část kódu, kde probíhají úpravy, tak můžeme zjednodušit doslova na jeden řádek:
// 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 });
}
Zkusme aplikaci spustit, zadat URL „Dinners/Edit/2“ a upravit název večeře. Po kliknutí na tlačítko „Uložit“ se změny skutečně uloží.
Zpracování chyb
Všechno teď funguje, jak má. Alespoň dokud uživatel nezadá něco špatně.
Pokud k tomu dojde, tak musíme uživateli ukázat chybovou hlášku, která jej navede k tomu, co opravit. Zároveň s tím by měl formulář zůstat vyplněný, aby uživatel nemusel vše vypisovat znovu. ASP.NET MVC obsahuje pár hezkých věcí pro usnadnění práce, upravme naši přetíženou metodu Edit tak, aby vypadala následovně:
// 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);
}
}
Tento kód se od původního moc neliší, jen jsme přidali try/catch bloky a v catch bloku přidáváme všechny nalezené chyby do ModelState objektu (co to je, si řekneme za chvíli) a nakonec zobrazíme view.
Abychom to zkusili v praxi, aplikaci spustíme, zkusíme upravit nějakou večeři a zadat někam něco špatně. Jakmile se pokusíme večeři uložit, objeví se výčet chybových hlášek:
Jak sami vidíte, špatně vyplněná okénka formuláře se zvýrazní červeně, objeví se u nich hvězdička a nahoře se vypíší jednotlivé chyby (nefunguje to pro datum, protože jsme žádnou kontrolu formátu neimplementovali). Zároveň s tím zůstává formulář vyplněný, aby uživatel nemusel nic psát znovu.
Asi se ptáte, jak se všechno tohle udělalo, když my jsme vlastně nenapsali ani řádek kódu pro obarvení polí nebo pro vypsání chyb? Není v tom žádná magie, jen jsme využili vestavěných nástrojů ASP.NET MVC.
ModelState
Controllery třídy mohou využít kolekce ModelState, do které ukládáme chyby na modelu (večeři). Záznam v ModelState se skládá z názvu vlastnosti, která je chybová, a z chybové hlášky.
Metoda UpdateModel automaticky naplní kolekci ModelState, jakmile narazí na chybu při přiřazování hodnot z formuláře do vlastností modelu. Například vlastnost EventDate je typu DataTime, ale my se do ní snažíme přiřadit typ string („bla“). Když nebyla metoda UpdateModel schopná tuto hodnotu dosadit, přidala do kolekce ModelState záznam.
Chyby můžeme přidávat i explicitně, jako tu děláme uvnitř catch bloku v předchozím kódu.
Integrace pomocných metod s ModelState
Pomocné HTML metody jako ty, které jsme si v tomto díle už ukázali, kontrolují kolekci ModelState před tím, než vypíší výstup. Pokud najdou v kolekci nějakou chybu, vypíší chybovou hlášku a aplikují CSS styl (proto jsou chybová pole v naší aplikaci červená).
Příklad: Pro vypsání obsahu vlastnosti EventDate používáme tento kód:
<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>
Při renderování se metoda Html.TextBox podívá do kolekce ModelState, najde chybu, protože jsme do pole zadali string, a vygeneruje takovéto HTML:
<input class="input-validation-error" id="EventDate" name="EventDate" type="text" value="bla"/>
Vzhled chyby můžeme upravit jednoduchými změnami CSS souboru v adresáři „contentsite.css“. Výchozí CSS třída „input-validation-error“ je definovaná následovně:
.input-validation-error
{
border: 1px solid #ff0000;
background-color: #ffeeee;
}
Metoda Html.ValidationMessage
Tato metoda může být použita k vypsání chybové hlášky z ModelState týkající se nějaké vlastnosti:
<%= Html.ValidationMessage("EventDate")%>
Tento kód v případě chyby vygeneruje následující HTML s defaultní chybovou hláškou:
<span class="field-validation-error"> The value ‘bla' is invalid</span>
Tato metoda má také přetíženou verzi, která přijímá druhý parametr. To je právě ta verze, kterou používáme i my v naší aplikaci. Do druhého parametru se totiž zadá chybová hláška a ta přepíše tu výchozí (my používáme hvězdičku):
<%= Html.ValidationMessage("EventDate","*") %>
Výsledné HTML pak vypadá takto:
<span class="field-validation-error">*</span>
Metoda Html.ValidationSummary
Tato metoda se používá k vypsání shrnující chybové hlášky, společně se seznamem jednotlivých chyb uložených v kolekci ModelState. V naší view šabloně je to tento řádek:
<%=Html.ValidationSummary("Opravte prosím všechny chyby a zkuste to znovu.") %>
Pomocí CSS můžeme dále měnit styly vypsaného textu.
Použití pomocné metody AddRuleViolations
Původní implementace HTTP-POST Edit metody obsahovala uvnitř catch bloku cyklus foreach, který procházel jednotlivá porušení pravidel a přidával je do kolekce ModelState:
foreach(var issue in dinner.GetRuleViolations())
{
ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
}
Tento kód můžeme udělat trochu přehlednější tím, že přidáme do projektu třídu „ControllerHelpers“, ve které budeme později shromažďovat všechny námi vytvořené pomocné metody, a přidáme do ní rozšiřující metodu (extension method; novinka v .NET 3.5), která se bude jmenovat AddRuleViolations. Tato metoda jen obalí foreach cyklus, který plní kolekci ModelState.
Vytvoříme si v projektu NerdDinner nový adresář Helpers, do něj přidáme novou třídu ControllerHelpers a tam umístíme tento kód:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using NerdDinner.Models;
namespace NerdDinner.Controllers
{
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);
}
}
}
}
Teď už nám nic nebrání poupravit catch blok přetížené metody Edit:
catch
{
ModelState.AddRuleViolations(dinner.GetRuleViolations());
return View(dinner);
}
Hezké je, že naše controllery ani views nemusí vědět vůbec nic o jakýchkoliv validačních pravidlech. To nám umožňuje přidat kdykoliv se nám zachce další pravidla bez toho, abychom museli něco dál měnit.
Dnes to bylo dlouhé, že? Ale nebojte, v příštím díle už toho tolik nebude, jen přidáme podporu pro mazání a přidávání večeří, která bude hodně vycházet z dnešního dílu. Zatím na viděnou.