Este artículo proviene de un motor de traducción automática.

Localización de Silverlight

Consejos y trucos para cargar recursos de configuración regional de Silverlight, parte 2

Matthew Delisle

Descargar el ejemplo de código

En el primer artículo de esta serie (msdn.microsoft.com/magazine/gg650657), cubre la carga de recursos en Silverlight mediante un servicio de Windows Communication Foundation (WCF), un esquema de base de datos simple y cliente de código que notificó a la interfaz de usuario de un cambio en los recursos. En esta solución, los recursos predeterminados se cargaron a través de una llamada de servicio Web durante la inicialización de la aplicación. En este artículo, John Brodeur y le mostraremos cómo cargar los recursos predeterminados sin hacer ninguna llamada de servicio Web.

El proceso de localización estándar

En el proceso de localización estándar, como se indica en el artículo anterior, hay algunas formas de recuperar recursos de configuración regional. Un método común es incrustar archivos .resx en la aplicación en tiempo de diseño, como se muestra en figura 1.

Resource Files Embedded in the Application

Figura 1 archivos de recursos incrustados en la aplicación

Lo malo de cualquier método que incorpora recursos en la aplicación es que todos los recursos, a continuación, se descargan con la aplicación. Todos los métodos de localización nativo disponibles en Silverlight incrustar los recursos en la aplicación de alguna manera.

Una solución mejor es incrustar sólo un conjunto de recursos predeterminado y cargar cualquier otros recursos bajo demanda. Cargar recursos bajo demanda se puede lograr de varias formas: a través de la recuperación de archivos .xap o .resx o, como los descritos en la parte 1 de esta serie, mediante el uso de Web services para recuperar cadenas XML .resx. Lo que sigue siendo, sin embargo, es que la configuración regional predeterminada no sea escenario principal del usuario. Los usuarios cuyo escenario difiere el valor por defecto siempre tendrá que recuperar los recursos del servidor.

La mejor solución es generar un archivo .xap bajo demanda con recursos de configuración regional del usuario actual en la .xap. Con esta solución, no hay llamadas a servicios Web necesarios para cargar los recursos predeterminados para cualquier usuario. Se necesita una llamada de servicio Web sólo al cambiar la configuración regional en tiempo de ejecución. Esta es la solución que analizaremos en el resto de este artículo.

Un proceso de localización personalizadas

La solución de localización personalizadas en el presente artículo consta de componentes de cliente y servidor y se basa en el proyecto de CustomLocalization creado en la parte 1. Describiremos el inicio del proceso con el archivo .aspx que contiene el objeto de Silverlight.

Los parámetros para el HttpHandler deban ser atravesado en la dirección URL de la aplicación de Silverlight. Para pasar de la cultura de navegador, agregar un parámetro de URL y rellenarlo con la referencia cultural del subproceso actual:

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

También tenemos que añadir una importación del espacio de nombres System.Threading para utilizar la clase de subproceso:

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

Y hemos añadido un parámetro denominado rs que representa el recurso a recuperar.

Eso es todo lo que necesitan en el archivo .aspx. Configuración regional del usuario se pasa en el HttpHandler, que se va a incrustar los recursos especificados por que la cultura en un archivo .xap.

Crear el controlador

Ahora vamos a crear un archivo llamado XapHandler en la raíz del proyecto Web. Esta clase implemente IHttpHandler y especificará que es irrecuperable. Añadiremos tres campos para compartir los objetos CultureInfo, HttpContext y ResourceSet entre métodos. Hasta ahora, el código se ve así:

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

En el método ProcessRequest, queremos recuperar la cultura y recursos establecer, validar la cultura, cree un archivo .xap localizadas y transmitir el archivo al cliente. Para recuperar los parámetros, a utilizarlos de la matriz de parámetros del objeto de la solicitud:

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

Para validar la cultura, a tratar de crear un objeto CultureInfo; Si se produce un error en el constructor, la cultura se supone que no es válido:

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

Este es un buen lugar para crear una clase de utilidades para celebrar que algunos utilizan en funciones para su reutilización. Comenzaremos con una función que envía una respuesta al cliente y, a continuación, se cierra el objeto response. Esto es útil para enviar mensajes de error. Aquí está el código:

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

Y utilizaremos ese método para enviar un error cuando se especifica una cultura no válida:

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

Después de validar la cultura, el siguiente paso es crear el archivo .xap localizadas y volver a la ruta del archivo.

Crear el archivo XAP localizadas

Es donde ocurre toda la magia. Vamos a crear un método denominado CreateLocalizedXapFile con un parámetro de tipo cadena. El parámetro especifica la ubicación en el servidor el archivo de aplicación .xap que no contiene recursos incrustados. Si la .xap archivo sin recursos no existe en el servidor, el proceso no puede continuar, por lo que nos tira un error, así:

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

