Introduction
ASP.NET MVC already offers support for localization through ressource files. However, if you need to read texts from a database things get more complicated. Especially if you still want to use DataAnnotations in your model for display text and validation messages.
Background
In a model, DataAnnotations can be used to define labels and validation messages for your model properties. Let's take a simple model that is used on a login page:
public class LoginModel
{
[Required(ErrorMessage = "User name is required!")]
[Display(Name = "User name")]
public string UserName { get; set; }
[Required(ErrorMessage = "Passwort is required!")]
[Display(Name = "Password")]
public string Password { get; set; }
}
In the corresponding view the following code can be used to automatically render a label, a textbox and a (initially invisible) validation message for the "UserName
" property in your model:
@Html.LabelFor(m => m.UserName)
@Html.TextBoxFor(m => m.UserName)
@Html.ValidationMessageFor(m => m.UserName)
The view might look like this in the browser:
Localization from the database
Now as we want to read the localized texts from a database, we simple write the text-id (or whatever unique key you use in your database) into the annotation attributes:
[Required(ErrorMessage = "27")]
[Display(Name = "42")]
public string UserName { get; set; }
To replace the text-id of the Display attribute with the text from the database, we need to create our own MetadataProvider
:
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 displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
if (displayAttribute != null)
{
int textId;
if (Int32.TryParse(displayAttribute.Name, out textId))
{
metadata.DisplayName = "DB Text with id " + textId;
}
}
}
return metadata;
}
}
This class must then be registered in the Application_Start()
method of Global.asax.cs:
ModelMetadataProviders.Current = new MetadataProvider();
For the validation attributes it is a bit more complicated. The first class we need is a ValidatorProvider
, which tells the MVC validation system which class to use to perform model validation. Our implementation in the class LocalizableModelValidatorProvider
returns an instance of LocalizableModelValidator
.
public class LocalizableModelValidatorProvider : DataAnnotationsModelValidatorProvider
{
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
{
var validators = base.GetValidators(metadata, context, attributes);
return validators.Select(validator => new LocalizableModelValidator(validator, metadata, context)).ToList();
}
}
Our validation provider also needs to be registered in the Application_Start()
method of Global.asax.cs:
var provider = ModelValidatorProviders.Providers.FirstOrDefault(p => p.GetType() == typeof(DataAnnotationsModelValidatorProvider));
if (provider != null)
{
ModelValidatorProviders.Providers.Remove(provider);
}
ModelValidatorProviders.Providers.Add(new LocalizableModelValidatorProvider());
As you can see, we remove the existing validator provider that is of type DataAnnotationsModelValidatorProvider
and replace it with our own implementation in the LocalizableModelValidatorProvider
class.
The second class we need is the actual ModelValidatorProvider
, which we implement in the class LocalizableModelValidatorProvider
:
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<ModelClientValidationRule> 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))
{
rule.ErrorMessage = "DB_Text_" + textId;
}
}
return modelClientValidationRules;
}
public override IEnumerable<ModelValidationResult> Validate(object container)
{
var results = innerValidator.Validate(container);
return results.Select(result =>
{
int textId;
if (Int32.TryParse(result.Message, out textId))
{
result.Message = "DB text with id " + textId;
}
return new ModelValidationResult() { Message = result.Message };
});
}
}
The first method GetClientValidationRules()
is responsible for client side validation. It will be called for each property in your model that has a validation attribute each time a view renders your model. However if you disable client side validation in Web.config this method is never called.
The second method ModelValidationResult()
is responsible for server side validation. It will be called for each property in your model that has a validation attribute AFTER the form has been posted back to the server.
Using the code
My examples do not show how to read a text from a database. You will find plenty of examples of that using ADO.NET or Entity Framework. For best performance do not hit the database every time you need to read a single text. Better build your own caching, for example read all texts into a dictionary once and then get them from there.
History
2016-04-13 | Initial version |