Implementación de Windows Forms de ensamblaje único en varios idiomas (ILMerge y ensamblajes satelitales / localización) – ¿posible?

Tengo una aplicación simple de Windows Forms (C #, .NET 2.0), desarrollada con Visual Studio 2008.

Me gustaría admitir varios idiomas de UI, y al usar la propiedad “Localizable” del formulario y los archivos .resx específicos de la cultura, el aspecto de localización funciona de manera sencilla y fácil. Visual Studio comstack automáticamente los archivos resx específicos de la cultura en ensamblajes satelitales, por lo que en mi carpeta de aplicaciones comstackdas hay subcarpetas específicas de la cultura que contienen estos ensamblajes satelitales.

Me gustaría que la aplicación se implemente (copie en su lugar) como un único ensamblaje y, sin embargo, conserve la capacidad de contener varios conjuntos de recursos específicos de cada cultura.

Usando ILMerge (o ILRepack ), puedo fusionar los ensamblajes satelitales en el ensamblado ejecutable principal, pero los mecanismos de recuperación de .NET ResourceManager estándar no encuentran los recursos específicos de la cultura comstackdos en el ensamblaje principal.

Curiosamente, si tomo mi ensamblado fusionado (ejecutable) y coloco copias de él en las subcarpetas específicas de la cultura, ¡entonces todo funciona! De forma similar, puedo ver los recursos principales y específicos de la cultura en el ensamblado fusionado cuando uso Reflector (o ILSpy ). Pero copiar el ensamblaje principal en subcarpetas específicas de la cultura vence el objective de la fusión de todos modos: realmente necesito que haya una sola copia del ensamblaje único …

Me pregunto si hay alguna manera de secuestrar o influir en los mecanismos de respaldo de ResourceManager para buscar los recursos específicos del cultivo en el mismo ensamblado en lugar de en las subcarpetas del GAC y del nombre cultural . Veo el mecanismo alternativo descrito en los artículos siguientes, pero no tengo idea de cómo se modificaría: Artículo del blog del equipo BCL en ResourceManager .

¿Alguien tiene alguna idea? Esta parece ser una pregunta relativamente frecuente en línea (por ejemplo, otra pregunta aquí sobre Stack Overflow: ” ILMerge y ensambles de recursos localizados “), pero no he encontrado ninguna respuesta autorizada en ninguna parte.


ACTUALIZACIÓN 1: Solución Básica

Siguiendo la recomendación de casperOne a continuación , finalmente pude hacer que esto funcionara.

Estoy poniendo el código de la solución aquí en la pregunta porque CasperOne brindó la única respuesta, no quiero agregar la mía.

Logré que funcionara sacando las agallas de los mecanismos de recuperación de recursos del Marco implementados en el método “InternalGetResourceSet” y haciendo que la búsqueda en el mismo ensamblaje sea el primer mecanismo utilizado. Si el recurso no se encuentra en el ensamblado actual, llamamos al método base para iniciar los mecanismos de búsqueda predeterminados (gracias al comentario de @Wouter a continuación).

Para hacer esto, obtuve la clase “ComponentResourceManager” y anulé solo un método (y volví a implementar un método de marco de trabajo privado):

class SingleAssemblyComponentResourceManager : System.ComponentModel.ComponentResourceManager { private Type _contextTypeInfo; private CultureInfo _neutralResourcesCulture; public SingleAssemblyComponentResourceManager(Type t) : base(t) { _contextTypeInfo = t; } protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) { ResourceSet rs = (ResourceSet)this.ResourceSets[culture]; if (rs == null) { Stream store = null; string resourceFileName = null; //lazy-load default language (without caring about duplicate assignment in race conditions, no harm done); if (this._neutralResourcesCulture == null) { this._neutralResourcesCulture = GetNeutralResourcesLanguage(this.MainAssembly); } // if we're asking for the default language, then ask for the // invariant (non-specific) resources. if (_neutralResourcesCulture.Equals(culture)) culture = CultureInfo.InvariantCulture; resourceFileName = GetResourceFileName(culture); store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); //If we found the appropriate resources in the local assembly if (store != null) { rs = new ResourceSet(store); //save for later. AddResourceSet(this.ResourceSets, culture, ref rs); } else { rs = base.InternalGetResourceSet(culture, createIfNotExists, tryParents); } } return rs; } //private method in framework, had to be re-specified here. private static void AddResourceSet(Hashtable localResourceSets, CultureInfo culture, ref ResourceSet rs) { lock (localResourceSets) { ResourceSet objA = (ResourceSet)localResourceSets[culture]; if (objA != null) { if (!object.Equals(objA, rs)) { rs.Dispose(); rs = objA; } } else { localResourceSets.Add(culture, rs); } } } } 

Para utilizar realmente esta clase, debe reemplazar System.ComponentModel.ComponentResourceManager en los archivos “XXX.Designer.cs” creados por Visual Studio, y deberá hacer esto cada vez que modifique el formulario diseñado. Visual Studio reemplaza ese codificar automáticamente (El problema se discutió en ” Personalizar el Diseñador de Windows Forms para usar MyResourceManager “, no encontré una solución más elegante: uso fart.exe en un paso previo a la comstackción para reemplazar automáticamente).


ACTUALIZACIÓN 2: Otra consideración práctica: más de 2 idiomas

En el momento en que informé sobre la solución anterior, en realidad solo estaba admitiendo dos idiomas, e ILMerge estaba haciendo un buen trabajo al fusionar mi ensamblaje satelital en el ensamblado fusionado final.

Recientemente comencé a trabajar en un proyecto similar donde hay múltiples idiomas secundarios, y por lo tanto múltiples ensamblajes satelitales, e ILMerge estaba haciendo algo muy extraño: en lugar de fusionar los múltiples ensambles satelitales que había solicitado, estaba fusionando el primer ensamblaje satelital en múltiples ocasiones !

por ejemplo, línea de comandos:

 "c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1InputProg.exe %1es\InputProg.resources.dll %1fr\InputProg.resources.dll 

Con esa línea de comandos, obtenía los siguientes conjuntos de recursos en el ensamblado fusionado (observado con el descomstackdor ILSpy):

 InputProg.resources InputProg.es.resources InputProg.es.resources <-- Duplicated! 

Después de jugar un poco, terminé dándome cuenta de que esto es solo un error en ILMerge cuando encuentra varios archivos con el mismo nombre en una sola llamada de línea de comandos. La solución es simplemente fusionar cada ensamblaje de satélite en una llamada de línea de comando diferente:

 "c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1es\InputProg.resources.dll "c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1fr\InputProg.resources.dll 

Cuando hago esto, los recursos resultantes en el ensamblado final son correctos:

 InputProg.resources InputProg.es.resources InputProg.fr.resources 

Entonces, finalmente, en caso de que esto ayude a aclarar, aquí hay un archivo por lotes completo después de la comstackción:

 "%ProgramFiles%\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1es\InputProg.resources.dll IF %ERRORLEVEL% NEQ 0 GOTO END "%ProgramFiles%\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1fr\InputProg.resources.dll IF %ERRORLEVEL% NEQ 0 GOTO END del %1InputProg.exe del %1InputProg.pdb del %1TempProg.exe del %1TempProg.pdb del %1es\*.* /Q del %1fr\*.* /Q :END 

ACTUALIZACIÓN 3: ILRepack

Otra nota rápida: una de las cosas que me molestó con ILMerge fue que es una herramienta de Microsoft propietaria adicional, no instalada por defecto con Visual Studio, y por lo tanto una dependencia adicional que hace que sea un poco más difícil para un tercero comenzar. con mis proyectos de código abierto.

Recientemente descubrí ILRepack , un código abierto (Apache 2.0) equivalente que hasta ahora funciona igual de bien para mí (reemplazo directo) y se puede distribuir libremente con las fonts de tu proyecto.


Espero que esto ayude a alguien por ahí!

La única forma en que puedo ver que esto funciona es creando una clase que se deriva de ResourceManager y luego reemplaza los métodos InternalGetResourceSet y GetResourceFileName . Desde allí, debe poder anular dónde se obtienen los recursos, dada una instancia de CultureInfo .

Un enfoque diferente:

1) agregue su resource.DLL como recursos embebidos en su proyecto.

2) agregar un controlador de eventos para AppDomain.CurrentDomain.ResourceResolve. Este controlador se activará cuando no se pueda encontrar un recurso.

  internal static System.Reflection.Assembly CurrentDomain_ResourceResolve(object sender, ResolveEventArgs args) { try { if (args.Name.StartsWith("your.resource.namespace")) { return LoadResourcesAssyFromResource(System.Threading.Thread.CurrentThread.CurrentUICulture, "name of your the resource that contains dll"); } return null; } catch (Exception ex) { return null; } } 

3) Ahora tiene que implementar LoadResourceAssyFromResource algo así como

 private Assembly LoadResourceAssyFromResource( Culture culture, ResourceName resName) { //var x = Assembly.GetExecutingAssembly().GetManifestResourceNames(); using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resName)) { if (stream == null) { //throw new Exception("Could not find resource: " + resourceName); return null; } Byte[] assemblyData = new Byte[stream.Length]; stream.Read(assemblyData, 0, assemblyData.Length); var ass = Assembly.Load(assemblyData); return ass; } 

}

