Click here to Skip to main content
Click here to Skip to main content

Declarative ASP.NET globalization

, 16 Apr 2004 CPOL
Rate this:
Please Sign up or sign in to vote.
An article on how to implement globalization support for ASP.NET pages through attributes and reflection

Introduction

Even as an old-school C++ programmer who initially regarded reflection as mildly evil I must admit that you can actually do pretty cool things with it. Reflection together with attributes is a powerful tool that allows for creative ways of implementing things. In this article, I will show how to create multi-lingual ASP.NET web pages declaratively through attributes and reflection.

To keep the size of this article reasonable I'm going to concentrate on localizing text contents of simple controls on a web page. Localizing content and more complex controls warrant an article of their own. In practice the localization of a simple control boils down to retrieving a language specific string from a resource and assigning it to a property of the control. We'll implement this code in a small framework so the web page developer doesn't have to write it.

The goals for our globalization support framework will be three-fold. First, we’ll want to make things as convenient as possible for the web page developer. This means that in order to get a server side control localized, in the simple case the developer will not have to write code other than declaring an attribute. Second, we’ll want to implement the framework in non-intrusive manner. Some globalization solutions are based on inheritance requiring the page or the control class to derive from a class that handles the localization tasks. Using attributes is less intrusive as there might already be a common base class for the web pages and inheriting from another class might not be feasible. Finally, we’ll want to make the globalization framework extensible to allow the developer to handle more complex localization situations.

Making a globalization-aware web page

We'll begin by taking a look at how a developer creates a globalization-aware web page using our framework. To localize controls on a web page, the developer tags them with Localize attributes and provides the appropriate language specific strings in a resource. In the simplest case, this is all the code the developer has to write. The following code snippet shows what the code looks like in the code behind page class:

[Localize(Mode=LocalizeMode.Fields,
          ResourceBaseName="MyWebApp.Strings")]
public class WebForm1 : System.Web.UI.Page
{
    [Localize()]
    protected System.Web.UI.WebControls.Label lblCopyright;
    // etc...
}       

The points of interest here are the Localize attributes attached to the page class and to the lblCopyright label control. This attribute will cause the text in the control to be replaced with a language specific string at runtime. Technically, the Localize attribute attached to the WebForm1 page class is not absolutely necessary; we could just as well have provided all the information through Localize attributes attached to the individual controls. I decided to implement the globalization support to allow a kind of a root attribute to be attached to the web page class. This way the web page developer can have common localization information in the root attribute and does not need to repeat it in every attribute attached to controls on one page. This makes sense as most of the resources for one web page are likely to reside in one source, e.g., a resource assembly. The root attribute also allows the implementation to quickly decide whether a page needs localization or not.

In addition to attaching attributes to the controls, there is some configuration to do. We need to make two additions to the web application configuration file. First, ASP.NET needs to be told about the GlobalizationMod HttpModule that does most of the leg work in the globalization. Second, the web site's default language needs to be communicated to the globalization module. Both additions go to the web.config file and are shown below:

<system.web>
    <httpModules>
        <add name="GlobalizationModule" 
          type="GlobalizationModule.GlobalizationMod, GlobalizationModule" />
    </httpModules>
</system.web>
        
<appSettings>
    <add key="DefaultLanguage" value="en-US"/>
</appSettings>

Hooking into the HTTP pipeline

Now that we’ve seen what the code looks like from the web page programmer’s point of view, let’s take a look at the implementation. We need to replace the text in the controls with language specific strings after the page is constructed and initialized but before it renders the HTML into the output stream. In order to do this, we need to hook into the ASP.NET HTTP pipeline. The figure below depicts the pipeline.

