Развертывание мультиязычных Windows Forms с одной сборкой (ILMerge и спутниковые сборки / локализация) - возможно?
У меня есть простое приложение 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", и сделав наш поиск по той же сборке первым использованным механизмом. Если ресурс не найден в текущей сборке, то мы вызываем базовый метод для запуска механизмов поиска по умолчанию (благодаря комментарию @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 на этапе предварительной сборки для автоматической замены.)
ОБНОВЛЕНИЕ 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 ответов
Единственный способ увидеть эту работу - создать класс, производный от ResourceManager
а затем переопределить InternalGetResourceSet
а также GetResourceFileName
методы. Оттуда вы должны иметь возможность переопределить, где ресурсы получены, учитывая CultureInfo
пример.
Другой подход:
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;
}
}
Опубликовано как ответ, так как комментарии не предоставили достаточно места:
Я не мог найти ресурсы для нейтральных культур (en
вместо en-US
) с решением ОП. Итак, я продлил 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);
}
}
}
}
Просто мысль.
Вы сделали шаг и создали свой 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);
}
}
Это должно сделать усилия по включению спутниковых сборок в процесс погружения устаревшими.
У меня есть предложение для части вашей проблемы. В частности, решение шага обновления файлов.Designer.cs для замены ComponentResourceManager на SingleAssemblyComponentResourceManager.
Переместите метод InitializeComponent() из.Designer.cs в файл реализации (включая #region). Visual Studio продолжит автоматически генерировать этот раздел, без проблем, насколько я могу судить.
Используйте псевдоним C# в верхней части файла реализации, чтобы ComponentResourceManager связывался с SingleAssemblyComponentResourceManager.
К сожалению, я не смог проверить это полностью. Мы нашли другое решение нашей проблемы и пошли дальше. Я надеюсь, что это поможет вам, хотя.