Antes de zambullirse en el método CreateLocalizedXapFile, echemos un vistazo a la estructura de directorios de esta solución en el servidor Web. Supongamos que tenemos una aplicación Web llamada acme en la carpeta raíz del Web. Dentro de la acme carpeta será el directorio de ClientBin, donde normalmente se almacenan las aplicaciones de Silverlight. Esto es donde se encuentran los archivos .xap sin recursos. En este directorio son otros directorios nombrados después de identificadores de configuración regional (en-US, es-MX, fr-FR etc.), y estos directorios son donde se crean y se almacenan los archivos de configuración regional .xap. Figura 2 muestra cómo podría ser la estructura de directorios.

Directory Structure for Localized XAP Files

Figura 2 estructura de directorios de archivos XAP localizados

Ahora vamos a sumergirse en el método CreateLocalizedXapFile. Hay dos rutas principales de ejecución en este método. La primera es si el archivo .xap localizadas existe y está actualizado. En este caso, el proceso es trivial y todo lo que sucede es que se devuelve la ruta de acceso completa al archivo .xap localizadas. El segundo camino es cuando el archivo .xap localizada no existe o no está actualizado. El archivo .xap localizados se considera obsoleto si es mayor que el archivo .xap normal o el archivo .resx que debe insertarse en ella. Los archivos .resx individuales se almacenan fuera del archivo .xap localizados para que se pueden modificar fácilmente, y estos archivos se utilizan para comprobar si el archivo .xap localizada es actual. Si el archivo .xap localizada es obsoleto, se sobrescribe con el archivo .xap llano y los recursos son inyectados en ese archivo. Figura 3 muestra el método comentado.

Figura 3 El método de CreateLocalizedXapFile

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

Se muestra el método de GetResourceFilePath en figura 4. Los parámetros de este método son el contexto, el conjunto de recursos y la cultura. Creamos una cadena que representa el archivo de recursos, compruebe si existe y, si es así, volver a la ruta del archivo.

Figura 4 El método de GetResourceFilePath

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

Inyección de recursos en un archivo XAP

Ahora pasemos al método InjectResourceIntoXAP. Como sabe la mayoría de los desarrolladores de Silverlight, un archivo .xap es un archivo .zip disfrazado. Crear un archivo .xap es tan fácil como zipping juntos los archivos correctos y asignar el resultado de una extensión de .xap. En este escenario, tenemos que tener un archivo .zip, el archivo .xap sin recursos y agregarle el archivo .resx de la cultura correspondiente. Para ayudar en la funcionalidad metiéndose, vamos a utilizar la biblioteca de DotNetZip, en dotnetzip.codeplex.com. Primero se intentó utilizar el System.IO.ZipPackage para realizar la compresión sin la biblioteca externa, pero se topó con problemas de compatibilidad con el archivo .xap resultante. El proceso debe ser posible utilizando sólo el espacio de nombres System.IO.ZipPackage, pero la biblioteca DotNetZip hizo mucho más fácil.

Aquí es un método de utilidad que hemos creado para ayudar con la funcionalidad de 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();
  } }

En el método InjectResourceIntoXAP, nos estamos envoltura sólo una llamada al método AddFileToZip con algún control de errores:

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

Lo que hemos pensado originalmente iba a ser una de las partes más complicadas de la solución resultó ser el más simple. Creo que de todos los usos para los archivos .xap creado dinámicamente!

Que se transmite el archivo al cliente

Vamos a nadar hasta la superficie ahora y terminar el método ProcessRequest. Cuando estábamos pasados aquí, hemos añadido el código para llamar al método CreateLocalizedXapFile y devuelve la ruta de acceso al archivo .xap, pero nosotros no hemos hecho nada con ese archivo. A fin de ayudar a transmitir archivos al cliente, voy a crear otro método de utilidad. El método, llamado TransmitFile, establece los encabezados, tipo de contenido y caducidad de la caché del archivo y, a continuación, utiliza el método TransmitFile de la clase HttpResponse para enviar el archivo directamente en el cliente, sin almacenamiento en búfer. Figura 5 muestra el código.

Figura 5 El método TransmitFile

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

En el método ProcessRequest, llamamos al método TransmitFile, dándole el contexto y la ruta del archivo .xap localizadas y especificar no para eliminar el archivo (caché lo) una vez finalizada la transmisión:

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

Lo que trabajo

En este momento, tenemos un trabajo .xap controlador, y ahora tenemos que conectar en la aplicación Web. Vamos a agregar el controlador de la sección httpHandlers de web.config. La ruta de acceso del controlador será la ruta del archivo .xap con asterisco insertado antes de la extensión del archivo. Esto será vía cualquier solicitud de ese archivo .xap, independientemente de los parámetros, al controlador. Se utiliza la sección de configuración system.web con Cassini y IIS 6 y la sección system.webServer es para uso con 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>

