pátek 9. ledna 2015

Validace v ASP.NET MVC podruhé

Na konci předchozího příspěvku jsem zmínil, že bude nutné rozšířit validaci adres i o adresy z České republiky.  Smyslem tohoto dílu bude tedy mimo jiné i ukázat, jak udělat klientskou i serverovou aplikaci vlastnosti, která je závislá na jiné vlastnosti.

 Pokud US adresa vypadala takto:

  • Ulice          povinné
  • Město        povinné
  • Okres        nepovinné
  • Stát           výběr z dropdownu
  • ZIP kód    povinný, má tvar NNNNN  a nebo NNNNN-NNNN(NN)
Tak ta česká má tyto požadavky:

  • Ulice          povinné
  • Město        povinné
  • Okres        nepovinné
  • Stát           výběr z dropdownu
  • PSČ          povinné ve tvaru NNN NN
a navíc musí přibýt pole Country pro zadání státu (pole Stát z US adresy má jiný význam). Navíc se musí aplikace i lokalizovat, přeci jen by bylo dobré zobrazit názvy jednotlivých prvků v češtině. Lokalizaci lze naštěstí vyřešit snadno přidáním resource souborů a použitím atributu Display. O přepínání jazyků se starat nebudeme a necháme ASP.NET, aby požilo resource dle nastavení prohlížeče uživatele, config webu by tedy měl obsahovat toto:


<system.web>
  <httpRuntime targetFramework="4.5" />
  <compilation debug="true" targetFramework="4.5" />
  <globalization enableClientBasedCulture="true" culture="auto:en-US" uiCulture="auto:en" />

PSČ vs ZIP

Jak z přehledu vyplývá, tak se liší tvary tohoto čísla a tedy nám nepomůže atribut RegularExpression použitý pro pole ZipCode.  Ideálně by se měla hodnota validovat na základě hodnoty Country - a když uživatel vybere US, měla by se provést validace dle amerického vzoru, když CZ, tak podle českého.
Založíme si tedy vlastní třídu  pro validaci s názvem. Bude potomkem třídy ValidationAttribute  a zároveň bude implementovat rozhraní IClientValidatable.  Konstruktor třídy bude vypadat takto:

public ZipCodeValidatorAttribute(string dependentProperty)
{
    this.dependentProperty = dependentProperty;
    this.patterns = new Dictionary<string, string> { {"US", @"^\d{5}(-(\d{4}|\d{6}))?$"} , {"CZ", @"^\d{3} ?\d{2}$"}};            
}

Nyní přetížíme metodu IsValid takto - nejprve zjistíme hodnotu vlastnosti Country, poté získáme příslušný regulární výraz pro danou zemi a nakonec provedeme vyhodnocení (pozor, kód neobsahuje ošetření vyjímek či nečekaných stavů, je to jen ukázka):

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
    var dependentProperty = validationContext.ObjectInstance.GetType().GetProperty(this.dependentProperty);
    var dependentPropertyValue = Convert.ToString(dependentProperty.GetValue(validationContext.ObjectInstance, null));
    
    var regex = new Regex(this.patterns[dependentPropertyValue]);
    var fieldVal = value != null ? value.ToString() : string.Empty;

    if (regex.IsMatch(fieldVal))
    {
        return ValidationResult.Success;
    }
    else
    {
        return new ValidationResult(this.ErrorMessageString);
    }
}


Nyní je tedy vyřešena validace na serveru (po bindování se provede tato validace) a zbývá ještě dořešit validaci v prohlížeči, tedy klientskou. K tomu nám pomůže metoda z rozhraní IClientValidateble:


public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
    var viewContext = (ViewContext)context;
    var dependantPropertyId = this.dependentProperty;

    var rule = new ModelClientValidationRule();
    rule.ErrorMessage = this.ErrorMessageString;
    rule.ValidationType = "zipvalidator";
    rule.ValidationParameters.Add("dependantproperty", dependantPropertyId);
    rule.ValidationParameters.Add("patterns", new JavaScriptSerializer().Serialize(this.patterns));

    yield return rule;
}

Pokud nějaký vstupní prvek měl být validní, tak při renderování view byl výstupem následující HTML kód:

<input id="Line1" type="text" value="" name="Line1" data-val-required="This is required" data-val="true">


Atributy data-val... jsou pak použity javascriptem pro validaci a případné zobrazení chybových hlášení. Pro pole ZIP jsme ale výše uvedeným postupem dosáhli vygenerování následujících atributů (pole ZIP je i nadále vyžadováno):

<input id="ZipCode" type="text" value="" name="ZipCode" data-val-zipvalidator-patterns="{"US":"^\\d{5}(-(\\d{4}|\\d{6}))?$","CZ":"^\\d{3} ?\\d{2}$"}" data-val-zipvalidator-dependantproperty="CountryCode" data-val-zipvalidator="Provide valid Zip code" data-val-required="This is required" data-val="true">



Je jasné, že standardní javascriptové validátory nic o zipvalidatoru nevědí a tak musíme na naší stránku přidat i příslušný javascript - náš kód má navíc k dispozici parametry získaná z HTML atributů data-val-zipvalidator-xxx - tedy dependentproperty a patterns :



(function ($) {
    $.validator.addMethod('zipvalidator', function (value, element, params) {
        var dependant = params.dependantproperty;

        var patterns = jQuery.evalJSON(params.patterns);

        var currentVal = value + '';

        var $dependant = $('#' + dependant);

        var dependantVal = ($dependant.attr('type') && $dependant.attr('type').toUpperCase() == "CHECKBOX") ?
                            ($dependant.attr("checked") ? "true" : "false") :
                            $dependant.val();

        var pattern = patterns[dependantVal];

        if (!pattern) return true;

        var regex = new RegExp(pattern);

        if (!currentVal.match(regex)) {
            return false;
        }

        return true;
    });

    $.validator.unobtrusive.adapters.add('zipvalidator', ['dependantproperty', 'patterns'], function (options) {
        options.rules['zipvalidator'] = {
            dependantproperty: options.params.dependantproperty,
            patterns: options.params.patterns
        };
        options.messages['zipvalidator'] = options.message;
    });
}(jQuery));

Tím je to hotovo, stačí jen atribut použít na vlastnost ZipCode našeho modelu. Ten nyní vypadá takto:

public class AddressViewModel
{
    [Display(Name = "Country", ResourceType = typeof(SimplyCleverResources))]
    public string CountryCode { get; set; }

    [Display(Name = "Street", ResourceType= typeof(SimplyCleverResources))]
    [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(SimplyCleverResources))]
    public string Line1 { get; set; }

    public string Line2 { get; set; }

    [Display(Name = "City", ResourceType = typeof(SimplyCleverResources))]
    [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(SimplyCleverResources))]
    public string City { get; set; }

    [Display(Name = "County", ResourceType = typeof(SimplyCleverResources))]
    public string County { get; set; }

    [Display(Name = "State", ResourceType = typeof(SimplyCleverResources))]
    public string StateCode { get; set; }

    [Display(Name = "ZipCode", ResourceType = typeof(SimplyCleverResources))]
    [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(SimplyCleverResources))]
    [ZipCodeValidator("CountryCode", ErrorMessageResourceName = "ValidZipCode", ErrorMessageResourceType = typeof(SimplyCleverResources))]
    public string ZipCode { get; set; }
}

Přepínání zobrazení

Některá pole nebudou na formuláři pro českou republiku vidět a naopak. Toho dosáhneme jednoduchým javascriptem:

$("#CountryCode").change(function (e) {
    var countryCode = e.currentTarget.value;
    if (countryCode == 'US'){
        jQuery('#County').show();
        jQuery('#StateCode').show();
        jQuery('[for=County]').show();
        jQuery('[for=StateCode]').show();
    }
    else if(countryCode == 'CZ') {
        jQuery('#County').hide();
        jQuery('#StateCode').hide();
        jQuery('[for=County]').hide();
        jQuery('[for=StateCode]').hide();
    }
});

A to je vše, View vypadá takto:

@using (Html.BeginForm("Save", "Contact"))
{
    <div class="addressForm">
        <div class="row">
            <div class="label">@Html.LabelFor(m => m.CountryCode)</div>
            <div class="input">

                @Html.DropDownListFor(m => m.CountryCode, ViewHelper.GetCountries(Model.CountryCode), new { id = "CountryCode" })

            </div>
        </div>
        <div class="row">
            <div class="label">@Html.LabelFor(m => m.Line1)</div>
            <div class="input">
                @Html.TextBoxFor(m => m.Line1)
                @Html.TextBoxFor(m => m.Line2) <br/>
                @Html.ValidationMessageFor(m => m.Line1)
            </div>
        </div>

        <div class="row">
            <div class="label">@Html.LabelFor(m => m.City)</div>
            <div class="input">
                @Html.TextBoxFor(m => m.City)<br/>
                @Html.ValidationMessageFor(m => m.City)
            </div>
        </div>
        <div class="row">
            <div class="label">@Html.LabelFor(m => m.County)</div>
            <div class="input">
                @Html.TextBoxFor(m => m.County)<br/>
                @Html.ValidationMessageFor(m => m.County)
            </div>
        </div>
        <div class="row">
            <div class="label">@Html.LabelFor(m => m.StateCode)</div>
            <div class="input">
                @Html.DropDownListFor(m => m.StateCode, ViewHelper.GetStates(Model != null ? Model.StateCode : string.Empty))
            </div>
        </div>
        <div class="row">
            <div class="label">@Html.LabelFor(m => m.ZipCode)</div>
            <div class="input">
                @Html.TextBoxFor(m => m.ZipCode)<br/>
                @Html.ValidationMessageFor(m => m.ZipCode)
            </div>
        </div>

        <input type="submit" value="Save">
    </div>
}

Formulář nyní v závislosti na vybrané zemi zobrazuje jen vybraná pole a PSČ či ZIP kód validuje podle pravidel pro tu či onu zemi - ZIP kód pro US není akceptován v ČR:




Jak je ale vidět, řešení to není úplně dokonalé, formulář vypadá divně, některé názvy polí jsou také divné. Takže v dalším díle ukáži, jak aplikaci udělat skutečně univerzální a výše uvedených problémů se zbavit.


Žádné komentáře:

Okomentovat