Silverlight 本地化

加载 Silverlight 区域设置资源的技巧与诀窍,第 2 部分

马修 · 德利斯勒约翰 Brodeur

下载代码示例

在本系列的第一篇文章 (msdn.microsoft.com/magazine/gg650657),我所涵盖的 Silverlight 使用 Windows 通信基础 (WCF) 服务资源加载,一个简单的数据库架构和客户端代码,通知资源变化的用户界面。 在该解决方案中,通过 Web 服务调用,在初始化应用程序的过程中被加载默认资源。 在本文中,John Brodeur,并将向您展示如何加载默认资源,而不进行任何 Web 服务调用。

标准本地化流程

在标准的本地化过程中,在以前的文章中,所述有几种方法可以检索语言环境资源。 常用的方法是在设计时,在应用程序中嵌入.resx 文件中所示图 1

Resource Files Embedded in the Application

图 1应用程序中嵌入的资源文件

将资源嵌入应用程序中的任何方法的缺点在于所有的资源然后下载应用程序。 所有可用在 Silverlight 的本机定位方法将资源嵌入到应用程序中以某种方式。

更好的解决方案是嵌入只有默认的资源集,加载任何其他资源上的需求。 加载资源的需求,可以完成各种不同的方式: 通过检索的.xap 或.resx 文件,或如本系列的第 1 部分中所述,通过使用的 Web 服务检索.resx XML 字符串。 然而,问题是默认区域设置可能不是用户的主区域设置。 用户的区域设置不同于默认值将总是要从服务器中检索资源。

最好的解决办法是生成与嵌入在.xap 中的当前用户的区域设置特定资源的需求上的.xap 文件。 使用此解决方案,没有 Web 服务调用,以加载任何用户的默认资源。 仅当更改区域设置在运行时需要一个 Web 服务调用。 这是我们将在本文的其余部分中讨论的解决方案。

自定义本地化流程

在这篇文章中的自定义本地化解决方案包括客户端和服务器组件和第 1 部分中创建的 CustomLocalization 项目基础之上。 我们会与包含 Silverlight 对象的.aspx 文件描述过程开始。

任何参数的 HttpHandler 需要的 Silverlight 的应用程序的 URL 中传递。 为了通过浏览器文化中,我们添加 URL 参数和填充与当前线程的区域性:

<param name="source" value="ClientBin/CustomLocalization.xap?c=
   <%= Thread.CurrentThread.CurrentCulture.Name %>&rs=ui"/>

我们还需要添加 System.Threading 命名空间导入到使用 Thread 类:

<%@ Import Namespace="System.Threading" %>

我们添加名为 rs 的参数,表示设置为检索的资源。

这是在.aspx 文件中所需的一切。 用户的区域设置传递到 HttpHandler,将嵌入到.xap 文件中指定的这种文化的资源。

创建处理程序

现在我们要创建一个称为 Web 项目的根目录中的 XapHandler 文件。 此类将实现 IHttpHandler,我们会指定,非可重复使用。 我们将添加共享的 CultureInfo、 HttpContext 和 ResourceSet 的对象方法中的三个字段。 到目前为止,代码看起来像这样:

using System.Web;
namespace CustomLocalization.Web {
  public class XapHandler : IHttpHandler {
    private CultureInfo Culture;
    private HttpContext Context;
    private string ResourceSet;

    public bool IsReusable { get { return false; } }

    public void ProcessRequest(HttpContext context) {
      throw new System.NotImplementedException();
      } } }

在 ProcessRequest 方法中,我们要检索的文化和资源设置、 验证文化,然后创建本地化的.xap 文件传输到客户端文件。 若要检索的参数,我们将从请求对象的参数数组访问它们:

string culture = context.Request.Params["c"];]
ResourceSet = context.Request.Params["rs"];

若要验证的文化,我们会尝试创建 CultureInfo 对象 ; 如果该构造函数失败,文化被假定为无效:

