加载Silverlight区域设置资源的Silverlight本地化技巧

[原文发表地址]Silverlight LocalizationTips and Tricks for Loading Silverlight Locale Resources

[原文发表时间]2/3/2011 7:08 AM

载示例代码

Silverlight是一个很棒的创建富互联网应用(RIAs)的框架,但是它还不像Microsoft .NET Framework一样稳健支持本地化。Silverlight的确有.resx文件、简单的ResourceManager类和项目文件中的一个元素。但是仅此而已,没有自定义标记扩展,不支持DynamicResource类。

本文中我将演示如何补救所有这些问题。我将展示一个解决方案,让开发人员在运行时加载资源集、使用任何格式来存储资源、改动资源后不需要重新编译,还将演示资源的懒散加载。

本文分成三部分。首先,我将用Microsoft标准的本地化流程开发一个简单的应用程序。然后,我将演示优于标准流程的另一个本地化解决方案。最后, 我将阐述该解决方案所需的后台组件。

标准本地化流程

我首先创建一个使用Microsoft定义的本地化流程的Silverlight应用程序。 该流程的详细描述在msdn.microsoft.com/library/cc838238(VS.95)

UI包括一个TextBlock和一张图片, 如图1所示。

clip_image001

图1 应用程序

Microsoft定义的本地化流程使用.resx文件来存储资源数据。 .resx文件被内嵌在主程序集或附属程序集中,只在应用程序启动时运行一次。你们可以通过更改项目文件中的SupportedCultures来构建针对任意语言的应用程序。示例程序将被本地化为两种语言:英语和法语。 添加两个资源文件和两张相应的图片后, 项目结构将看起来如图2所示。

clip_image002

图2 添加.resx文件后的项目结构

我将图片的构建操作改为content,这样我就能使用更简洁的语法来引用图片。我将往每个文件中加两项。Textblock通过名为welcome的属性引用,图片控件则通过名位FlagImage的属性引用。

一旦Silverlight应用程序中生成资源文件,生成资源类的默认修饰符将被设定为internal。 不幸的是,即使internal成员和XAML在同一个程序集中,XAML仍然不能读取它们。为了解决这种情况,生成类的修饰符需要改为public。这可以在资源文件的设计视图中完成。 访问修饰符下拉菜单可以指定生成类的作用域。

一旦资源文件妥当,你需要将资源绑定到XAML。可以通过创建一个包含引用资源类实例的静态字段来实现。类很简单,像这样:

1.

2. privatestaticreadonly strings strings = new strings();

3. public strings Strings { get { return strings; } }

4. }

为了能从XAML访问该类,需要生成一个实例。这个示例中,我会在App类里创建实例,这样整个项目都可以访问它了。

1.

2. <local:StringResources x:Key=”LocalizedStrings”/>

3. </Application.Resources>

现在可以在XAML内进行数据绑定了。TextBlock和Image的XAML像这样:

1.

2. HorizontalAlignment=”Center”>

3. <TextBlock Text=”{Binding Strings.Welcome, Source={StaticResourceLocalizedStrings}}”

4. FontSize=”24″/>

5. </StackPanel>

6. <Image Grid.Row=”1″Grid.ColumnSpan=”2″

7. HorizontalAlignment=”Center”

8. Source=”{Binding Strings.FlagImage, Source={StaticResourceLocalizedStrings}}”/>

路径是字符串属性后面跟着资源项的键值。Source是App类里的StringResources的实例。

设置区域性

要让应用程序继承浏览器的区域性设置并显示合适的区域,应用程序必须配置三个设置。

第一个设置是.csproj文件里的SupportedCultures元素。 因为现在Visual Studio中没有对话框来编辑这项设置,所以项目文件必须被手动更改。你可以通过在Visual Studio外打开编辑项目文件,或通过加载项目然后在Visual Studio内从下拉菜单中选择编辑来编辑项目文件。

要在 应用程序内实现English和French,SupportedCultures元素的值需要这样设置:

<SupportedCultures>fr</SupportedCultures>

区域性值间用逗号隔开。特性区域性(neutral culture)被编译到主DLL内,无需特地指定。

下面是获取浏览器语言设置的必要步骤。网页上Silverlight内置对象需要添加一个参数。参数值为服务器端当前UI区域性。网页要求是一个.aspx文件,参数语句为:

<param name=”uiculture”

value=”<%=Thread.CurrentThread.CurrentCulture.Name %>” />

