July 2011

Volume 26 Number 07

Silverlight Localization - Tips and Tricks for Loading Silverlight Locale Resources, Part 2

By Matthew Delisle | July 2011

In the first article in this series (msdn.microsoft.com/magazine/gg650657), I covered the loading of resources in Silverlight using a Windows Communication Foundation (WCF) service, a simple database schema and client-side code that notified the UI of a change in resources. In that solution, the default resources were loaded through a Web service call during initialization of the application. In this article, John Brodeur and I will show you how to load the default resources without making any Web service calls.

The Standard Localization Process

In the standard localization process, as outlined in the previous article, there are a few ways to retrieve locale resources. A common method is to embed .resx files in the application at design time, as shown in Figure 1.

Resource Files Embedded in the Application

Figure 1 Resource Files Embedded in the Application

The downside of any method that embeds resources in the application is that all of the resources are then downloaded with the application. All of the native localization methods available in Silverlight embed the resources into the application in some way.

A better solution is to embed only a default resource set and load any other resources on demand. Loading resources on demand can be accomplished in a variety of ways: through the retrieval of .xap or .resx files, or, as outlined in Part 1 of this series, through the use of Web services to retrieve .resx XML strings. The issue that remains, however, is that the default locale may not be the user’s primary locale. Users whose locale differs from the default will always have to retrieve resources from the server.

The best solution is to generate a .xap file on demand with the current user’s locale-specific resources embedded in the .xap. With this solution, no Web service calls are needed to load the default resources for any user. A Web service call is needed only when changing the locale at runtime. This is the solution we’ll discuss in the rest of this article.

A Custom Localization Process

The custom localization solution in this article consists of both client and server components and builds on the CustomLocalization project created in Part 1. We’ll describe the process beginning with the .aspx file containing the Silverlight object.

Any parameters for the HttpHandler need to be passed in through the URL of the Silverlight application. In order to pass in the browser culture, we add a URL parameter and fill it with the current thread’s culture:

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

 We also need to add an import for the System.Threading namespace in order to use the Thread class:

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

And we added a parameter called rs that represents the resource set to retrieve.

That’s all that’s needed in the .aspx file. The user’s locale is passed into the HttpHandler, which will embed the resources specified by that culture into a .xap file.

Creating the Handler

Now we’re going to create a file called XapHandler in the root of the Web project. This class will implement IHttpHandler and we’ll specify that it’s non-reusable. We’ll add three fields to share the CultureInfo, HttpContext and ResourceSet objects among methods. The code so far looks like this:

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();
      } } }

In the ProcessRequest method, we want to retrieve the culture and resource set, validate the culture, and then create a localized .xap file and transmit the file to the client. To retrieve the parameters, we’ll access them from the Params array of the Request object:

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

To validate the culture, we’ll try to create a CultureInfo object; if the constructor fails, the culture is assumed to be invalid:

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

This is a good place to create a Utilities class to hold some commonly used functions for reuse. We’ll start with a function that sends a response to the client and then closes the response object. This is useful for sending error messages. Here’s the code:

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();
}

And we’ll use that method to send an error when an invalid culture is specified:

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;
  } }

After validating the culture, the next step is to create the localized .xap file and return the file path.

Creating the Localized XAP File

This is where all the magic happens. We’re going to create a method called CreateLocalizedXapFile with a parameter of type string. The parameter specifies the location on the server of the application .xap file that contains no embedded resources. If the .xap file without resources doesn’t exist on the server, the process can’t continue, so we throw an error, like so:

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);
}

Before diving into the CreateLocalizedXapFile method, let’s look at the directory structure of this solution on the Web server. Let’s say we have a Web application called acme in the root Web folder. Inside of the acme folder will be the ClientBin directory, where Silverlight applications are normally stored. This is where .xap files without resources are located. Under this directory are other directories named after locale identifiers (en-US, es-MX, fr-FR and so forth), and these directories are where the locale-specific .xap files are created and stored. Figure 2 shows what the directory structure could look like.

Directory Structure for Localized XAP Files

Figure 2 Directory Structure for Localized XAP Files

Now let’s dive into the CreateLocalizedXapFile method. There are two main paths of execution in this method. The first is if the localized .xap file exists and is up-to-date. In this case, the process is trivial and all that happens is that the full path to the localized .xap file is returned. The second path is when the localized .xap file does not exist or is out of date. The localized .xap file is considered out of date if it’s older than the plain .xap file or the .resx file that should be embedded in it. The individual .resx files are stored outside of the localized .xap file so that they can be easily modified, and these files are used to check whether the localized .xap file is current. If the localized .xap file is obsolete, it’s overwritten with the plain .xap file and the resources are injected into that file. Figure 3 shows the commented method.

Figure 3 The CreateLocalizedXapFile Method

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;
}

The GetResourceFilePath method is shown in Figure 4. The parameters to this method are the context, resource set and culture. We create a string representing the resource file, check to see if it exists and, if it does, return the file path.

Figure 4 The GetResourceFilePath Method

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;
}

Injecting Resources into a XAP file

