úterý 3. února 2015

Validace v ASP.NET potřetí a globálně - 2

Lokalizace popisek


MVC při zobrazování modelů používá metadata. Metadata k modelům poskytuje objekt ModelMetadataProviders. I přes množné číslo může být v MVC aktivní jen jeden takový objekt. Výchozím objektem je objekt třídy DataAnnotationsModelMetadataProvider - právě díký němu je možné vlastnosti třídy odekorovat atributy jako Required, DisplayName a pod.





Pokud je nutné napsat si vlastní provider, je tedy více než vhodné podědit právě z DataAnnotationsModelMetadataProvider třídy.

Takže pokud chceme mít lokalizované názvy, či klíče pro jejich vyhledání, uloženy v databázi, tak nám k tomu stačí nová třída SimplyCleverModelMetadataProvider, která dědí z DataAnnotationsModelMetadataProvider třídy.

Pokud nám výchozí metodata nevráti DisplayName, tak si v databazí vyhledáme název resource klíče a hodnotu do MVC metadata DisplayName si tak určíme sami - navíc se k ní přidá i *, pokud je údaj povinný:

public class SimplyCleverModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    readonly CountryData[] metadataCountries = null;

    public SimplyCleverModelMetadataProvider()
    {
        this.metadataCountries = CountryProvider.GetCountries();
    }

    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        // invoke the base method but skip the DisplayAttribute since it will use resource internally
        //var attributesWithoutDisplay = attributes.Where(a => a.GetType() != typeof(DisplayAttribute));

        var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);


        if (modelAccessor != null && containerType == typeof(AddressViewModel) && string.IsNullOrEmpty(metadata.DisplayName))
        {
            object target = modelAccessor.Target;
            AddressViewModel address = target.GetType().GetField("container").GetValue(target) as AddressViewModel;
            
            var metadataCountry = metadataCountries.FirstOrDefault(mc => mc.Code.Equals(address.CountryCode));
            

            try
            {
                 var key = "Address" +  metadataCountry.AddressFormatInfo.FieldLabels.First(l => l.Key.Equals(propertyName)).Value;
                string name = SimplyCleverResources.ResourceManager.GetString(key);
                
                metadata.DisplayName = name;
               
                bool isRequred = metadataCountry.AddressFormatInfo.FieldRules.First(fr => fr.FieldKey.Equals(propertyName)).Rules.Any(r => r.Key.Equals("Required"));
                if (isRequred)
                    metadata.DisplayName += "*";
            }
            catch
            {

            }
        }

        return metadata;
    }
}

Takto se lze použít bez atributu Display a přesto si lokalizovat popisky:

@Html.TextBoxFor(m => m.City)

Vlastní provider si pak nastavím v Global.asax voláním:

ModelMetadataProviders.Current = new SimplyCleverModelMetadataProvider();

Použití vlastního view

V předchozím dílu jsem zmínil, že pro ulehčení práce jsem si adresy roztřídil na několik skupin, každou zemi přiřadil do dané skupiny a pro danou skupinu jsem napsal i odpovídající view, tedy mám v projektu views:

  • AddressLayout1.cshtml, 
  • AddressLayout2.cshtml, 
  • AddressLayout3.csthtml

A jak MVC pozná, které view má použít? Nepovažuji za správné, aby v metodě controlleru byla nějaké zbytečný kód, který jen komplikuje čitelnost, takže jsem toto chování vyčlenil do atribut. Kód metody kontrolleru je tak jednoduchý:


public class AjaxController : Controller
{
    [AddressFormFilter]
    [ValidateInput(false)]
    public ActionResult SwitchCountry(AddressViewModel address)
    {
        if (ControllerContext.IsChildAction)
        {
            // CHECK.
            // in case whole page is rendered from AddressPage1, user parent's ViewData (to propagate ModelState)
            this.ViewData = ControllerContext.ParentActionViewContext.ViewData;
        }
        else
            ModelState.Clear();


        return PartialView(address);
    }
}

Daný atribut je stejně jednoduchý, jako třída pro získání metadat:

public class AddressFormFilterAttribute : ActionFilterAttribute
{

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        var metadataCountry = CountryProvider.GetCountries();

        ViewResultBase viewResult = filterContext.Result  as ViewResultBase;
        AddressViewModel address = filterContext.Controller.ViewData.Model as AddressViewModel;

        viewResult.ViewName = "~/Views/Contact/Address" + metadataCountry.First(mc => mc.Code.Equals(address.CountryCode)).AddressFormatInfo.FormatKey + ".cshtml";
    }
}

Protože celá tato série článků je myšlena spíše jako přehlídka možností MVC pro ASP.NET, je výpis zadaných adres řešen trochu jinak - opět existují šablony pro zobrazení jednotlivých typů adres, mám tedy tyto šablony:

@model MvcSimplyCleverPart3.Models.AddressViewModel

@Model.Line1
@if (!string.IsNullOrEmpty(Model.Line2))
{
    <br />@Model.Line2
}
@if (!string.IsNullOrEmpty(Model.County))
{
    <br />@Model.County
}
<br />
@Model.City @Model.StateCode, @Model.ZipCode
<br />
@ViewHelper.GetCountryName(Model.CountryCode)

nebo:

@model MvcSimplyCleverPart3.Models.AddressViewModel

@Model.Line1
@if (!string.IsNullOrEmpty(Model.Line2))
{
    <br />@Model.Line2
}
<br />
@Model.City
<br/>
@Model.ZipCode
<br />
@ViewHelper.GetCountryName(Model.CountryCode)

atd.

Adresy jsou vypisovány v seznamu pomocí tohoto kódu - podobná logika jako v AddressFormFilterAttribute je použita pro nalezení šablony pro každou položku:

@model MvcSimplyCleverPart3.Models.UserAddressViewModel[]


<div class="addresses">
    @foreach (var userAddress in Model)
    {
        <div class="address">
            @Html.Partial(GetTemplate(userAddress.Address.CountryCode), userAddress.Address)
           </div>
        <div class="modified">
           <span class="label">@SimplyCleverResources.LastModified</span>@Html.DisplayFor(m=> userAddress.LastModified)
        </div>
    }
</div>

@functions
{
    private string GetTemplate(string countryCode)
    {
        var metadataCountry = SimplyCleverMiddleTier.CountryProvider.GetCountries();
        return "~/Views/Contact/DisplayTemplates/Address" + metadataCountry.First(mc => mc.Code.Equals(countryCode)).AddressFormatInfo.FormatKey + ".cshtml";
    }
}


V následujícím příspěvku popíši lokalizaci cest a následně i způsob získání a prosazení validačních pravidel.

Žádné komentáře:

Okomentovat