if (!string.IsNullOrEmpty(culture)) {
  try {
    Culture = new CultureInfo(culture);
  }
  catch (Exception ex) {
    // Throw an error
  } }

这是一个好地方来创建实用程序类,用于容纳一些常用供重复使用的函数。 我们将从开始将响应发送到客户端,然后关闭响应对象的函数。 这是用于发送错误消息。 代码如下:

public static void SendResponse(HttpContext context, int statusCode,  
  string message) {
  if (context == null) return;
  context.Response.StatusCode = statusCode;
  if(!string.IsNullOrEmpty(message)) {
    context.Response.StatusDescription = message;
  }
  context.Response.End();
}

我们将使用该方法时指定了无效的区域性,则发送错误:

if (!string.IsNullOrEmpty(culture)) {
  try {
    Culture = new CultureInfo(culture);
  }
  catch (Exception ex) {
    // Throw an error
    Utilities.SendResponse(Context, 500,
    "The string " + culture + " is not recognized as a valid culture.");
     return;
  } }

后验证文化下, 一步是创建本地化的.xap 文件并返回文件的路径。

创建本地化的 XAP 文件

这是所有魔法的都发生。 我们要创建的字符串类型的参数调用 CreateLocalizedXapFile 方法。 该参数指定包含任何嵌入的资源的应用程序.xap 文件的服务器上的位置。 如果.xap 文件不在服务器上的资源并不存在,不能继续进行,所以我们抛出错误,如下所示:

string xapWithoutResources = Context.Server.MapPath(Context.Request.Path);
if (string.IsNullOrEmpty(xapWithoutResources) || !File.Exists(xapWithoutResources))
  Utilities.SendResponse(Context, 500, "The XAP file does not exist.");
  return;
}
else {
  string localizedXapFilePath = CreateLocalizedXapFile(xapWithoutResources);
}

CreateLocalizedXapFile 方法之前,让我们看看此解决方案的目录结构在 Web 服务器上。 假设我们有称为 acme 根 Web 文件夹中的 Web 应用程序。 内的 acme 文件夹将 ClientBin 目录,通常存储 Silverlight 的应用程序的位置。 这是没有资源.xap 文件位于何处。 在此目录下的其他目录命名区域设置标识符 (EN-US、 es-MX,FR-FR 等等),这些目录是创建和存储特定于区域设置的.xap 文件的。 图 2显示目录结构可能是什么样子。

Directory Structure for Localized XAP Files

图 2本地化 XAP 文件的目录结构

现在让我们深入 CreateLocalizedXapFile 方法。 有两个主要的路径,在此方法中执行。 第一个是如果本地化的.xap 文件存在,并且是最新的。 在这种情况下,过程是琐碎和发生的一切是返回本地化的.xap 文件的完整路径。 第二个路径时本地化的.xap 文件不存在或已过期。 本地化的.xap 文件被视为过时如果早于平原.xap 文件或应嵌入在它的.resx 文件。 个别.resx 文件存储以外的本地化的.xap 文件中,以便他们可以轻松地修改,和这些文件用来检查是否本地化的.xap 文件是当前。 如果本地化的.xap 文件是陈旧的它将覆盖平原.xap 文件和资源投入该文件。 图 3显示注释的方法。

图 3CreateLocalizedXapFile 方法