Tengo una sugerencia para parte de tu problema. Específicamente, una solución para el paso de actualizar archivos .Designer.cs para reemplazar ComponentResourceManager con SingleAssemblyComponentResourceManager.

  1. Mueva el método InitializeComponent () fuera de .Designer.cs y en el archivo de implementación (incluya la #región). Visual Studio continuará generando automáticamente esa sección, sin problemas por lo que yo sé.

  2. Use un alias de C # en la parte superior del archivo de implementación para que ComponentResourceManager tenga un alias de SingleAssemblyComponentResourceManager.

Desafortunadamente, no pude probar esto completamente. Encontramos una solución diferente a nuestro problema y seguimos adelante. Espero que te ayude sin embargo.

Solo un pensamiento.

Hiciste el paso y creaste tu SingleAssemblyComponentResourceManager

Entonces, ¿por qué te tomas el dolor de incluir tus ensamblajes de satélite en el ensamblaje de la lámpara?

Puede agregar ResourceName.es.resx sí mismo como un archivo binario a otro recurso en su proyecto.

Entonces podrías reescribir tu código

  store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); //If we found the appropriate resources in the local assembly if (store != null) { rs = new ResourceSet(store); 

con este código (no probado, pero debería funcionar)

 // we expect the "main" resource file to have a binary resource // with name of the local (linked at compile time of course) // which points to the localized resource var content = Properties.Resources.ResourceManager.GetObject("es"); if (content != null) { using (var stream = new MemoryStream(content)) using (var reader = new ResourceReader(stream)) { rs = new ResourceSet(reader); } } 

