c# собрать один - Возможно ли одноуровневое многоязычное развертывание Windows Forms (ILMerge и спутниковые сборки / локализация)?




3 Answers

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

файл проект

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

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

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

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

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

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

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

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

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

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

Я смог заставить его работать, вытащив кишки из механизмов резервного восстановления ресурсов Framework, реализованных в методе «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);
            }
        }
    }
}

Чтобы на самом деле использовать этот класс, вам нужно заменить System.ComponentModel.ComponentResourceManager в файлах «XXX.Designer.cs», созданных Visual Studio, - и вам нужно будет делать это каждый раз при изменении оформленной формы - Visual Studio заменяет это код автоматически. (Проблема обсуждалась в « Настроить конструктор Windows Forms для использования MyResourceManager », я не нашел более элегантного решения - я использую fart.exe на 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 , ILRepack Open Source (Apache 2.0), который до сих пор работает для меня (замена на замену) и может свободно распространяться с вашими источниками проекта.

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




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

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

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

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




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

Я не мог найти ресурсы для нейтральных культур ( en вместо en-US ) с решением OPs. Поэтому я распространил 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);
            }
        }
    }
}



Related