本流程最后的必需步骤是编辑web.config文件,往system.web元素内添加一个globalization元素,将其属性值设置为auto:

<globalization culture=”auto” uiCulture=”auto”/>

与早前提到的一样,Silverlight应用程序有非特定语言(neutral language)设置。该设置是通过项目文件属性内Silverlight选项卡中的Assembly Information实现的。非特定语言属性在对话框的底端,如图三所示:

clip_image003

图三 设置非特定语言

我建议将非特定语言设置为与地域无关的一种语言。这是一项后备设置,能设置为尽量覆盖更广的可能性地区会更加有用。 设置非特定语言会让assemblyinfo.cs里添加一个这样的程序集属性:

[assembly: NeutralResourcesLanguageAttribute(“en”)]

完成之后,本地化应用程序就能读取浏览器的语言设置并能加载合适的资源了。

自定义的本地化流程

标准本地化流程的局限性来自对ResourceManager和.resx文件的使用。ResourceManager类不会在运行时根据环境区域性的变更而更改任何资源集。对.resx文件的使用将开发人员局限在针对一种语言特定的资源集上,限制了维护资源的灵活性。

根据这些局限性,我们来看看一个使用动态资源的替代解决方案。

为实现动态资源,在活动资源集更改的时候,资源管理器需要发送通知。要在Silverlight里面发送通知,需要实现INotifyPropertyChanged接口。在该接口里面,每个资源集对应含一个键值和值类型字符的dictionary。

Prism框架和托管可扩展型框架(MEF) 在Silverlight开发中很受欢迎,在应用程序中这些框架被分解成多个.xap文件。本地化需要每个.xap文件有一个自己的资源管理器实例。要将通知发送到每个.xap文件(资源管理器的每个实例),我需要追踪每个创建的实例,并在需要发送通知时遍历实例列表。图四显示了这个SmartResourceManager功能的代码。

图四 SmartResourceManager

1. {

2. privatestaticreadonly List<SmartResourceManager> Instances =

3. new List<SmartResourceManager>();

4. privatestatic Dictionary<string, string>resourceSet;

5. privatestaticreadonly Dictionary<string,

6. Dictionary<string, string>>ResourceSets =

7. new Dictionary<string, Dictionary<string, string>>();

8.

9. public Dictionary<string, string>ResourceSet {

10. get { returnresourceSet; }

11. set { resourceSet = value;

12.// Notify all instances

13.foreach (varobjin Instances) {

14.obj.NotifyPropertyChanged(“ResourceSet”);

15. }

16. }

17.}

18.

19.publicSmartResourceManager() {

20.Instances.Add(this);

21.}

22.

23.publiceventPropertyChangedEventHandlerPropertyChanged;

24.publicvoidNotifyPropertyChanged(string property) {

25.varevt = PropertyChanged;

26.

27.if (evt != null) {

28.evt(this, newPropertyChangedEventArgs(property));

29. }

30.}

可以看到一个静态列表被创建用来保存所有资源管理器的实例。 活动资源集存储在resourceSet字段中, 每个加载的资源集存储在resourceSet字段中,每个加载的资源存储在ResourceSets列表中。在构造器中, 当前实例存储在实例列表中。 类以标准方式实现INotifyPropertyChanged。 当活动资源集更改时,它将遍历示例列表,触发每个PropertyChanged事件。

SmartResourceManager类需要一个办法来在运行时更改区域性,很简单,只需要一个CultureInfo对象方法:

1.

2. if (!ResourceSets.ContainsKey(culture.Name)) {

3. // Load the resource set

4. }

5. else {

6. ResourceSet = ResourceSets[culture.Name];

7. Thread.CurrentThread.CurrentCulture =

8. Thread.CurrentThread.CurrentUICulture =

9. culture;

10. }

11.}

该方法检查目标区域性是否被加载。如果没有,它将加载该区域性并将其设定为当前值。如果已经被加载,方法简单地将相应的资源集设置为当前值。此时我们暂时忽略加载资源的代码。

为完整起见,我将给你们演示通过程序加载资源的两个方法(参见图五)。第一种方法通过一个资源键值从当前的区域性中返回资源。第二种方法用一个资源和区域性名字返回针对特定区域性的资源。

图五 加载资源

public string GetString(string key) {

if (string.IsNullOrEmpty(key)) return string.Empty;

if (resourceSet.ContainsKey(key)) {

return resourceSet[key];

}

else {

return string.Empty;

}

}