Esto debería hacer obsoleto el esfuerzo por incluir los conjuntos satélites en el proceso de fusión.

Publicado como respuesta ya que los comentarios no proporcionaron suficiente espacio:

No pude encontrar recursos para culturas neutrales ( en lugar de en-US ) con la solución OPs. Así que extendí InternalGetResourceSet con una búsqueda de culturas neutrales que hicieron el trabajo por mí. Con esto, ahora también puede ubicar recursos que no definen la región. Este es en realidad el mismo comportamiento que el formador de recursos normal mostrará cuando no ILMergue los archivos de recursos.

 //Try looking for the neutral culture if the specific culture was not found if (store == null && !culture.IsNeutralCulture) { resourceFileName = GetResourceFileName(culture.Parent); store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); } 

Esto da como resultado el siguiente código para SingleAssemblyComponentResourceManager

 class SingleAssemblyComponentResourceManager : System.ComponentModel.ComponentResourceManager { private Type _contextTypeInfo; private CultureInfo _neutralResourcesCulture; public SingleAssemblyComponentResourceManager(Type t) : base(t) { _contextTypeInfo = t; } protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) { ResourceSet rs = (ResourceSet)this.ResourceSets[culture]; if (rs == null) { Stream store = null; string resourceFileName = null; //lazy-load default language (without caring about duplicate assignment in race conditions, no harm done); if (this._neutralResourcesCulture == null) { this._neutralResourcesCulture = GetNeutralResourcesLanguage(this.MainAssembly); } // if we're asking for the default language, then ask for the // invariant (non-specific) resources. if (_neutralResourcesCulture.Equals(culture)) culture = CultureInfo.InvariantCulture; resourceFileName = GetResourceFileName(culture); store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); //Try looking for the neutral culture if the specific culture was not found if (store == null && !culture.IsNeutralCulture) { resourceFileName = GetResourceFileName(culture.Parent); store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); } //If we found the appropriate resources in the local assembly if (store != null) { rs = new ResourceSet(store); //save for later. AddResourceSet(this.ResourceSets, culture, ref rs); } else { rs = base.InternalGetResourceSet(culture, createIfNotExists, tryParents); } } return rs; } //private method in framework, had to be re-specified here. private static void AddResourceSet(Hashtable localResourceSets, CultureInfo culture, ref ResourceSet rs) { lock (localResourceSets) { ResourceSet objA = (ResourceSet)localResourceSets[culture]; if (objA != null) { if (!object.Equals(objA, rs)) { rs.Dispose(); rs = objA; } } else { localResourceSets.Add(culture, rs); } } } }