ASP.NET MVC – validace

V tomto článku bych rád navázal na úvodní článek o validacích. Nejprve se podíváme na to, jakým způsobem vlastně vstupují data od uživatele do naší ASP.NET MVC aplikace, jak tato data validovat a nakonec to završíme tím, že si povíme o tom, jak o validačních chybách informovat uživatele.

Vstup dat – model bindery

Protože každá ASP.NET MVC aplikace je postavena na ASP.NET (kdo by to byl řekl), tak máme několik možností, jak číst data z požadavku od uživatele, které využívají ASP.NET a které leckdo využíval v klasických ASP.NET WebForms aplikacích. Jsou to především Request.Form a Request.QueryString, příp. sumaruzíjící Request.Params.

V ASP.NET se ale obyčejně nepracuje přímo s těmito prostředky z kontroleru, resp. akčních metod, ale dochází k mapování těchto vstupních dat na parametry akčních metod. Toto mapování má dvě vrstvy. První z nich pouze zajišťuje abstrakci nad výše uvedenými daty, které je možné získat z Requestu. Tato abstrakce je reprezentována pomocí interface IDictionary<string, ValueProviderResult>, jejímž nejčastěji používaným implementátorem je třída ValueProviderDictionary, která zajišťuje abstrakci pro přístup k Request.Form, Request.QueryString a také k aktuálním routovacím informacím (např. se hodí, když máme v URL uložené nějaké ID).

Druhou, mnohem zajímavější vrstvou, která stojí mezi value-providerem a parametry akčních metod, je model-binder, reprezentovaný rozhraním IModelBinder. Toto rozhraní má jedinou metodu BindModel, která má dva parametry, pomocí kterých se model-binderu říká, co se má bindovat, z čeho se to má bindovat a další parametry bindování. Přes parametry se tedy dostaneme např. k value-provideru, který se má použít pro čtení vstupních dat (což jsou v drtivé většině stringy).

public interface IModelBinder
{
    object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
} 

Zjednodušeně řečeno, model-binder slouží k inicializaci parametru akční metody. Jak ale určíme, jaký model-binder má obstarat inicializaci toho kterého parametru akční metody? Ke správě všech model-binderů v aplikaci slouží statická třída ModelBinders, pomocí níž můžeme specifikovat, pro jaký typ se má použít jaký model-binder, příp. jaký model-binder se má použít, pokud pro daný typ není žádný model-binder specifikovaný (ModelBinders.Binders.DefaultBinder). Toto nastavení model-binderů se typicky děje při startu aplikace. Celý tento postup vyhledání vhodného model-binderu můžeme navíc lokálně přetížit tím, že parametr akční metody odekorujume atributem poděděným od CustomModelBinderAttribute, např. ModelBinderAttribute, pomocí kterého můžeme určit, že pro jeden konkrétní odekorovaný parametr se má použít určený model-binder.

Celková situace vypadá tak, že před spuštěním akční metody se zinicializují všechny její parametry, což je logické, protože musíme vědět, jaké jsou hodnoty parametrů, abychom mohli metodu zavolat. Výběr model-binderu pro každý parametr akční metody probíhá následovně. Nejprve se ASP.NET MVC (konkrétně třída ControllerActionInvoker) podívá, zda není parametr odekorovaný atributem poděděným od CustomModelBinderAttribute. Pokud ano, zavolá jeho metodu GetBinder a vrácený model-binder použije pro zjištění hodnoty parametru. V opačném případě přichází ke slovu kolekce všech registrovaných model-binderů reprezentovaná statickou třídou ModelBinders. Pokud pro daný typ parametru existuje model-binder, použije se. Pokud ne, použije se defaultní model-binder. Voila, známe hodnoty všech parametrů akční metody a nic nám nebrání k přistoupení k jejímu spuštění…

Důležitou informací je, že v mnoha případech nám bude stačit defaultní model-binder reprezentovaný třídou DefaultModelBinder. Ten umí bindovat nejen primitivní typy jako string nebo int, ale také celé třídy včetně jejich properties, properties jejich properties etc. Při tomto využívá klasickou tečkovou notaci (např. „product.Customer.Name„), z čehož vyplývá, že se často budeme setkávat s tím, že názvy HTML vstupních elementů budou mít takováto jména s tečkami.

Další informací, která se může hodit, je to, že model-bindery se nepoužívají jen na zjištění hodnot parametrů akčních metod, ale můžeme pomocí nich inicializovat libovolné objekty. K tomu slouží metody UpdateModel a TryUpdateModel třídy Controller.

Validace v model-binderu

Protože dochází v model-binderu nejčastěji ke konverzi vstupního stringu na požadovaný typ parametru akční metody, zdá se být model-binder vhodným místem pro vstupní validaci. A skutečně tomu tak je – DefaultModelBinder je velmi dobře připraven nejen pro vstupní validaci, ale také pro business validaci. Ale popořadě.

