Vítám vás u čtvrtého dílu seriálu ASP MVC – from zero to hero. Přechozí díly seriálu můžete nalézt zde:
http://www.dotnetportal.cz/clanky/autor/montella
Dnes se podíváme na architekturu ASP MVC - konkrétně první 2 části této svaté trojice – tedy View a Controller. Mějte prosím na paměti, že se jedná o základy a mírně pokročilé věci ohledně této dvojice, složitější věci ohledně architektury atd., přijdou až v dalších dílech. Zároveň bych rád upozornil na to, že tento díl seriálu (a následující přibližně 4), píši již podruhé, jelikož mi to editor bohužel smazal. Toto mě vedlo k rozhodnutí, že budu jednotlivé díly seriálu vydávat sice menší, ale při větší frekvenci.
Na co se tedy dnes koukneme?
-
Těstoviny a pohádka o nich.
-
architektura MVC,
-
Controller, View, Layout, RenderSection, RenderBody, PartialView,
-
Razor, ViewEngine, syntaxe,
-
komunikace View a Controlleru, Data pro View,
-
ViewModel,
-
Model? ViewModel? View Specific Model? Data Object?
V dalším díle naleznete:
Scaffolding, UIHint
Bundling
Validace a další aspekty
Lokalizace
Helpery, Vlastní helpery, Fluent API
XHR – AJAX
Tipy a Triky pro View
Začněme tedy od začátku. Kdysi nebyl jeden návrhový vzor MVC,…
Pohádka o škaredých těstovinách
Architektura kódu se neustále vyvíjí, v následující části se koukneme na to, jaké jsou její horší varianty a také na to, čeho si přejeme dosáhnout. Podrobněji se na architekturu ideálního řešení koukneme v dalších dílech seriálu, kde se budeme zabývat tzv. SOC (seperation of concerns).
Spaghetti
Jeden z prvních “těstovinových hororů” v kódu. Vezmeme všechno co máme a napereme to ideálně všechno do 1 souboru, ať je to na jednom místě. Nebudeme rozlišovat co jaký kus kódu dělá, jednoduše to dáme vše dohromady a vyplivneme do prvního souboru, který nás napadne. Například index.php. Tento přístup přináší několik nevýhod. Jedna z nich je míchání věcí, které spolu nijak nesouvisí. Bohužel se mi nepovedlo sehnat ukázku “lepšího” kódu - věřte ale, že na internetu naleznete i ukázky, kde se v tomto samém souboru leze do databáze, provádějí výpočty a podobné věci.
Tento přístup přináší obrovské množství nevýhod. Těžko se udržuje, těžko se upravuje a těžko se testuje. Kromě toho vám hrozí, že vám za takovýto kód někdo v temné uličce roztrhá diplom,.. tedy snad pouze diplom. Takovýto kód lze popsat jednoduše pomocí špaget. Dlouhý nepřehledný kód, nikdo neví kde končí, kde začíná, jaká je jeho zodpovědnost a co dělá. Chcete vytáhnout jednu špagetu, ale musíte dát na stranu nejdříve všechny ostatní.
Hodnocení děsivosti: naprosto děsivé
Lasagne
Opakem kódu předchozího, kdy máte vše v jednom souboru a v jedné třídě je lasagne kód, tedy kde je funkcionalita rozdělena ne nepochopitelně mnoho vrstev, obsahující naprosto zbytečnou abstrakci a celkově ´je kód tzv. “overengineered”. Na světě není krásnější ukázky, než Enterprise implementace hry FizzBuzz, kterou pro pobavení naleznete na této adrese. Jedná se o implementaci jednoduché hry, která se zadává občas na přijímacích pohovorech pro zjištění, zda zvládáte alespoň základní podmíněné větvení programu. Na screenshotu můžete vidět například implementaci “vložení” nové řádky do standardního výpisu.
Lasagne kód = kód který je zbytečně složitý, než by musel být a nepřináší žádné výhody (snad kromě jistě přínosného know-how v podobě výuky nových sprostých slov u studie kódu). Lasagne jsou tedy v kódu stejně špatné jako špagety.
Hodnocení děsivosti: naprosto děsivé
Ravioli
Kód který je správně rozdělený na jednotlivé vrstvy a nemíchá dohromady věci, které k sobě nepatří – například UI, aplikační logiku, práci s daty atp., se dá přirovnat k Ravioli. Jednotlivé části jsou striktně odděleny, je přesně definováno, jaká část kódu má dělat co. Za odměnu dostanete přehledný kód, který je lehce rozšiřitelný a čitelný. Zároveň vám nebude problém ho otestovat, případně jakoukoliv část vzít a zaměnit ji za jinou. Klíčem k tomuto je dbání na několik principů, které jsou shrnuty v SOLID, SOC a pomůže nám k tomu například abstrakce. Blíže si o tom všem ještě povíme.
Hodnocení děsivosti: pohádka
architektura MVC
V našem kódu se pokusíme co nejvíce psát Ravioli kód. K tomu nám pomůže několik návrhových vzorů. Prvním z nich je návrhový vzor MVC, který je - jak již název napovídá, základem ASP MVC. Tento návrhový vzor se dá vyobrazit tímto diagramem:
Tvoří ho 3 role. Model, View a Controller. Každá tato část má za úkol jiné věci – má jinou zodpovědnost a neměli bychom je míchat. V dnešní části se budeme věnovat View a trochu se koukneme i na Controllery. K čemu jsou tyto jednotlivé části tedy určeny?
View
View je ta vrstva, která slízne smetanu za vaši práci, protože ta jediná je pro uživatele vaší aplikace vidět. View je vaše prezentační vrstva. Má za úkol vzít určitá data, která dostane od Controlleru a ty nějakým způsobem zobrazit. Kromě tohoto úkolu, musí ještě dávat vědět Controlleru, že došlo k nějaké akci v klientovi – kterým je u webových aplikací prohlížeč.
Pokud uživatel na něco klikne, případně nastane nějaká jiná událost, prohlížeč zašle požadavek na server, kde se dostane přes mašinerii routování až ke Controlleru. Controller se na základě vstupních dat od klienta nějakým způsobem zachová (většinou zavolá nějakou metodu v Modelu, odpověď Modelu přetransformuje a vrátí zpět jako odpověď klientovi.)
Zatímco u ostatních dvou vrstev uvidíte pouze C# kód, View se bude skládat především ze směsi HTML, CSS, JavaScriptu a speciální syntaxe šablonovacího enginu Razor. I ve View lze psát C# kód, není to ale dobrý přístup a pokud vám View obsahuje C#´kód, většinou to indikuje nepochopení celého principu MVC.
View tedy komunikuje s Controllery a k Modelu nemá přímý přístup.
Model
Model je místem, kde by se měla vyskytovat byznys logika vaší aplikace, odkud jsou věci řízeny. Většinou není model takto plochý, ale bývá rozdělen na několik vrstev, které zajišťují logiku, přístup do databáze, mapování na / z byznys objektů (DTO) a tak podobně. Tomuto se budeme věnovat za pár dílů. Pro začátek vám bude stačit vědět, že logika aplikace by měla být v modelu a naopak by se neměla vyskytovat například v controlleru.
Co je byznys logika je potřeba odlišit například od logiky UI a nastanou i situace, kdy není úplně jednoznačné, kam danou věc umístit – kde ji implementovat. Často je nutné ji implementovat vícekrát na více vrstvách, typickým příkladem je validace vstupních dat – například formulář ve vaší webové aplikaci.
Je velmi vhodné validovat vstupní data již na klientovi, aby nebylo při každé změně nutné odesílat data na server, když je jisté, že nejsou validní – už na klientovi. Vzhledem k tomu, jak lehce jde kód v prohlížeči upravit (případně modifikovat data na cestě po síti), musíme validovat data zároveň i na serveru – většinou v controlleru. Standardně tedy validaci implementujeme jak na klientovi (view vrstva), tak třeba v controlleru. Je potřeba ale validovat data i v modelu?
Model bývá často v .NETu projekt typu class library, který není nijak závislý na MVC, tedy de facto není ani nijak vázaný na web. Je tedy chybou dělat validaci vstupu i na modelu? Je to jedna z metod defenzivního programování, konkrétně ošetření vstupu. Nebudu zde doporučovat, zda je vhodné psát validaci i na modelu, je to čistě na vás a závisí to například i na tom, zda budete chtít tuto část vašeho kódu sdílet i na jiném, než ASP MVC projektu.
Model komunikuje přímo pouze s controllerem a k view nemá přímý přístup.
Controller
Poslední část trinity má za úkol obstarávat komunikaci mezi View a Modelem. Často zde dochází také k transformaci dat – z Data Objectu na ViewModely a naopak. Najdeme zde také pár řádek na validaci vstupních dat. Controllery v ASP MVC vracejí vždy nějaký výsledek akce - ActionResult, tento výsledek může být cokoliv vás napadne. Od jednoduchého HTML (vrací View), Data, přesměrování, až po například REST data, případně skvělé ODATA end pointy. Nejste tedy nijak omezeni.
Controller je ve své podstatě API, které volá klient. V okamžiku, kdy pošlete request na server, dostane se na konkrétní controller, ke konkrétní akci controlleru.
Zde se koukneme na způsob zpracování requestu, který přijde na server:
(zdroj: http://blog.thedigitalgroup.com/chetanv/)
1) První co je potřeba udělat, je rozhodnout se na základě requestu a routovacích pravidel, který controller a jaká akce bude zpracovávat požadavek,
2) po zvolení controlleru, je potřeba ho vytvořit (ano, controller je implicitně vytvářen při každém requestu) a controlleru poskytnout věci, které potřebuje – dochází k zjišťování závislostí. K tomuto se nejčastěji používá nějakého DI containeru. My budeme používat Unity a Castle windsor. v novém ASP 5, je však Dependency injection už v základu frameworku,
3) po vytvoření controlleru je nutné ještě zvolit akci, která ke zpracování requestu existuje. Poté dochází k akci, které se říká model binding. Jednotlivé části requestu se v .NETu namapují na vaše .NET objekty, podle určitých pravidel (nejčastěji typ a název dat v requestu). Přesto, že je tento základní Model binding už poměrně inteligentní, občas si budeme muset napsat vlastní. To si také ukážeme ,
4) akce controlleru vrací nějaký ActionResult, pokud se jedná o View (nebo jeho variace – viz později), ve vaší aplikaci se najde konkrétní View (.cshtml), to se zpracuje – provede se Razor kód a výsledek se pak vrátí jako ˇodpověď klientovi.
Pokud není akcí nějaké View, vrátí se data rovnou.
Do celého tohoto procesu lze jednoduše zasáhnout a chování upravit. Slouží nám k tomu nejrůznější filtry a interceptory. Můžeme tedy upravit request, než dorazí ke controlleru (například interceptnout proces, pokud uživatel nemá na něco právo), nebo naopak můžeme například upravit odpověď, kterou vygeneroval controller, před tím, než ji vrátíme klientovi – například JSON data zabalit ještě do nějakého balíku nesoucí metadata. Toto se nám velmi bude hodit, jak brzy zjistíme.
Controller, View, PartialView, Layout
Pro pokračování v této části si otevřeme solution, tam kde jsme skončili. Pokud ho nemáte, můžete si ho vytvořit jednoduše pomocí založení nového ASP MVC projektu (viz. předchozí díl), nebo stáhnout zde:
Po otevření solutionu budete muset povolit obnovu NuGet balíčků a solution zbuildit, tím se automaticky balíčky postahují.
Nyní zajděte do složky AppStart a otevřete soubor RouteConfig.cs. Jak již soubor napovídá, zde se nastavují jednotlivé routy. Tedy, můžete zde nastavit na jaký controller a jakou akci se má požadavek přesměrovat (kde se má požadavek zpracovat) na základě konkrétní URL.
Podrobnosti ohledně routingu si povíme v některém z dalších dílu o controllerech, nyní nám bude stačit opravdu jednoduchý popis. Jako první v metodě najdeme IgnoreRoute. Zde je to z toho důvodu, že pokud soubor na disku neexistuje (pokud ho IIS nenajde), postoupí zpracování requestu MVC routování, tato řádka toto chování upravuje a říká, že všechny requesty, které směrují na *.axd, mají být ignorovány a nemají být routěny pomocí ASP MVC.
Další část této metody je pro nás o hodně důležitější. Všimněte si především řádky URL a řádky Defaults.
Řádka URL nám říká, jak má vypadat URL žádosti, aby tato žádost byla odchycena (vyhovovala) této routě. V tomto případě zde najdeme "{controller}/{action}/{id}". Tedy daná adresa má obsahovat název controlleru, název akce a parametr id.
Řádka Defaults nám dále říká, že pokud není uveden název controlleru, má být použit controller home. Pokud nebude uveden název akce, má být zvolena akce controlleru index. Poté zde najdeme část id = UrlParameter.Optional. Tím jsme řekli, že parametr ID u této routy může být null. (Toto lze i říci v controlleru pomocí NULLable typu – např. int? id).
Na základě tohoto pravidla, budou vyhovovat např. tyto URL této routě:
/home/index/5
/home/index/
/home/index/jiricek
/home
/
Dejte si pozor na to, že nepovinné části routy se berou “zprava”, stejně jako je tomu u nullable parametrů metod. Tedy například u URL:
/index/
se vezme jako controller “index” a jako akce “index” a u URL:
/index/jirik
se vezme controller “index” a akce “jirik”. Nikoliv controller “home”, akce “index” a id “jirik”.
Ještě je důležité zmínit – přesto, že se k tomu dostaneme později, že se vždy použije první Route, které vyhovuje Url. Vybírá se tedy shora dolů, tak jak po sobě následuje registrace v kódu.
Nyní se již můžeme podívat na HomeController.cs, který se nachází ve složce Controllers. Do všech public metod Controlleru (které se nazývají u controlleru akce), si umístíme breakpoint pomocí F9 a aplikaci spustíme.
Aplikaci můžeme spustit buďto s debuggem (F5), nebo bez debugu Ctrl+F5, případně klikneme pravým tlačítkem na libovolný Html soubor a dáme View In Browser. Pokud nemáme spuštěný debug, jsme schopni celý kód libovolně upravovat, přidávat soubory do solution a další věci. Po úpravě v C# kódu, je však potřeba opět zbuildit solution, aby se změny projevily. I při debugu je možné dělat změny v C# kódu, aniž by jste museli vypínat a zapínat znovu aplikaci. Stačí kliknout pravým tlačítkem na váš webový projekt a v záložce web zapnout Edit and Continue.
Tato možnost vám dovolí při “přistání” na breakpointu upravit velkou část kódu a poté pokračovat dále ve vykonání programu.
Po spuštění se nám hned aplikace zastaví na breakpointu v akci Index:
Je to logické, jelikož URL vypadá takto: “/”, tedy controller se zvolí defaultní Home a akce defaultní Index. To samé i pro ostatní akce:
ActionResult
Každá akce controlleru vrací určitý výsledek. Je několik druhů těchto výsledků a my se na ně jistě v dalších dílech koukneme. Všichni mají společné jedno a sice, že dědí od typu ActionResult. Většinou pokud vrací akce *Result, uvidíme v akci vracet výsledek jako return *;
Tedy například pro ViewResult bude akce vracet return View(), pro PartialViewResult return PartialView() atd.
Akci Index() by jsme tedy mohli změnit takto a nic by se nezměnilo:
public ViewResult Index()
{
return this.View();
}
Pro začátek nám bude stačit, pokud budeme znát 2 základní ActionResulty a to je View a PartialView. Jak jste se asi už mohli dovtípit, View nám vrací samotnou stránku, stejně tak jako PartialView, přesto je mezi nimi podstatný rozdíl, který si brzy ukážeme. Pro pochopení si ale nejdříve musíme vysvětlit, jak se výsledná HTML stránka poskládá.
Pokud kliknete v akci About na View(); pravým tlačítkem a dáte “Go to view” (případně si toto View najdete ručně ve složce Views/Home/About.cshtml, uvidíte kódu skutečně pomálu.
Kde se vezme ten zbytek stránky? Patička? Hlavička? Styly? Odpovědí je Layout. Rozložení stránky, jakási kostra, kam se vždy výsledek dané akce (pokud je to View/PartialView) includne. View najdeme ve složce Views/Shared/_Layout.cshtml. Pro WebForms vývojáře můžeme říci, že Layout je takový MasterPage.
V okamžiku, kdy tedy vrátíme jako výsledek nějaké akce View(), automaticky se použije Layout. Než se koukneme na rozdíl mezi View a PartialView, zastavme se zde u Layoutu. Kromě standardního HTML, zde také najdeme speciální tagy začínající na @. Tyto tagy náleží šablonovacímu enginu Razor, přesněji R@zor. Tagum @Styles a @Scripts se budeme věnovat později, stejně jako dalším, nyní se zaměříme na dva a to @RenderBody a @RenderSection.
@RenderBody a @RenderSection
Namísto @RenderBody se do Layoutu vloží konkrétní vew, které vrací akce controlleru. Pokud tedy jste na andrese /home/about, naleznete namísto RenderBody v layoutu kód z About.cshtml. RenderSection je velmi podobný, avšak je trošku užitečnější.
Umožňuje totiž definovat v Layoutu místo, kam lze načíst různé části View. Dejme tomu že by jsme chtěli do patičky stránky (Layoutu) umístit vždy nějaký text z View. Definujeme si tedy v patičce novou sekci:
<footer>
@RenderSection("InFooter")
<p>© @DateTime.Now.Year - My ASP.NET Application</p>
</footer>
V běžném View si tuto sekci jednoduše definujeme pomocí @section InFooter { }:
Pokud zajdeme na /Home/About, zjistíme, že nám stránka bez problémů funguje:
Při příchodu na Index, dostaneme ale exception, která nám přesně říká, kde je problém:
Index.cshtml (ani žádný další) nemá totiž definován sekci InFooter – tu jsme definovali pouze v About.cshtml. Pokud v Layout.cshtml upravíme naši @RenderSection() o druhý parametr false, určíme tím, že tato sekce není povinná.
Sekcí se používá velmi často právě pro různé hlavičky a patičky. Zároveň ho téměř pokaždé nalezneme na stránkách jako optimalizaci načítání JavaScriptu. Kdy se JavaScript potřebný pouze pro stránku “A”, načítá až na stránce “A” v @section Scripts, tak jako můžete vidět ve vygenerované šabloně. Načítání JavaScriptu až za tělem stránky je také jedna z optimalizací loading time.
Jak se určí Layout u View
V okamžiku, kdy se vrátí z akce controlleru View, se na základě názvu controlleru a akce, začne hledat soubor Viewčka ve složce Views. Controller Home, akce About –> Views/Home/About.cshtml. V okamžiku, kdy je soubor nalezen se engine podívá, zda se v něm nenachází explicitně stanovený Layout pomocí kódu:
@{
Layout = "PathToLayout";
}
Pokud ano, použije se layout na této cestě – pokud není layout nalezen = chyba.
V okamžiku, kdy se zpracovává View, se engine pokouší najít i soubor _ViewStart.cshtml a to ve stejné složce jako se nachází view a pokud nenajde, postupuje o složku výše. Kód tohoto souboru se automaticky vykoná. Takto lze sdílet různé nastavení proměnných pro view na stejné úrovni a nižší, například – jak už asi tušíte, nastavení Layout. Těchto _ViewStart.cshtml lze mít více a lze tak ovlivňovat view, jaké potřebujete na zákaldě hierarchie ve složkách (v solutionu)
View() vs PartialView()
Z akce můžeme kromě View vracet i PartialView. Narozdíl od View(), které se vloží do Layoutu, PartialView vloženo nikam není, pokud tedy změníme v About metodě return View() za return PartialView(), dostaneme pouze obsah stránky About.cshtml:
Výsledek bude stejný i v případě, kdy sice vrátíme View(), ale v .cshtml povíme, že Layout je null:
¨
Přesto, že je výsledek View s Layout=null a PartialView stejný, upřednostňujte připravenou variantu s PartialView(). Je totiž rychlejší než vracení standardního View, které navíc ještě hledá soubor _ViewStart.cshtml a kontroluje nastavení Layoutu. PartialView s Layoutem nikdy nepočítá a tak rovnou zpracovává samotné View.
Razor, ViewEngine, syntaxe
ASP MVC má v základu dva šablonovací (chcete-li renderovací) enginy - Razor a ASPX (Web Forms View Engine). My se bdueme věnovat tomu novějšímu, který přišel právě s ASP MVC / WebPages a to je Razor. ASPX přenecháme pánům z WebForms. Implicitně jsou v ASP MVC zapnuty oba dva enginy, což může trošku brzdit – minimálně se vyhledávají View nejen s příponami .cshtml (CSharp Razor) a .vbhtml (Visual Basic Razor), ale i ASPX, což se nám příliš nehodí a hlavně je nám to k ničemu. Z tohoto důvodu si ASPX engine odebereme a necháme si pouze Razor.
Otevřeme si soubor Global.asax a v metodě Application_Start přidáme následující dvě řádky:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());
Razor
Syntaxe Razoru je velmi jednoduchá a rychle se ji i naučíte. V podstatě vše začíná znakem zavináče @, Pomocí razoru lze psát jakékoliv konstrukty jazyka C#, navíc lze jim i vložit blok kódu, pomocí @{}, takže ani složitější textové konstrukty nejsou problém – prostě složíte string tak jako to děláte v C#. Nebudu zde ručně opisovat celou tabulku Razoru a tak prachsprostě ukradnu tabulku z mého oblíbeného blogu Haacked.com:
komunikace View a Controlleru, Data pro View
Jak jsem již zmínil, mezi Víew a Controller probíhá určitá komunikace. Controller připravuje data pro view a view naopak odesílá určité requesty, které dorazí do akce (public metody) controlleru. Předat data pro view, můžeme z controlleru několik způsoby. Popíšeme si jak jednotlivé způsoby fungují povíme si jejich klady a zápory a jedno velké pokrevní tajemství mezi těmito způsoby.
Pro tuto potřebu si v controlleru nasimulujeme datové úložiště pomocí statického pole stringu, které nám bude reprezentovat emaily. Proč statické? Jak jsem již řekl, pro každý request se vytváří vždy nový controller a tak je nutné mít toto úložiště dostupné i mezi jednotlivými requesty.
Přidejme si nový controller EmailController. Vytvoříte ho kliknutím pravým tlačítkem na složku Controllers->Add->Controller. V nově otevřeném okně zvolte, že chcete prázdný controller.
V controlleru budeme mít dvě akce – List() a Add() a statické readonly pole na naše emaily. Váš controller by měl vypadat tedy nějak takto:
public class EmailController : Controller
{
private static readonly List<string> EmaiList = new List<string>
{
"[email protected]",
"[email protected]"
};
// GET: Email
public ActionResult List()
{
return View();
}
public ActionResult Add()
{
return View();
}
}
View Specific Model (aka “@model”)
Prvním způsobem jak můžeme předat data View je poslat je jako parametr při vracení View. Abychom si to vyzkoušeli, musíme si nejdříve přidat View pro List – nyní ho vidíme červeně a IDE nám hlásí, že View neexistuje. Klikněme na “View” pravým tlačítkem a zvolme Add View. Prozatím nebudeme využívat generování View a tak necháme vše v implicitním nastavení:
V controlleru View předáme data
public ActionResult List()
{
return View(EmailController.EmaiList);
}
Nyní je potřeba tyto data použít. Jakmile předáváme přes model, můžeme k těmto datum přistoupit pomocí @Model ve View. Uděláme si jednoduchou tabulku:
@{
ViewBag.Title = "List";
}
<h2>List</h2>
<div class="container">
<div class="row">
<table>
<thead>
<tr>
<th>
Email:
</th>
</tr>
</thead>
<tbody>
@foreach (string email in Model)
{
<tr>
<td>
@email
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
Výsledek:
Toto je první a nejčastější způsob jak předávat data View, tedy pomocí modelu. Pokud ve View přejedeme myší nad @Model zjistíme, že je typu dynamic. Tedy dynamický a jaká je dynamika? Špatná! Pokud nemá View stanovený přesný typ vstupních dat (model), tak není považováno za tzv. strongly-typed.
Toto se nám tedy rozhodně nelíbí a my budeme chtít říci, jakého přesně typu náš @Model je. Toto uděláme jednoduše pomocí @model (s malým m). Víme, že vstupní data do View, jsou typu List<string>().
ViewData
Dalším způsobem, jak lze poslat nějaká data do View, je pomocí ViewData. ViewData je Dictionary přístupné kdekoliv v controlleru. Zacházíme s ním tedy jako s asociativním polem:
:
ViewData nijak nemusíme předávat View, jsou automaticky k dispozici.
ViewBag
ViewBag je podobně jako ViewData přístupný kdekoliv v Controlleru. Oproti ViewData je ale ViewBag dynamic a to jak v Controlleru, tak ve View:
Pokrevní tajemství
Hlavní způsob jakým předávat data do View, by měl být model, jelikož vám poskytuje strongly typed kontrolu nad vaším kódem a další věci, aniž by jste museli přetypovat. Sice vám IDE s Resharperem občas i ve View napoví u ViewBagu, jaké obsahuje informace, ale i tak musíte vrácenou hodnotu nejdříve přetypovat, než s ní budete moci plně pracovat. ViewBag a ViewData se používají poskromnu a spíše v případech, kdy jste líní dělat nějaký složitější model, případně více modelů a různě využít jejich vzájemnému dědění a kompozici.
Přesto, že by jste se měli snažit vnímat ViewBag jako jedno velké zlo, vám něco prozradím. ViewBag a ViewModel jsou ve skutečnosti jen ViewData.
@Model je to samé, jako by jste napsali ViewData[“Model”], pokud tedy pošlete do View(data), je to to samé, jako by jste udělali v controlleru ViewData[“Model”] = data;
Oproti tomu ViewBag je naopak pouze obalující object nad ViewDaty, který z tohoto slovníku dělá dynamic objekt. Pamatujete si na přechozí díly? Říkal jsem, že dynamický typ je v .NETu pouze syntactic sugar pro Dictionary. Zde je tedy ViewBag pouze syntactic sugar pro ViewData. Pokud tedy napíšete:
ViewBag.Promenna = 5;
jedná se o stejnou funkci i pokud by jste napsali:
ViewData[“Promenna”] = 5;
TempData
Ještě je velmi dobré zmínit zde TempData, přesto, že jejich význam je úplně jiný, než ostatních. TempData je speciální Dictionary, které se udržuje i do následujícího JEDNOHO requestu. Lze ho tedy použít pouze v případě, kdy jste si jistí, jaký bude další request. TempData se na tajno uloží do Session s tím, že se tam uchová pouze do dalšího requestu. Z tohoto důvodu jediné, kde se dá TempData použít, aniž by jste se střelili do nohy, je Redirect.
Ptáte se k čemu jsou TempData vůbec dobrá? Zkuste si zde na DotNetPortalu přidat nějakou zprávu do nějaké diskuze a pak ji smažte. Po smazání zmáčkněte F5, objeví se vám následující okno prohlížeče:
Takovéto chování lze snadno zrealizovat i na MVC. Prohlížeč chce při zmáčknutí F5 znovu odeslat vaši akci z “minula”, což bylo smazání vašeho příspěvku. Čím je toto způsobeno?
1) Chcete akci pro nějaké například přidání dat z formuláře.. budete mít tedy POST akci na serveru
2) V Post akci po přidání do databáze vrátíte View.
3) Z pohledu prohlížeče se vykonalo POST /data/add a jako návratová hodnota mu přišel content, tedy zobrazí opět stránku formuláře a nastaví URL na /data/add
4) Při F5 se prohlížeč snaží zopakovat Request, kterým dostal právě zobrazený obsah, tedy opět zkusí poslat POST (který obsahoval původně i data z formuláře), zeptá se tedy, zda je má přiložit znovu.
Správné řešení je tzv. PRG pattern, což je zkratka pro Post-Redirect-Get, neboli v POST akci data uložíte a přesměrujete klienta na normální GET request, který vrací View.
Právě k implementaci věcí jako je PRG pattern, nám poslouží skvěle TempData. Toto je ale záležitost Controlleru a povídáme se na to podrobněji v dalších dílech. Zpět k View.
Komunikace z View do Controlleru
Pokud chceme z View (klient – browser) komunikovat s Controllerem, jedná se o standardní HTTP komunikaci. Zde můžeme využít klasických dvou metod – GET a POST. Zatímco GET posílá hodnoty v URL – tedy například /email/add/[email protected], POST posílá data v těle requestu. Toto doufám již všichni znáte. Nyní se podíváme jak se tyto metody chovají ve spojení s nastavením ASP VC routes a jak se namapují do controlleru.
Pro ukázku si do View přidáme jednoduchý formulář do view využívající GET metodu:
<div class="row">
<form action="/email/add/pepicek" method="GET">
<input type="submit" value="odeslat." />
</form>
</div>
A v controlleru do Akce list přidáme jeden parametr a ten pak přidáme do našeho statického pole emailu:
public ActionResult Add(string email)
{
EmailController.EmaiList.Add(email);
return View("List", EmailController.EmaiList);
}
Přidáme si brakepoint a aplikaci spustíme, zkusíme odeslat nový email.
Zjistíme ovšem, že Email je NULL. Proč? Get requesty a jejich parametry se mapují na základě routy, pokud si tedy otevřeme RouteConfig.cs a přidáme tam tuto routu:
routes.MapRoute(
name: "EmailRoute",
url: "email/add/{email}",
defaults: new { controlleer = "Email", action = "Add", email = UrlParameter.Optional }
);
Hodnota se nám do controlleru správně namapuje. Zde můžeme vidět, že se bindují hodnoty na základě názvu.
Všimněme si ještě jiných parametrů u View. Jako první parametr můžu použít string a říci, jaké přesně View chci zobrazit, druhý parametr je poté můj model. Na toto si dejte pozor, pokud máte jednoduchý model tvořený stringem, například string model = “jirik” a zavoláte return View(model), tak se ve skutečnosti použije přetížená verze metody, která jako první přijímá název View! MVC tedy nebude brát “jirik”, jako data, ale jako název View!
Druhou obvyklou metodou, jak poslat data, je POST. Odstraňme naši přidanou routu a upravme si formulář tímto způsobem:
<div class="row">
<form action="/email/add/" method="POST">
<input type="text" name="email" />
<input type="submit" value="odeslat."/>
</form>
</div>
Při POST metodě se použijí hodnoty z těla requestu, opět se bindují na základě názvu – ve formu klasicky stanovíme název pomocí attributu name u inputu. Takto můžeme poslat libovolné množství dat, i složitější struktury.
Data můžete posílat libovolně pomocí GET a POST requestu. Přesto platí poučka, že GET použijete v momentě, kdy chcete pomocí URL stanovit určité místo ve vaší aplikaci, kdežto POST, pokud potřebujete pouze odeslat nějaké data, vykonat nějakou akci atp. Z tohoto důvodu nikdy neposílejte pomocí GET citlivé informace. Zaprvé jsou vidět v URL adrese a za druhé se velmi těžce ošetřují některé druhy útoků – jiné ani nejdou. Bezpečnosti webové aplikace v ASP MVC se budeme věnovat v samostatném díle.
Složitější modely
Ukažme si, jak se bindují složitější modely. Změníme si Email na objekt – data objekt Email. Kromě samotné hodnoty “value”, bude ještě obsahovat FirstName, LastName a bool hodnotu NotFromWeb indikující, zda tento email NEBYL přidán skrz webovou aplikaci. Zde nám bude tedy vyhovovat, že bool je implicitně false. Při přidání do “databáze” se tedy automaticky – pokud nenastavíme jinak, nastaví NotFromWeb = false.
Vytvoříme si tedy classu do složky Models:
public class Email
{
public string Value { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool NotFromWeb { get; set; }
}
A upravíme si i Controller, aby pracoval s třídou Models.Email:
public class EmailController : Controller
{
private static readonly List<Email> EmaiList = new List<Email>
{
new Email{ Value = "[email protected]", FirstName = "Jirik", LastName = "Jirikuv"},
new Email{ Value = "[email protected]", FirstName = "Petr", LastName = "Petrikuv"}
};
// GET: Email
public ActionResult List()
{
ViewData["ViewDataTest"] = 123456;
ViewBag.ViewBagTest = 6654321;
return View(EmailController.EmaiList);
}
public ActionResult Add(Email email)
{
EmailController.EmaiList.Add(email);
return View("List", EmailController.EmaiList);
}
}
A pozměníme i naše View:
@using DotNetPortal.Models;
@{
ViewBag.Title = "List";
}
@model List<Email>
<h2>List</h2>
<h3>ViewData: @ViewData["ViewDataTest"]</h3>
<h3>ViewBag: @ViewBag.ViewBagTest</h3>
<div class="container">
<div class="row">
<table>
<thead>
<tr>
<th>
Email:
</th>
<th>
FirstName:
</th>
<th>
LastName:
</th>
</tr>
</thead>
<tbody>
@foreach (Email email in Model)
{
<tr>
<td>
@email.Value
</td>
<td>
@email.FirstName
</td>
<td>
@email.LastName
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="row">
<form action="/email/add/" method="POST">
<input type="text" name="email.value"/>
<input type="text" name="email.firstname"/>
<input type="text" name="email.lastname" />
<input type="submit" value="odeslat."/>
</form>
</div>
</div>
Po spuštění, vše funguje tak, jak jsme očekávali:
Nic nám tedy nebrání takovýmto způsobem zobrazovat ve View libovolně složité struktury a naopak libovolně složité struktury zasílat serveru. Všimněte si v kódu tečkové konvence u name=”” attributu inputu. Zároveň vás mohlo zaujmout, že máme @using ve View, abychom nemuseli všude psát celý namespace k nalezení našeho Email modelu. V dalším díle si ukážeme, jak nastavit všem View určité vlastnosti – například namespace, pomocí nějaké základní BasePage, ze které budou všechny View dědit.
ViewModel
Než se dostaneme k ViewModelu, musíme si vysvětlit jednu věc. Email.cs obsahuje třídu Email o které jsme si řekli, že je data object, tedy že reprezentuje data, které se týkají logiky. Tím máme na mysli logiku aplikace a né logiku UI. Je tedy v pořádku, že je posíláme View jako data? Není. V dalších dílech se budeme čím dál častěji dostávat k vylepšením naší architektury, jaké objekty kam předávat, kde jaké objekty používat a kde ne. U každého použití si ukážeme výhody takového řešení. Pro tuto chvíli se ale koukneme na využití ViewModelu, jako filtru automatického bindování hodnot.
Jak jsem již řekl, Email.cs nám slouží jako reprezentace Emailové adresy včetně jména a příjmení majitele. Nyní přidáváme instanci této třídy v akci Add() do kolekce a bool “NotFromWeb” je nastavován na false (defaultní hodnota bool), což přesně chceme. Bude to takto vždy? Ve formuláři přeci máme pouze vstup pro 3 hodnoty – email, jméno a příjmení.
Koukněme se na následující problém:
Formulář si ručně upravme v prohlížeči pomocí vývojářské konzole takto:
<form action="/email/add/" method="POST">
<input type="text" name="email.value">
<input type="text" name="email.firstname">
<input type="text" name="email.lastname">
<input type="text" name="email.notfromweb" value="true" />
<input type="submit" value="odeslat.">
</form>
A odešleme nový záznam, na server dorazí takováto odpověď:
Je to z toho důvodu, že díky úpravě formuláře se nám na server odeslala i hodnota pro NotFromWeb (je v těle requestu), automatický ModelBinder, který binduje hodnoty requestu na hodnoty parametru akce (v tomto případě Email email), nabinduje i tuto hodnotu. Jednoduchou úpravou formuláře nám tedy kdokoliv může narušit logiku naší aplikace. Tomuto problému se říká OverBinding a je taky jedním z útoku na webové aplikace obecně.
Řešení jsou primárně 3, kde až poslední je pro nás ideální. Dekorátor [Bind], přímý přístup přes FormCollection a ViewModel.
Dekorátor Bind[]
Pomocí dekorátoru Bind[] můžeme explicitně říci, které hodnoty chceme bindovat (Include=””), nebo naopak nebindovat (Exclude=””)
Include je přímým opakem Exclude – tedy musíme vyjmenovat veškeré property, které si přejeme nabindovat od ModelBinderu.
Přímý přístup přes FormCollection
U akce můžeme také dát parametr typu FormCollection. Jedná se o jednoduchý Dictionary, obsahující všechny hodnoty, které nám v requestu přišly.
Hodnota form[“cokoliv”] je ale vždy string, budeme tedy často nuceni ručně přetypovat a případně i kontrolovat, zda bylo přetypování úspěšné. Toto se stává často velmi pracné.
ViewModel
Na pomoc nám přichází vzor ViewModel. ViewModel, neboli View Specific Model je “věc”, která obsahuje informace pro prezentační vrstvu. Nemusí obsahovat nutně pouze data – která bývají z pravidla nějakou podmnožinou dat z Data Objectu, ale i nějaké detaily ohledně reprezentace těchto dat ve View – například, jak se mají zobrazit, jak se mají validovat atp. Tyto informace se vztahují jen a pouze k reprezentaci dat a z toho důvodu by neměly být v nějakém Data Objectu, který se pohybuje napříč celou aplikací (například v servisách, v data access layer atp.
ViewModel může obsahovat opravdu hodně informací, které nám opravdu pomohou s generováním frontendu, s validací a s opravdu hodně věcmi, nyní se ale koukneme pouze na to, jak ViewModel používat.
Vytvoříme si novou složku ViewModels v solution exploreru a v ní vytvoříme EmailViewModel.cs, zároveň si do něj vytvoříme ručně mapování na Email a naopak. Výsledná třída by měla tedy vypadat nějak takto:
public class EmailViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Value { get; set; }
public EmailViewModel()
{
}
public EmailViewModel(Email email)
{
FirstName = email.FirstName;
LastName = email.LastName;
Value = email.Value;
}
public EmailViewModel(string firstName, string lastName, string value)
{
FirstName = firstName;
LastName = lastName;
Value = value;
}
public Email ToEmail()
{
return new Email
{
Value = this.Value,
LastName = this.LastName,
FirstName = this.FirstName
};
}
}
Mapování z ViewModelu na Model je řešení pomocí metody ToEmail(). Mapování na ViewModel je pomocí constructoru. Všimněte si, že mám v kódu i bezparametrický constructor. Ten je potřeba pro ModelBinder. Pokud by jste neměli k dispozici bezparametrický constructor a využívali tento ViewModel jako parametr akce, aplikace vám spadne.
Nyní je potřeba upravit ještě controller:
public class EmailController : Controller
{
private static readonly List<Email> EmaiList = new List<Email>
{
new Email{ Value = "[email protected]", FirstName = "Jirik", LastName = "Jirikuv"},
new Email{ Value = "[email protected]", FirstName = "Petr", LastName = "Petrikuv"}
};
// GET: Email
public ActionResult List()
{
ViewData["ViewDataTest"] = 123456;
ViewBag.ViewBagTest = 6654321;
var data = EmailController.EmaiList.Select(email => new EmailViewModel(email)).ToList();
return View(data);
}
public ActionResult Add(EmailViewModel vm)
{
var data = EmailController.EmaiList.Select(email => new EmailViewModel(email)).ToList();
EmailController.EmaiList.Add(vm.ToEmail());
return View("List", data);
}
}
A samozřejmě View:
@using DotNetPortal.ViewModel
@{
ViewBag.Title = "List";
}
@model List<EmailViewModel>
<h2>List</h2>
<h3>ViewData: @ViewData["ViewDataTest"]</h3>
<h3>ViewBag: @ViewBag.ViewBagTest</h3>
<div class="container">
<div class="row">
<table>
<thead>
<tr>
<th>
Email:
</th>
<th>
FirstName:
</th>
<th>
LastName:
</th>
</tr>
</thead>
<tbody>
@foreach (EmailViewModel emailVM in Model)
{
<tr>
<td>
@emailVM.Value
</td>
<td>
@emailVM.FirstName
</td>
<td>
@emailVM.LastName
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="row">
<form action="/email/add/" method="POST">
<input type="text" name="vm.value"/>
<input type="text" name="vm.firstname"/>
<input type="text" name="vm.lastname" />
<input type="submit" value="odeslat."/>
</form>
</div>
</div>
Nyní máme náš Controller a naše View využívající náš ViewModel. Přesto, jsem udělal jednu chybu. Pro ViewModely platí určitá pravidla, které by se měly ctít:
1) 1 View = 1 ViewModel
2) ViewModel je určen z datové části View. Tedy ViewModel obsahuje pouze ty public property, které jsou potřeba pro zobrazení ve View, nebo které View odesílá controlleru (velmi často to samé).
3) ViewModel nesmí obsahovat logiku aplikace
4) ViewModel se pohybuje pouze mezi View a Controllerem. Nikam jinam NESMÍ přijít. Pokud se předávají data do jiné vrstvy aplikace, dochází k mapování na data object. Buďto toto uděláme ručně (jako to děláme nyní), nebo použijeme nějaký mapovací tool, který mapuje na základě konvence (například AutoMapper).
Zde jsem porušil první dvě pravidla. Mám sice mapování z Emailu na EmailVM, ale tímto jsem vytvořil ViewModel pouze pro Formulář a tabullku. Pokud by byly na stránce ještě další informace, měl bych si vytvořit ještě “container” obsahující informace pro celou stránku a využít kompozice.
Model? ViewModel? View Specific Model? Data Object?
Ok, teď musíte mít skutečně problém identifikovat, co znamená co. Prozatím jsem vám s tím nepomohl při střídání názvů pro pořád ty samé věci a popravdě nepomáhají k tomu ani základní šablony ASP MVC, které narvou vše do složky Models a dál se o to nestarají. Navíc některé věci mají více názvů a tak je z toho potom jeden velký maglajz.
Model : nazývaný často business modelem, business logikou atp. Jedná se o logiku aplikace, často se dělí na několik vrstev jako DAL, Service layer,..
Data Object : také jako business object, data object, DTO, PDO, Plain Data Object. Jedná se o logické data, které se pohybují v aplikaci – kontejnery, přepravky,..
ViewModel : také bohužel často označovaný jako “model”, lépe jako View Specific Model. Jedná se o reprezentaci dat souvisejících s prezentační vrstvou.
Pár slov na závěr tohoto dílu
V tomto díle jsme si řekli něco o architektuře MVC a kouknuli se na to, jak spolu komunikuje View a Controller. Mohli jste si často všimnout, že nedodržuji některé věci, které jsem řekl v minulých dílech – například ohledně uhlazenosti kódu, názvu proměnných atp. Je to z toho důvodu, že se zde stále učíme novým věcem a tak by jsme na uhlazování kódu strávili zbytečně moc času. V tuto chvíli. Zároveň není architektura tak, jak by měla být v ideálním případě, k tomu se ale také dostaneme. Vždy, jakmile nabereme větší část know-how, které bude potřeba – vytvoříme si menší aplikaci, ve které už budeme respektovat vše, co jsme se naučili.
Závěrečnou implementaci tohoto dílu, lze stáhnout zde:
Ještě jako poslední typ na závěr si povíme, proč jsem všude v kódu používal public property a né public proměnné (public fieldy).
Představme si dvě implementace té samé třídy pomocí dvou odlišných způsobů zápisu public memberu a e jejich použití:
public class PropertyStyle
{
public int Cislo { get; set; }
}
public class VariableStyle
{
public int Cislo;
}
private void usage()
{
PropertyStyle propertyStyle = new PropertyStyle();
VariableStyle variableStyle = new VariableStyle();
var a = propertyStyle.Cislo;
var b = variableStyle.Cislo;
propertyStyle.Cislo = 5;
variableStyle.Cislo = 7;
}
Stejné že? Nyní chceme upravit chování přiřazení do Cislo, že dojde ke změně hodnoty pouze pokud je vstup větší než-li 0. Tedy jakési defenzivní programování. U PropertyStyle jde o jednoduchou úpravu geteru. U VariableStyle musíme celou třídu refaktorizovat – především změnit všude Cislo z fieldu (public variable) na Propertu. Toto je občas jednoduchá operace, jindy složitá.
Problém je ale ještě jinde. Pokud tato změna z Fieldu na Propertu nastane v nějaké externí Library (.dll), přestane nám fungovat naše zbuilděná aplikace, jelikož ta počítala s Fieldem a najednou se jedná o propertu (jednoduchou metodu). Dojde tedy k tzv. narušení contractu. Toto může být občas velmi nepříjemný problém.
Zároveň je Field velmi omezující oproti Propertě, nelze ho debugovat (umístit do něj brakepoint) a je pracné ho v budoucnu nějakým způsobem upravovat. Public proměnné dané třídy, tedy implementujte jako Public property. V .NET je jejich použití trapně snadné (oproti např. Javě, kde je jejcih použití velmi velmi otravné) a navíc můžete použít auto propert.
Připomínám, že budu rád za každý komentář pod článkem a působí na mě vždy diskuze jako silná motivace pokračovat dále. Prosím tedy zanechte svůj názor pod článkem.
Přeji vám hodně úspěchů a zase brzy u dalšího dílu.
MB.