Thursday, August 5, 2010

Localize ASP.NET MVC 2 DataAnnotations validation messages with a custom resource provider

The problem

If in ASP.NET applications you are using a Customized Resource Provider and System.ComponentModel.DataAnnotations for validating your model, then you will have problems to localize messages. To be more precise, I am using a Database Resource Provider, which I wrote some years ago, for pulling localization information from a Database and to localize, in this way, ASP.NET and ASP.NET MVC Web Applications. Data Annotations are using two properties for localizing messages, ErrorMessageResourceType and ErrorMessageResourceName, which accept a Type (i.e. a compiled resource file) and a String respectively. Having a Customized Resource Provider (= Database Resource Provider) does not allow me to specify a Type for the ErrorMessageResourceType, since resources are not compiled.

Approaches to solve the problem:

  1. Writing a Custom Build Provider, which compiles resources from the Database.
  2. Creating a Dynamic Object, which transforms the property name to a key for the Resource Manager.
  3. Extending Attributes of Data Annotations.
  4. Using a T4 Template for generating a class containing the resources.

First approach

The Idea was to have a Custom Build Provider (namely DataBaseResourceBuildProvider) for compiling resources that are in a Database. So I started with a simple implementation. During this task I want to underline, that the tool CodeDom Assistant (http://www.codeproject.com/KB/cs/codedom_assistant.aspx) was helping me a lot, since I could convert some pieces of source code into a CodeDom object. Finally I registered the Build Provider in my web.config file and I found out that it is not working. It took me some time to find out that adding a Customized Build Provider class to the web.config file works in an ASP.NET Web Site but does not work in an ASP.NET Web Application project. In a Web Application project, the code that is generated by the Build Provider class cannot be included in the application. This information I found in Microsoft MSDN (http://msdn.microsoft.com/en-us/library/system.web.compilation.buildprovider.aspx). I was very surprised and angry about this, therefore abandoned this approach and moved on to the second one.

Second approach

My second approach was to create a Dynamic Object and to encapsulate the call to the Resource Manager in a general implementation. Basically, the idea was that you can call any property that you want on this object, even if this property was not explicitly defined. In the background the property name would be used as a key for retrieving the resources. First, I extended the class System.Dynamic.DynamicObject and I overrode the function TryGetMember. This is a sketch of the implementation:

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
    result = HttpContext.GetLocalResourceObject("", "Resource_KEY822.Title") as string
    return (result != null);
}

It turned out that the Data Annotations needs static property for retrieving resources, and therefore I discarded this approach.

Third approach

I tried to avoid this approach, because I would have had to extend several classes, which requires time. It consists to extend all Attributes of Data Annotations and to write some logic for displaying localized messages. To show that it works, I extended on Attribute, namely the DisplayNameAttribute. This is a sketch of the implementation:

public class LocalizedDisplayNameAttribute : DisplayNameAttribute
{
    public LocalizedDisplayNameAttribute(string displayNameKey)
        : base(displayNameKey)
    {
    }
    
    public override string DisplayName
    {
        get
        {
            string s = HttpContext.GetLocalResourceObject("", base.DisplayName) as string;
            if (string.IsNullOrWhiteSpace(s))
                return base.DisplayName;
            else
                return s;
        }
    }
}

Fourth approach and Solution

I use T4 Template for generating a wrapper class, which encapsulates all resources of the Database as properties. To be more precise, I generate a property with the name of the key of the resource in the Database. In this way, I generate a class where every property corresponds to a call to the Resource Manager that, given the key, returns the localized resource message. Moreover, I generate a constant of type String, whose name is composed of a prefix Prop and the key. The value of the constant is the name of the property. Having constants and properties allows me to use Data Annotations for validating my model: for the property ErrorMessageResourceType I will pass the type of my generated class; for the property ErrorMessageResourceName I will set it to the constant that encapsulates the name of the property. I would like to emphasize that properties have to be declared as static for working with Data Annotations. Using constants for specifying the ErrorMessageResourceName has the following two fundamental advantages:

  1. Using of IntelliSense
  2. Error detection at compile time

The following example shows a sketch of the T4 Template:

public class Resources
{
<#
string connectionString = CurrentConnectionStrings["xxx"].ConnectionString;
string providerName = CurrentConnectionStrings["xxx"].ProviderName;
string applicationName = CurrentApplicationName["xxx"].Value;
DbProviderFactory factory = DbProviderFactories.GetFactory(providerName);
DbConnection connection = factory.CreateConnection();
connection.ConnectionString = connectionString;
DataBaseResourceProvider provider = new DataBaseResourceProvider(applicationName, connection);
foreach (DictionaryEntry entry in provider.ResourceReader)
    {
        string propName = createUnderscore(Convert.ToString(entry.Key));
        WriteLine(string.Format("    public const string Prop_{0}=\"{1}\";", propName, propName));
        WriteLine(string.Format("    public static string {0}", propName));
        WriteLine("    {");
        WriteLine("        get { return HttpContext.GetLocalResourceObject(\"a\", \""+Convert.ToString(entry.Key)+"\") as string; }");
        WriteLine("    }");
        WriteLine("");
    }
#>    
}

7 comments:

  1. Can you post the full t4 template? this looks like the solution I am looking for but I am not familiar with t4 templates.

    ReplyDelete
  2. Hallo Ricardo,
    This is the full T4 template!

    ReplyDelete
  3. Hello ,
    Can you please send sample application using above example... Its urgent requeirement for me.. I need your help.

    ReplyDelete
  4. There's much better way.

    Please see

    http://buildstarted.com/2010/09/14/creating-your-own-modelmetadataprovider-to-handle-custom-attributes/

    ReplyDelete
  5. Where is the implementation of CurrentConnectionStrings? CurrentApplicationName? DbProviderFactory?

    ReplyDelete
  6. The information that you provided was thorough and helpful. I will have to share your article with others
    Localization

    ReplyDelete
  7. Another useful article :
    http://afana.me/post/aspnet-mvc-internationalization-store-strings-in-database-or-xml.aspx

    ReplyDelete