Click here to Skip to main content
15,891,033 members
Articles / Web Development / ASP.NET

Declarative ASP.NET globalization

Rate me:
Please Sign up or sign in to vote.
4.91/5 (44 votes)
16 Apr 2004CPOL8 min read 219.9K   1.7K   93  
An article on how to implement globalization support for ASP.NET pages through attributes and reflection
<!--------------------------------------------------------------------------->
<!--                           INTRODUCTION                                

 The Code Project article submission template (HTML version)

Using this template will help us post your article sooner. To use, just 
follow the 3 easy steps below:
 
     1. Fill in the article description details
     2. Add links to your images and downloads
     3. Include the main article text

That's all there is to it! All formatting will be done by our submission
scripts and style sheets. 

-->
<!--------------------------------------------------------------------------->
<!--                        IGNORE THIS SECTION                            --><html><head>
        <title>The Code Project</title>
        <STYLE> BODY, P, TD { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt }
	H2,H3,H4,H5 { color: #ff9900; font-weight: bold; }
	H2 { font-size: 13pt; }
	H3 { font-size: 12pt; }
	H4 { font-size: 10pt; color: black; }
	PRE { BACKGROUND-COLOR: #FBEDBB; FONT-FAMILY: "Courier New", Courier, mono; WHITE-SPACE: pre; }
	CODE { COLOR: #990000; FONT-FAMILY: "Courier New", Courier, mono; }
	</STYLE>
        <link href="http://www.codeproject.com/styles/global.css" type="text/css" rel="stylesheet"></head>
    <body bgColor="#ffffff" color="#000000">
        <!--------------------------------------------------------------------------->
        <!-------------------------------     STEP 1      --------------------------->
        <!--  Fill in the details (CodeProject will reformat this section for you) --><pre>Title:       Declarative ASP.NET globalization
Author:      Sami Vaaraniemi
Email:       samiva@jippii.fi
Environment: .NET, C#, ASP.NET
Keywords:    ASP.NET, globalization, attribute, reflection, resource, satellite assembly
Level:       Intermediate
Description: An article on how to implement globalization support for ASP.NET pages through
             attributes and reflection
Section      ASP.NET
SubSection   General
</pre>
        <!-------------------------------     STEP 2      --------------------------->
        <!--  Include download and sample image information.                       -->
        <ul>
            <li>
                <A href="http://www.icustomsoftware.net/MyWebApp/webform1.aspx">View live application</A>
            <li class="download">
                <A href="DeclarativeGlobalization_src.zip">Download source -&nbsp;39 Kb</A>
            </li>
        </ul>
        <!-------------------------------     STEP 3      --------------------------->
        <!--  Add the article text. Please use simple formatting (<h2>, <p> etc)   -->
        <h2>
            Introduction
        </h2>
        <p>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.
        </p>
        <p>To keep the size of this article reasonable I'm going to concentrate on 
            localizing text contents&nbsp;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&nbsp;the control. We'll implement this code in a small framework so the web 
            page developer doesn't have to write it.
        </p>
        <P>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.
        </P>
        <h2>Making a globalization-aware web page</h2>
        <p>
            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 <code>Localize</code> 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:
        </p>
        <pre lang="cs">
[Localize(Mode=LocalizeMode.Fields,
          ResourceBaseName="MyWebApp.Strings")]
public class WebForm1 : System.Web.UI.Page
{
    [Localize()]
    protected System.Web.UI.WebControls.Label lblCopyright;
    // etc...
}       </pre>
        <p>The points of interest here are the <code>Localize</code> attributes attached to 
            the page class and to the <code>lblCopyright</code> label control. This 
            attribute will cause the text in the control to be replaced with a language 
            specific string at runtime. Technically, the <code>Localize</code> attribute 
            attached to the <code>WebForm1</code> page class is not absolutely necessary; 
            we could just as well have provided all the information through <code>Localize</code>
            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.
        </p>
        <P>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 <code>GlobalizationMod</code> 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:</P>
        <pre>&lt;system.web&gt;
    &lt;httpModules&gt;
        &lt;add name="GlobalizationModule" type="GlobalizationModule.GlobalizationMod, GlobalizationModule" /&gt;
    &lt;/httpModules&gt;
&lt;/system.web&gt;
        
&lt;appSettings&gt;
    &lt;add key="DefaultLanguage" value="en-US"/&gt;
&lt;/appSettings&gt;</pre>
        <h2>Hooking into the HTTP pipeline</h2>
        <p>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.
        </p>
        <p><IMG width="584" height="117" alt="ASP.NET HTTP pipeline" src="httppipeline.gif"></p>
        <p>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.
        </p>
        <p>Implementing an HttpModule boils down to creating a class that implements the <code>IHttpModule</code>
            interface. It has one method of interest, <code>void Init(HttpApplication)</code>, 
            in which we prepare to handle the <code>PreRequestHandlerExecute</code> event 
            of the HttpApplication:
        </p>
        <pre lang="cs">public class GlobalizationMod : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.PreRequestHandlerExecute += new EventHandler(this.OnPreRequest);
    }
    // etc�
}       </pre>
        <P></P>
        <p>This is where things get a bit tricky. In our <code>OnPreRequest</code> 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 <code>PreRender</code> event of the web page 
            class. Therefore, <code>OnPreRequest</code> simply adds a handler to the <code>PreRender</code>
            event of the web page:
        </p>
        <pre lang="cs">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);
}       </pre>
        <p>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 <code>OnPreRender</code>.
        </p>
        <h2>Processing Localize attributes</h2>
        The real work begins in the <code>OnPreRender</code> method of the <code>GlobalizationMod</code>
        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 <code>Localize</code> attribute 
        attached to it. If so, we call an internal helper function <code>LocalizeObject</code>
        with the page and the attribute as parameters.
        <pre lang="cs">
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 &amp;&amp; typeAttrs.Length &gt; 0)
    {
        LocalizeObject(null, sender, (LocalizeAttribute)typeAttrs[0]);
    }        
}</pre>
        <p><code>LocalizeObject</code> 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 <code>LocalizeMode.Fields</code>, 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 <code>LocalizeObject</code> recursively for each 
            child object that has the <code>Localize</code> attribute attached to it. The 
            code for <code>LocalizeObject</code> is shown below:
        </p>
        <pre lang="cs">
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 &amp;&amp; typeAttrs.Length &gt; 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_);
        }
    }
}</pre>
        <p>For those objects whose localization mode is not <code>LocalizeMode.Fields</code>, 
            the code proceeds to localize the object itself. The actual logic of the 
            localization is in the <CODE>LocalizeObject</CODE> 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.
        </p>
        <P>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 <code>LocalizeAttribute.LocalizeObject</code>
            is enough:</P>
        <pre lang="cs">
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("&lt;font color=\"red\"&gt;NO RESOURCE FOUND FOR CULTURE: {0}&lt;/font&gt;", 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 });
    }
}</pre>
        <P>The code above loads the string for the current language from a satellite 
            assembly. The name of the target object (e.g., <code>lblCopyright</code>) is 
            used as the name of the resource. Finally, the code calls <code>InvokeMember</code>
            with the <code>BindingFlags.SetProperty</code> flag to assign the string to a 
            property of the target object. By default, the value of the attribute's <code>Action</code>
            property is 'Text', and therefore by default the string is assigned to the <code>Text</code>
            property of the target object. In other words, in the case of the <code>lblCopyright</code>
            control, the code does effectively this:</P>
        <P>&nbsp;&nbsp;&nbsp;&nbsp;<code>lblCopyright.Text = 
                resourceManager.GetString("lblCopyright", CurrentCulture);</code></P>
        <P>
            In cases where the localization of a control requires more complex logic, the 
            developer can derive a new class from <code>LocalizeAttribute</code> and attach 
            the new attribute to the control. To implement the special localization logic, 
            the developer overrides the <code>LocalizeObject</code> method and the 
            framework will call her code when it is time to localize the control.</P>
        <P>Problems with missing resources are handled by&nbsp;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&nbsp;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?</P>
        <h2>Summary</h2>
        <P>
            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.</P>
        <P>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. 
            <!-------------------------------    That's it!   ---------------------------></P>
    </body>
</html>

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
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