Controlling Page-Level Culture Settings
ASP.NET
2.0 makes it easy to change the culture settings on a page-by-page
basis. You simply add the UICulture and the Culture attributes to the
Page directive within an .aspx page like so.
Early in the page lifecycle, an .aspx page running with these
attribute settings will initialize the current thread's CurrentUICulture
property and CurrentCulture property with the appropriate CultureInfo
objects. If you add the attributes shown above to an .aspx file during
your testing, then add a built-in ASP.NET Web control such as the
Calendar control to the page, you'll see immediately that things are
working properly, as Figure 1 illustrates. The Calendar
control on the left has been rendered on a page with a culture setting
of en-US while the Calendar control on the right has been rendered on a
page with a culture setting of fr-BE.
<%@ Page UICulture="fr" Culture="fr-BE" %>
Figure 1 Localized Calendar Controls
In
most production Web sites, however, it doesn't make sense to hardcode
specific culture settings into your pages like this. Instead, users who
speak and read different languages should see different localized
renderings of the same page. ASP.NET can automatically initialize
culture settings for you on a per-request basis if you assign a value of
"auto" to both the UICulture and the Culture attributes.
<%@ Page UICulture="auto" Culture="auto" %>
ASP.NET
initializes the settings by examining HTTP headers sent by the
browsers. You can test different localized versions of your pages in
Internet Explorer® by changing the language preference settings in the Internet Options dialog.
Typically,
you'll want all of the pages in your site to conform to the same
culture settings. You can assign a site-wide value of "auto" for the
UICulture and Culture attributes so you don't have to touch every page
individually. Just add the following element into the web.config file
located at the root of a site:
<globalization uiCulture="auto" culture="auto" />
You
can also specify a default culture (in addition to the auto setting)
that ASP.NET will use if it can't find an HTTP header to determine the
user's preferred culture:
<globalization uiCulture="auto:en" culture="auto:en-US" />
Tracking Language Preferences Using Profiles
While
the auto setting certainly makes things easy, it isn't always
convenient for the user. Suppose, for example, a user prefers to read
technical Web sites in English and business-related Web sites in French.
He'd find it really annoying to change browser settings as he moved
back and forth between sites. This user would really appreciate a Web
site that let him select a language preference.
To
see how to build a UI and a supporting implementation that makes it
easy for users to switch back and forth between different languages
download this month's code sample, an ASP.NET 2.0 Web site named
LitwareWebApp. Its UI is shown in Figure 2.
Figure 2a English Version
Figure 2b Localized Version
The
LitwareWebApp site tracks user language preferences using the new
profile feature introduced in ASP.NET 2.0. You can add a string-based
profile property named LanguagePreference that supports anonymous
identification by adding the following elements to the site's web.config
file:
<configuration>
<system.web>
<anonymousIdentification enabled="true"/>
<profile>
<properties>
<add name="LanguagePreference" type="string"
defaultValue="Auto" allowAnonymous="true" />
</properties>
</profile>
</system.web>
</configuration>
The
LitwareWebApp site is designed with a standard layout in a Master Page
containing a RadioButtonList control named lstLanguage. Note that while
this control displays friendly language names such as U.S. English and
Belgian French, it also tracks the real culture names such as en-US and
fr-BE using the SelectedValue property. When a user changes the language
preference, the SelectedIndexChanged event of the lstLanguage control
fires and executes the following code to update the LanguagePreference
profile property:
Profile.LanguagePreference = lstLanguage.SelectedValue
Response.Redirect(Me.Request.Url.AbsolutePath)
The
call to Response.Redirect forces a new round-trip from the browser to
the Web server, which causes the lifecycle of the page to start again
after the profile property has been set up with the desired language
preference.
The
next thing you have to deal with is programmatically adjusting the
culture setting at the appropriate time in the page lifecycle. The
proper way to do this in ASP.NET 2.0 is to override a method of the Page
class named InitializeCulture. The page lifecycle has been designed to
always call InitializeCulture before the page itself or any of its child
controls have done any work with localized resources.
The
design of the sample site requires that you add an overridden
implementation of InitializeCulture to every page in the site.
Unfortunately, you can't override the InitializeCulture method at the
level of a Master Page because the MasterPage class does not inherit
from the Page class. Furthermore, it would be tedious to override the
InitializeCulture method separately for every page in a Web site. This
would lead to many redundant implementations that would pose a serious
maintenance issue.
A
more efficient site-wide approach to initializing culture settings is
to create a common Page-derived base class and then have all your .aspx
page files inherit from that, and that's what I did in the LitwareWebApp
sample site. My Page-derived base class named LitwarePage (see Figure 3)
was defined in a source file named LitwarePage, which was added to the
App_Code directory so that it is automatically compiled by ASP.NET and
made available to other code in the current Web site.
Figure 3 Site-Wide Override of the InitializeCulture Method
Imports System.Globalization
Imports System.Threading
Public Class LitwarePage : Inherits Page
Protected Overrides Sub InitializeCulture()
‘*** make sure to call base class implementation
MyBase.InitializeCulture()
‘*** pull language preference from profile
Dim LanguagePreference As String = _
CType(Context.Profile, ProfileCommon).LanguagePreference
‘*** set the cultures
If LanguagePreference IsNot Nothing Then
Me.UICulture = LanguagePreference
Me.Culture = LanguagePreference
End If
End Sub
End Class
Once
you've created a Page-derived base class, you can update your .aspx
page definitions to derive from it instead of from the standard Page
class. For example, you can modify the partial class in default.aspx.vb
to look like this.
Partial Class _Default : Inherits LitwarePage
'*** page class definition goes here
End Class
At
this point, I have a Web site capable of tracking user language
preferences and initializing culture settings on a per-request basis.
Now I have to localize the string literals for an ASP.NET 2.0 Web site
using resource files so I can accommodate users who speak and read
different languages.
Resource Files in ASP.NET 2.0
Since, by default, Visual Studio®
2005 doesn't use projects to manage ASP.NET 2.0 Web sites, there's no
project-level resource file as there is for a Windows Forms application
or a class library DLL. Instead, you must explicitly create resource
files and add them to your Web site. Furthermore, you must use special
folders that were introduced with ASP.NET 2.0: resource files containing
global resources should be added to the App_GlobalResources folder, and
local resources specific to a single file should be added to the
App_LocalResources folder. Global resources are those that can be used
on a site-wide basis from within pages as well as from within other
files such as a site map. ASP.NET files types that support local
resources include pages (.aspx files), Master Pages (.master files) and
user controls (.ascx files).
Another
difference with ASP.NET 2.0 is that you don't have to compile resource
files ahead of time, as you do when developing an internationalized
Windows Forms application. Instead, the ASP.NET runtime compiles global
and local resource files into DLLs in a just-in-time fashion just as it
does with .aspx files. This is a powerful feature because a company can
add localization support for a new language simply by XCOPYing .resx
files to a production Web server.
Let's
work through an example of creating and using a global resource file in
an ASP.NET 2.0 Web site with Visual Studio 2005. You can create a new
global resource file by choosing the Add New Item command and then
selecting Resource File.
When
you click the Add button to create a new global resource file, Visual
Studio 2005 prompts you with a dialog box recommending that you place
the new resource file in the App_GlobalResources directory. Click Yes.
If you put it elsewhere, ASP.NET will not automatically compile the
resource file into a DLL.
Working
with resource files in ASP.NET is similar to working with them in a
Windows Forms application. You begin by creating a resource file with
string literals localized for the default culture. In our sample Web
site, there's a global resource file for this purpose named Litware.resx
as shown in Figure 4. Once you've added all the named
strings for the default culture, you can copy the resource file and
rename it to something such as Litware.fr.resx to provide strings
localized in French. You can also copy that French resource file and
rename it to Litware.fr-BE.resx to maintain strings that have been
regionally localized for Belgian French.
Figure 4 Localized Resources
Adding
and maintaining named strings within a resource file is easy because
Visual Studio 2005 provides the convenient resource editor shown in Figure 5.
Remember that resource files do not limit you to localizing strings.
You can add in other types of resources such as image files, cascading
style sheets and client-side JavaScript files.
Figure 5 The Visual Studio 2005 Resouce Editor
Now
let's create pages that retrieve named strings from a global resource
file. It's very easy to do. Just as when you're developing an
internationalized Windows Forms application, there's no need to program
directly against the ResourceManager class supplied by the .NET
Framework. That's because ASP.NET and Visual Studio 2005 build a
strongly typed resource class behind the scenes for each global resource
file and make it available through IntelliSense®.
The
named strings you add to a global resource file are accessible through a
strongly typed class nested within a top-level namespace named
Resources. It takes a single line of code to assign a localized string
to a control's property value:
lblApplicationName.Text = Resources.Litware.ApplicationName
In
addition to programmatic access, ASP.NET 2.0 also introduces
declarative syntax you can use to bind a named string to a property of a
page or control. The syntax involves using the dollar sign ($) followed
by the Resources namespace, the name of the resource file and the name
of the string:
<%$ Resources:Litware, ApplicationName %>
For
example, if you want to bind the string named ApplicationName to the
Text property of a label within an .aspx page, you can write the tag to
look like this:
<asp:Label ID="lblApplicationName" runat="server"
Text="<%$ Resources:Litware, ApplicationName %>" />
Visual Studio 2005 also provides a convenient tool named the Expression Builder, shown in Figure 6.
This utility can help you generate the required syntax for binding
named strings in a resource file to the properties of controls and
pages. Once you've added one or more global resource files with named
strings, you can access the Expression Builder by putting an .aspx page
into design view and accessing the Expressions property through the
Property sheet.
Figure 6 Expression Builder
Note
that declarative resource binding expressions are not limited to .aspx
files, .ascx files and .master files. They can also be used to localize
string literals from a site map defined in a Web.sitemap file. Figure 7
shows the XML from the site map used in the LitwareWebApp Web site to
localize the link titles shown in the site's navigation menu.
Figure 7 Retrieving Literal Strings from a Global Resource File
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0"
enableLocalization="true" >
<siteMapNode url="default.aspx"
title="$resources: Litware, HomePageLink" >
<siteMapNode url="AddCustomer.aspx"
title="$resources: Litware, AddCustomerPageLink" />
<siteMapNode url="Controls.aspx"
title="$resources: Litware, ControlsPageLink" />
<siteMapNode url="About.aspx"
title="$resources: Litware, AboutPageLink" />
</siteMapNode>
</siteMap>
Working with Local Resources
A
local resource file contains resources for a single file-based item
within a site such as a page, a Master Page or a User Control. Each
local resource file must be properly named and added to the
App_LocalResources folder to be compiled by ASP.NET.
A
local resource file should be named in accordance with the file-based
item to which it is supplying its resources. For example, the local
resource file containing default culture resources for the
AddCustomer.aspx page should be named AddCustomer.aspx.resx. The local
resource file containing French resources should be named
AddCustomer.aspx.fr.resx.
Once
you've added named strings to a local resource file, you can access
them from within a page or a user control in three different ways.
First, you can access them programmatically. Second, you can bind to
them declaratively using explicit syntax. Third, you can bind to them
declaratively using implicit syntax. Let's examine each of these
approaches.
Imagine
you've created local resource files to localize all the control
captions shown on the page AddCustomer.aspx. To localize the caption
shown on the page's submit button, you can create a localized string
named btnSubmit.Text. Once you've added this named string to the local
resource file, you can access it through code by calling the
GetLocalResourceObject method and converting the return value to a
string:
'*** code inside AddCustomer.aspx.vb
btnSubmit.Text = _
Me.GetLocalResourceObject("btnSubmit.Text").ToString()
This
code isn't as nice as the code shown earlier to access a named string
from with a global resource using a strongly typed class. Local resource
files do not have associated strongly typed classes, so you won't
benefit from IntelliSense and you must explicitly convert the
Object-based return value when calling GetLocalResourceObject.
If
you want to use an explicit declarative binding syntax, it works almost
identically as with global resources. The only difference is that you
omit the name of the resource file when you're working with local
resources:
<asp:Button ID="btnSubmit" runat="server"
Text="<%$ Resources:btnSubmit.Text %>" />
Implicit
declarative binding syntax is the most powerful option. You begin by
adding a special attribute named meta:resourcekey to a control tag or to
an ASP.NET directive such as Page, Master or Control. For example, if
you want to use implicit declarative binding syntax with a Button
control in an .aspx file, you would write its tag to look like this:
<asp:Button ID="btnSubmit" runat="server"
meta:resourcekey="btnSubmit" />
Once
you've added the meta:resourcekey attribute, the only other thing to
worry about is making sure there are strings in the local resource file
with the proper name. In my example, ASP.NET will automatically load
the localized string named btnSubmit.Text and assign it to the Text
property of the control named btnSubmit.
The
key point is that implicit binding is based on creating strings that
have names matching targets defined by the meta:resourcekey attribute
plus the name of a property. Since the meta:resourcekey in this example
is targeted at btnSubmit, you can bind to several other property values
in addition to Text simply by adding more named strings into local
resource files as shown in Figure 8.
Figure 8 Adding Named Strings
Note
that Visual Studio 2005 provides a convenient command called Generate
Local Resource in the Tools menu when a Page, User Control, or Master
Page is open in design view editor. This command automates the creation
of a local resource file for the default culture. It also adds
meta:resourcekey attributes throughout the page and creates
corresponding string values in the local resource file to act as targets
for meta:resourcekey attribute entries.
Finally,
note that there's a new ASP.NET 2.0 component called the Localize
control, which lets you localize any element on an .aspx page. It
provides a design time feature not offered by its base class, the
Literal control; in particular, the Localize control provides design
time editing of static content so you can see a default value while
working in page design mode.
Embedding Resources in a DLL Project
I'm
going to depart from the topics of internationalization and
localization for a moment to discuss a new ASP.NET technique for using
embedded resources in a class library DLL. This technique allows you to
embed resources such as images files, cascading style sheet files, and
JavaScript files inside a DLL and expose them through a URL on the
hosting Web server.
Note
that this technique requires the use of a class library DLL targeting
ASP.NET 2.0 Web sites. The new functionality was added by the ASP.NET
team specifically to give server-side control authors a better way to
distribute the resource files that accompany their custom controls and
Web Parts. Instead of having to distribute resource files along with a
DLL and ensure they are copied into an accessible path on the hosting
Web server, resource files can be distributed inside the DLL itself and
exposed through URLs generated by ASP.NET at run time.
The
LitwareWebApp Web site contains a class library DLL project named
LitwareWebComponents that demonstrates this technique. Inside this
project, an image file named LitwareSlogan.png has been embedded as a
resource. You can embed a resource into an assembly by changing the
file's Build Action to Embedded Resource, as shown in Figure 9.
Figure 9 Embed a Resource
To
provide Web-based access to an embedded resource file inside a DLL, you
must add an assembly-level attribute named WebResource. When you add
the WebResource attribute, you must include the qualified name of the
resource file along with its MIME type. Within a Visual Basic® class library DLL project, the qualified resource file name includes the project name.
'*** inside AssemblyInfo.vb
Imports System.Web.UI
<Assembly: WebResource( _
"LitwareWebComponents.LitwareSlogan.png", "image/png")>
The
WebResource attribute is what allows to you to provide the ASP.NET
runtime with the metadata it needs to retrieve the resource file from
the DLL using a URL that can be generated at run time. To generate the
URL to the resource file from code within a server-side control you call
a method named GetWebResourceUrl as shown in Figure 10.
Figure 10 Accessing an Embedded Resource File
Public Class LitwareCustomControl
Inherits WebControl
Protected imgSlogan As Image
Protected Overrides Sub CreateChildControls()
‘*** determine Url to Web resource
Dim ResourceName, ResourceUrl As String
ResourceName = "LitwareWebComponents.LitwareSlogan.png"
ResourceUrl = Me.Page.ClientScript.GetWebResourceUrl( _
Me.GetType(), ResourceName)
‘*** Point Image control to Web Resource Url
imgSlogan = New Image
imgSlogan.ImageUrl = ResourceUrl
Controls.Add(imgSlogan)
End Sub
End Class
Here's
what happens behind the scenes to make this technique work. A call to
GetWebResourceUrl generates a URL pointing to a built-in HTTP handler
named WebResource.axd. The dynamically generated URL also includes a
query string identifying the name of the target DLL and the embedded
resource file. The ASP.NET runtime responds to requests for
WebResource.axd by loading a custom HttpHandler class named the
AssemblyResourceLoader.
When
the AssemblyResourceLoader class is called upon to load a resource file
from a DLL, it reads the metadata supplied by the WebResource
attribute. The AssemblyResourceLoader class has been implemented to
extract the request resource file from the image of the DLL and stream
it back to the caller. The AssemblyResourceLoader class even supplies a
caching algorithm which can reuse the same resource file across multiple
requests after it has been loaded into memory on the front-end Web
server host.
Displaying Localized Images
Though
using embedded resource files and the WebResource attribute is
powerful, there are some notable limitations. First, you can only use
this technique inside a DLL project that's targeted for ASP.NET 2.0 Web
sites. You can't use it directly inside an ASP.NET 2.0 Web site. Second,
this technique doesn't really support any form of localization. You'll
have to resort to a different approach if your Web site has resource
files such as graphic images and cascading style sheets that have been
localized.
The
LitwareWebApp Web site displays a graphic image named
LitwareSlogan.png. The site has been designed to display a different
version of the image depending whether the current user prefers English
or French. While ASP.NET 2.0 doesn't directly support localizing image
files, it doesn't require too much custom code to achieve the desired
effect.
You
can start by adding the localized versions of an image file to
localized versions of a global resource file. For example, the English
version of LitwareSlogan.png has been added to the global resource file
named Litware.resx while the French version of LitwareSlogan.fr.png has
been added to Litware.fr.resx. The resources in both resource files have
been given the same name of LitwareSlogan.
When
you have localized versions of the image file in different localized
versions of a global resource file, you can load them conditionally
based on the user's language preference using the custom handler file
named LitwareSlogan.ashx shown in Figure 11.
Figure 11 Loading Localized Images
<%@ WebHandler Language="VB" Class="LitwareSlogan" %>
Imports System
Imports System.Web
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Threading
Imports System.Globalization
Public Class LitwareSlogan : Implements IHttpHandler
Public Sub ProcessRequest(ByVal context As HttpContext) _
Implements IHttpHandler.ProcessRequest
‘*** switch to correct MIME type
context.Response.ContentType = "image/png"
‘*** change UICulture before loading image
Dim LanguagePreference As String = CType(context.Profile, _
ProfileCommon).LanguagePreference
If LanguagePreference IsNot Nothing AndAlso _
LanguagePreference <> "Auto" Then
‘*** change culture UI setting fro current thread
Thread.CurrentThread.CurrentUICulture = _
New CultureInfo(LanguagePreference)
End If
‘*** load in image from resource file and send to browser
Resources.Litware.LitwareSlogan.Save( _
context.Response.OutputStream, ImageFormat.Png)
End Sub
Public ReadOnly Property IsReusable() As Boolean _
Implements IHttpHandler.IsReusable
Get
Return False
End Get
End Property
End Class
The
custom handler class defined in LitwareSlogan.ashx initializes the
current thread's CurrentUICulture setting before retrieving the image
file from the global resource file using logic similar to what you saw
earlier in the custom InitializeCulture method. You might notice that to
load the proper resource file, you must initialize the current thread's
CurrentUICulture property, but it is not necessary to initialize
CurrentCulture property.
Once
this custom handler has properly initialized the CurrentUICulture
setting, it can then access the image file through the strongly typed
resource class for Litware.resx. Then it is simply a matter of writing
the bits of the image file to the HTTP response stream. The very last
step to display the localized image is to assign the LitwareSlogan.ashx
URL to the ImageUrl property of an Image control on any page within the
site.
Summing Up
ASP.NET
2.0 makes internationalizing Web sites and resources easier. It's
trivial to initialize the culture settings for pages within a Web site
by examining HTTP headers sent by the browser. Plus it's easy to design a
more elaborate scheme that allows users to personalize their experience
by configuring their desired language preferences.
No comments:
Post a Comment