private string CreateLocalizedXapFile(string filePath) {
  FileInfo plainXap = new FileInfo(filePath);
  string localizedXapFilePath = plainXap.FullName;

  try {
    // Get the localized XAP file
    FileInfo localizedXap = new FileInfo(plainXap.DirectoryName + 
      "\\" + Culture.Name + "\\" + plainXap.Name);
                
    // Get the RESX file for the locale
    FileInfo resxFile = new FileInfo(GetResourceFilePath(
      Context, ResourceSet, Culture.Name));

    // Check to see if the file already exists and is up to date
    if (!localizedXap.Exists || (localizedXap.LastWriteTime < 
      plainXap.LastWriteTime) || 
      (localizedXap.LastWriteTime < resxFile.LastWriteTime)) {
    if (!Directory.Exists(localizedXap.DirectoryName))  {
      Directory.CreateDirectory(localizedXap.DirectoryName);
     }
                    
     // Copy the XAP without resources
     localizedXap = plainXap.CopyTo(localizedXap.FullName, true);

     // Inject the resources into the plain XAP, turning it into a localized XAP
     if (!InjectResourceIntoXAP(localizedXap, resxFile)) {
       localizedXap.Delete();
  } }                
     if (File.Exists(localizedXap.FullName)) {
       localizedXapFilePath = localizedXap.FullName;
     } }
  catch (Exception ex) {
    // If any error occurs, throw back the error message
    if (!File.Exists(localizedXapFilePath)) {
      Utilities.SendResponse(Context, 500, ex.Message);
    } }
  return localizedXapFilePath;
}

GetResourceFilePath 方法所示图 4。 此方法的参数是上下文、 资源组和文化。 我们创建一个字符串,表示该资源文件、 检查以查看是否存在,如果是,返回的文件路径。

图 4GetResourceFilePath 方法

