I have a simple Windows Forms (C#, .NET 2.0) application, built with Visual Studio 2008.
I would like to support multiple UI languages, and using the "Localizable" property of the form, and culture-specific .resx files, the localization aspect works seamlessly and easily. Visual Studio automatically compiles the culture-specific resx files into satellite assemblies, so in my compiled application folder there are culture-specific subfolders containing these satellite assemblies.
I would like to have the application be deployed (copied into place) as a single assembly, and yet retain the ability to contain multiple sets of culture-specific resources.
Using ILMerge, I can merge the satellite assemblies into the main executable assembly, but the standard .NET ResourceManager fallback mechanisms do not find the culture-specific resources that were compiled into the main assembly.
Interestingly, if I take my merged (executable) assembly and place copies of it into the culture-specific subfolders, then everything works! Similarly, I can see the main and culture-specific resources in the merged assemby when I use Reflector. But copying the main assembly into culture-specific subfolders defeats the purpose of the merging anyway - I really need there to be just a single copy of the single assembly...
I'm wondering whether there is any way to hijack or influence the ResourceManager fallback mechanisms to look for the culture-specific resources in the same assembly rather than in the GAC and culture-named subfolders. I see the fallback mechanism described in the following articles, but no clue as to how it would be modified: BCL Team Blog Article on ResourceManager.
Does anyone have any idea? This seems to be a relatively frequent question online (for example, another question here on Stack Overflow: "ILMerge and localized resource assemblies"), but I have not found any authoritative answer anywhere.
Following casperOne's recommendation below, I was finally able to make this work.
I'm putting the solution code here in the question because casperOne provided the only answer, I don't want to add my own.
I was able to get it to work by pulling the guts out of the Framework resource-finding fallback mechanisms implemented in the "InternalGetResourceSet" method. This is not a fix to the built-in mechanisms, but rather replacing the whole thing with a much simpler mechanism that will only find the resources if they are in the executing assembly.
To do this, I derived the "ComponentResourceManager" class, and overrode just one methods (and re-implemented a private framework method):
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;
if (this._neutralResourcesCulture == null)
{
this._neutralResourcesCulture = GetNeutralResourcesLanguage(this.MainAssembly);
}
//if we're asking for the default language, then ask for the invaliant (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);
}
}
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);
}
}
}
}
To actually use this class, you need to replace the System.ComponentModel.ComponentResourceManager in the "XXX.Designer.cs" files created by Visual Studio - and you will need to do this every time you change the designed form - Visual Studio replaces that code automatically. (The problem was discussed in "Customize Windows Forms Designer to use MyResourceManager", I did not find a more elegant solution.)
Note: this will only work if you always merge all your satellite assemblies into the main/executable assembly! I did this with a post-build batch file, but there are definitely more elegant solutions available out there using advanced MSBuild features.
I hope this helps someone out there!