Pro vývoj webových aplikací na platformě .NET se povětšinou používá ASP.NET WebForms. Ten si ale s sebou nese pár negativ, které se (nejen) mně nelíbí a které vznikly především proto, že se Microsoft snažil vývoj webových aplikací co nejvíce přiblížit vývoji klasických okýnkových WinForms aplikací. Důsledkem je, že potřebujeme mezi jednotlivými požadavky někde udržovat ViewState (většinou ve skrytém poli __VIEWSTATE), což je popis stavu stránky, v jakém byla při jejím generování, dále nemáme úplnou kontrolu nad generovaným kódem (pokud nejdeme dostatečně hluboko do útrob) a vůbec celý proces zpracování stránky je dosti přebujelý, dochází k vyvolání mnoha událostí a zorientovat se v tom, kdy je vhodné jakou použít a na co ji použít, není vůbec jednoduchá záležitost. Možná i právě proto přišel s řešením ve formě ASP.NET MVC, které je značně jednodušší na pochopení, díky jasnému rozdělení úkolů není problém psát pro něj unit testy, máme přímou kontrolu nad generovaným HTML, ale to vše za cenu menšího komfortu. Je pak na vývojáři, jakou technologii pro konkrétní úlohu zvolí.
ASP.NET MVC je v době psaní tohoto článku teprve ve verzi Preview 5 a finální verze se dočkáme snad do konce roku. V současné době ani neexistuje žádná nápověda, dokonce není ani nic v IntelliSense, takže veškeré informace získáme buď ze zdrojových kódů, přes Reflector a/nebo hlavně na různých blozích, především bych vypíchnul blog Scotta Guthrieho a Stephena Walthera.
Příspěvek byl ale updatován, aby odpovídal ASP.NET MVC Beta.
Zpracování požadavku
Zpracování požadavku na stránku probíhá tak, že nějaký program (typicky sedící na portu 80) detekuje přicházivší připojení – v případě ASP.NET to bývá IIS. Podle různých kritérií pak IIS zvolí, jaký ISAPI modul se použije pro zpracování požadavku – v našem případě je to ASP.NET. Zde pak máme na výběr, jak s požadavkem naložíme – můžeme stránku přímo zapsat do výstupního streamu, můžeme použít WebForms nebo použijeme MVC. Protože MVC je postaveno nad stejným ASP.NET jako WebForms, tak můžeme klidně používat kontrolky z WebForms, ale protože v MVC nemáme view state, budou fungovat omezeně (jakoby v read-only režimu).
Zpracování požadavku v ASP.NET MVC
Jak už jsem psal výše, proces zpracování stránky ve WebForms je dosti komplikovaný a přebujelý. V ASP.NET MVC je mnohem jednodušší a průhlednější a přímo následuje architekturu model-view-controller.
Přicházivší požadavek je předán do kontroleru, který se podívá na požadavek a podle něj provede změnu v modelu a/nebo získá data z modelu, a tato získaná data přechroustá pro pohled.
Kontroler tedy reaguje na vstup od uživatele ve formě požadavku a komunikuje s modelem a pohledem. Model je reprezentace informací, s nimiž se pracuje – v praxi tedy business vrstva. No a konečně pohled je samotné renderování stránky.
Z hlediska klasické třívrstvé architektury můžeme model považovat za business vrstvu a kontroler a pohled za prezentační vrstvu.
Protože ASP.NET MVC umožňuje mít v jedné aplikaci více kontrolerů, přibývá jedna komponenta navíc – routing. Ten je zodpovědný za výběr správného kontroleru, který se použije.
Zjednodušeně můžeme říct, že cesta od požadavku k výsledné stránce vede přes routing, kontroler, model a pohled.
Když si vytvoříme nový ASP.NET MVC projekt, máme v něm složky, které odpovídají výše popsané struktuře. Ve složce Controllers máme třídy poděděné od System.Web.Mvc.Controller. Metodám těchto tříd se říká akce. Ve složce Models bychom měli mít naši business logiku. A konečně složka Views obsahuje klasické aspx stránky, které reprezentují jednotlivé pohledy. Routing je tak jednoduchý, že je přímo v souboru Global.asax.cs v metodě Application_Start.
Routing
Jak bylo řečeno, routing je zodpovědný za správný výběr kontroleru na základě adresy požadované stránky. Informace z routingu se ale používají také pro opačnou věc – generování odkazů na stránky.
V souboru Global.asax.cs můžeme nalézt přibližně takovýto kód:
RouteTable.Routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
Extension metoda MapRoute zajistí to, že se do routingu přidá záznam o tom, že routování bude zajišťovat třída System.Web.Mvc.MvcRouteHandler, která dělá to, že rozparseruje adresu požadavku podle vzoru v třetím řádku. S naparsovanými informacemi pak pracuje tak, že najde kontroler s odpovídajícím názvem a v něm příslušnou metodu (akci). Pokud není některá z částí URL dostupná, použijeme se hodnota definovaná ve čtvrtém řádku.
Pokud máme např. adresu stránky Products/Detail/5, zavolá se metoda Detail třídy ProductsController s parametrem 5.
V hodně aplikacích myslím nebude třeba toto defaultní routování měnit a v drtivé většině aplikací bude použit defaultní System.Web.Mvc.MvcRouteHandler.
Kontroler
Kontroler není nic jiného než třída zděděná z System.Web.Mvc.Controller. Její metody se nazývají akce (ale může mít i metody, které akcemi nejsou – stačí je odekorovat atributem NonAction) a každá metoda (akce) by měla pouze zanalyzovat přicházejí požadavek (ve většině případů hlavně POST data), na základě toho provést nějaké operace nad modelem (tedy volat busines vrstvu) a nakonec připravit data pro pohled (např. načíst data z modelu a nějak je přetransformovat).
Jediným zdrojem dat pro kontroler tedy nemusí být jen parametr vycucnutý z URL adresy, ale také POST data z formuláře. Jak se k těmto datům dostat?
První možností je přidat akci další parametry, jejichž jména odpovídají názvům POST položek. Pokud parametry nenadeklarujeme jako string, ale třeba jako int, tak pokud POST položka nepůjde na int přetypovat, tak nám vyskočí výjimka. Příklad signatury metody:
public ActionResult Register(string userName, string password, int age)
Pokud bychom chtěli nějakou akci použít pro zpracování dat z více různých formulářů, můžeme jako typ posledního parametru dát System.Web.Mvc.FormCollection a pak v něm budeme mít pěkně přístupné všechny POST položky. Ukázka signatury a možného začátku metody:
public ActionResult List(int categoryID, FormCollection formData)
{
if (!string.IsNullOrEmpty(formData["postItem1"]))
{
Stejné hodnoty ale můžeme získat i z položky this.Request.Form, ale jako robustnější řešení bych ale doporučil použít položku this.Request.Params, která kombinuje data z formulářů, query stringu, serverových proměnných a cookies. Pozor ale na možnost zneužití tohoto přístupu!
Poslední metodou, jak dostat data z formuláře do kontroleru, kterou zde zmíním, je binding. To znamená, že jako typ parametru uvedeme nějakou třídu a specifikujeme, jakým způsobem se provede binding z POST položek do položek třídy. Tento binding provádí třídy implementující rozhraní IModelBinder a přiřazení konkrétního binderu naší třídě provedeme buď pomocí odekorování naší třídy atributem System.Web.Mvc.ModelBinder nebo pomocí statické třídy System.Web.Mvc.ModelBinders. Nejčastější způsob bindování bude IMHO tento:
[System.Web.Mvc.ModelBinder(typeof(Microsoft.Web.Mvc.ComplexModelBinder))]
public class MyClass { }
To nám zajistí, že pokud budeme mít parametr typu MyClass pojmenovaný myModel, tak se pro inicializaci položek třídy použijí POST položky pojmenované „myModel.Property1„, tedy názvy properties jsou prefixovány názvem parametru.
V Betě už ale umí toto komplexní bidování defaultní binder, takže toto odekorování není potřeba. Navíc přibyl atribut Bind aplikovatelný na parametr, pomocí kterého můžeme určit prefix (ten byl v našem případě standardně myModel) a také to, jaké properties bindovaného objektu se mají nabindovat.
Signatura metody používající bindovanou třídu je pak inutitivní:
public ActionResult MyAction(MyClass myModel)
Teď jsem si řekli tři způsoby, jak dostat data z formuláře do kontroleru. Dalším úkolem kontroleru je pak komunikace s business vrstvou (modelem), což je věc zcela závislá na konkrétní aplikaci a proto ji nemá cenu zde moc rozebírat.
Posledním úkolem kontrolu je připravit data pro pohled, tedy pro aspx stránku. To je možné téměř výhradně přes položku ViewData dostupné ve třídě Controller. ViewData je typu ViewDataDictionary, což je třída odvozená od Dictionary<string, object>, což myslím mluví samo za sebe. Třída ViewDataDictionary dále obsahuje položku ModelState, která se používá při validaci pro kumulaci chybových hlášek. Poslední zajímavou položkou je Model (typu object), jejíž význam ukážeme dále.
Za zmínku ještě stojí návratová hodnota akcí (metod kontrolerů), která musí být typu System.Web.Mvc.ActionResult. V praxi budeme používat dvě hlavní konstrukce – buď budeme vracet výsledek metody View nebo metody RedirectToAction, které se postarají o vytvoření instance správné třídy. První z nich zobrazí zadanou aspx stránku, druhá provede klasické přesměrování na zadanou stránku.
Metoda View vrací defaultně takovou instanci, že jako View se použije stránka se stejným názvem jako je název akce, která je umístěna v adresáři se stejným jménem jako je jméno kontroleru. Pokud tedy zavoláme metodu View bez parametrů z metody Detail kontroleru ProductsController, bude se hledat stránka Views/Products/Detail.aspx.
Jednoduchý kontroler s jedinou akcí může vypadat nějak takto:
public class AccountController : Controller
{
public ActionResult Login(string userName, string password)
{
using (var bl = new BusinessLayer())
{
Models.LoggedUser user = bl.CheckLogin(userName, password);
if (user != null)
{
this.HttpContext.Session.Add("user", user);
return RedirectToAction("Index");
}
else
{
this.ViewData.ModelState.AddModelError("login", null, "Login invalid.");
return View(new { userName = userName, password = password });
}
}
}
}
Z parametrů můžeme vyčíst, že tato akce je volána po stisku tlačítka, kdy dojde k odeslání přihlašovacích údajů z inputíků nazvaných userName a password. Pokud se povede úspěšně zalogovat, uloží se informace o zalogování do session a provede se přesměrování na akci Index téhož kontroleru (zde žádná není, takže by došlo k chybě).
Pokud je zalogování neúspěšné, uloží se chyba do ViewData.ModelState (lze vypsat při vykreslení stránky) a provede se zobrazení stránky Views/Account/Login.aspx. Jako model předáme anonymní třídu, která má properties pojmenované stejně jako jsou pojmenované inputíky na stránce, a tyto properties nainicializujeme na původní zadané hodnoty – tím zajistíme opětovné vykreslení zadaných hodnot.
Pohled
Pohled je reprezentován aspx stránkami, které jsou odvozeny ze třídy System.Web.Mvc.ViewPage (která je odvozena od System.Web.UI.Page). V té jsou pro nás důležité hlavně položky ViewData (data předaná z kontroleru), a dále Html a Ajax – to jsou pomocné třídy pro různé rutinní operace.
Nyní přichází ke slovu položka Model z ViewData. Stránku totiž nemusíme odvodit jen z System.Web.Mvc.ViewPage, ale také z generické verze této třídy – jediný generický parametr pak představuje typ modelu. Pak už nemáme v položce ViewData.Model „nějaký“ object, ale přímo konkrétní typ. Přepis bázové třídy na generickou provedeme v souboru Stránka.aspx.cs například takto:
public partial class Login : ViewPage<Models.LoginModel>
{
}
Jednoduchá stránka pro zalogování pak může vypadat nějak takto:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="MvcTestApplication.Views.Home.Login" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
<% using(Html.Form<HomeController>(c => c.Login(), FormMethod.Post))
{
%>
<table class="table-centered">
<tr>
<td>Username:</td>
<td><%= Html.TextBox("model.UserName")%></td>
</tr>
<tr>
<td>Password:</td>
<td><%= Html.Password("model.Password")%></td>
</tr>
<tr>
<td></td>
<td>Remember me <%= Html.CheckBox("model.Remember", true) %></td>
</tr>
<tr>
<td></td>
<td><%= Html.SubmitButton("submit", "Login") %></td>
</tr>
</table>
<%
}
%>
</asp:Content>
Jak je vidět, jedná se o klasickou aspx stránku, která obsahuje mixovaný statický a dynamický kód. Bez problémů lze používat master pages; renderování inputíků stejně jako formu je uděláno pomocí metod property Html. Možná trošku zvláštní může být definování akce, která bude provedena po stisku submitovacího tlačítka, což je provedeno přes lambda funkci. Generická metoda Html.Form přijímá jako první parametr expression tree, takže uvedený kód se nikdy nevykoná, pouze se zanalyzuje a použije pro sestavení správného odkazu (v našem případě „Controllers/Home/Login.aspx„).
Prefixování názvů inputíků řetězcem „model.“ je provedeno proto, protože akce zpracovávající tato data má bindovaný parametr pojmenovaný „model„. Její signatura vypadá takto:
public ActionResult Login(Models.LoginModel model)
Druhým důvodem pro toto prefixování je to, že se takto získají hodnoty properties modelu (kvůli tomu není možné v tomto případě použít jiný prefix – v Betě už si s tím jde pohrát díky atributu Bind parametru akce).
Pro úplnost třída LoginModel vypadá takto:
[System.Web.Mvc.ModelBinder(typeof(Microsoft.Web.Mvc.ComplexModelBinder))]
public class LoginModel
{
public string UserName
{
get;
set;
}
public string Password
{
get;
set;
}
}
To, že ve stránce máme checkbox pojmenovaný „model.Remember“ a v naší třídě nemáme odpovídající položku, ničemu nevadí.
Odekorování atributem si můžeme v Betě už odpustit.
Shrnutí
Článek je myslím docela obsáhlý a pro lepší pochopení by to chtělo konkrétnější příklad. O ten bych se pokusil v dalším článku, kde bych na jednoduché aplikaci ukázal, jak se vypořádat s běžnými úkoly webového vývoje v ASP.NET MVC.
A co byste si měli z tohoto článku odnést? ASP.NET MVC je bratříček ASP.NET WebForms, odděluje uživatelské rozhraní od aplikační logiky, čímž zajišťuje snadnou testovatelnost, je blíže HTML než ASP.NET WebForms.
Zpracování požadavku na stránku probíhá tak, že routing vybere kontroler (třídu) a jeho akci (metodu třídy). V této vybrané metodě (akci) se provede zpracování dat z formuláře (např. uložení nového produktu do databáze) a vybere se, co se má stát. Typicky dojde buď k vyrenderování aspx stránky (pohled) nebo k přesměrování na jinou akci. A požadovaná stránka je na světě!
ad přebujelost standardního modelu zpracování Asp.Web.Page – je to o zvyku, reálně člověk využije 2-3 eventy, ale občas se hodí i prerender apod. Navíc na webu se dají najít pěkná schémátka jak celý postback probíhá. Co se týče samotných WebForms komponent, prakticky každá vyžaduje aspoň nějakou úpravu, pokud se mají nasazovat pro normální aplikace, od ASP.NET 2.0 už jsou alespoň v základu použitelné (GridView apod.).
Druhá věc je využití AJAX, neznám přesně MS implementaci (používám vlastní) MS AJAX Toolkit, jde jí použít s MVC?
To se mi líbíTo se mi líbí
Ad WebForms – jo, ty schémátka zpracování požadavku se dají najít a i pochopit, ale ten způsob vývoje pořád tlačí člověka k tomu, aby mixoval PL a BL. Samozřejmě kvalitní vývojář si to ohlídá, ale začínající lamičky tak imho zbytečně získávají špatné návyky. Navíc ASP.NET MVC se dá díky jasnému rozdělení dobře unit-testovat. Nevýhodou je menší komfort při vývoji.
Ad AJAX – na to bych se chtěl určitě v budoucnu mrknout a v ASP.NET MVC se s ním samozřejmě počítá – k MS AJAX Framework je tam přidána ještě nějaká nadstavba (ještě jsem ji nezkoumal) a kontrolery umožňují vracet „JsonResult“.
To se mi líbíTo se mi líbí
Článek byl upraven, aby odpovídal ASP.NET MVC Beta – změn ale moc nebylo…
To se mi líbíTo se mi líbí