Ahora, al mover archivos de recursos en carpetas para cada configuración regional del servidor, la solución es trabajar. Siempre que actualice los archivos .resx, la .xap localizadas archivos obsoleto y son regenerado bajo demanda. Así, hemos creado una solución que nos permite implementar una aplicación de Silverlight con recursos para cualquier idioma sin hacer un solo servicio Web llamar. Ahora vamos a dar un paso más. El origen de la verdad para la información de configuración regional no es los archivos .resx. La fuente de verdad es la base de datos y los archivos .resx son subproductos de la base de datos. En una solución ideal, no tienes que hacer con los archivos .resx; sólo se podría modificar la base de datos cuando se agregan o actualizan recursos. Derecho de ahora, los archivos .resx deben actualizarse cuando los cambios de base de datos y esto pueden ser un proceso tedioso, incluso con una herramienta semiautomatizado. La siguiente sección mira a automatizar el proceso.

Mediante un proveedor de recursos personalizado

Crear un proveedor de recursos personalizado es una tarea compleja y no dentro del ámbito de este artículo, pero Rick Strahl tiene un artículo bien escrito debatiendo una aplicación similar a bit.ly/ltVajU. Para este artículo, estamos utilizando un subconjunto de su solución de proveedor de recursos. El método principal, GenerateResXFileNormalizedForCulture, realizará consultas a nuestra base de datos para el conjunto completo de recursos de una cadena de referencia cultural. Al construir el recurso para una cultura, el estándar.Jerarquía de administrador de recursos netos se mantiene para cada clave por primera coincidencia de la referencia cultural, entonces la cultura Neutral (o idioma) y finalmente los recursos de referencia cultural específica.

Por ejemplo, una solicitud de la en-nos cultura tendría como resultado la combinación de los siguientes archivos: ui.resx, ui.en.resx y ui.en-us.resx.

Utilizando los recursos incrustados

En la parte 1, la solución había obtenido todos los recursos mediante llamadas a servicios Web, y si el servicio Web no estaba disponible, sería retroceder a un archivo almacenado en el directorio Web que contiene las cadenas de recursos predeterminado. Ninguno de estos procedimientos es necesario más. A eliminar el archivo con las cadenas de recursos predeterminado y quitar la configuración de aplicación apuntando a ella. El siguiente paso es modificar el SmartResourceManager para cargar los recursos incrustados cuando se inicia la aplicación. El método ChangeCulture es la clave para la integración de los recursos incrustados en la solución. El método se ve ahora así:

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

En lugar de hacer una llamada a la operación de GetResourcesAsync de inmediato, vamos a intentar cargar los recursos de un archivo de recurso incrustado, y si esto falla, luego hacen la llamada al servicio Web. Si los recursos incrustados se carga correctamente, actualizaremos el conjunto de recursos activos. Aquí está el código:

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

Lo que queremos hacer en el LoadEmbeddedResource método es buscar un archivo en la aplicación en el formato de resourceSet.culture.resx. Si se encuentra el archivo, queremos cargar como un XmlDocument, analizar en un diccionario y, a continuación, agregar al diccionario ResourceSets. Figura 6 muestra el aspecto del código.

Figura 6 el método 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;
}

El método SetCulture es trivial; actualiza el recurso si una entrada existe o agrega uno si no es así.

Conclusión

Este artículo completa la solución de la parte 1, integración de componentes de servidor para administrar archivos .xap y .resx. Con esta solución, no hay necesidad para llamadas a servicios Web recuperar los recursos predeterminados. La idea de empaquetar los recursos predeterminados en una aplicación puede ampliarse para incluir cualquier número de recursos que solicita el usuario.

Esta solución reduce el mantenimiento necesario para las cadenas de recursos. Generar archivos .resx de los medios de demanda de base de datos existe poca administración necesario para los archivos .resx. Rick Strahl ha codificado una herramienta de localización útiles que puede utilizar para leer los recursos de la configuración regional de la base de datos, modificarlos y crear archivos .resx! Encontrará la herramienta en bit.ly/kfjtI2.

Hay muchos lugares para enganchar en esta solución, por lo que se puede personalizar para hacer casi lo que quieras. ¡Feliz codificación!

Matthew Delisle trabaja para Schneider Electric en una aplicación de Silverlight de vanguardia que trata de ahorrar dinero mediante el ahorro de energía. Visita su blog en mattdelisle.net.

John Brodeur es un arquitecto de software de Schneider Electric, con extensa Web desarrollo y aplicación de diseño experiencia.

Gracias a los siguiente experto técnico para revisar este artículo:Shawn Wildermuth