Развертывание Windows Forms с одной сборкой на нескольких языках (ILMerge и вспомогательные сборки / локализация) - возможно?


у меня есть простое приложение Windows Forms (C#, .NET 2.0), построенное с помощью Visual Studio 2008.

Я хотел бы поддерживать несколько языков пользовательского интерфейса и использовать свойство "Локализуемое" формы и специфичное для языка и региональных параметров .resx файлы, аспект локализации работает легко и без проблем. Visual Studio автоматически компилирует файлы resx для конкретного языка и региональных параметров в вспомогательные сборки, поэтому в моей папке скомпилированного приложения есть подпапки для конкретного языка и региональных параметров, содержащие эти вспомогательная сборка.

Я хотел бы, чтобы приложение было развернуто (скопировано на место) как единую сборку, и все же сохранить возможность содержать несколько наборов специфичных для культуры ресурсов.

используя ILMerge (или ILRepack), я могу объединить вспомогательные сборки в основную исполняемую сборку, но стандартные резервные механизмы .NET ResourceManager не находят ресурсы, зависящие от языка и региональных параметров, которые были компилируется в основную сборку.

интересно, что если я возьму свою объединенную (исполняемую) сборку и помещу ее копии в подпапки для конкретных языков и региональных параметров, то все работает! Аналогично, я могу видеть основные и специфические для культуры ресурсы в объединенной сборке, когда я использую отражатель (или ILSpy). Но копирование основной сборки в подпапки, зависящие от культуры, в любом случае побеждает цель слияния - мне действительно нужно, чтобы была только одна копия единая сборка...

мне интересно есть ли какой-либо способ захватить или повлиять на резервные механизмы ResourceManager для поиска ресурсов, зависящих от языка и региональных параметров, в одной сборке, а не в подпапках GAC и языка и региональных параметров. Я вижу резервный механизм, описанный в следующих статьях, но не знаю, как он будет изменен:ОУЗ статье блога разработчиков на объект ResourceManager.

у кого-нибудь есть идеи? Этот кажется, это относительно частый вопрос в интернете (например, еще один вопрос здесь о переполнении стека:"ILMerge и локализованные сборки ресурсов"), но я нигде не нашел авторитетного ответа.


обновление 1: основное решение

после casperOne ниже, я, наконец, смог сделать эту работу.

Я ставлю код в вопрос, потому что casperOne предоставил только ответьте, Я не хочу добавлять свой собственный.

я смог заставить его работать, вытащив кишки из резервных механизмов поиска ресурсов фреймворка, реализованных в методе "InternalGetResourceSet", и сделав наш же поиск сборки первый механизм. Если ресурс не найден в текущей сборке, то мы вызываем базовый метод для запуска механизмов поиска по умолчанию (благодаря комментарию @Wouter ниже).

чтобы сделать это, я получил класс" ComponentResourceManager " и переопределил только один метод (и повторно реализовал метод private framework):

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

чтобы фактически использовать этот класс, вам нужно заменить систему.ComponentModel.ComponentResourceManager в "ХХХ.Дизайнер.cs " файлы, созданные Visual Studio - и вам нужно будет делать это каждый раз, когда вы изменяете разработанную форму - Visual Studio автоматически заменяет этот код. (Проблема обсуждалась в разделе"настройка конструктора Windows Forms для использования MyResourceManager" я не нашел более элегантное решение - я использую фарт.exe на этапе предварительной сборки для автоматической замены.)


обновление 2: Еще одно практическое соображение-более 2 языков

в то время, когда я сообщил о решении выше, я фактически поддерживал только два языка, и ILMerge отлично справлялся с объединением моей вспомогательной сборки в окончательную объединенную сборку.

недавно я начал работать над подобным проект, где есть несколько вторичные языки и, следовательно, несколько вспомогательных сборок, и ILMerge делал что-то очень странное: вместо объединения нескольких вспомогательных сборок, которые я просил, он объединял первую вспомогательную сборку несколько раз!

командной строки, например:

"c:Program FilesMicrosoftILMergeILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1InputProg.exe %1esInputProg.resources.dll %1frInputProg.resources.dll

С помощью этой командной строки я получал следующие наборы ресурсов в объединенной сборке (наблюдается с ILSpy декомпилятор):

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

после некоторых игр вокруг, я в конечном итоге понял, что это просто ошибка в ILMerge, когда он сталкивается несколько файлов с одинаковым именем в одном вызове командной строки. Решение состоит в том, чтобы просто объединить каждую вспомогательную сборку в другой вызов командной строки:

"c:Program FilesMicrosoftILMergeILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1esInputProg.resources.dll
"c:Program FilesMicrosoftILMergeILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1frInputProg.resources.dll

когда я делаю это, результирующие ресурсы в окончательной сборке верны:

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

Итак, наконец, в случае, если это поможет прояснить, вот полный пакетный файл после сборки:

"%ProgramFiles%MicrosoftILMergeILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1esInputProg.resources.dll 
IF %ERRORLEVEL% NEQ 0 GOTO END

"%ProgramFiles%MicrosoftILMergeILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1frInputProg.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), и может быть свободно распространен с вашими источниками проекта.


Я надеюсь, что это поможет кому-то там!

5 52

5 ответов:

единственный способ увидеть эту работу - это создать класс, производный от ResourceManager и затем переопределить InternalGetResourceSet и GetResourceFileName методы. Оттуда, вы должны быть в состоянии переопределить, где ресурсы получены, учитывая CultureInfo экземпляра.

другой подход:

1) Добавить свой ресурс.Библиотеки DLL как встроенные ресурсы в вашем проекте.

2) добавить обработчик событий для AppDomain.CurrentDomain.Ресурсы решают. Этот обработчик будет срабатывать, когда ресурс не может быть найден.

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

}

У меня есть предложение по части вашей проблемы. В частности, решение на этапе обновления .Дизайнер.cs-файлы для замены ComponentResourceManager на SingleAssemblyComponentResourceManager.

  1. переместить метод InitializeComponent () из .Дизайнер.cs и в файл реализации (включая #регион). Visual Studio будет продолжать автоматически генерировать этот раздел, без проблем, насколько я могу судить.

  2. использовать C# псевдоним в верхней части файла реализации, так что ComponentResourceManager имеет псевдоним для SingleAssemblyComponentResourceManager.

к сожалению, я не получил, чтобы проверить это полностью. Мы нашли другое решение нашей проблемы и двинулись дальше. Я надеюсь, что это поможет вам, хотя.

просто мысли.

вы сделали шаг и создал свой 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);
    }
}

это должно сделать усилие, чтобы включить sattelite assembiles в процессе ilmerge устарел.

опубликовано в качестве ответа, так как комментарии не предоставили достаточно места:

Я не мог найти ресурсы для нейтральных культур (en вместо en-US) С решением OPs. Так что я продлил InternalGetResourceSet С поиском нейтральных культур, которые сделали эту работу за меня. Теперь вы можете также найти ресурсы, которые не определяют регион. Это на самом деле то же самое поведение, что обычный resourceformatter покажет, когда не 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);
            }
        }
    }
}