Monday, 9 June 2008

MonoRail & ActiveRecord localized validation messages

I'm currently developing .NET web application using MonoRail framework and ActiveRecord design pattern. One of the requirements for the project is easy localisation. In general it's pretty straightforward but I had some troubles figuring out how to localize the validation messages coming from ActiveRecord Business Objects.

Here's my solution for that:

  1. Create validation messages in resource file
    In your resource file (e.g Validation.resx) add entries for validation messages, e.g.


    Of course, you'll need to define those entries in resource files for all languages your application should support (e.g. in Validation.de.resx for German, Validation.es.resx for Spanish, etc.).

  2. Add business objects validation attributes
    Let's use a sample business object representing User, which only includes name property. We have to add ActiveRecord attributes for validation e.g. ValidateNonEmpty and ValidateLength using appropriate parameters. These both attributes take the validation message as one of the arguments. Instead of giving here the actual message we can use its ID (name) from the resource file e.g. "vldNameRequired". Here is sample code of such Business Object:
    [ActiveRecord("User")]
    public class User : ActiveRecord
    {
    [Property(NotNull = true, Length = 50)]
    [ValidateNonEmpty("vldNameRequired")]
    [ValidateLength(1, 50, "vldNameLength")]
    public string Name { get; set; }
    }
    For more validation attributes please refer to ActiveRecord documentation.

  3. Catch validation errors
    Now, we have to enable validation for appropriate action. Let's say we want to validate data provided by the user while creating new account. For registering new user I've created the Register method together with appropriate data binds. Firstly we need to allow validation for that action (Validate = true).
    Catching validation errors is described in comments in code below:
    (...)
    using System.Resources;

    public class UserController : SmartDispatcherController
    {
    (...)
    public void Register([DataBind("User", Validate=true)]User user)
    {
    // Check whether the binded object is valid by calling the
    // HasValidationError inherited from SmartDispatcherController
    if (HasValidationError(user))
    {
    // If there are validation errors we can access the error
    // summary using GetErrorSummary method
    ErrorSummary summary = GetErrorSummary(user);

    // Now we have to populate Flash item with localized
    // validation messages by calling local private
    // method GetLocalizedMessages
    Flash["validationErrors"] = GetLocalizedMessages(summary);
    PropertyBag["user"] = user;
    }
    else
    {
    //If there are no errors save the new user and redirect
    //to other action
    User.Save(user);
    RedirectToAction('DoSomethingElse');
    }
    }

    /// <summary>
    /// Translates the message IDs to actual messages
    /// </summary>
    /// <param name="messageIDs">Message IDs to translate</param>
    /// <returns>Array of translated messages</returns>
    private string[] GetLocalizedMessages(string[] messageIDs)
    {
    string[] messages = new string[messageIDs.Length];
    for (int i=0;i<messageIDs.Length;i++)
    {
    // YourNamespace.Validation is the full name of your
    // resource file with validation messages
    messages[i] = YourNamespace.Validation.
    ResourceManager.GetString(messageIDs[i]);
    if (messages[i] == null)
    {
    // ERR_NO_RESOURCE is constant storing ID of default
    // error msg
    messages[i] = Validation.ResourceManager.
    GetString(ERR_NO_RESOURCE);
    }
    }
    return messages;
    }
    }
  4. Display localized validation messages
    Now you have to display validation error messages on the page. This is the sample code that displays all validation errors coming from ActiveRecord validation (I'm using NVelocity as my view engine):

    #if($validationErrors != null && $validationErrors.Length > 0)
    <ul class="validationErrors">
    #foreach($ve in $validationErrors)
    <li>$ve</li>
    #end
    </ul>
    #end
That's it. There are of course many possibilities for implementing that functionality. One of the enhancements could translate the ErrorSummary to the Dictionary<PropertyName, ValidationErrorMsg> so you can display error messages next to appropriate controls.