【发布时间】:2009-12-23 12:51:14
【问题描述】:
我有一个使用 Visual Studio 2008 构建的简单 Windows 窗体(C#、.NET 2.0)应用程序。
我想支持多种 UI 语言,并使用表单的“Localizable”属性和特定于文化的 .resx 文件,本地化方面可以无缝且轻松地工作。 Visual Studio 会自动将特定区域性的 resx 文件编译为附属程序集,因此在我编译的应用程序文件夹中有包含这些附属程序集的特定于区域性的子文件夹。
我希望将应用程序作为单个程序集进行部署(复制到位),同时保留包含多组特定文化资源的能力。
使用ILMerge(或ILRepack),我可以将附属程序集合并到主可执行程序集中,但标准的.NET ResourceManager 后备机制找不到编译到主程序集中的特定区域性资源。
有趣的是,如果我将合并的(可执行)程序集放入特定文化的子文件夹中,那么一切正常!同样,当我使用Reflector(或ILSpy)时,我可以在合并的程序集中看到主要资源和特定于文化的资源。但是将主程序集复制到特定于文化的子文件夹中无论如何都会破坏合并的目的 - 我真的需要单个程序集的一个副本......
我想知道是否有任何方法可以劫持或影响 ResourceManager 后备机制以在同一程序集中而不是在 GAC 和以文化命名的子文件夹中查找特定于文化的资源。我看到以下文章中描述的回退机制,但不知道如何修改它:BCL Team Blog Article on ResourceManager。
有人知道吗?这似乎是网上比较常见的一个问题(例如Stack Overflow上的另一个问题:“ILMerge and localized resource assemblies”),但我在任何地方都没有找到任何权威的答案。
更新 1:基本解决方案
在casperOne's recommendation below 之后,我终于能够完成这项工作。
我将解决方案代码放在问题中,因为 casperOne 提供了唯一的答案,我不想添加自己的答案。
我能够通过从“InternalGetResourceSet”方法中实现的框架资源查找回退机制中抽出胆量并使我们的相同程序集搜索成为使用的第一个机制来使其工作。如果在当前程序集中没有找到资源,那么我们调用 base 方法来启动默认的搜索机制(感谢下面@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);
}
}
}
}
要实际使用这个类,你需要替换 Visual Studio 创建的“XXX.Designer.cs”文件中的 System.ComponentModel.ComponentResourceManager - 每次更改设计的表单时都需要这样做 - Visual Studio 会自动替换该代码。 (问题在“Customize Windows Forms Designer to use MyResourceManager”中讨论过,我没有找到更优雅的解决方案 - 我在预构建步骤中使用fart.exe 来自动替换。)
更新 2:另一个实际考虑 - 超过 2 种语言
在我报告上述解决方案时,我实际上只支持两种语言,而 ILMerge 在将我的附属程序集合并到最终合并程序集方面做得很好。
最近我开始从事一个类似的项目,其中有多种辅助语言,因此有多个附属程序集,而 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
最后,如果这有助于澄清,这里有一个完整的构建后批处理文件:
"%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) 等价物,到目前为止对我来说同样有效(直接替换),并且可以与您的项目源一起免费分发。
我希望这对那里的人有所帮助!
【问题讨论】:
-
经过更多(相对痴迷的)搜索后,我发现了一些有前途的 sn-ps:neowin.net/forum/lofiversion/index.php/t625641.htmlsocial.msdn.microsoft.com/Forums/en/vsx/thread/… 看起来创建自定义 ResourceManager 将是可行的方法 - 我走了反编译默认的 ResourceManager(救援的反射器!),看看我是否能更好地理解它的作用/它是如何工作的。
-
你好。我需要做同样的事情。我已经添加了这个新类并在设计器文件中进行了替换,但是我在以下行得到了 NullReferenceException:this.outputPath.Properties.AutoHeight = ((bool)(resources.GetObject("outputPath.Properties.AutoHeight"))) ; (resources 是一个 SingleAssemblyComponentResourceManager) outputPath - 是一个 DevExpress 控件。即使在合并了所有 DevExpress 程序集之后。
-
你知道什么是 Null 吗?是实际上没有初始化的“资源”对象吗?不确定这是我帮助您编写代码的最佳论坛,但请随时通过 klerks dot biz 向我发送详细信息。
-
return base.InternalGetResourceSet(culture, createIfNotExists, tryParents);store == null或rs == null时,它在合并之前有效 -
@Wouter:谢谢你让我觉得自己像个白痴! :) - 我今晚会玩这个并更新问题/代码
标签: c# .net winforms localization assemblies