【问题标题】:Single-assembly multi-language Windows Forms deployment (ILMerge and satellite assemblies / localization) - possible?单程序集多语言 Windows 窗体部署(ILMerge 和附属程序集/本地化) - 可能吗?
【发布时间】: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 == nullrs == null 时,它在合并之前有效
  • @Wouter:谢谢你让我觉得自己像个白痴! :) - 我今晚会玩这个并更新问题/代码

标签: c# .net winforms localization assemblies


【解决方案1】:

我可以看到这个工作的唯一方法是创建一个派生自ResourceManager 的类,然后覆盖InternalGetResourceSetGetResourceFileName 方法。从那里,您应该能够覆盖获取资源的位置,给定 CultureInfo 实例。

【讨论】:

  • 谢谢,我最终能够让它工作,我在上面添加了我的代码,因为你的代码在技术上就是答案。请注意,实际需要重写的唯一方法是“InternalGetResourceSet”。我本来希望只根据需要修改它,但现有代码广泛使用其他内部和/或私有框架代码,更容易全部剥离并仅实现本地程序集加载。
【解决方案2】:

另一种方法:

1) 将您的 resource.DLL 作为嵌入资源添加到您的项目中。

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

【讨论】:

【解决方案3】:

由于 cmets 没有提供足够的空间,因此作为答案发布:

我找不到使用 OPs 解决方案的中性文化资源(en 而不是 en-US)。所以我扩展了InternalGetResourceSet,查找了对我有用的中性文化。有了这个,您现在还可以找到未定义区域的资源。这实际上与普通资源格式化程序在不 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);
            }
        }
    }
}

【讨论】:

    【解决方案4】:

    对于您的部分问题,我有一个建议。具体来说,更新 .Designer.cs 文件以将 ComponentResourceManager 替换为 SingleAssemblyComponentResourceManager 的步骤的解决方案。

    1. 将 InitializeComponent() 方法从 .Designer.cs 中移出并移到实现文件中(包括 #region)。 Visual Studio 将继续自动生成该部分,据我所知没有任何问题。

    2. 在实现文件的顶部使用 C# 别名,以便将 ComponentResourceManager 别名为 SingleAssemblyComponentResourceManager。

    很遗憾,我没有完全测试这个。我们为我们的问题找到了不同的解决方案,因此继续前进。不过希望对你有帮助。

    【讨论】:

    • 感谢您的建议,这是一个有趣的建议(我不知道您可以移动 InitializeComponent 方法并让设计器继续工作)!不幸的是,每次设计器替换“ComponentResourceManager”引用时,它都使用完全限定的类型名称“System.ComponentModel.ComponentResourceManager”,因此别名似乎没有帮助。再次感谢您的提示!
    【解决方案5】:

    只是一个想法。

    您完成了这一步并创建了您的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);
    

    使用此代码(未经测试但应该可以使用)

    // 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 过程中的努力过时。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-18
      • 2012-10-20
      • 1970-01-01
      • 2014-10-21
      • 1970-01-01
      相关资源
      最近更新 更多