Возможно ли одноуровневое многоязычное развертывание Windows Forms (ILMerge и спутниковые сборки / локализация)?
У меня есть простое приложение Windows Forms (C #, .NET 2.0), созданное с помощью Visual Studio 2008.
Я хотел бы поддерживать несколько языков пользовательского интерфейса и использовать свойство Localizable формы, а также файлы .resx, относящиеся к культуре, аспект локализации работает легко и просто. Visual Studio автоматически компилирует файлы resx, относящиеся к культуре, в спутниковые сборки, поэтому в моей скомпилированной папке приложения есть вложенные в культуру подпапки, содержащие эти спутниковые сборки.
Я хотел бы, чтобы приложение было развернуто (скопировано на место) как отдельная assembly и все же сохраняло возможность содержать несколько наборов ресурсов для конкретной культуры.
- C # компиляция для 32/64 бит или для любого процессора?
- _addcarry_u64 и _addcarryx_u64 с MSVC и ICC
- Не удалось загрузить файл или сборку HRESULT: 0x80131515 (При добавлении controllerа в проект MVC с ссылками на сборку на сетевом диске)
- Поиск всех пространств имен в сборке с использованием Reflection (DotNET)
- Сделать муравей тихий без флага -q?
Используя ILMerge (или ILRepack ), я могу объединить спутниковые сборки в основную исполняемую сборку, но стандартные резервные механизмы .NET ResourceManager не находят ресурсы, специфичные для культуры, которые были скомпилированы в основную сборку.
Интересно, что если я возьму свою объединенную (исполняемую) сборку и поместил ее в соответствующие вложенные в культуру подпапки, тогда все будет работать! Точно так же я вижу основные и культурные ресурсы в объединенной сборке, когда я использую Reflector (или ILSpy ). Но копирование основной сборки в зависимые от культуры подпапки наносит ущерб цели слияния в любом случае – мне действительно нужно, чтобы она была единственной копией единственной сборки …
Мне интересно , есть ли способ захватить или повлиять на резервные механизмы ResourceManager, чтобы искать ресурсы, специфичные для культуры, в одной и той же сборке, а не в подпапках GAC и в названии культуры . Я вижу механизм резервного копирования, описанный в следующих статьях, но не знаю, как он будет изменен: BCL Team Blog Article on ResourceManager .
Кто-нибудь есть идеи? Это, по-видимому, довольно частый вопрос онлайн (например, еще один вопрос здесь о переполнении стека: « ILMerge и локализованные сборки ресурсов »), но я нигде не нашел никакого авторитетного ответа.
ОБНОВЛЕНИЕ 1: основное решение
Следуя приведенной ниже рекомендации casperOne , я наконец смог выполнить эту работу.
Я помещаю код решения здесь в вопрос, потому что casperOne предоставил единственный ответ, я не хочу добавлять свои собственные.
Я смог заставить его работать, вытащив кишки из механизмов резервного восстановления ресурсов Framework, реализованных в методе «InternalGetResourceSet», и сделав наш аналогичный compilation первым используемым механизмом. Если ресурс не найден в текущей сборке, мы вызываем базовый метод для инициирования механизмов поиска по умолчанию (благодаря комментарию @ Wouter ниже).
Для этого я получил class «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); } } } }
Чтобы на самом деле использовать этот class, вам нужно заменить System.ComponentModel.ComponentResourceManager в файлах «XXX.Designer.cs», созданных Visual Studio, – и вам нужно будет делать это каждый раз при изменении оформленной формы – Visual Studio заменяет это код автоматически. (Проблема обсуждалась в « Настроить конструктор Windows Forms для использования 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 , эквивалентный Open Source (Apache 2.0), который до сих пор работает для меня (замена на замену) и может свободно распространяться с вашими источниками проекта.
Надеюсь, это поможет кому-то!
- Как скомпилировать и запустить программу C в Sublime Text 2?
- Как получить вывод ассемблера из источника C / C ++ в gcc?
- Определить версию сборки (CLR) сборки
- Почему mulss занимает всего 3 цикла на Хасуэлле, отличном от таблиц инструкций Агнера?
- Примеры предварительной выборки?
- Самый быстрый способ вычисления 128-битного целого по модулю 64-разрядного целого числа
- Как я могу перечислить все загруженные сборки?
- Почему XCHG reg, reg 3 инструкции по микрооперации на современных архитектурах Intel?
Единственный способ увидеть это – создать class, который происходит из ResourceManager
а затем переопределить методы InternalGetResourceSet
и GetResourceFileName
. Оттуда вы сможете переопределить, где будут получены ресурсы, с учетом экземпляра CultureInfo
.
Другой подход:
1) добавьте свои ресурсы.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; }
}
У меня есть предложение по части вашей проблемы. В частности, решение для этапа обновления файлов .Designer.cs для замены ComponentResourceManager с помощью SingleAssemblyComponentResourceManager.
-
Переместите метод InitializeComponent () из .Designer.cs и в файл реализации (включая #region). Visual Studio продолжит автоматическое создание этого раздела, без каких-либо проблем, насколько я могу судить.
-
Используйте псевдоним C # в верхней части файла реализации, чтобы ComponentResourceManager был псевдонимом SingleAssemblyComponentResourceManager.
К сожалению, я не смог полностью проверить это. Мы нашли другое решение нашей проблемы и поэтому двинулись дальше. Надеюсь, вам это поможет.
Просто мысль.
Вы сделали шаг и создали свой 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.
Добавлено как ответ, так как комментарии не обеспечивали достаточного пространства:
Я не мог найти ресурсы для нейтральных культур ( 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); } } } }