单组装多语言Windows窗体部署(ILMerge和卫星程序集/本地化) – 可能吗?
我有一个简单的Windows窗体(C#,.NET 2.0)应用程序,使用Visual Studio 2008构build。
我想支持多种UI语言,并使用表单的“Localizable”属性和特定于文化的.resx文件,本地化方面可以无缝,轻松地工作。 Visual Studio自动编译特定于文化的resx文件到附属程序集,所以在我编译的应用程序文件夹中有特定文化的子文件夹包含这些附属程序集。
我希望将应用程序部署(复制到位)作为一个单一的程序集 ,但仍然保留包含多组文化特定资源的能力。
使用ILMerge (或ILRepack ),我可以将附件程序集合到主要的可执行程序集中,但标准的.NET ResourceManager回退机制找不到编译到主程序集中的区域性资源。
有趣的是,如果我把我的合并(可执行)程序集,并将其复制到文化特定的子文件夹,然后一切正常! 同样,当我使用Reflector (或ILSpy )时,我可以看到合并组合中主要和文化特定的资源。 但是将主要程序集复制到特定于文化的子文件夹中,无论如何都会破坏合并的目的 – 我真的需要那里只有单个程序集的单个副本…
我想知道是否有任何方法来劫持或影响ResourceManager后备机制在相同的程序集中查找文化特定的资源,而不是在GAC和文化命名的子文件夹中查找 。 我看到以下文章中描述的回退机制,但不知道如何修改: BCL团队博客文章ResourceManager 。
有没有人有任何想法? 这似乎是一个相对频繁的在线问题(例如,另一个关于堆栈溢出的问题:“ ILMerge和本地化的资源集合 ”),但我还没有find任何权威的答案。
更新1:基本解决scheme
遵循casperOne的build议 ,我终于能够完成这项工作。
我把解决scheme的代码放在这里,因为casperOne提供了唯一的答案,我不想自己添加。
我可以通过从“InternalGetResourceSet”方法中实现的Framework资源寻找回退机制中取出内容,并使我们的相同程序集search所使用的第一个机制,从而得以实现。 如果在当前程序集中找不到资源,那么我们调用基本方法来启动默认的search机制(感谢下面的@ Wouter的注释)。
为此,我派生了“ComponentResourceManager”类,并重写了一个方法(并重新实现了一个私有的框架方法):
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); } } } }
为了实际使用这个类,需要replace由Visual Studio创build的“XXX.Designer.cs”文件中的System.ComponentModel.ComponentResourceManager – 每次更改devise表单时都需要执行此操作 – Visual Studio将replace自动编码。 (问题在“ 自定义Windows窗体devise器使用MyResourceManager ”中讨论过,我没有find一个更优雅的解决scheme – 我在预生成步骤中使用fart.exe来自动replace。)
更新2:另一个实际考虑 – 超过2种语言
当我报告上述解决scheme时,我实际上只支持两种语言,而ILMerge在把我的卫星assembly合并到最终的合并assembly中做得很好。
最近我开始研究一个类似的项目,那里有多种辅助语言,因此有多个卫星组件,ILMerge做了一件非常奇怪的事情:不是合并我所要求的多个卫星组件,而是多次合并第一个卫星组件!
例如命令行:
"c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1InputProg.exe %1es\InputProg.resources.dll %1fr\InputProg.resources.dll
通过该命令行,我在合并的程序集中获得了以下几组资源(使用ILSpy反编译器进行了观察):
InputProg.resources InputProg.es.resources InputProg.es.resources <-- Duplicated!
在玩了一段时间后,我意识到这只是ILMerge中的一个错误,当它在单个命令行调用中遇到多个具有相同名称的文件时 。 解决方法是简单地将每个卫星程序集合在一个不同的命令行调用中:
"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
当我这样做时,最后汇编中所产生的资源是正确的:
InputProg.resources InputProg.es.resources InputProg.fr.resources
所以最后,如果这有助于澄清,这里有一个完整的后期制作batch file:
"%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
更新3:ILRepack
另一个快速注释 – ILMerge困扰我的一个问题是它是一个额外的专有的Microsoft工具,并不是默认安装在Visual Studio中,因此还有一个额外的依赖关系,使得第三方开始有点困难与我的开源项目。
我最近发现了ILRepack ,一个开源的(Apache 2.0),到目前为止,我也同样适用(drop-in replacement),并且可以和你的项目源一起免费发布。
我希望这可以帮助那里的人!
我可以看到这个工作的唯一方法是创build一个派生自ResourceManager
的类,然后重写InternalGetResourceSet
和GetResourceFileName
方法。 从那里,你应该能够覆盖哪里获得资源,给一个CultureInfo
实例。
一种不同的方法:
1)将您的resource.DLL作为embedded的资源添加到您的项目中。
2)为AppDomain.CurrentDomain.ResourceResolve添加一个事件处理程序。 这个处理程序会在找不到资源时触发。
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)现在你必须实现LoadResourceAssyFromResource
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; }
}
我有一个你的问题的一部分的build议。 具体来说,更新.Designer.cs文件以将ComponentResourceManagerreplace为SingleAssemblyComponentResourceManager的步骤的解决scheme。
-
将InitializeComponent()方法从.Designer.cs中移出并移入实现文件(包括#region)。 Visual Studio将继续自动生成该部分,据我所知,没有任何问题。
-
在实现文件的顶部使用C#别名,以便ComponentResourceManager别名到SingleAssemblyComponentResourceManager。
不幸的是,我没有得到充分的testing。 我们发现了一个不同的解决scheme,我们的问题,继续前进。 我希望它可以帮助你。
只是一个想法。
你做了这个步骤并创build了你的SingleAssemblyComponentResourceManager
那么,为什么你要把你的卫星组件包括在一起呢?
您可以将ResourceName.es.resx
本身作为二进制文件添加到项目中的另一个资源。
比你可以重写你的代码
store = this.MainAssembly.GetManifestResourceStream( this._contextTypeInfo, resourceFileName); //If we found the appropriate resources in the local assembly if (store != null) { rs = new ResourceSet(store);
与此代码(未testing,但应该工作)
// 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); } }
这应该致力于将ilmerge过程中的卫星组件包括在内。
作为回答发布,因为评论没有提供足够的空间:
OPs解决scheme找不到中性文化的资源(而不是en-US
)。 所以我扩展InternalGetResourceSet
查找中性文化,为我做的工作。 有了这个,你现在可以find没有定义区域的资源。 这实际上与正常的资源格式化器在不ILMerging资源文件时显示的行为相同。
//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); }
这将导致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); } } } }