Now let’s move on to the InjectResourceIntoXAP method. As most Silverlight developers know, a .xap file is a .zip file in disguise. Creating a .xap file is as easy as zipping the correct files together and assigning the result a .xap extension. In this scenario, we need to take an existing .zip file—the .xap file without resources—and add the .resx file of the appropriate culture to it. To assist in the zipping functionality, we’ll use the DotNetZip library, located at dotnetzip.codeplex.com. We first attempted to use the System.IO.ZipPackage to do the compression without the external library, but we ran into compatibility issues with the resulting .xap file. The process should be possible using just the System.IO.ZipPackage namespace, but the DotNetZip library made it much easier.

Here’s a utility method we created to help with the zip functionality:

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();
  } }

In the InjectResourceIntoXAP method, we’re just wrapping a call to the AddFileToZip method with some error handling:

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;
}

What we originally thought was going to be one of the most complicated parts of the solution turned out to be the simplest. Think of all the other uses for dynamically created .xap files! 

Transmitting the File to the Client

We’re going to swim back up to the surface now and finish the ProcessRequest method. When we were last here, we added the code to call the CreateLocalizedXapFile method and returned the path to the .xap file, but we haven’t done anything with that file. In order to assist in transmitting files to the client, I’m going to create another utility method. The method, called TransmitFile, sets the headers, content type and cache expiration of the file and then uses the TransmitFile method of the HttpResponse class to send the file directly to the client, without buffering. Figure 5 shows the code.

Figure 5 The TransmitFile Method

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(); }
     } }

In the ProcessRequest method, we call the TransmitFile method, giving it the context and the localized .xap file path and specifying not to delete the file (cache it) after the transmission completes:

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

Making It Work

At this point, we have a working .xap handler, and now we need to wire it up in the Web application. We’re going to add the handler in the httpHandlers section of the web.config. The path of the handler will be the file path of the .xap file with an asterisk inserted before the extension. This will route any request to that .xap file, no matter the parameters, to the handler. The system.web configuration section is used with Cassini and IIS 6 and the system.webServer section is for use with IIS 7:

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

Now, by moving resource files into folders for each locale on the server, the solution is working. Whenever we update the .resx files, the localized .xap files become obsolete and are regenerated on-demand. Thus we’ve created a solution that lets us deploy a Silverlight application with resources for any language without making a single Web service call. Now let’s take it a step further. The source of truth for the locale information is not the .resx files. The source of truth is the database, and the .resx files are byproducts of the database. In an ideal solution, you wouldn’t have to deal with .resx files; you’d only modify the database when resources are added or updated. Right now, the .resx files need to be updated when the database changes, and this can be a tedious process, even with a semi-automated tool. The next section takes a look at automating the process.

Using a Custom Resource Provider

Creating a custom resource provider is a complex undertaking and not within the scope of this article, but Rick Strahl has a well-written article discussing a similar implementation at bit.ly/ltVajU. For this article, we’re using a subset of his Resource Provider solution. The main method, GenerateResXFileNormalizedForCulture, will query our database for the complete resource set of a given culture string. When constructing the resource set for a culture, the standard .NET resource manager hierarchy is maintained for each key by first matching the Invariant culture, then the Neutral (or language) culture and finally the Specific culture resource.

For example, a request for the en-us culture would result in the combination of the following files: ui.resx, ui.en.resx and ui.en-us.resx.

Using the Embedded Resources

In Part 1, the solution retrieved all resources using Web service calls, and if the Web service was unavailable, it would fall back to a file stored in the Web directory that contained the default resource strings. Neither of these procedures is necessary anymore. We’ll delete the file with the default resource strings and remove the application setting pointing to it. The next step is to modify the SmartResourceManager to load the embedded resources when the application initializes. The ChangeCulture method is the key to integrating the embedded resources into the solution. The method looks like this right now:

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;
  } }

Instead of making a call to the GetResourcesAsync operation right away, we’re going to try to load the resources from an embedded resource file—and if that fails, then make the call to the Web service. If the embedded resources load successfully, we’ll update the active resource set. Here’s the code:

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;
} }

What we want to do in the LoadEmbeddedResource method is search for a file in the application in the format of resourceSet.culture.resx. If we find the file, we want to load it as an XmlDocument, parse it into a dictionary and then add it to the ResourceSets dictionary. Figure 6 shows what the code looks like.

Figure 6 The LoadEmbeddedResource Method

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;
}

The SetCulture method is trivial; it updates the resource set if an entry exists or adds one if it doesn’t.

Wrapping Up

This article rounded out the solution from Part 1, integrating server-side components to manage .xap and .resx files. With this solution, there’s no need for Web service calls to retrieve the default resources. The idea of packaging the default resources in an application can be expanded to include any number of resources the user asks for.

This solution decreases the maintenance needed for the resource strings. Generating .resx files from the database on-demand means there’s little management needed for the .resx files. Rick Strahl has coded a useful localization tool that you can use to read the locale resources from the database, modify them and create .resx files! You’ll find the tool at bit.ly/kfjtI2

There are many places to hook into this solution, so you can customize it to do almost whatever you want. Happy coding!


Matthew Delisle works for Schneider Electric on a leading-edge Silverlight application that deals with saving money by saving energy. Visit his blog at mattdelisle.net.

John Brodeur is a software architect for Schneider Electric with extensive Web development and application design experience.

Thanks to the following technical expert for reviewing this article: Shawn Wildermuth