Tired of looking for errors in log files? Use OneTrueError - Automatic exception management for .NET.

Easy model and validation localization in ASP.NET MVC3

Update I’ve written a more complete article about the framework at codeproject.com.


If you have googled a bit you’ll read that you should use DescriptionAttribute and DisplayNameAttribute to get localization which will result in a view model that looks something like this:

    public class UserViewModel
    {
        [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        [LocalizedDisplayName(ErrorMessageResourceName = "UserId", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        [LocalizedDescription(ErrorMessageResourceName = "UserIdDescription", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        public int Id { get; set; }

        [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        [LocalizedDisplayName(ErrorMessageResourceName = "UserFirstName", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        [LocalizedDescription(ErrorMessageResourceName = "UserFirstNameDescription", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        public string FirstName { get; set; }

        [Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        [LocalizedDisplayName(ErrorMessageResourceName = "UserLastName", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        [LocalizedDescription(ErrorMessageResourceName = "UserLastNameDescription", ErrorMessageResourceType = typeof(Resources.LocalizedStrings))]
        public string LastName { get; set; }
    }

Don’t do that.

The solution to get cleaner DataAnnotation localization is to derive the attributes and do some magic in the derived classes. The problem with that solution is that client-side validation stops working unless you create new adapters for your derived attributes.

Don’t do that.

The easy solution

I found an excellent post by Brad Wilson that goes through most of the new stuff in MVC3. It’s a must read. When reading it I came up with the idea to use the new Meta Data providers to do the localization. The solution works quite well and you’re models become clean again:

    public class UserViewModel
    {
        [Required]
        public int Id { get; set; }

        [Required]
        public string FirstName { get; set; }

        [Required]
        public string LastName { get; set; }
    }

You need to specify the new providers (created by me) in your global.asax:

        protected void Application_Start()
        {
            var stringProvider = new ResourceStringProvider(Resources.LocalizedStrings.ResourceManager);
            ModelMetadataProviders.Current = new LocalizedModelMetadataProvider(stringProvider);
            ModelValidatorProviders.Providers.Clear();
            ModelValidatorProviders.Providers.Add(new LocalizedModelValidatorProvider(stringProvider));
        }

That’s it. No more messy attributes.

Defining the localization resources

I created an interface called ILocalizedStringProvider which is used by both providers. This means that you do not have to use String Tables. Just create a new class and implement that interface to get support for your database, XML file or whatever you choose.

In my example I used a string table and the strings in it looks like this:

As you see, you should name your strings as “ModelName_PropertyName” and “ModelName_PropertyName_MetadataName” to include meta data like Watermark and NullDisplayText. Refer to MSDN to see which kind of meta data there is.

As for validation attributes, the string table names should simply be the attribute name without the “Attribute” suffix.

Code and documentation

The code is available at github.
Documentation is available here.

Update

Comments are now closed. Either open tickets at github or ask questions at stackoverflow.com with the tag “griffin.mvccontrib”. Write a comment starting with “@jgauffin” if I don’t answer within reasonable time ;)

This entry was posted in CodeProject and tagged , . Bookmark the permalink.
  • Manuel

    Hello,

    this is a really nice feature that allows clean models with multilingual support.

    However, I have a question concerning some attributes (like “Required”).
    Is it also possible to “bind” attributes like “Required” to a specific model? As it can be seen in your strings table the key “Required” has no prefix. I am thinking also about attributes like “RegularExpression” where it would come in very handy to bind those to the models since there will be the need to have several explanations for regular expressions.

    Kind regards,
    Manuel

    • http://www.gauffin.org jgauffin

      Nice idea. I will implement it. It will be as any other metadata (will search for “ModelType_PropertyName_AttributeName”)

      I’m also working with a 100% automated framework for all translations. You’ll get a new Area named “http://yourapp/griffin/” where you can translate models, validation attributes and views (the role “administrator” or “translater” is required).

      The view localization is pretty easy. Just change

      <div>Hello world</div>
      

      to

      <div>@T("Hello world")</div>
      
    • http://www.gauffin.org jgauffin

      The feature is implemented now.

  • John G

    Hi,

    I have looked at your code, and it seems to me that this solution is not threadsafe. You update the attribute’s ErrorMessage. As I understand the an attribute is a single instance. What happens when two simultanious requests with different culture updates the attribute?

    • http://www.gauffin.org jgauffin

      Great comment, but you are incorrect. Take a look here. The attributes are created per model being validated by using factories.

      • John G

        Thanks for the quick answer!

        I have a small testproject, and when I step through the code, for each new request the old value for ErrorMessage is being keept from the previous request, indicating that the attribute is singleton after all.

        Had a look at the decompiled code referred by your link, isn’t it the Adapters that are created by the factory, rather than the attributes?

        • http://www.gauffin.org jgauffin

          each call to GetCustomAttributes returns a new instance of the attribute. Which means that my approach is thread safe.

  • Marius

    Hi,

    This is a very nice and clean solution. However it looks like NinjectMVC3 (installed via NuGet package NinjectMVC3) conflicts with your solution. I am not sure where the problem is with how NinjectMVC3 sets up the MvcApplication or not.

    I will try to work around this limitation by not using the NinjectMVC3 and use the default Ninject StandardKernel to setup the IoC dependencies.

    If you have any insights on what needs to be done to have NinjectMVC3 working with your solution I would really appreciate any advise.

    Thank you!

    Marius

    • http://www.gauffin.org jgauffin

      The Mvc3 module registers NinjectDataAnnotationsModelValidatorProvider which overrides mine (IIRC MVC3 tries to use DependencyResolver first to find the proper provider).

      You can find the source code for the ninject module here: https://github.com/ninject/ninject.web.mvc/blob/master/mvc3/src/Ninject.Web.Mvc/MvcModule.cs

      • Marius

        Thank you very much for your reply! I think that, for now, I will use the FluentValidation framework (as this has support for NinjectMVC integration) and create my own ResourceProvider to get the localized messages from a DB.

        If you find an easy way to overcome the Ninject override I would appreciate it very much if you can share the knowledge.

        Thank you!

      • http://cultiv.nl Sebastiaan Janssen

        I’m quite new to MVC and I don’t really know how to work around this issue. Got any pointers? I mean, I see what Ninject is doing there, but how should I tell it not to do that..?

  • http://www.rcdmk.com RCDMK

    Hi. I’m a newbie in ASP.Net and MVC 3.

    Can you guide me, pleaze?

    How can I implement this in a new project?(I just want the Localization part):
    – A NuGet package? What command?
    – Downloading the source and including some files in my project? What files are needed?

    One thing to note is that I’m from Brazil and my Apps/Sites have pt-BR as the default culture. I don’t know if this can cause any problems.

    I’ve tried to open the project downloaded from GitHub, but I can’t with VSWD 2010 Express with MVC 3 installed (Project type not supported). I can only open the files individually.

    • http://www.gauffin.org jgauffin

      There is a nuget package available now. Instructions at github.

  • Francis

    Thank you for a great piece of code. It works really well!

    However, I am missing support for custom attributes, for example System.Web.Mvc.CompareAttribute which I cannot get to work. Do you have any idea how to fix this? It was a bit tricky to figure out where to fix it :)

    Also, I think it would be nice to allow these attributes per property, for example you could have a global

    Compare = "Default message"
    MyModel_MyProperty_Compare = "X needs to be the same as Y."

    • http://www.gauffin.org jgauffin

      Please post issues at github. Somone else asked for that too. And I’m about to start with mvccontrib again now since my GriffinTable jQuery plugin is done.

    • http://www.gauffin.org jgauffin

      The latest nuget package should have that feature now.

      • http://www.silvaware.net/ Thiago

        what would the resource string key look like in a resource file for more complex or custom attributes, such as Compare and StringLength?

        For exampl, if I have something like the following:
        [StringLength(100, ErrorMessage="should have {0} length", MinimumLength = 6)]

        • http://www.gauffin.org jgauffin

          YourModelName_PropertyName_AttributeName

          i.e. User_FirstName_StringLength

  • Andy

    Hi, I have been looking through your dat aannotations localization and I must say I am very impressed. I would like touse your code in my project but I am having a little bit of a thick moment in respect of implementing my data access. For example all of my translations are held within xml files drawn from a database, how do I provide the custom resource manager?

    var stringProvider = new ResourceStringProvider(Resources.LocalizedStrings.ResourceManager); ModelMetadataProviders.Current = new LocalizedModelMetadataProvider(stringProvider); ModelValidatorProviders.Providers.Clear(); ModelValidatorProviders.Providers.Add(new LocalizedModelValidatorProvider(stringProvider));

    I am assuming it is this line that needs to be changed.

    var stringProvider = new ResourceStringProvider(Resources.LocalizedStrings.ResourceManager);

    But what do I replace the resourcemanager with?

    I have created a new class and implemented ILocalizedStringProvider but I am stuck as to what to do next?

    Many thanks for your help.

  • Andy

    Doh! Never mind, I was definately having a thicko moment! I now realize that I simply replace

    var stringProvider = new ResourceStringProvider(Resources.LocalizedStrings.ResourceManager);

    With my own implementation

    var stringProvider = new MyImplementation();

    Doh! :oops:

  • Andy

    Hi, I do have another question. My implementation of localization consist of XML files that are held within a database. There is an XML file for each page of text and there is an equivalent XML file for each language supported. The XML to retrieve is based upon the language id that is stored within the session state for any given user. My question is what would be the best point within your code to access the session data or failing that how would I be able to pass the required values from the session state down into my localization implemtation so that I can retrieve the applicable XML for the chosen language?

    • http://www.gauffin.org jgauffin

      It’s up to your string provider to do that. You can use HttpContext.Current to access the session from within it.

      • Andy

        Excellent, thank you very much for your reply.