views:

817

answers:

1

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!

+5  A: 

The only way I can see this working is by creating a class that derives from ResourceManager and then overriding the InternalGetResourceSet, GetNeutralResourcesLanguage and GetResourceFileName methods. From there, you should be able to override where resources are obtained, given a CultureInfo intance.

casperOne
thanks, I was eventually able to make it work, I added my code above as yours was technically the answer. Note, the only method that actually needed to be overridden was "InternalGetResourceSet". I would have liked to only modify it as required, but the existing code made extensive use of other internal and/or private framewoprk code, it was easier to rip ity all out and implement local assembly loading only.
Tao