When a web request comes from the network, IIS handles it and passes it over to aspnet_isapi.dll which is registered to handle requests for .ASPX pages. aspnet_isapi.dll then passes the request to the appropriate HttpApplication instance. The HttpApplication massages the request and hands it over to one or more HttpModule objects. HttpModules perform tasks such as authentication and caching, and they get to act in the pipeline before, during and after an HttpHandler (i.e., the web page class) processes the request. By implementing an HttpModule we can do the localization logic at the right place in the pipeline.

Implementing an HttpModule boils down to creating a class that implements the IHttpModule interface. It has one method of interest, void Init(HttpApplication), in which we prepare to handle the PreRequestHandlerExecute event of the HttpApplication:

public class GlobalizationMod : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.PreRequestHandlerExecute += new EventHandler(this.OnPreRequest);
    }
    // etc…
}       

This is where things get a bit tricky. In our OnPreRequest handler, we can easily get our hands on the web page instance we’re about to localize. But it turns out that the child controls of the web page class have not been instantiated yet at this point - the data members of the page are null. In order get to the child controls after they are created but before they render themselves, we need to hook the PreRender event of the web page class. Therefore, OnPreRequest simply adds a handler to the PreRender event of the web page:

public void OnPreRequest(object sender, EventArgs eventArgs)
{
    // Get the IHttpHandler from the HttpApplication
    HttpContext ctx = ((HttpApplication)sender).Context;
    IHttpHandler handler = ctx.Handler;

    // Handle the PreRender event of the page
    ((System.Web.UI.Page)handler).PreRender += new EventHandler(
         this.OnPreRender);
}       

Now that we've successfully hooked into the proper place in the HTTP pipeline, we are finally ready to write the meat of the globalization support implementation in OnPreRender.

Processing Localize attributes

The real work begins in the OnPreRender method of the GlobalizationMod HttpModule. The code first checks if the current language is the web site’s default language, and if so, it exits as there is nothing to do. Otherwise, the code proceeds to see if the page class has the Localize attribute attached to it. If so, we call an internal helper function LocalizeObject with the page and the attribute as parameters.
public void OnPreRender(object sender, EventArgs eventArgs)
{
    // If current culture is the same as the web site's
    // default language, then we do nothing
    CultureInfo currentCulture = 
      System.Threading.Thread.CurrentThread.CurrentUICulture;
    if (ConfigurationSettings.AppSettings["DefaultLanguage"] == 
            currentCulture.Name)
        return;

    System.Web.UI.Page page = (System.Web.UI.Page)sender;

    // Localize the page if it has the Localize attribute
    object[] typeAttrs = page.GetType().GetCustomAttributes(
         typeof(LocalizeAttribute), true);
    if (typeAttrs != null && typeAttrs.Length > 0)
    {
        LocalizeObject(null, sender, (LocalizeAttribute)typeAttrs[0]);
    }        
}

LocalizeObject is by far the most complex function in the framework. It first looks at the attribute associated with the target object. If the attributes localize mode is LocalizeMode.Fields, it indicates that we should localize the fields of the object rather than the object itself. In this case, the code retrieves the fields of the object through reflection and calls LocalizeObject recursively for each child object that has the Localize attribute attached to it. The code for LocalizeObject is shown below:

public class GlobalizationMod : IHttpModule
{
    // ...
    
