Generalizando recursos.


Una de las características importantes de las aplicaciones en el mundo globalizado es la necesidad de utilizar recursos para poder localizar mensajes, textos y representaciones gráficas acordes a cada posible usuario.

Esto incluye, como es lógico, cualquier comunicación que parta desde nuestras bibliotecas.

Sin embargo, considerando a futuro, el agregado de idiomas, resulta bastante engorroso mantener archivos de recursos en cada biblioteca que definamos.

¿Por qué no tener centralizados los mismos en una sola biblioteca?

Las herramientas que nos permiten manejar archivos de recursos en nuestros proyectos, generan clases que exponen dichos recursos pero que no se exponen a otras bibliotecas (no son públicas).

Lo primero que se nos ocurriría sería agregarles el modificador public, pero, el generador de Visual Studio re crea la clase completa ante cada cambio y, por tanto, deberíamos ir a modificarla en cada ocasión. L

Nos hace falta otro mecanismo que mantenga una clase, pública, que exponga todos nuestros recursos aun cuando agreguemos o eliminemos algunos.

Y para ello, nos podemos apoyar en la transformación de Texto.

Transformación de Texto (T4).

T4 aparece en Visual Studio 2010 y es en lo que se basó la generación de clases a partir del modelo de datos de una base SQL Server, en Entity Framework.

(del tema de EF… mejor hablamos en otra ocasión) L

El concepto es simple: tienes una plantilla que se usa para generar código (o páginas HTML, o texto plano), la cual puedes usar para reconstruir clases, o generar textos basados en datos (como reportes sencillos), o cosas asi.

En este caso, la idea es simple: crear una clase que exponga como público un recurso, y utilizarlo luego como plantilla para, recorriendo los miembros definidos en un archivo de recursos re exponerlos como públicos.

Los recursos.

Para mantener orden en nuestro desarrollo, definamos una nueva biblioteca de clases, dentro de la cual, definimos un archivo de recursos con un nombre específico para poder identificar la clase generada fácilmente.

   

El espacio de nombres será DS.Resources, y el archivo de recursos (y su clase derivada) CommonResources.

Una interesante herramienta de manejo de recursos.

Entre todos los componentes que podemos encontrar como extensión, tenemos una que facilita la edición de los recursos para múltiples idiomas.

Además de permitir definir los mismos en más de un idioma (y agregar idiomas a posteriori), facilita la exportación e importación de los mismos, para obtener traducciones desde profesionales del tema.

La clase plantilla

Esta clase que usaremos como ejemplo, expone una de los recursos contenidos en el archivo CommonResources desde una clase pública.

Public Class Resources
Public shared ReadOnly Property Copyright  As String
    Get
        Return My.Resources.CommonResources.Copyright
    End Get
End Property
End Class

 

 

using DS.Resources;
using System;
namespace DS.Resources
{
    public static class Resources
    {
        public static String Copyright { get 
            { return CommonResources.Copyright; } }
    }
}

 

La transformación de texto.

Agregamos ahora a nuestro proyecto, un archivo de tipo Plantilla de Texto

(Existen dos tipos de plantilla de texto. Hay una específica para que sea utilizada por tu proyecto en tiempo de ejecución. En este caso, no es esa).

El nombre que le des a ese archivo, será el que tendrá la clase luego. Por tanto, a este archivo le llamamos Resources.

Pegamos el código de la clase en este archivo, que utilizaremos para reemplazar los valores de nombre de la propiedad y tipo de datos, por los que recuperaremos de los definidos en la clase CommonResources

 

Public Class Resources
Public shared ReadOnly Property <#= sName #>  As <#= sType #>
    Get
        Return My.Resources.CommonResources.<#= prop.Name #>
    End Get
End Property
End Class

 

Explicando, los elementos encerrados entre <# y #> son código ejecutable en tiempo de transformación, mientras que el resto, se toman literalmente.

Utilizando Reflection (si, de nuevo), y el modelo de objetos de Visual Studio, podemos obtener los elementos definidos en el archivo de recursos, e incluirlos en un bucle con este código, para generar cada una de las propiedades públicas.

Este sería el código completo de la plantilla.

<#@ template debug="true" hostspecific="true" language="VB" #>
<# ' Define which assemblies must be imported #>
<#@ assembly name="System.Core" #>
<#@ assembly name="EnvDTE" #>  
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".vb" #>
Public Class Resources
<#
    dim serviceProvider as IServiceProvider= Me.Host  
    dim dte as EnvDTE.DTE = serviceProvider.GetService(gettype(EnvDTE.DTE)) 
    dim item = dte.Solution.FindProjectItem(Host.TemplateFile).ContainingProject
    dim OutPutFile as string= item.Properties("FullPath").value & "bin\debug\" & item.Properties("OutputFileName").Value
    Dim ass As System.Reflection.Assembly = System.Reflection.Assembly.LoadFrom(OutPutFile)
    Dim t As Type = ass.GetType("DS.Resources.My.Resources.CommonResources")
    Dim props = (From el In DirectCast(t, System.Reflection.TypeInfo).DeclaredProperties Where Not {"ResourceManager", "Culture"}.Contains(el.Name)).ToList
    For Each prop As System.Reflection.PropertyInfo In props
    dim sName as string=prop.Name
    dim sType as string=prop.PropertyType.Name
#>
Public shared ReadOnly Property <#= sName #>  As <#= sType #>
    Get
        Return My.Resources.CommonResources.<#= prop.Name #>
    End Get
End Property
<#
    Next
    ass=nothing
