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

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

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

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

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

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

Мне интересно , есть ли способ угнать или повлиять на механизмы отката ResourceManager для поиска специфичных для культуры ресурсов в той же сборке, а не в GAC и подпапках с именами культур . Я вижу механизм отката, описанный в следующих статьях, но не знаю, как он будет изменен: Статья блога команды BCL на ResourceManager .

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


ОБНОВЛЕНИЕ 1: Базовое решение

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

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

Мне удалось заставить его работать, вытащив кишки из механизмов отката поиска ресурсов Framework, реализованных в методе «InternalGetResourceSet», и сделав наш поиск по той же сборке используемым механизмом first . Если ресурс не найден в текущей сборке, то мы вызываем базовый метод для запуска механизмов поиска по умолчанию (благодаря комментарию @ 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);
            }
        }
    }
}

Чтобы на самом деле использовать этот класс, вам необходимо заменить System.ComponentModel.ComponentResourceManager в файлах "XXX.Designer.cs", созданных Visual Studio, - и вам придется делать это каждый раз, когда вы изменяете разработанную форму - Visual Studio заменяет этот код автоматически. (Проблема обсуждалась в « Настройка Windows Forms Designer для использования MyResourceManager », я не нашел более элегантного решения - я использую fart.exe на этапе предварительной сборки для автоматического -replace.)


ОБНОВЛЕНИЕ 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), который до сих пор работает для меня так же хорошо (замена в виде вставки) и может свободно распространяться с источниками вашего проекта.


Надеюсь, это кому-нибудь поможет!

Ответы [ 5 ]

24 голосов
/ 23 декабря 2009

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

1 голос
/ 18 февраля 2016

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

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

}

0 голосов
/ 04 марта 2015

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

Мне не удалось найти ресурсы для нейтральных культур (en вместо en-US) с решением OP. Поэтому я расширил 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);
            }
        }
    }
}
0 голосов
/ 06 февраля 2014

Просто мысль.

Вы сделали шаг и создали свой 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 устаревшими.

0 голосов
/ 18 марта 2011

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

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

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

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

...