public string GetString(string key, string culture) {

if (ResourceSets.ContainsKey(culture)) {

if (ResourceSets[culture].ContainsKey(key)) {

return ResourceSets[culture][key];

}

else {

return string.Empty;

}

}

else {

return string.Empty;

}

}

如果马上运行应用程序,因为没有资源集被下载,所以所有的本地化字符串都是空的。要下载初始资源集,我将创建一个Initialize的方法,它有非特定语言的文件和区域性标识符两个参数。这个方法在应用程序生命周期间只应被调用一次(参考图6)。

图6 初始化非特定语言

1.

2. if (Instances.Count == 0) {

3. ChangeCulture(Thread.CurrentThread.CurrentUICulture);

4. }

5. Instances.Add(this);

6. }

7.

8. publicvoid Initialize(stringneutralLanguageFile,

9. stringneutralLanguage) {

10.lock (lockObject) {

11.if (isInitialized) return;

12.isInitialized = true;

13. }

14.

15.if (string.IsNullOrWhiteSpace(neutralLanguageFile)) {

16.// No neutral resources

17.ChangeCulture(Thread.CurrentThread.CurrentUICulture);

18. }

19.else {

20.LoadNeutralResources(neutralLanguageFile, neutralLanguage);

21. }

22.}

绑定到XAML

自定义扩展标记为本地化资源提供最流畅的绑定语法。不幸的是,自定义扩展标记在Silverlight中不支持。在Silverlight3和之后的版本中可以绑定到dictionary,语法如下:

<StackPanelGrid.ColumnSpan=”2″ Orientation=”Horizontal”

HorizontalAlignment=”Center”>

<TextBlock Text=”{Binding Path=ResourceSet[Welcome], Source={StaticResource

SmartRM}}” FontSize=”24″/>

</StackPanel>

<Image Grid.Row=”1″ Grid.ColumnSpan=”2″

HorizontalAlignment=”Center”

Source=”{Binding ResourceSet[FlagImage], Source={StaticResourceSmartRM}}”/>

路径包括键值在方括号内的dictionary属性名称。如果你在使用Silverlight2,有2种选择:创建一个ValueConventer类或者通过反射获得一个强类型对象。用反射创建强类型对象不在本文讨论范围内。ValueConverter的代码如图7所示:

图7 自定义ValueConverter

public class LocalizeConverter : IValueConverter {

public object Convert(object value,

Type targetType, object parameter,

System.Globalization.CultureInfo culture) {

if (value == null) return string.Empty;

Dictionary<string, string> resources =

value as Dictionary<string, string>;

if (resources == null) return string.Empty;

string param = parameter.ToString();

if (!resources.ContainsKey(param)) return string.Empty;

return resources[param];

}

}

LocalizeConverter类传入dictionary和参数,返回dictionary内该键的值。创建converter的实例之后,绑定语句类似这样:

<StackPanelGrid.ColumnSpan=”2″ Orientation=”Horizontal”

HorizontalAlignment=”Center”>

<TextBlock Text=”{Binding Path=ResourceSet, Source={StaticResourceSmartRM}, Converter={StaticResourceLocalizeConverter}, Convert-erParameter=Welcome}” FontSize=”24″/>

</StackPanel>

<Image Grid.Row=”1″ Grid.ColumnSpan=”2″ HorizontalAlignment=”Center”

Source=”{Binding ResourceSet, Source={StaticResourceSmartRM}, Converter={StaticResourceLocalizeConverter}, ConverterParameter=FlagImage}”/>

虽然使用转换器会更灵活,但是语句会更加冗长。这样,在本文余下的部分中,我将不会使用转换器,而是用字典绑定语法继续讲解。

区域设置Redux

在Silverlight应用程序获取区域性之前,应用程序需要配置两个设置。 他们是和讨论标准本地化流程中所讲的一样。 Web.config文件中的globalization元素需要将culture和uiCulture的值设置为auto:

<globalization culture=”auto” uiCulture=”auto”></globalization>

还有,.aspx文件中的silverlight对象也需要被作为参数传入到线程当前UI区域性值中:

<param name=”uiculture”

value=”<%=Thread.CurrentThread.CurrentCulture.Name %>” />

我将添加一些方便更改区域性的按钮来展示应用程序的动态本地化操作,如图8所示。 English按钮的点击事件类似这样:

1. ).ChangeCulture(

2. newCultureInfo(“en”));

clip_image004

图 8 更改区域性按钮

