One of our ASP.NET 1.1 applications is localized for 7 languages. I recently converted this app to ASP.NET 2.0, using Visual Studio 2005, and encountered some interesting challenges related to the language resources along the way.
1.1 Project Setup
We have the .aspx files along with their associated resx files (default resx, plus one for each language) in the root directory. When built, this results in the standard setup of default resources in the main code-behind assembly (bin\webSite.dll), and the language specific satellite resources in their respective directories (bin\de\webSite.resources.dll).
We have all pages in the application derive from a common BasePage, where we have some resource handling logic. Here in the base page, we create a ResourceManager for the current page, then walk all the controls on the page, and find the appropriate resource text for each control (if provided). The highlights of this code is shown below. Pretty standard / straight forward, right?
ResourceManager rm = new ResourceManager( this.GetType().BaseType.FullName, System.Reflection.Assembly.GetExecutingAssembly() );
The Conversion
When I opened up this project in VS 2005, I was presented with the project conversion wizard as expected, but found it interesting what the conversion process did with my resource files. Most of the resource files were moved to the newly created “App_GlobalResources” directory. But…it LEFT the base resource file for each page (i.e. Home.aspx.resx) in the main directory where the aspx pages are.
The Build
When this new 2.0 project layout is built, the base resx files in the main aspx directory are completely ignored, and the “App_GlobalResources” directory is built into it’s own assembly. This causes problems with the above BasePage code, trying to create a ResourceManager for the currently executing page, and its associated resources. The ResourceManager will find no resources for the controls on the page, since they are in the separate App_GlobalResources assembly.
The Problem
Obviously, the start of the problem is that the 2nd parameter to ResourceManager constructor above (in red) is incorrect, since the currently executing assembly in ASP.NET 2.0 is the “web” assembly, which is different than the assemblies that hold the resources (global and local).
I know ASP.NET 2.0 comes with new support for resources. However, the programming model is quite a bit different. After adding resources to App_GlobalResources, you can refer to them in your code by using the Resources class which is dynamically generated as a wrapper for the contents of App_GlobalResources. Something like:
// use this in code-behind to set label text based on App_GlobalResources lblFoo.Text = Resources.SomePageName.Foo; // use this in aspx page markup to set label text based on App_LocalResources <?xml:namespace prefix = asp />
The problem is, I didn’t want to retrofit all of our pages to use either of the above syntax – rather, I wanted to come up with a way to keep the 1.1 code flow – allowing the BasePage to create a proper ResourceManager that would be pointing to those resources from App_GlobalResources. But – how do you create a ResourceManager at runtime pointing to the App_GlobalResources assembly when this assembly filename changes with every build?
Some Workarounds / Solutions
After digging around, I came up with a few solutions, some better than others, and the ultimate one, using an (as far as I can see) undocumented feature.
The 2.0 techniques for retrieving resources all seem to bypass the ResourceManager class all-together. At least the calling code doesn’t get the ResourceManager to use.
// for the Default page, whose class name is _default // #1 - use built in dynamically generated Resources class. Provides // strongly typed names for each resource lblFoo.Text = Resources._default.foo; // #2 - use TemplateControl.GetGlobalResourceObject() lblFoo.Text = GetGlobalResourceObject("_default", "foo").ToString();
Neither of these help in my quest to keep my 1.1 code working as is. I need a ResourceManager object that refers to the resources for the current page for the rest of the control-walking code to work.
The trick is, to create the proper ResourceManager, you need 2 things: the type name (easy, this is the class name of the code-behind class for the currently running page), and the assembly where the resources are. We don’t know the name of the App_GlobalResources assembly since it’s different each time we compile (ex: App_GlobalResources.fsfa3s_h.dll).
How should we find the App_GlobalResources assembly?
Here are the thoughts I came up with along the way, finally tripping over the last solution, based on a clue mentioned in passing in a ASP.NET 2.0 webcast.
// #1 - HUGE HACK! Since the page/code-behind executing assembly // references the App_GlobalResources assembly, // walk the references and find it. { Assembly asmGlobal = null; Assembly asmCode = Assembly.GetExecutingAssembly(); foreach (AssemblyName name in asmCode.GetReferencedAssemblies()) { // munged name will at least have App_GlobalResources in it's name if (name.Name.IndexOf("App_GlobalResources") != -1) { asmGlobal = Assembly.Load(name); break; } } if (asmGlobal != null) { string resName = String.Format( "Resources.{0}", this.GetType().BaseType.Name ); ResourceManager rm = new ResourceManager(resName, asmGlobal); lblFoo.Text = rm.GetString ("foo" ); } }
// #2 - Slightly better. If you know at least one resource name in the // App_GlobalResources assembly, at compile time, you can use it to give // you a type, which gives you the containing assembly. // At least we don't have to walk a list of references... { // can use this same name for all calls, just using to // get the type/assembly Type t = typeof(Resources._default); Assembly asm = t.Assembly; string resName = String.Format("Resources.{0}", this.GetType().BaseType.Name); ResourceManager rm = new ResourceManager(resName, asm); lblFoo.Text = rm.GetString( "foo" ); }
// #3 - Shortcut for #2 above. Just use the Resources dynamic // wrapper class, and it has a // ResourceManager property. { ResourceManager rm = Resources._default.ResourceManager; lblFoo.Text = rm.GetString( "foo" ); }
// #4 - Uses the undocumented support for Assembly.Load with the special // name "App_GlobalResources". Turns out, the ASP.NET 2.0 runtime wires // up the AppDomain.AssemblyResolve event, which // fires when an assembly fails to load based on the provided name. See // below, if you ask to load the "App_GlobalResources" assembly, which // doesn't exist (at least by that name), the AssemblyResolve() event // handler finds the correct global resources assembly, and loads it. // Someday in my spare time, I'll try to find the code where this happens. { // This will immediately find the desired assembly, // regardless of the name. Assembly asmGlobal = Assembly.Load( "App_GlobalResources" ); // just need short name, not full strong name string resName = String.Format( "Resources.{0}", this.GetType().BaseType.Name ); ResourceManager rm = new ResourceManager( resName, <Assembly.Load("App_GlobalResources") ); lblFoo.Text = rm.GetString( "foo" ); }
The Bottom Line
As you can see above, there are many ways to find the App_GlobalResources assembly at runtime if you need to. If you’re using the new ASP.NET 2.0 support for resources, you most likely don’t care what/where the assembly is, but to support legacy code, you might just have to find it.
I like #3 above, a quick 2 lines of code to create the proper ResourceManager object. Although it’s good to know you can directly find the App_GlobalResources assembly with the Assembly.Load() special handling.
Special note: you can also find the App_Code assembly with
Assembly.Load( "__code" );