#>

End Class

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="EnvDTE" #>  
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>

using DS.Resources;
using System;

namespace DS.Resources
{
   public static class Resources
    {
    <#
    IServiceProvider serviceProvider = (IServiceProvider)this.Host;  
    EnvDTE.DTE dte = (EnvDTE.DTE) serviceProvider.GetService(typeof(EnvDTE.DTE));  
    var item = dte.Solution.FindProjectItem(Host.TemplateFile).ContainingProject;
    string OutPutFile = item.Properties.Item("FullPath").Value + "bin\\debug\\" + item.Properties.Item("OutputFileName").Value;
    System.Reflection.Assembly ass   = System.Reflection.Assembly.LoadFrom(OutPutFile);
            Type t = ass.GetType("DS.Resources.CommonResources");
    System.Reflection.TypeInfo tInfo=(System.Reflection.TypeInfo)t;
            string[] noAdd = new string[] { "ResourceManager", "Culture" };
    var props = (from el in ((System.Reflection.TypeInfo)t).DeclaredProperties where !noAdd.Contains(el.Name) select el).ToList();
    foreach( System.Reflection.PropertyInfo prop in props)
    {
        string sName =prop.Name;
        string sType =prop.PropertyType.Name;
    #>
            public static <#= sType #> <#= sName #> { get { return CommonResources.<#= sName #>; } }
    <#
    }
    #>
    }
}

 

Algunas aclaraciones de la plantilla.

<#@ template debug=”true” hostspecific=”true” language=”xx” #>
Determina que se trata de una plantilla, a la cual
podemos querer depurar, y que es específica para el “host” (en este caso, el entorno de Visual Studio. En esta plantilla en particular es importante, ya que necesitamos obtener información del proyecto donde se encuentra la plantilla. En forma predeterminada este valor viene en falso, y por tanto, nos daría un error al tratar de acceder a información del proyecto, como veremos luego.

<#@ assembly name=”EnvDTE” #>  

Necesitamos incluir este ensamblado, dado que vamos a obtener información del entorno de desarrollo (ese ensamblado aplica precisamente, a comunicarse con el propio entorno de Visual Studio).

dim serviceProvider as IServiceProvider= Me.Host  
dim dte as EnvDTE.DTE = serviceProvider.GetService(gettype(EnvDTE.DTE)) 

Obtiene la instancia actual del entorno de desarrollo de Visual Studio.

dim item = dte.Solution.FindProjectItem(Host.TemplateFile).ContainingProject

dim OutPutFile as string= item.Properties(“FullPath”).value & “bin\debug\” & item.Properties(“OutputFileName”).Value

Obtiene el Proyecto actual y de este, genera en la variable OutPutFile, el camino completo a la DLL resultante de la compilación del proyecto.

Dim ass As System.Reflection.Assembly = System.Reflection.Assembly.LoadFrom(OutPutFile)
Dim t As Type = ass.GetType(“DS.Resources.My.Resources.CommonResources”)

Cargamos por reflexión dicha dll, y obtenemos la definición de la clase CommonResources, que es la que contiene nuestros recursos.

Dim props = (From el In DirectCast(t, System.Reflection.TypeInfo).DeclaredProperties Where Not {“ResourceManager”, “Culture”}.Contains(el.Name)).ToList

En la variable Props, obtenemos todas las propiedades definidas en dicha clase, excluyendo, las dos que siempre se definen en los archivos de recursos: ResourceManager y Culture, dado que no son definidas por nosotros.

Luego, sencillamente, recorremos esa lista, y generamos las distintas propiedades de la clase pública.

Al grabar la plantilla, el entorno transformará el mismo generando la clase.

Nota: por estar generando código dinámicamente, el entorno alerta de dicha situación, por temas de seguridad


Quedando el resultado como el siguiente ejemplo.

 

Public Class Resources
    Public Shared ReadOnly Property Copyright As String
        Get
            Return My.Resources.CommonResources.Copyright
        End Get
    End Property
    Public Shared ReadOnly Property ErrorTitle As String
        Get
            Return My.Resources.CommonResources.ErrorTitle
        End Get
    End Property
    Public Shared ReadOnly Property FKError As String
        Get
            Return My.Resources.CommonResources.FKError
        End Get
    End Property
    Public Shared ReadOnly Property NameValueList_MissingType As String
        Get
            Return My.Resources.CommonResources.NameValueList_MissingType
        End Get
    End Property
    Public Shared ReadOnly Property NotAllowedBySystem As String
        Get
            Return My.Resources.CommonResources.NotAllowedBySystem
        End Get
    End Property
End Class

using System;

namespace DS.Resources
{
   public static class Resources
    {    public static String Copyright { get { return CommonResources.Copyright; } }    public static String ErrorTitle { get { return CommonResources.ErrorTitle; } }    public static String FKError { get { return CommonResources.FKError; } }    public static String NameValueList_MissingType { get { return CommonResources.NameValueList_MissingType; } }    public static String NotAllowedBySystem { get { return CommonResources.NotAllowedBySystem; } }
        }
}

 

Un detalle: si revisas el código resultante, verás que no es similar. Esto es porque en los dos lenguajes, la ubicación por espacio de nombres es diferente. En VB, los recursos se incluyen dentro del espacio de nombres especial My, que se refiere al proyecto completo, que en C# no existe.

Pero, para los objetivos buscados, son idénticos.

Hasta la próxima, donde probaremos estas cosas. (Adecuadamente)

 

Anuncios

Comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s