Oops! Log On page crashed

Some web application only can be viewed once logged on, such as some CRM system used inside the company. It means before any action was performed the application should be redirected to the log on page if the user was not identitied. Let’s have a look on what will be in our sample application. Just add the Authorize attribute on the home controller.

Localization in ASP.NET MVC (中英文切换下)_IT

Oops! It gave me a 404 error which means the application cannot find the resource for /Account/LogOn. This is because our routes. We have 2 routes registered in the system, the Localization route has 4 parttens: lang, controller, action and id; the Default route has 3 parttens: controller, action and id. The incoming request will be checked through all routes based on their rules and once it’s matched the value would be identified by that route. For example if the URL was http://localhost/en-US/Home/Index it matches the Localization route (lang = en-US, controller = Home, action = Index, id is optional). If the URL was http://localhost/ it was not match the Localization route (lang cannot be null or empty) so it matches the Default route since it allows all parttens to be null.

Now let’s have a look on this URL http://localhost/Account/LogOn, the Account could be the lang partten and LogOn could be the controller, since the action and id partten are all optional it matchs the Localization route so it means: language = Account, controller = LogOn, action = Index (by default). But there is no controller named LogOn and action named Index so it’s the reason why it returned 404 error.

Since the logon URL was redirected by the ASP.NET authorizing model which means we cannot add the language data into the URL we have to add a new route individually for it.

   1: routes.MapRoute(
   2:     "LogOn", // Route name
   3:     "Account/{action}", // URL with parameters
   4:     new { controller = "Account", action = "LogOn" } // Parameter defaults
   5: );
   6:  
   7: routes.MapRoute(
   8:     "Localization", // Route name
   9:     "{lang}/{controller}/{action}/{id}", // URL with parameters
  10:     new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
  11: );
  12:  
  13: routes.MapRoute(
  14:     "Default", // Route name
  15:     "{controller}/{action}/{id}", // URL with parameters
  16:     new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
  17: );

 

In front of the Localization and Default route I added a new one named LogOn, it only accept the Account controller so for this URL http://localhost/Account/LogOn the controller would be Account and action would be LogOn which is correct. Let’s have a look on the result.

Localization in ASP.NET MVC (中英文切换下)_.NET_02

 

Refactoring the codes

This application is a fairly simple example, like all example the microsoft provided, it only contains one project which is my website. In the real projects we might not want to organize our application in one project. We would like to separate the webiste, the controllers, the models, and maybe we need the data access and business layers in the different projects. So let's refactor my example a little bit and see what we should do then to make it localizable.

Here I just want to separte the controllers and view models out of my website project but would not create the data access and business logic layer since normally they would not be related with the localization.

Localization is the matter we should solve at the persentation layer. We should never pass any localized information from the business layer. For example, if the password was incorrect when authorizing an user at the business layer we should always return a flag (int or enum) to indicate the error instead of any error message strings.

Localization in ASP.NET MVC (中英文切换下)_.NET_03

I moved the controllers and models out of the my main website project and let them referenced by the webiste project. I added the necessary assemblies and built it. All worked well except my localization code under the home controller class. I set the message string into the ViewData from the resource class which defined in my website project, now it cannot be accessed through my controller project.

Localization in ASP.NET MVC (中英文切换下)_WEB_04

So what I should do here is to move the resource files out of the website project since it's at the bottom of the references hierarchy.

Localization in ASP.NET MVC (中英文切换下)_MVC_05

After moved the resource files to the new project and added the refereneces, I built it again but I got more error. This is because the accessing classes generated for the resource files are defined as "internal" by default which cannot be invoked out of the project. So what I can do is to create the new resource files for localization and update their access model to "Public".

Localization in ASP.NET MVC (中英文切换下)_IT_06And this time our application works well.

Localization in ASP.NET MVC (中英文切换下)_WEB_07

 

Localizing messages in ViewModels

In the ASP.NET MVC application the message can be defined directly on the view pages, controllers and the view models. One of the scenario is to define the error messages on the view model classes through the attributes provided by System.ComponentModel.DataAnnotations. With the DataAnnotations attributes we can add the validation method and the error messages on the propties of the model classes through the AOP approch. For example when loggin on the user name field should be displayed as "User name" and should be mandatory. So the model would be defined like this, whichi doesn’t support localization.

   1: [Required]
   2: [DisplayName("User name")]
   3: public string UserName { get; set; }

 

We can defined the error messages for the Requeired attribute. And the pre-defined DataAnnotations attributes supports localization which means we can define the resource key and resource type then it will find the relevant resources and returned the content back. Assuming that I had defined 2 resources one for the display name the other for error message, then the attribute would be changed like this.

   1: [Required(ErrorMessageResourceName = "LogOnModel_UserName_Required", 
   2:           ErrorMessageResourceType = typeof(Resources.Global))]
   3: [Display(Name = "LogOnModel_UserName_Required", 
   4:          ResourceType = typeof(Resources.Global))]
   5: public string UserName { get; set; }

 

You might noticed that I changed the DisplayName attribute to Display attribute. This is because the DisplayName attribute does not support localization. Let's execute and see what's happen.

Localization in ASP.NET MVC (中英文切换下)_IT_08

Oops! I think we are running into a lot of problems.

  • The localization doesn't work. You can see on the URL it's in Chinese version but the user name displayed in English.
  • The display name doesn't work. In my model attributes I specified its resource name but now it shown the property's name.
  • The error message was not localized.

Let's explain one by one.

For the first one, the localization didn't work at all. That is because we implemented the localization by the action filter attribute we created before. But the validation logic was performed by the default model binder which was invoked before the action filter attribute was performed. So when the model binder failed the validation and attempted to retrieve the error message from the resource files the culture of the current thread has not been changed by the action filter.

Localization in ASP.NET MVC (中英文切换下)_C#_09

In order to make the localization (culture setting) being invokde before the model binder was executed we should move the localization logic to the Controller.ExecuteCode method, which is earlier than the model binder and validation. So I created a new class named BaseController and let it inherited from the abstract Controller class and then overrided the ExecuteCode method with the localization logic. Then I updated all controllers in the application to inherit from this BaseController.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Web.Mvc;
   6: using System.Threading;
   7: using System.Globalization;
   8: using System.Web;
   9:  
  10: namespace ShaunXu.MvcLocalization.Controllers
  11: {
  12:     public abstract class BaseController : Controller
  13:     {
  14:         protected override void ExecuteCore()
  15:         {
  16:             if (RouteData.Values["lang"] != null &&
  17:                 !string.IsNullOrWhiteSpace(RouteData.Values["lang"].ToString()))
  18:             {
  19:                 // set the culture from the route data (url)
  20:                 var lang = RouteData.Values["lang"].ToString();
  21:                 Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
  22:             }
  23:             else
  24:             {
  25:                 // load the culture info from the cookie
  26:                 var cookie = HttpContext.Request.Cookies["ShaunXu.MvcLocalization.CurrentUICulture"];
  27:                 var langHeader = string.Empty;
  28:                 if (cookie != null)
  29:                 {
  30:                     // set the culture by the cookie content
  31:                     langHeader = cookie.Value;
  32:                     Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
  33:                 }
  34:                 else
  35:                 {
  36:                     // set the culture by the location if not speicified
  37:                     langHeader = HttpContext.Request.UserLanguages[0];
  38:                     Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(langHeader);
  39:                 }
  40:                 // set the lang value into route data
  41:                 RouteData.Values["lang"] = langHeader;
  42:             }
  43:  
  44:             // save the location into cookie
  45:             HttpCookie _cookie = new HttpCookie("ShaunXu.MvcLocalization.CurrentUICulture", Thread.CurrentThread.CurrentUICulture.Name);
  46:             _cookie.Expires = DateTime.Now.AddYears(1);
  47:             HttpContext.Response.SetCookie(_cookie);
  48:  
  49:             base.ExecuteCore();
  50:         }
  51:     }
  52: }

 

For the second and third problem that is because the DisplayName attributes was defined on .NET 4.0 platform but currently the ASP.NET MVC runtime was built on .NET 3.5 which cannot invoke the assemblies under .NET 4.0.

If you downloaded the source code of the ASP.NET MVC 2 there is a solution named MvcFuturesAspNet4 which will create some assemblies built on .NET 4.0 and support the DisplayName attribute. Here I would like to use another approch to work around it.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.ComponentModel;
   6: using System.ComponentModel.DataAnnotations;
   7:  
   8: namespace ShaunXu.MvcLocalization.Models
   9: {
  10:     public class LocalizationDisplayNameAttribute : DisplayNameAttribute
  11:     {
  12:         private DisplayAttribute display;
  13:  
  14:         public LocalizationDisplayNameAttribute(string resourceName, Type resourceType)
  15:         {
  16:             this.display = new DisplayAttribute()
  17:             {
  18:                 ResourceType = resourceType,
  19:                 Name = resourceName
  20:             };
  21:         }
  22:  
  23:         public override string DisplayName
  24:         {
  25:             get
  26:             {
  27:                 return display.GetName();
  28:             }
  29:         }
  30:     }
  31: }

 

I created another attribute which wapped the DisplayName one. It will create an inner instance of the DisplayAttribute and set the resource key and type accordingly. Then when the DisplayName was invoked it will perform the DisplayAttribute.GetName method which supports localization.

So the view model part should be changed like this below.

   1: [Required(ErrorMessageResourceName = "LogOnModel_UserName_Required",
   2:           ErrorMessageResourceType = typeof(Resources.Global))]
   3: [LocalizationDisplayName("LogOnModel_UserName_Required", 
   4:                          typeof(Resources.Global))]
   5: public string UserName { get; set; }

And the let's take a look.

Localization in ASP.NET MVC (中英文切换下)_IT_10

 

Summary

In this post I explained about how to implement the localization on an ASP.NET MVC web application. I utilized the resource files as the container of the localization information which provided by the ASP.NET runtime. And I also explain on how to update our solution while the project was being grown and separated which more usefule when we need to implement in the real projects.

The localization information can be stored in any places. In this post I just use the resource files which I can use the ASP.NET localization support classes. But we can store them into some external XML files, database and web services. The key point is to separate the content from the usage. We can isolate the resource provider and create the relevant interface to make it changable and testable.