代码之家  ›  专栏  ›  技术社区  ›  Tao

单程序集多语言Windows窗体部署(ILmerge和附属程序集/本地化)-可能吗?

  •  52
  • Tao  · 技术社区  · 16 年前

    我有一个简单的Windows窗体(C,.NET 2.0)应用程序,它是用Visual Studio 2008构建的。

    我希望支持多种UI语言,并且使用表单的“可本地化”属性和特定于区域性的.resx文件,本地化方面可以无缝、轻松地工作。Visual Studio会自动将区域性特定的Resx文件编译为附属程序集,因此在“已编译的应用程序”文件夹中有包含这些附属程序集的区域性特定子文件夹。

    我希望将应用程序作为 单组件 同时保留包含多组特定于文化的资源的能力。

    使用 ILMerge (或) ILRepack ,我可以将附属程序集合并到主可执行程序集中,但是标准的.NET ResourceManager回退机制找不到编译到主程序集中的特定于区域性的资源。

    有趣的是,如果我将合并的(可执行的)程序集放在特定于区域性的子文件夹中,那么一切都正常!同样,当我使用时,我可以看到合并资产中的主要资源和特定于文化的资源 Reflector (或) ILSpy )。但是,将主程序集复制到特定于区域性的子文件夹中会破坏合并的目的——我真的需要只有单个程序集的一个副本……

    我想知道 是否有任何方法劫持或影响ResourceManager回退机制,以便在同一程序集中而不是在GAC和文化命名子文件夹中查找特定于文化的资源 . 我在下面的文章中看到了回退机制,但是没有关于如何修改它的线索: BCL Team Blog Article on ResourceManager .

    有人知道吗?这似乎是一个相对频繁的在线问题(例如,堆栈溢出的另一个问题:“ ILMerge and localized resource assemblies “),但是我在任何地方都没有找到任何权威的答案。


    更新1:基本解决方案

    跟随 casperOne's recommendation below ,我终于能做到这一点。

    我把解决方案代码放在这里,因为casperone提供了唯一的答案,我不想添加我自己的答案。

    我可以通过将其从“InternalGetResourceSet”方法中实现的框架资源查找回退机制中拉出来并使我们的相同程序集搜索 第一 使用的机制。如果在当前程序集中找不到资源,那么我们调用基方法来启动默认的搜索机制(感谢下面@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!
    

    在玩了几次之后,我终于意识到这只是我遇到的一个错误。 多个同名文件 在单个命令行调用中。解决方案只是在不同的命令行调用中合并每个附属程序集:

    "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让我困扰的事情之一是它是一个额外的专有的微软工具,默认情况下不与Visual Studio一起安装,因此它是一个额外的依赖项,这使得第三方很难开始我的开源项目。

    我最近发现 伊莱克包 这是一个开放源码(apache 2.0)等价物,到目前为止它对我(插入替换)同样有效,并且可以与您的项目源代码自由分发。


    我希望这能帮到别人!

    5 回复  |  直到 9 年前
        1
  •  23
  •   casperOne    14 年前

    我唯一能看到这种工作的方法是创建一个派生自 ResourceManager 然后超越 InternalGetResourceSet GetResourceFileName 方法。从那里,您应该能够覆盖获得资源的位置,给定 CultureInfo 实例。

        2
  •  1
  •   jm.    9 年前

    另一种方法:

    1)将resource.dlls添加为项目中的嵌入资源。

    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
  •  0
  •   Josh Buedel    14 年前

    我对你的部分问题有个建议。具体来说,更新.designer.cs文件以用singleassemblycomponentresourcemanager替换componentresourcemanager步骤的解决方案。

    1. 将initializecomponent()方法移出.designer.cs并移入实现文件(包括区域)。Visual Studio将继续自动生成该部分,据我所知,没有任何问题。

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

    不幸的是,我没有充分测试这个。我们找到了解决问题的不同方法,然后继续前进。不过,我希望它能帮助你。

        4
  •  0
  •   Jürgen Steinblock    12 年前

    只是一个想法。

    你完成了这一步,创造了 SingleAssemblyComponentResourceManager

    那么,为什么要在ilmerged程序集中包含卫星程序集呢?

    您可以添加 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进程中包含卫星组件的工作变得过时。

        5
  •  0
  •   Marwie    10 年前

    由于评论没有提供足够的空间,发布为答案:

    我找不到中立文化的资源( en 而不是 en-US )使用OPS解决方案。所以我延长了 InternalGetResourceSet 找一个中立的文化为我做的工作。通过这个,您现在还可以找到不定义区域的资源。这实际上与正常的resourceformatter在不合并资源文件时显示的行为相同。

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