private static string GetResourceFilePath(
  HttpContext context, string resourceSet, string culture) {
  if (context == null) return null;
  if (string.IsNullOrEmpty(culture)) return null;

  string resxFilePath = resourceSet + "." + culture + ".resx";
string folderPath = context.Server.MapPath(ResourceBasePath);
FileInfo resxFile = new FileInfo(folderPath + resxFilePath);

if (!resxFile.Exists) {
  Utilities.SendResponse(context, 500, "The resx file does not exist 
    for the locale " + culture);
}
return resxFile.FullName;
}

资源注入 XAP 文件

现在让我来的 InjectResourceIntoXAP 方法。 大多数 Silverlight 开发人员知道,.xap 文件是变相的.zip 文件。 创建.xap 文件是一样容易一起压缩了正确的文件并将结果分配.xap 扩展名。 在这种情况下,我们需要把现有的.zip 文件 — —.xap 文件没有资源 — — 并向其中添加适当的文化的.resx 文件。 为协助 zipping 功能,我们将使用 DotNetZip 的库,位于dotnetzip.codeplex.com。 我们第一次尝试使用 System.IO.ZipPackage 来做没有外部库,压缩,但我们与生成.xap 文件遇到兼容性问题。 此过程应该有可能使用只是 System.IO.ZipPackage 命名空间,但 DotNetZip 库使它更加容易。

这里是我们创建的 zip 功能帮助实用程序方法:

public static void AddFileToZip(string zipFile, string fileToAdd, 
  string directoryPathInZip) {
  if (string.IsNullOrEmpty(zipFile) || string.IsNullOrEmpty(fileToAdd)) return;

  using (ZipFile zip = ZipFile.Read(zipFile)) {
    zip.AddFile(fileToAdd, directoryPathInZip);
    zip.Save();
  } }

在 InjectResourceIntoXAP 方法中,我们只环绕在调用 AddFileToZip 方法具有一些错误处理:

private bool InjectResourceIntoXAP(FileInfo localizedXapFile, 
  FileInfo localizedResxFile) {
  if (localizedXapFile.Exists && localizedResxFile.Exists) {
    try {
      Utilities.AddFileToZip(localizedXapFile.FullName, 
        localizedResxFile.FullName, string.Empty);
      return true;
    }
    catch { return false; }
  }
  return false;
}

什么我们本来以为是要解决方案的最复杂的部分之一原来是最简单。 想想动态创建的.xap 文件的所有其它用途 !

传输到客户端文件

我们要去游泳回到表面现在和完成的 ProcessRequest 方法。 当我们最后一次在这里时,我们添加代码以调用 CreateLocalizedXapFile 方法,并返回路径到.xap 文件中,但我们还没有进行任何与该文件。 为协助客户端传输文件,我要创建另一个实用程序方法。 方法,称为 TransmitFile,设置标题、 内容类型和文件缓存过期,然后使用 HttpResponse 类的 TransmitFile 方法将文件直接发送到客户端,无缓冲。 图 5显示的代码。

图 5TransmitFile 方法

public static void TransmitFile(HttpContext context, string filePath, 
  string contentType, bool deleteFile) {
  if (context == null) return;
  if (string.IsNullOrEmpty(filePath)) return;

  FileInfo file = new FileInfo(filePath);
   try {
     if (file.Exists) {
       context.Response.AppendHeader("Content-Length", file.Length.ToString());
       context.Response.ContentType = contentType;
       if (!context.IsDebuggingEnabled) {                      
         context.Response.Cache.SetCacheability(HttpCacheability.Public);
         context.Response.ExpiresAbsolute = DateTime.UtcNow.AddDays(1);
         context.Response.Cache.SetLastModified(DateTime.UtcNow); 
       }

       context.Response.TransmitFile(file.FullName);
     if (context.Response.IsClientConnected) {
       context.Response.Flush();
     }  }
     else {
       Utilities.SendResponse(context, 404, "File Not Found (" + filePath + ")."); }
     }
     finally {
       if (deleteFile && file.Exists) { file.Delete(); }
     } }

在 ProcessRequest 方法中,我们调用 TransmitFile 方法,为它提供上下文和本地化的.xap 文件路径,并指定不删除文件 (缓存) 的传输完成后:

Utilities.TransmitFile(context, localizedXapFilePath, "application/x-silverlight-app", false);

使它工作

说到这里,我们有工作.xap 处理程序中,而我们现在需要去它安装在 Web 应用程序中。 我们打算在 web.config 的 httpHandlers 节中添加处理程序。 处理程序的路径将.xap 文件扩展名之前插入一个星号的文件路径。 这将路由到.xap 文件,不管该处理程序的参数的任何请求。 System.web 配置节用卡西尼和 IIS 6 和 IIS 7 system.webServer 条用于:

<system.web>
    <httpHandlers>
      <add verb="GET" path="ClientBin/CustomLocalization*.xap" 
        type="CustomLocalization.Web.XapHandler, CustomLocalization.Web"/>
    </httpHandlers>
  </system.web>
<system.webServer>
    <handlers>
      <add name="XapHandler" verb="GET" path=
        "ClientBin/CustomLocalization*.xap" 
        type="CustomLocalization.Web.XapHandler, CustomLocalization.Web"/>
    </handlers>
  </system.webServer>

现在,通过移动到文件夹中为每个区域设置的资源文件在服务器上,该解决方案工作。 每当我们更新的.resx 文件,本地化的.xap 文件变得过时,而重新生成的需求。 因此,我们创造了一种解决方案,让我们部署 Silverlight 应用程序的任何一种语言的资源,而不使单个 Web 服务调用。 现在让我们进一步采取这一步。 区域设置信息的来源不是真理的.resx 文件。 真理的来源是数据库,和.resx 文件是数据库的副产品。 在一个理想的解决方案,您不会要处理.resx 文件 ; 资源添加或更新时,只会修改数据库。 现在,.resx 文件需要更新,当数据库的更改,而这可以是单调乏味的过程中,甚至有一种半自动化的工具。 下一节看了看过程自动化。

使用自定义资源提供商

创建自定义资源提供程序是一项复杂的工作,并不在这篇文章,但施特拉尔里克的范围内有一篇好的文章,讨论在执行类似的情况bit.ly/ltVajU。 这篇文章中,我们使用他的资源提供商解决方案的一个子集。 主要方法,GenerateResXFileNormalizedForCulture,将查询我们的数据库,为给定的区域性的字符串的完整资源集。 当构建为一种文化,标准设置的资源。网络资源管理器层次结构中的每个键备第一个匹配使用固定区域性,然后中性 (或语言) 文化和最后的特定区域性的资源。

例如,请求 en-我们文化会导致以下文件的组合: ui.resx、 ui.en.resx 和 ui.en-us.resx。

使用嵌入的资源

第 1 部分,在解决方案中检索使用 Web 服务调用的所有资源和 Web 服务是不可用的它会倒存储在包含默认资源字符串的 Web 目录中的文件。 这些程序都有必要了。 我们会删除默认资源字符串的文件,并删除指向它的应用程序设置。 下一步是修改 SmartResourceManager,在应用程序启动时加载嵌入的资源。 ChangeCulture 方法是将嵌入的资源集成到解决方案的关键。 方法现在看起来像这样:

public void ChangeCulture(CultureInfo culture) {
  if (!ResourceSets.ContainsKey(culture.Name)) {
    localeClient.GetResourcesAsync(culture.Name, culture);
  }
  else {
    ResourceSet = ResourceSets[culture.Name];
    Thread.CurrentThread.CurrentCulture = 
      Thread.CurrentThread.CurrentUICulture = culture;
  } }

GetResourcesAsync 操作马上打电话,而不是,我们要去尝试从嵌入的资源文件中加载资源 — — 如果失败,然后使 Web 服务调用。 如果嵌入的资源加载成功,我们将更新活动资源集。 代码如下:

if (!ResourceSets.ContainsKey(culture.Name)) {
  if (!LoadEmbeddedResource(culture)) {
    localeClient.GetResourcesAsync(culture.Name, culture);    
  } 
else {
  ResourceSet = ResourceSets[culture.Name];
  Thread.CurrentThread.CurrentCulture = 
    Thread.CurrentThread.CurrentUICulture = culture;
} }

我们想要做的 LoadEmbeddedResource 方法是搜索的 resourceSet.culture.resx 格式的应用程序中的文件。 如果我们找到该文件,我们需要加载为 XmlDocument、 解析到字典,然后将其添加到 ResourceSets 词典。 图 6显示的代码看起来像。

图 6 LoadEmbeddedResource 方法

private bool LoadEmbeddedResource(CultureInfo culture) {
  bool loaded = false;
  try {
    string resxFile = "ui." + culture.Name + ".resx";
    using (XmlReader xmlReader = XmlReader.Create(resxFile)) {
      var rs = ResxToDictionary(xmlReader);
      SetCulture(culture, rs);
      loaded = true;
   } }
   catch (Exception) {
     loaded = false;
  }
return loaded;
}

SetCulture 方法是微不足道的 ; 它更新如果存在的一项,或添加一个,如果不是设置的资源。

总结

这篇文章,从第 1 部分整合服务器端组件.xap 和.resx 文件管理解决方案舍入。 使用此解决方案,没有需要为 Web 服务调用来检索默认资源。 包装在应用程序中的默认资源的想法可以扩大到包括任意数量的用户要求的资源。

此解决方案减少了所需的资源字符串的维护。 有小管理所需的.resx 文件的数据库上需求手段从生成的.resx 文件。 里克 · 施特拉尔已编码一种有用的本地化工具,可用于从数据库中读取的区域设置资源,对其进行修改和创建.resx 文件 ! 你会发现在工具bit.ly/kfjtI2

有很多地方来挂接到此解决方案,使您可以自定义它做几乎任何你想要的。 编码愉快 !

马修 Delisle 适合施耐德电气对前沿 Silverlight 应用程序处理省钱节省能源。访问他在博客上的mattdelisle.net

约翰 Brodeur 是软件架构师的施耐德电气与广泛 Web 开发和应用程序设计经验。

衷心感谢以下技术专家对本文的审阅:Shawn Wildermuth