利用一些模拟数据,应用程序会运行并显示适当的语言。此处的解决方案允许运行时的动态本地化,它利用自定义逻辑来加载资源,再灵活不过了。

下一部分着重解决:资源存储在哪里? 他们是怎么返回的?

服务器端组件

现在让我们创建一个数据库来存储资源和一个

Windows Communication Foundation (WCF)服务来返回那些资源。在大些的应用程序中,你会想要创建数据和业务层,但是在本例中,我不会使用任何抽象。

我之所以选择WCF服务是因为创建WCF的易用性与稳健性。我之所以选择

将资源存储在一个关系数据库中是为了方便维护和管理。可以创建管理应用程序来让翻译人员很容易地修改资源。

在这里,我使用SQL Server 2008 Express。数据模式如图9所示:

clip_image005

图9 SQL Server 2008 Express中的本地化表格架构

Tag是一组命名后的资源。StringResource是代表资源的实体。 LocaleId列表示资源所属的区域名。 Comment列用来兼容.resx格式。CreatedDate和ModifiedDate列用来审核。

StringResource可以与多个Tag关联。这样做的好处是你可以创建特定的组合(比如:针对单个界面的资源),并且只下载那些所需的资源。坏处是你可以将不同的几个资源赋给相同的LocaleID、Key和Tag。那样, 你可能需要编写一个触发器来管理资源的创建和修改,或者当返回资源集时用ModifiedDate列来决定哪个是最新的资源。

我将用LINQ to SQL来获取数据。 第一步将通过一个区域名称来返回所有与该区域相关的资源。接口是这样的:

1.

2. publicinterfaceILocaleService {

3. [OperationContract]

4. Dictionary<string, string>GetResources(stringcultureName);

5. }

下面是实现:

1.

2. privateacmeDataContextdataContext = newacmeDataContext();

3.

4. public Dictionary<string, string>GetResources(stringcultureName) {

5. return (from r indataContext.StringResources

6. where r.LocaleId == cultureName

7. select r).ToDictionary(x =>x.Key, x =>x.Value);

8. }

9. }

该操作简单地找出所有LocaleId与cultureName参数相同的资源。dataContext字段是连到SQL Server数据库的LINQ to SQL类的一个实例。 就这样!LINQ和WCF让事情变得如此简单。

现在该将WCF服务链接到SmartResourceManager类了。往Silverlight应用程序中添加了一个服务引用(Service Reference)之后,在构造器内注册返回GetResources操作的完整事件:

1.

2. Instances.Add(this);

3. localeClient.GetResourcesCompleted +=

4. localeClient_GetResourcesCompleted;

5. if (Instances.Count == 0) {

6. ChangeCulture(Thread.CurrentThread.CurrentUICulture);

7. }

8. }

回调函数必须将资源集添加到资源集列表,并将其设为当前资源集。 代码如图10 所示。

图10 添加资源

1. privatevoidlocaleClient_GetResourcesCompleted(object sender,

2. LocaleService.GetResourcesCompletedEventArgs e) {

3. if (e.Error != null) {

4. varevt = CultureChangeError;

5.

6. if (evt != null)

7. evt(this, newCultureChangeErrorEventArgs(

8. e.UserStateasCultureInfo, e.Error));

9. }

10.else {

11.if (e.Result == null) return;

12.

13.CultureInfo culture = e.UserStateasCultureInfo;

14.

15.if (culture == null) return;

16.

17.ResourceSets.Add(culture.Name, e.Result);

18.ResourceSet = e.Result;

19.Thread.CurrentThread.CurrentCulture =

20.Thread.CurrentThread.CurrentUICulture = culture;

21. }

}

需要更改ChangeCulture方法来调用WCF操作:

1.

2. if (!ResourceSets.ContainsKey(culture.Name)) {

3. localeClient.GetResourceSetsAsync(culture.Name, culture);

4. }

5. else {

6. ResourceSet = ResourceSets[culture.Name];

7. Thread.CurrentThread.CurrentCulture =

8. Thread.CurrentThread.CurrentUICulture = culture;

9. }

10.}

加载非特定区域性

当web服务连接不上或超时的时候,应用程序需要正常工作。作为调用服务的备用方案并改善性能,必须将包含非特定语言资源的文件存储在web服务外面,并在启动时加载。

我将创建另一个含两个参数的SmartResourceManager构造器:指向非特定语言资源文件的URL和指定资源文件的区域代码(图11)。

图 11 加载中性区域