Pro udržování informací (nejen) o validačních chybách nám slouží v ASP.NET MVC objekt typu ModelStateDictionary, ke kterému se z kontroleru i view dostaneme přes ViewData.ModelState. Tato kolekce slouží k uložení informací o všech vstupních datech a to včetně informace o tom, zda je vstupní hodnota validní. Velmi často využívaná je vlastnost IsValid, která se používá ke zjištění, zda jsou všechny zadané hodnoty bez chyb.

Málo se o tom mluví, ale ModelStateDictionary hraje také velkou roli při zobrazování. Nejen, že slouží jako zdroj dat pro výpis validačních chyb, ale slouží také k výpisu samotných hodnot – např. Html.Textbox se vždy nejprve podívá do ViewData.ModelState, zda tam není uložena nějaká hodnota, a pouze pokud není, tak použije předanou hodnotu, příp. hodnotu z ViewData.

Jak už bylo řečeno, DefaultModelBinder typicky konvertuje stringy do nějakých jiných typů. Pokud se jedná o primitivní typy (inty, floaty, …), tak se DefaultModelBinder pokusí přes všemožné convertory překonvertit hodnotu do požadovaného cílového typu. Pokud toto selže, odchytí DefaultModelBinder vzniklou výjimku a informaci o neúspěšné vstupní validaci uloží do ModelState. Toto se děje, když používáme jako parametry akční metody primitivní typy i pokud dochází k bindování properties primitivních typů. Toto je (podle mého názoru dostatečná) podpora pro vstupní validaci.

Pokud dochází k bindování komplexních typů (typicky tříd), musí DefaultModelBinder nejprve vytvořit instanci třídy (virtuální metoda CreateModel). Následně zavolá virtuální metodu OnModelUpdating, která defaultně vrací true (má se pokračovat v bindování) a která je ideálním místem pro nějakou vlastní do-inicializaci či před-validaci modelu. Poté dojde k nabindování všech properties a nakonec k zavolání virtuální metody OnModelUpdated, která je ideálním místem pro komplexní business validaci celého modelu (což je nějaká třída). Zde je tedy ideální místo pro kontrolu konzistence. Pokud je něco v nepořádku, můžeme jednoduše zapsat chybu do bindingContext.ModelState.

Podobně jako bindování celého modelu je vyřešeno zmíněné bindování properties. Nejprve dojde k vyhodnocení hodnoty, která bude přiřazena do property (pomocí model-binderu pro daný typ), následně k zavolání metody OnPropertyValidating, dále k vlastnímu přiřazení hodnoty do property a nakonec k zavolání metody OnPropertyValidated. Při bindování property může dojít k vložení chyby do ModelState na dvou místech – při vstupní validaci (selže konverze do cílového typu) nebo při přiřazení do property – DefaultModelBinder tedy přímo podporuje business validaci v setterech.

Ale nejen to! Metody OnModelUpdated a OnPropertyValidated ve třídě DefaultModelBinder nejsou prázdné, ale jsou připraveny pro práci s rozhraním System.ComponentModel.IDataErrorInfo, což je standardní rozhraní z assembly System.

public interface IDataErrorInfo
{
    // Properties
    string Error { get; }
    string this[string columnName] { get; }
} 

DefaultModelBinder se v metodě OnModelUpdated podívá, jestli není nějaká chyba globálního charakteru – tu by vrátila property Error. V metodě OnPropertyValidated se pak kontroluje přes indexer, jestli není nějaký problém s property daného jména.

Jako programátorům by Vám snad mohl pomoci v chápání předchozího následující pseudokód:

CreateModel();
if (OnModelUpdating())
{
    foreach (Property p in Properties)
    {
        if (OnPropertyValidating(p))
        {
            p.Value = GetValueUsingModelBinder();
            OnPropertyValidated();
        }
    }
    OnModelUpdated();
} 

DefaultModelBinder lze tedy použít velmi snadno (vůbec nic se nemusí nastavovat nebo konfigurovat) na vstupní validaci, přičemž chybovou hlášku vkládanou do ModelState lze samozřejmě přes resources lokalizovat. DefaultModelBinder můžeme ale stejně snadno použít i pro business validaci – zde lze použít dva odlišné přístupy (příp. není problém je kombinovat). Buď můžeme nevaliditu indikovat tím, že v setteru vyvoláme výjimku, nebo můžeme využít rozhraní IDataErrorInfo. Pro úplnost uvedu příklad, jak může vypadat třída implementující toto rozhraní:

public class MyBusinessClass : IDataErrrorInfo
{
    protected Dictionary<string, string> Errors = new Dictionary<string, string>();

    public string Error
    {
        get
        {
            return null; // can contain consistence check
        }
    }

    public string this[string columnName]
    {
        get
        {
            return this.Errors.ContainsKey(columnName) ? this.Errors[columnName] : null;
        }
    }

