Mittels DataAnnotations auf den Model Properties lassen sich die Texte für Labels und Validierungsfehler festlegen. Hardcodierte Texte und Texte in Ressourcedateien werden durch das MVC Framework bereits unterstützt. Konkret sieht dass dann z.B. so aus:

public class LoginModel
{
    [Required(ErrorMessage = "Benutzername darf nicht leer sein.")]
    [Display(Name = "Benutzername")]
    public string UserName { get; set; }

    [Required(ErrorMessageResourceName = "PasswordRequired", 
        ErrorMessageResourceType = typeof(Properties.Resources))]
    [Display(Name = "Password",
        ResourceType = typeof(Properties.Resources))]
    public string Password { get; set; }
}

In der View werden Anzeigename, Eingabefeld und Validierung folgendermassen ausgegeben:

@Html.LabelFor(m => m.UserName):
@Html.TextBoxFor(m => m.UserName)
@Html.ValidationMessageFor(m => m.UserName)

Jetzt sollen die Texte aber aus einer Datenbank gelesen werden. In den DataAnnotations wird darum nur noch die Datenbank-ID der entsprechenden Texte angegeben:

[Required(ErrorMessage = "27")]
[Display(Name = "42")]
public string UserName { get; set; }

Um diese IDs in Texte umzuwandeln sind noch einige Zusatzklassen notwendig. Dabei funktionieren Anzeigename (Display Attribut) und Validierung (Required Attribut) komplett unterschiedlich.

Lokalisierung des Anzeigenamens

Wir erstellen uns einen Metadata-Provider:


public class MetadataProvider : AssociatedMetadataProvider
{
    protected override ModelMetadata CreateMetadata(
        IEnumerable<Attribute> attributes, Type containerType,
        Func<object> modelAccessor, Type modelType,
        string propertyName)
    {
        var metadata = new ModelMetadata(this,
            containerType, modelAccessor, modelType, propertyName);
        if (propertyName != null)
        {
            var displayAttr = attributes.
                OfType<DisplayAttribute>().FirstOrDefault();
            if (displayAttr != null)
            {
                int textId;
                if (Int32.TryParse(displayAttr.Name, out textId))
                {
                    // TODO: read text from database
                    metadata.DisplayName = ...
                }
            }
        }

        return metadata;
    }
}

Unsere Klasse muss jetzt noch in Global.asax.cs in der Application_Start() Methode registriert werden:

ModelMetadataProviders.Current = new MetadataProvider();

Lokalisierung der Validierungsfehler

Hierzu sind zwei zusätzliche Klassen notwendig. Zuerst einmal der ModelValidatorProvider:

public class LocalizableModelValidatorProvider :
    DataAnnotationsModelValidatorProvider
{
    protected override IEnumerable GetValidators(
        ModelMetadata metadata, ControllerContext context,
        IEnumerable attributes)
    {
        var validators = base.GetValidators(metadata,
          context, attributes);
        return validators.Select(validator =>
            new LocalizableModelValidator(validator, metadata,
                context)).ToList();
    }
}

Diese Klasse sorgt dafür, dass wir unsere eigene Klasse "LocalizableModelValidator" zur Validierung des Models verwenden können. Wie schon beim MetadataProvider muss auch der ModelValidatorProvider in Global.asax.cs in der Application_Start() Methode registriert werden:

var provider = ModelValidatorProviders.Providers.FirstOrDefault(p =>
    p.GetType() == typeof(DataAnnotationsModelValidatorProvider));
if (provider != null)
{
    ModelValidatorProviders.Providers.Remove(provider);
}
ModelValidatorProviders.Providers.Add(
    new LocalizableDataAnnotationsModelValidatorProvider());

Eine bereit vorhandener Provider vom Typ DataAnnotationsModelValidatorProvider wird dabei zuerst entfernt.

Und schlussendlich noch die etwas umfangreichere Implementierung des ModelValidators:

public class LocalizableModelValidator : ModelValidator
{
    private readonly ModelValidator innerValidator;

    public LocalizableModelValidator(ModelValidator innerValidator,
        ModelMetadata metadata, ControllerContext controllerContext)
        : base(metadata, controllerContext)
    {
        this.innerValidator = innerValidator;
    }

    public override IEnumerable
        GetClientValidationRules()
    {
        var rules = innerValidator.GetClientValidationRules();
        var modelClientValidationRules = 
            rules as ModelClientValidationRule[] ?? rules.ToArray();
        foreach (var rule in modelClientValidationRules)
        {
            int textId;
            if (Int32.TryParse(rule.ErrorMessage, out textId))
            {
                // TODO: read text from database
                rule.ErrorMessage = ...
            }
        }
        return modelClientValidationRules;
    }

    public override IEnumerable
        Validate(object container)
    {
        var results = innerValidator.Validate(container);
        return results.Select(result =>
        {
            int textId;
            if (Int32.TryParse(result.Message, out textId))
            {
                // TODO: read text from database
                result.Message = ...
            }
            return new ModelValidationResult() {
                Message = result.Message };
        });
    }
}