    protected void LocalizeObject(FieldInfo fieldInfo, object target, 
        LocalizeAttribute attr)
    {
        if (attr.Mode == LocalizeAttribute.LocalizeMode.Fields)
        {
            //
            // Localize child objects
            //
            FieldInfo[] fields = null;
            Type targetType = null;

            if (target is System.Web.UI.Page)
            {   // Remember the web page class
                targetPage_ = (System.Web.UI.Page)target;
                // Remember root attribute
                rootAttribute_ = attr;
                targetType = target.GetType().BaseType;
            }
            else
            {
                targetType = target.GetType();
            }

            //
            // Localize fields that have the Localize attribute
            //
            fields = targetType.GetFields(BindingFlags.Instance|
                BindingFlags.NonPublic|BindingFlags.Public);
            foreach (FieldInfo f in fields)
            {
                // Get the child instance
                object child = f.GetValue(target);
                if (child != null)
                {
                    // Localize this object if it has the Localize attribute
                    object[] typeAttrs = f.GetCustomAttributes(
                       typeof(LocalizeAttribute), true);
                    if (typeAttrs != null && typeAttrs.Length > 0)
                    {
                        LocalizeObject(f, child, (LocalizeAttribute)
                              typeAttrs[0]);
                    }
                }
            }
        }
        else
        {
            // If this attribute has no resource name specified,
            // use the root attribute's resource name as base resource name.
            if (attr.ResourceBaseName == null)
                attr.ResourceBaseName = rootAttribute_.ResourceBaseName;

            // If this attribute has no resource name specified,
            // use the target object's name as resource name.
            if (attr.ResourceName == null)
                attr.ResourceName = fieldInfo.Name;

            // The actual localization of the target object is performed
            // in a virtual method of the attribute. This way the developer
            // can implement her own localization logic by deriving a class
            // from LocalizeAttribute and overriding the Localize method.
            attr.LocalizeObject(target, targetPage_);
        }
    }
}

For those objects whose localization mode is not LocalizeMode.Fields, the code proceeds to localize the object itself. The actual logic of the localization is in the LocalizeObject method of the attribute class. Having the actual logic in a virtual method of the attribute class makes it possible for the developer to extend the globalization framework.

In simple cases where the localization is done by loading the language specific string from a satellite assembly and assigning the string to a property of the target object, the implementation in the LocalizeAttribute.LocalizeObject is enough:

public class LocalizeAttribute : Attribute
{
    // ...        

    public virtual void LocalizeObject(object target, System.Web.UI.Page page)
    {                        
        // User's page class is superclass of the ASP.NET page class
        Type userPageClass = page.GetType().BaseType;
        // The user's assembly is the one that holds his/her page class
        Assembly targetAssembly = userPageClass.Assembly;

        CultureInfo culture = 
             System.Threading.Thread.CurrentThread.CurrentUICulture;
        ResourceManager resMan = new ResourceManager(ResourceBaseName, 
           targetAssembly);                

        // There are a number of ways we could handle the 
        // case when a resource
        // is not found - we could e.g., simply allow the exception 
        // to pass and
        // let the global OnError handler handle it. Instead, we'll show
        string s = string.Format(
            "<font color=\"red\">NO RESOURCE FOUND FOR CULTURE: {0}</font>", 
            culture.Name);
        try
        {
            s = resMan.GetString(ResourceName, culture);
        }
        catch (MissingManifestResourceException)
        {}

        // Invoke the target's Action property. Most of the time this
        // means we'll set the target's Text property.
        target.GetType().InvokeMember(
            this.Action,
            BindingFlags.SetProperty,
            null,
            target,
            new object[]{ s });
    }
}

The code above loads the string for the current language from a satellite assembly. The name of the target object (e.g., lblCopyright) is used as the name of the resource. Finally, the code calls InvokeMember with the BindingFlags.SetProperty flag to assign the string to a property of the target object. By default, the value of the attribute's Action property is 'Text', and therefore by default the string is assigned to the Text property of the target object. In other words, in the case of the lblCopyright control, the code does effectively this:

lblCopyright.Text = resourceManager.GetString("lblCopyright", CurrentCulture);

In cases where the localization of a control requires more complex logic, the developer can derive a new class from LocalizeAttribute and attach the new attribute to the control. To implement the special localization logic, the developer overrides the LocalizeObject method and the framework will call her code when it is time to localize the control.

Problems with missing resources are handled by putting a special error string into the target control. This is fine during development but you'll want to have something less prone to error before you actually release your web application. A good way to ensure that no missing resources slip into a release is to have a unit test that visits all pages with all supported languages and asserts that the error string is not present on any of the pages. That should not be difficult to do since we all use unit test frameworks that make it a breeze writing such tests, don't we?

