Monday, March 31, 2008

Localisation and Profiles: Programmatically changing the user's preferred culture.

A while ago I wrote a post on how to use Localisation in ASP.NET using resource files etc. We'll since then I been asked about how to change the user's culture programmatically by clicking a button or similar.

In that previous article I described how culture selection is based on browser preferences and HTTP language headers. While this is pretty good, there are many scenarios where this could be improved. Say you are travelling overseas and are accessing your appl remotely from someone's elses machine, if the culture of that browser is set to Japanese, then your application might not be readable to you. You might not be able to change your language preferences either because that might not be available in a controlled environment.

What options do we have?

In general, to get manage the user's preferences we have:

  • Use the browser settings ( not that great as described above but good enough for 80% of the time)
  • When a user registers of first uses your application, they can choose language of choice and then save this in the database for later retrieval
  • Persist the above choice in Session state or Cookies.
  • Use ASP.NET Profiles.
  • Let the users choose this in the application. ( buttons. ddls etc ) 

In ASP.NET is very straightforward to select the preferred culture based on the user's browser settings, you can do it at page level like I did in in my prev post but you can also set it up for all your pages in the web.config file saving you the pain of adding the attribute to all your pages throughout your application.

<system.web>
        <globalization culture="auto" uiCulture="auto" fileEncoding="utf-8" requestEncoding="utf-8" 
            responseEncoding="utf-8" />
 ....
What if I want to save the user settings after they register/access the app for the first time?

Think about the options you have to persist the preferred culture: Session: Session is not the best for many reasons, most importantly Session is not there forever, for example by default ASP.NET assumes that the user left the site after no requests have been made for 20 mins ,  your user might get up to get a cup of tea and his Session might expire, therefore when she gets back she might not have the application in her culture of choice. Also, you might not be able to access the Session object when you want to access the user's preferences. So I wouldn't use Session myself.

What about Cookies?

Cookies are more favourable if all you want is to persist a simple user' preference like the culture or their favourite colour I think, it will get hard pretty quickly if you where storing more than just their colour and horoscope preferences.

For example in Global.asax you could implement the Application_BeginRequest event and read the cookie there and setup your CurrentThread to the value stored in the cookie that you created after the user made the choice etc.

void Application_BeginRequest(Object sender, EventArgs args)
{
    HttpCookie myPreferencesCookie = (HttpCookie)Request.Cookies["Culture"];
    // check for null etc etc
    string culture = myPreferencesCookie.Value;
    System.Threading.Thread.CurrentThread.CurrentUICulture = 
         new System.Globalization.CultureInfo(culture);
    System.Threading.Thread.CurrentThread.CurrentCulture = 
         System.Globalization.CultureInfo.CreateSpecificCulture(culture);
}

But I think it will get complicated very quickly and I don't think is the best way. Much better I think is ASP.NET's Profile

The Profile objects is strongly typed and persisted, and you can even implement your own ProfileProvider!

The easiest way to create a Profile is by creating some properties in the your root web.config. Just like the Resource files, ASP.NET compiles the Profile's propertied dynamically and then you have this strongly typed profile assets.

<profile>
   <properties>
      <add name="MyFavouriteNumber" allowAnonymous="true"/>
      <group name="Preferences">
        <add name="Culture" allowAnonymous="true" />
        <add name="Color" allowAnonymous="true"  />
       </group>
   </properties>
 </profile>

Note the attributes, name is very simple, but note alllowAnonymous: This allows anonymous users to read/write properties, you have to set it explicitly because by default, this is set to false. ASP.NET cannot know which user has which profile unless the user is authenticated. So to use this anonymous feauture you have to enable anonymous indentification in your web.config too. More details in MSDN here.

All I did what this...

<anonymousIdentification  enabled="true" />

You can also set defaultvalues, type, readonly attributes to your profile entries too.

Also I defined what is called 'Profile Groups', that lets you organise the properties better and into more logical groups. Then in your app the intellisense will pick it up beautifully! ( Profile.Preferences.Culture )

So far so good, we can store it in the Profile Object but using Profile is a bit of a problem in the same league as the Sesssion Object: Can we access Profile in Global.asax BeginRequest()? Nop, we can't, the only way is to write some code to access the data store where you are persisting the user's preferences. The reason this is the case is that just like Session, Profile is not initialised until Session is ready to roll.

Getting started with a solution to the problem

So far we know we don't want to use Session, we don't want to use Cookies and that Profile is good but we can't really use it straight out of the box.

Question I had was, how can I localise pages based on the choices the user made? and what if the user wants to change back and forth this setting for say language preferences. Anyway, I started easy, lets give them a UI so they can change languages for example.

I created a MasterPage and added some big, impossible to miss flags that can be associated to languages and cultures as shown below. The plan is that by clicking on the flag, the culture associated with the flags will be the Thread.Culture that the page will be running under.

Flags

Simple code too..just a few asp:Images..

<asp:ImageButton CommandName="es-UY" OnCommand="Flag_ClickedCommand" AlternateText="Spanish"
       CssClass="Flag" ImageUrl="~/Profile/FlagsImages/uy.gif" ID="imgUruguay" runat="server"
       />
<asp:ImageButton CommandName="zh-CN" OnCommand="Flag_ClickedCommand" AlternateText="Chinese"
       CssClass="Flag" ImageUrl="~/Profile/FlagsImages/cn.gif" ID="ImageButton2" runat="server"
        />
<asp:ImageButton CommandName="en-AU" OnCommand="Flag_ClickedCommand" AlternateText="English"
       CssClass="Flag" ImageUrl="~/Profile/FlagsImages/au.gif" ID="ImageButton1" runat="server"
        />

OK, note that I have setup an EventHandler for the OnCommand event, when this fires, Flag_ClickedCommand will be called and then culture that that particular flag represents will be passed on as CommandName.

protected void Flag_ClickedCommand(Object sender, CommandEventArgs args)
{
      if (!args.CommandName.IsValidCulture()) return;        
       Profile.Preferences.Culture = args.CommandName;
       Response.Redirect(Request.Path);        
}

Note what I set the Profile.Preferences.Culture to the command name passed on from the ImageButton, but this could well be a simple button or a dropdownlist value etc. I redirect the page to itself since if the user clicks on a flag, they'll expect the changes to take change immendiately! ( I would!)

I created an extension method for strings to check if the culture passed along was valid too. Not relevant to this post but since they are very neat I'll copy it here too. :-)

public static bool IsValidCulture(this string cult)
{
     if (Constants.ChineseCulture == cult
              || Constants.EnglishCulture == cult
              || Constants.SpanishCulture == cult)
     {
         return true;
     }
     return false;    
}

Setting the Thread.CurrentThread.CurrentCulture to your preferred culture

What we now want to do is for a page to load and pick the user's preferred culture.

I noted that you can override the InitializeCulture() method solely to intialise the page's culture. This function gets called very early in the page life cycle, well before and controls are created, meaning that if you want to get some values from controls, you must get them directly from the request using Form!

protected override void InitializeCulture()
{       
    string culture = Profile.Preferences.Culture;
    if (!culture.IsEmpty())
    {
        Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(culture);
    }
}

Unfortunately you have to override this function for every page, not very practical if you have a zillion pages. Next best is to provide a base common class and then inherit from this class so all your webforms can benefit and you don't have to implement the above in every form. This works fine!

Note that the Thread culture is only set for the current page and will not affect your "other" pages, initially I thought that the CurrentThread's culture will be set to the new values and we'll all happy. But then I also thought what If a user has a different entry point to the application, so the need for all your pages to be able to read and set the user's fav culture.

public partial class LocalisationBase : System.Web.UI.Page
{
    protected override void InitializeCulture()
    {
        ProfileCommon common = HttpContext.Current.Profile as ProfileCommon;
        string culture = common.Preferences.Culture;

        if (!culture.IsEmpty())
        {
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
            Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(culture);
        }
    }    
}
So base class it was and all my other pages inherited from this..
public partial class Profile_Default : LocalisationBase
{
    protected void Page_Load(object sender, EventArgs e)
    {
    }
...........

Now lets have a look at my initial UI that I wanted to localise and see if this works.

 englishUI

Just a simple form with some dates, numbers and a calendar what we hope we can localise and so the right names and formatting appear for the right culture setting. For the form labels I used resource files just like in my previous post. I have three resource files, one for english, one for spanish and one for mandarin.

The currency is interesting. All I have is a number where I call ToString("c")

private void FillPersonalisedMoney()
{
    lblMoney.Text = (500.23D).ToString("c");
}

Now when the user click on the Chinese flag...then the Chinese version of the page will render, dates will be rendered accordingly and the calendar will be in Chinese too..

chinaUI

And in Spanish too...

spanishUI

Clicking on Employee Details will take you to a page that also inherits from LocalisationBase and where Employees from ubiquitous Northwind will be displayed with to their likely annoyance, their date of birth, nicely formatted according to the culture of choice!

EmployeesDetails

EmployeesChineseDetails

And there you have it.

In conclusion I used the native Profile ASP.NET objects, you can implement your own Provider or you can use SQLProfileProvider that ASP.NET offers, either way I honestly think that these guys offer you much more flexibility and power when you want to customize your apps according to user's preferences.

Au revoir!

Resources:

#Must read for any localisation entrepreneurs

http://quickstarts.asp.net/QuickStartv20/aspnet/doc/localization/default.aspx

# ASP.NET 2.0 Localization (MSDN Article)

http://msdn2.microsoft.com/en-us/library/ms379546.aspx

#Profiles

http://msdn2.microsoft.com/en-us/library/at64shx3(VS.80).aspx

No comments: