AJ's blog

January 19, 2014

ASP.NET MVC I18n – Part 7: Model Attributes

Filed under: .NET, .NET Framework, ASP.NET MVC, HTML5, Internationalization — ajdotnet @ 5:19 pm

The last post introduced resources and used them directly. When it comes to localizing views aspects based on data – i.e. databinding against models – resources are also the means to localize the data aspects.

Note: This post is part of a series

Localizing the displayed data is something that is fairly broadly documented, therefore I can keep the basics to the minimum and focus on the stuff that is rarely mentioned. If you need more background, the ASP.NET site provides the perfect starting point.

This post will concentrate on localizing the data related labels and validation messages; localizing the actual validation is going to be addressed in the next post.

 

The M-part in MVC is the model, in ASP.NET MVC usually consisting of POCOs. Data validation as well as localizing labels in MVC is based on attributes put on the data model. The very first question – and usually one that is not addressed – is one more related to architecture than to plain code: Which class shall I attribute?

Remember the contract data model that I introduced in the initial post:

namespace MyStocks.BusinessContract
{
    [DebuggerDisplay("BusinessContract.Stock: {ID} {Isin} {Name} {Price} {Date}")]
    public class Stock
    {
        public int ID { get; set; }
        /// <summary>
        /// http://en.wikipedia.org/wiki/International_Securities_Identification_Number
        /// 12-character alpha-numerical code
        /// </summary>
        public string Isin { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public DateTime Date { get; set; }
    }
}

Technically, we could just put attributes on this contract data model. However, that would result in UI-specific information bleeding into the business contract. (I elaborated on this topic in an earlier post.) So, to make it short, do NOT put view related attributes on those classes. Instead provide model classes intended for the presentation layer. For our little example the contract class is just copied to the model folder of our MVC project, but in my experience models in ASP.NET MVC have a tendency to become view related models (to avoid the term viewmodel) with the actual data just being one aspect. Now we can apply the necessary validation attributes:

[DebuggerDisplay("Models.Stock: {ID} {Isin} {Name} {Price} {Date}")]
public class Stock
{
    public int ID { get; set; }
    
    [Required]
    [RegularExpression(@"[a-zA-Z]{2}[\w]{9}[\d]{1}")]
    public string Isin { get; set; }
    
    [Required]
    public string Name { get; set; }
    
    [Required]
    [DisplayFormat(DataFormatString = "{0:f2}")] // currency would include currency sign
    public decimal Price { get; set; }
    
    [Required]
    [DataType(DataType.Date)]
    public DateTime Date { get; set; }
}

This of course requires mapping the data back and forth. For more complex applications you might consider something like AutoMapper for this task, but in simple cases plain code will do:

using ContractStock = MyStocks.BusinessContract.Stock; // business contract model
using ModelStock = MyStocks.Mvc.Models.Stock; // mvc model

namespace MyStocks.Mvc.Models
{
    static class Mapper
    {
        public static ModelStock MapToUI(this ContractStock stock)
        {
            return new ModelStock
            {
                Date = stock.Date,
                ID = stock.ID,
                Isin = stock.Isin,
                Name = stock.Name,
                Price = stock.Price
            };
        }
        
        public static ContractStock MapToContract(this ModelStock stock)
        {
            return new ContractStock
            {
                Date = stock.Date,
                ID = stock.ID,
                Isin = stock.Isin,
                Name = stock.Name,
                Price = stock.Price
            };
        }
    }
}

And call these methods as appropriate in the controller:

public class StockController : Controller
{
    protected MyStocks.BusinessContract.IStockService StockService { get; set; }
    
    public StockController()
    {
        this.StockService = new MyStocks.BusinessService.StockService();
    }
    
    //
    // GET: /Stock/
    public ActionResult Index()
    {
        var model = StockService.GetAllStocks().Select(Mapper.MapToUI);
        return View(model);
    }

(Of course, in a real-word application this is the place to employ DI, e.g. Unity.)

The (mostly generated) view picks the information up using extension methods:

@model IEnumerable<MyStocks.Mvc.Models.Stock>
@{
   
 ViewBag.Title = Labels.Navigation_Stock_Index;
}
<h2>@ViewBag.Title</h2>
<table>
    <tr>
        <th>@Html.DisplayNameFor(model => model.Isin)</th>
        <th>@Html.DisplayNameFor(model => model.Name)</th>
        <th>@Html.DisplayNameFor(model => model.Price)</th>
        <th>@Html.DisplayNameFor(model => model.Date)</th>
    </tr>
    @foreach (var item in Model)
    {
        <tr>
            <td>@Html.ActionLink(item.Isin, "Details", new { id = item.ID })</td>
            <td>@Html.DisplayFor(modelItem => item.Name)</td>
            <td style="text-align:right">@Html.DisplayFor(modelItem => item.Price) </td>
            <td>@Html.DisplayFor(modelItem => item.Date)</td>
        </tr>
    }
</table>
<br />
<br />
@Html.ActionLink(Labels.Navigation_Back, "Index", "Home")

If you run the application, the usual outcome is something like this:

As you can see, property names are used for field labels. Jumping to details and edit view, we will also get validation messages based on our attributes from above:

You might already see the respective validation messages properly translated. This depends on the language version of the installed .NET Framework. Generally I would be careful to rely on the fact that all necessary languages are installed in production. Latest when custom feedback is required you need to provide the message in any case.

Providing localized validation messages, as well as display names for labels, can be done providing the „link“ to the respective resources, using attributes in the MVC model. DisplayAttribute for the display name, and the validation attributes for respective error messages:

[DebuggerDisplay("Models.Stock: {ID} {Isin} {Name} {Price} {Date}")]
public class Stock
{
    public int ID { get; set; }
    
    [Required(ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Validation_Required")]
    [RegularExpression(@"[a-zA-Z]{2}[\w]{9}[\d]{1}", ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Stock_Isin_RegEx")]
    [Display(ResourceType = typeof(Localizations.Models), Name = "Stock_Isin")]
    public string Isin { get; set; }
    
    [Required(ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Validation_Required")]
    [Display(ResourceType = typeof(Localizations.Models), Name = "Stock_Name")]
    public string Name { get; set; }
    
    [Required(ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Validation_Required")]
    [Display(ResourceType = typeof(Localizations.Models), Name = "Stock_Price")]
    [DisplayFormat(DataFormatString = "{0:f2}")] // currency would include currency sign
    public decimal Price { get; set; }
    
    [Required(ErrorMessageResourceType = typeof(Localizations.Models), ErrorMessageResourceName = "Validation_Required")]
    [Display(ResourceType = typeof(Localizations.Models), Name = "Stock_Date")]
    [DataType(DataType.Date)]
    public DateTime Date { get; set; }
}

That much can be found in any tutorial. What is also rarely mentioned though (much less documented), is that these localized messages can use string.Format placeholders. Let’s state that clearly: You do not have to provide „field XY is required“ messages for each and every field! (as some posts imply…).

All validation attributes support at least a placeholder for the field name, most also their additional parameters, such as the length in MinLengthAttribute. In other words, those resources would be sufficient for all respective fields:

Validation_Required= „{0} is a required field!“
Validation_MaxLength= „{0} must be shorter or equal to {1} characters!“

BTW: In case you do not like to clutter your source with all these addition parameters in your attributes, Phil uses the MVC infrastructure to resolve and inject localized messages using a convention based approach.

 

Once this is done, we get localized validation messages – with one final hole to patch: Look at all those nicely localized labels and messages… and the one culprit refusing to cooperate, the error message on the date (“Datum”) field:

The reason this does not work is that this message does not come from validation, but from binding. Binding is what turns the text into the appropriate data type (which implies „syntactic correctness“), while semantic validation checks the resulting value against some rules. See here for background.

The immediate consequence is that localization is done differently. Actually there are only two possible cases, numbers and dates, and there is only one way to supply them:

  • Define a resource file in App_GlobalResources – yes, I said, don‘t use it. This is the exception that proves the rule!
  • Define two messages with exactly these names:

  • Tell the model binder to use that particular resource file:

public static void InitializeLocalization()
{
    ClientDataTypeModelValidatorProvider.ResourceClassKey = "GlobalResources";
}

You might have seen DefaultModelBinder.ResourceClassKey for a similar purpose. Apparently this property is not used any more…

And now it works:

Wonder why it works in that twisted way?

“Aside from the standard localization of MVC, this message isn’t easily changeable (it was added at the last minute, and we didn’t offer an override unfortunately).” see here, one of Brad Wils‘ comments.

LOL!

 

Just for completeness: All this is based on the fact that MVC uses jQuery unobtrusive validation. Should you be using jQuery validation directly, you have to adjust $.validator.messages. See here for a complete example.

That’s all for now folks,
AJ.NET

1 Comment »

  1. works in IE, works in Fire-Fox. But not in Crome: : “the specified value ‘19.05.2015’ does not conform the required Format, ‘yyyy-MM-dd’

    Comment by otto — May 16, 2015 @ 11:38 am


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: