У меня есть простое приложение 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), который до сих пор работает для меня так же хорошо (замена в виде вставки) и может свободно распространяться с источниками вашего проекта.
Надеюсь, это кому-нибудь поможет!