    public string SomeImportantName
    {
        get { return this.mSomeImportantName; }
        set
        {
            if (string.IsNullOrEmpty(value))
            {
                this.Errors["SomeImportantName"] = "Invalid name."; // should be localized
            }
            else
            {
                this.mSomeImportantName = value;
            }
        }
    }
    protected string mSomeImportantName;
} 

Vlastní model-binder

V předchozích odstavcích jsem se snažil ukázat, že DefaultModelBinder umí tolik věcí, že často vůbec nemáme potřebu psát vlastní model-binder. V některých případech nám ale nic jiného nezbývá. Pokud nám např. nevyhovuje rozhraní IDataErrorInfo, tak není problém podědit si vlastní model-binder od třídy DefaultModelBinder a přetížit metody tak, aby model-binder pracoval také s naším vlastním rozhraním.

V předchozím článku o validacích jsem psal o tom, že business validace může být vhodné mít definované také deklarativně, tedy pomocí atributů. V .NET Frameworku 4.0 bude přítomna knihovna System.ComponentModel.DataAnnotations, která obsahuje standardní atributy, které právě toto umožní zapsat. Máme k dispozici atributy jako Required, Range nebo RegularExpression, jejichž význam je předpokládám zřejmý. Všechny tyto atributy jsou odvozeny od ValidationAttribute a povětšinou overridují metodu IsValid, která vrací bool podle toho, zda je daná hodnota validní pro daný atribut.

Na oficiální stránce ASP.NET MVC si pak můžete stáhnout třídu DataAnnotationsModelBinder, což je třída poděděná od DefaultModelBinder, která v OnModelUpdated a OnPropertyValidating projede všechny validační atributy a zkontroluje, zda není některý z nich porušený. Pokud ano, vloží zprávu o chybě do ModelState.

Pokud odekorujeme properties naší třídy validačními atributy, přenášíme tím zodpovědnost o úroveň výše. Teoreticky by měl každý, kdo chce přiřadit do property, provést validaci vůči všem použitým validačním atributům (jak to dělá DataAnnotationsModelBinder) a setter by tedy nikdy neměl být volán s nevalidní hodnotou. Na to se ale samozřejmě nemůžeme spolehnout a proto je vhodné mít kontrolu také v setteru. Sice musíme kontrolu zapsat dvakrát (pomocí atributu a v setteru), ale je to alespoň přesně na jednom místě. Samozřejmě bychom mohli nechat zapojit do hry nějaký sofistikovaný nástroj, který by na základě validačních atributů vygenerovat kód setteru. Nezkoušel jsem to, ale myslím, že takový PostSharp by to mohl zvládnout.

Deklarativním zápisem validací si také otevíráme cestu k automatickému generování dalších validačních kódů, např. na základě validačních atributů můžeme generovat klientský JavaScript, který bude provádět předběžnou validaci už u klienta.

Zobrazení validačních chyb

Validační chyby je třeba nějakým vhodným způsobem prezentovat uživateli. V drtivé většině případů si vystačíme se standardními metodami Html.ValidationSummary a Html.ValidationMessage. První z nich zobrazí všechny validační chyby pěkně pohromadě a druhá slouží v případnému výpisu chyby přímo vedle validovaného HTML elementu. Oba způsoby výpisu validačních chyb se často kombinují, oba využívají ke čtení chyb ViewData.ModelState a jejich vzhled lze upravit pomocí CSS tříd „input-validation-error„, „field-validation-error“ a „validation-summary-errors„.

Shrnutí

Vstupní data od uživatele jsou dostupná v objektu Request, stejně jako v každé jiné ASP.NET aplikaci. K těmto datům ale přistupujeme z ASP.NET MVC raději přes tenkou abstrakci, které říkáme value-provider.

Další zastávkou vstupních dat od uživatele je model-binder. To je třída, která vyhodnocuje skutečné hodnoty parametrů akční metody. Čte tedy zadané údaje pomocí daného value-provideru a provádí jejich konverzi na typ parametru akční metody. Při tom provádí vstupní validaci a zároveň podporuje business validaci pomocí vyvolávání výjimek v setteru a pomocí rozhraní IDataErrorInfo. Přímo od vývojářů ASP.NET MVC máme navíc k dispozici rozšířený model-binder, který umožňuje také business validaci pomocí validačních atributů z assembly System.ComponentModel.DataAnnotations. Tento deklarativní zápis validačních pravidel nám umožňuje automaticky generovat JavaScript, který bude provádět jednoduché validace okamžitě, bez odeslání požadavku na server.

Pro zobrazení validačních chyb lze použít metody Html.ValidationSummary a Html.ValidationMessage.

Velice pěkný framework na validace v ASP.NET MVC najdete zde a blog jeho autora také stojí za omrknutí…

Zanechat odpověď

Vyplňte detaily níže nebo klikněte na ikonu pro přihlášení:

Logo WordPress.com

Komentujete pomocí vašeho WordPress.com účtu. Odhlásit /  Změnit )

Facebook photo

Komentujete pomocí vašeho Facebook účtu. Odhlásit /  Změnit )

Připojování k %s

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.