1. publicSmartResourceManager(stringneutralLanguageFile, stringneutralLanguage) {

2. Instances.Add(this);

3. localeClient.GetResourcesCompleted +=

4. localeClient_GetResourcesCompleted;

5.

6. if (Instances.Count == 1) {

7. if (string.IsNullOrWhiteSpace(neutralLanguageFile)) {

8. // No neutral resources

9. ChangeCulture(Thread.CurrentThread.CurrentUICulture);

10. }

11.else {

12.LoadNeutralResources(neutralLanguageFile, neutralLanguage);

13. }

14. }

15.}

如果没有非特定资源文件,将实行调用WCF的正常流程。 LoadNeutralResources方法用一个WebClient来从服务器端返回资源文件。 然后解析文件,将XML字符转换为Dictionary对象。 这里我将不会展示那些代码,因为他们有点长而且有些繁琐。 但是感兴趣的话你可以在本文可供下载的代码中找到他们。

要调用参数化的SmartResourceManager构造器,我需要将SmartResourceManager的实例化移到App类的隐藏代码中(因为Silverlight不支持XAML2009)。因为我不想硬编码这些资源文件或区域性代码,所以我需要创建一个自定义的ConfigurationManager类,你也可以在下载的代码中去看看。

将ConfigurationManager集成到App类中后,启动事件回调函数类似这样:

private void Application_Startup(object sender, StartupEventArgs e) {

ConfigurationManager.Error += ConfigurationManager_Error;

ConfigurationManager.Loaded += ConfigurationManager_Loaded;

ConfigurationManager.LoadSettings();

}

启动回调函数现在用来下载应用程序设置并注册回调。如果你真要选择将那些设置的配置在后台调用,请注意会遇到的一些竞赛冒险。这是ConfigurationManager事件的回调函数:

1. privatevoidConfigurationManager_Error(object sender, EventArgs e) {

2. Resources.Add(“SmartRM”, newSmartResourceManager());

3. this.RootVisual = newMainPage();

4. }

5.

6. privatevoidConfigurationManager_Loaded(object sender, EventArgs e) {

7. Resources.Add(“SmartRM”, newSmartResourceManager(

8. ConfigurationManager.GetSetting(“neutralLanguageFile”),

9. ConfigurationManager.GetSetting(“neutralLanguage”)));

10.this.RootVisual = newMainPage();

11.}

Error事件回调函数加载时没有非特定语言设置,Loaded时间回调函数加载时则有。

我需要将资源文件放在一个更改时不需要重编译任何东西的地方。我将它放在Web项目的ClientBin地址下,并将其后缀名改为.xml以供公开访问,这样WebClient类就可以从Silverlight应用程序中访问它。因为它可以公开访问,请不要放置任何敏感数据在里面。

ConfigurationManager也从ClientBin目录下读取所需信息。它会去搜索一个名为appSettings.xml的文件,像这样:

<AppSettings>

<Add Key=”neutralLanguageFile” Value=”strings.xml”/>

<Add Key=”neutralLanguage” Value=”en-US”/>

</AppSettings>

一旦appSettings.xml和strings.xml在同一个地方了,ConfigurationManager和SmartResourceManager就能一起加载非特定语言了。这个流程还有改进的空间,因为当进程的活动区域性与中性区域性不同,而且web服务又关闭的情况下,进程的当前区域设置与当前资源集是不同的。留作给你们的练习吧。

结束语

在本文中我没有详细讲述的是在服务器端规范资源。应该说fr-FR资源比fr资源少两个关键因素。当请求fr-FR资源时,web服务应该从更多的一般fr资源中插入缺少的键值。

在构建这个解决方案中我没有讲到的另一方面是通过区域性和资源集一起加载资源而不仅仅通过区域性。这在加载针对单个的屏和单个的.xap文件时很有用。

我演示的解决方案可以让你们去实现很少的几个比较有用的操作,但是也涵盖了运行时资源集的加载、使用任何方式来存储资源、更改资源无需重编译和懒散加载资源。

本文所展示的解决方案是通用的,你可以在不同的地方用到它,大大改变实现。 希望这会帮助减少你们的日常编程负担。

更多关于国际化的深层阅读,请参考书本“.NET国际化:构建全球Windows和Web应用程序的开发人员指南”(Addison-Wesley,2006),作者:Guy Smith-Ferrier。 Smith-Ferrier的网站上还有一段非常好的视频,名为 “Internationalizing Silverlight at SLUGUK” (bit.ly/gJGptU)。