Summary

The framework presented in this article allows the developer to create multi-lingual websites declaratively. Controls are localized by attaching attributes to them and by providing the appropriate language specific string resources in satellite assemblies.

The sample project contains the source code to a web application that has all strings localized to four languages (apologies for any incorrect language). Additionally it shows how to localize an ImageButton and also how the framework can be extended to localize a more complex control such as a DataList. I hope you find the ideas useful.

History

  • 16 April 2004 - Source code updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sami Vaaraniemi
Web Developer
Finland Finland
Sami Vaaraniemi has been working as a software developer since 1990, primarily on Microsoft technologies. After 12 years of Win32 API and C++ he switched to .NET. He currently works as an independent consultant and can be contacted through his website at www.capehill.net.

Comments and Discussions

 
Questioncheck out PinmemberMember 77342194-Mar-12 20:14 
GeneralBest Localization Plug-in for Visual Studio. PinmemberAlexander Nesterenko17-Dec-08 22:34 
Generalsupport the localization of new control types (RadiobuttonList) PinmemberRam@1117-Sep-08 22:38 
QuestionHow to place the resource file in a folder?? Pinmemberwimdeblende26-Jun-08 6:32 
QuestionIn VB.NET PinmemberStephen Saw10-Feb-08 22:01 
GeneralProblem with user controls added at runtime PinmemberLuong Dinh Dung26-Oct-05 3:47 
QuestionConversion to Asp.Net 2.0 PinsussJulian Philip Jelfs6-Sep-05 1:05 
GeneralUser Controls Pinmemberramyb6-May-05 3:06 
GeneralPossible Extension Pinmemberricherm4-Apr-05 14:06 
Generallocalize custom classes Pinmemberpatbe15-Feb-05 2:36 
QuestionCan it be done this way ?? Pinmemberkedares3-Nov-04 0:07 
Questionserver.transfer? PinmemberHousten22-Sep-04 9:34 
AnswerRe: server.transfer? Pinmemberkedares4-Nov-04 7:04 
Generaldemo PinmemberoOomen19-Sep-04 11:55 
GeneralTttle Bar Pinmembermikethelad3-Jun-04 1:43 
GeneralOn Improving Performance PinmemberEron Wright7-May-04 5:25 
GeneralRe: On Improving Performance PinmemberSami Vaaraniemi9-May-04 21:46 
QuestionResource solution in Winforms?? PinsussAnonymous21-Apr-04 9:30 
GeneralRadioButtonList and CheckBoxList Pinmemberashara215-Apr-04 4:12 
GeneralRe: RadioButtonList and CheckBoxList PinmemberSami Vaaraniemi16-Apr-04 23:02 
GeneralMultiple properties per control. Pinmembersjsfly3-Feb-04 12:05 
Hi Sami,
 
Thanks for posting this great article. I really appreciate you taking the time to share your work (and for writing in English!). I also read the MSDN article on globalization referenced in the comments.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaspp/html/entloctoolkit.asp
 
This prompted one question concerning your code: with your code, is it possible to specify different strings for multiple properties of a particular control? Or will I need to extend your code to support this? For example, I need to specify both the Text property and the Tooltip property on a Label WebControl. But it appears that the default assignment of the resource string is to the Text property.
 
Thanks! Michael
GeneralRe: Multiple properties per control. PinmemberSami Vaaraniemi4-Feb-04 1:04 
GeneralRe: Multiple properties per control. Pinmemberashara215-Apr-04 4:07 
GeneralRe: Multiple properties per control. PinmemberSami Vaaraniemi16-Apr-04 22:57 
AnswerRe: Multiple properties per control. [modified] Pinmembern1ck0s19-Jul-06 3:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 17 Apr 2004
Article Copyright 2003 by Sami Vaaraniemi
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid