Google analytics script

Latest jQuery CDN with code tiggling.

Tuesday, 25 October 2011

Application model entity localisation in Asp.net MVC

If you are an Asp.net MVC developer and live in non English speaking country, then you've faced the challenge of application localisation. Although frameworks these days support localisation it's usually not a straightforward process. Especially when it comes to web applications. There's always a dilemma how to implement localisation and how to choose request locale. Should it follow browser language settings or user preference? Either way there's an underlying base foundation that can be used to implement each.

This post is not about localisation in general but rather just about application model classes and their property names localisation when presented in Asp.net MVC views by means of Html.LabelFor() extension methods. There already is a class DisplayNameAttribute but it lacks capabilities we need.

This blog post is related to .net framework 3.5 and older because there's a new attribute provided by the .net framework 4 and newer. It's called DisplayAttribute which has even more capabilities than those implemented below.

Existing DisplayNameAttribute deficiency

This attribute works great when we want to display human-friendly names of our application model properties in Asp.net MVC but fails miserably when it comes to localisation. All they've implemented is a static string we can provide to constructor and that's it. No resource referencing whatsoever. That's why I've taken the plunge and implemented a similar attribute that inherits from this existing class (so it'll be used by Asp.net MVC framework without any additional code) but has localisation capabilities.

This is how existing attribute is used (code excerpt): [DisplayName("Email address")]
public string Email { get; set; }
…and this is how we define localised property human-friendly names: [Display(typeof(Resources.Person), "Email")]
public string Email { get; set; }

Attribute code

As mentioned previously, this attribute class will automatically work. Asp.net MVC will automatically read their information whenever you'll use Html.LabelFor() or any other extension methods that check for DisplayNameAttribute declarative information. Let's get to the code.

   1:  /// <summary>
   2:  /// Specifies the display name for a class, property, event or public void method which takes no arguments.
   3:  /// </summary>
   4:  [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event, AllowMultiple = false, Inherited = false)]
   5:  public sealed class DisplayAttribute : DisplayNameAttribute
   6:  {
   7:      /// <summary>
   8:      /// Gets or sets the type of the resource.
   9:      /// </summary>
  10:      /// <value>The type of the resource.</value>
  11:      public Type ResourceType { get; private set; }
  12:   
  13:      /// <summary>
  14:      /// Gets or sets the resource key.
  15:      /// </summary>
  16:      /// <value>The resource key.</value>
  17:      public string ResourceKey { get; private set; }
  18:   
  19:      /// <summary>
  20:      /// Gets the display name for a property, event, or public void method that takes no arguments stored in this attribute.
  21:      /// </summary>
  22:      /// <returns>
  23:      /// The display name.
  24:      /// </returns>
  25:      public override string DisplayName
  26:      {
  27:          get { return this.GetDisplayName(); }
  28:      }
  29:   
  30:      /// <summary>
  31:      /// Initializes a new instance of the <see cref="DisplayAttribute"/> class.
  32:      /// </summary>
  33:      /// <param name="resourceType">Type of the resource.</param>
  34:      /// <param name="resourceKey">The resource key.</param>
  35:      public DisplayAttribute(Type resourceType, string resourceKey)
  36:      {
  37:          if (resourceType == null)
  38:          {
  39:              throw new ArgumentNullException("resourceType");
  40:          }
  41:   
  42:          if (string.IsNullOrEmpty(resourceKey))
  43:          {
  44:              throw new ArgumentNullException("resourceKey");
  45:          }
  46:   
  47:          this.ResourceType = resourceType;
  48:          this.ResourceKey = resourceKey;
  49:      }
  50:   
  51:      /// <summary>
  52:      /// Gets the display name from the resource.
  53:      /// </summary>
  54:      /// <returns>Returns a formatted string with display name.</returns>
  55:      private string GetDisplayName()
  56:      {
  57:          PropertyInfo property = this.ResourceType.GetProperty(this.ResourceKey, BindingFlags.Public | BindingFlags.Static);
  58:          if (property == null)
  59:          {
  60:              throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.DisplayAttribute.ResourceTypeDoesNotHaveProperty, new object[] { this.ResourceType.FullName, this.ResourceKey }));
  61:          }
  62:          if (property.PropertyType != typeof(string))
  63:          {
  64:              throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.DisplayAttribute.ResourcePropertyNotStringType, new object[] { property.Name, this.ResourceType.FullName }));
  65:          }
  66:          return (string)property.GetValue(null, null);
  67:      }
  68:  }

What about view code

Upper class is of course used on application model entity class whose excerpt has been shown. For better understanding let me provide all the code that plays a role here. Let me first define a simple application model POCO.

   1:  public class Person
   2:  {
   3:      public int Id { get; set; }
   4:      
   5:      [Required(ErrorMessageResourceType = typeof(Resources.Validation), ErrorMessageResourceName = "ValidationRequired")]
   6:      [Display(typeof(Resources.Person), "Name")]
   7:      public string Name { get; set; }
   8:      
   9:      [Display(typeof(Resources.Person), "Address")]
  10:      public string Address { get; set; }
  11:      
  12:      [Required(ErrorMessageResourceType = typeof(Resources.Validation), ErrorMessageResourceName = "ValidationRequired")]
  13:      [RegularExpression(@"[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+(?:[a-zA-Z]{2}|com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum)\b", ErrorMessageResourceType = typeof(Resources.Validation), ErrorMessageResourceName = "ValidationRegularExpression")]
  14:      [Display(typeof(Resources.Person), "Email")]
  15:      public string Email { get; set; }
  16:  }

And now the relevant part of the view that displays this application model entity form which actually uses upper DisplayAttribute declarative information:

   1:  <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<Person>" %>
   2:  ...
   3:  <% using (Html.BeginForm()) { %>
   4:      <ul>
   5:          <li class="required">
   6:              <%= Html.LabelFor(m => m.Name) %>
   7:              <%= Html.TextBoxFor(m => m.Name) %>
   8:              <%= Html.ValidationMessageFor(m => m.Name) %>
   9:          </li>
  10:          <li>
  11:              <%= Html.LabelFor(m => m.Address) %>
  12:              <%= Html.TextBoxFor(m => m.Address) %>
  13:              <%= Html.ValidationMessageFor(m => m.Address) %>
  14:          </li>
  15:          <li class="required">
  16:              <%= Html.LabelFor(m => m.Email) %>
  17:              <%= Html.TextBoxFor(m => m.Email) %>
  18:              <%= Html.ValidationMessageFor(m => m.Email) %>
  19:          </li>
  20:      </ul>
  21:  <% } %>

And that's it really. Use and change it at your own will if you like. I know I do.

5 comments:

  1. Nice!

    PS: I see, I have forgot, just how strange code looks with default ASP.NET view engine. Razor is really much more clear!

    ReplyDelete
  2. @Peter: Unfortunately I'm on .net 3.5 for now... no Razor for me... :(

    ReplyDelete
  3. Thanks for sharing your info. I really appreciate your efforts and I will be waiting for your further write ups thanks once again.

    ReplyDelete
  4. Thanks a lot for your useful information and instructions with regard to .NET Development. As per your word, I've found the steps simple for the Hello World. I appreciate the guide for new developers to web to incorporate some new features. Thanks for the information.

    ReplyDelete
  5. Robert, if you don't have that many strings in your app, I recommend you have a look at the software localization tool https://poeditor.com/

    You can translate up to 1000 strings for free using this online platform, and you can bring your own l10n team to do the job. I think it's a useful resource.

    ReplyDelete