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" />
<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}$"}};
}
{
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);
}
}
{
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;
}
{
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));
$.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; }
}
{
[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();
}
});
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>
}
{
<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