So I was screwing around today with strongly typed resources in ASP.NET. Another cool side-effect of using Web Application Projects as opposed to stock ASP.NET projects is that WAP uses strongly typed global resources. If you create a global resource file, WAP automatically creates a strongly typed mapping class which gets compiled into the App_GlobalResource.dll at compile time.
Strongly typed resources don’t work with stock projects although it seems that this should be easily doable. In fact, I thought I could rig this myself by using some tools the StronglyTypedResourceBuilder class to generate the class for me. It’s fairly straight forward to do this and it works with any type of resource manager (I’m using a custom database driven resource set here):
/// <summary>
/// Generates a strongly typed assembly from the resources
/// </summary>
/// <param name="ResourceSetName"></param>
/// <param name="Namespace"></param>
/// <param name="Classname"></param>
/// <param name="FileName"></param>
/// <returns></returns>
public bool CreateStronglyTypedResource(string ResourceSetName,string Namespace,
string Classname, string FileName)
{
try
{
//wwDbResourceDataManager Data = new wwDbResourceDataManager();
//IDictionary ResourceSet = Data.GetResourceSet("", ResourceSetName);
// *** Use the custom ResourceManage to retrieve a ResourceSet
wwDbResourceManager Man = new wwDbResourceManager(ResourceSetName);
ResourceSet rs = Man.GetResourceSet(CultureInfo.InvariantCulture, false, false);
IDictionaryEnumerator Enumerator = rs.GetEnumerator();
// *** We have to turn into a concret Dictionary
Dictionary<string, object> Resources = new Dictionary<string, object>();
while (Enumerator.MoveNext())
{
DictionaryEntry Item = (DictionaryEntry) Enumerator.Current;
Resources.Add(Item.Key as string, Item.Value);
}
string[] UnmatchedElements;
CodeDomProvider CodeProvider = null;
string FileExtension = Path.GetExtension(FileName).TrimStart('.').ToLower();
if (FileExtension == "cs")
CodeProvider = new Microsoft.CSharp.CSharpCodeProvider();
else if(FileExtension == "vb")
CodeProvider = new Microsoft.VisualBasic.VBCodeProvider();
CodeCompileUnit Code = StronglyTypedResourceBuilder.Create(Resources,
ResourceSetName, Namespace, CodeProvider, false, out UnmatchedElements);
StreamWriter writer = new StreamWriter(FileName);
CodeProvider.GenerateCodeFromCompileUnit(Code, writer, new CodeGeneratorOptions());
writer.Close();
}
catch (Exception ex)
{
this.ErrorMessage = ex.Message;
return false;
}
return true;
}
This can be called with:
Exporter.CreateStronglyTypedResource("resources",
"AppResources","Resources",
Server.MapPath("App_Code/Resources/Resources.cs"));
I’m generating the resources into APP_CODE into a Resources directory and that works just fine. Unfortunately the generated code doesn’t work because of the funky project handling in stock projects.
There are a couple of problems actually with the key one being that the class generated assumes the resources and the class exist in the same assembly, which is not the case in stock projects. In all ASP.NET projects resources get generated into App_GlobalResources and the class is generated into App_Code so the following instantiation code for the ResourceManager doesn’t work:
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AppResources.resources", typeof(resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
You can manually fix the code by using:
System.Resources.ResourceManager temp =
new System.Resources.ResourceManager("resources.resources",
Assembly.Load("App_GlobalResources"));
Notice the funky ResourceSet naming syntax. The actual ‘resource’ path to the App_GlobalResources is resources.resources.resources (of which only the last resources is mine! <s>).
At this point I can also use my custom resource manager:
System.Resources.ResourceManager temp = new wwDbResourceManager("resources");
And that works.
It seems this coulda been built into the ASP.NET system automatically. In fact, it would have been real nice if this could have worked in conjunction with the ResourceProvider to provide access to the internal ResourceManager, but alas, ASP.NET doesn’t even require use of a ResourceManager at all – you can get away with just creating a ResourceProvider.
Incidentally in WAP too, the designer generated strongly typed resources will not work with a custom ResourceProvider because the resources are hardcoded to use the stock ResourceManager which uses ResX files.
I suspect the reason that this hasn’t been implemented in ASP.NET forms is that it’s not very efficient. One of the problems is that the Page already has its own ResourceManagers for local and global resources. These strongly typed resources create another copy of the resource manager for another resourceset and that effectively duplicates the resources cached in memory.
I’m thinking that the solution to this problem is to build a custom generator that has the ability to override the ResourceManager used. <shrug>
Incidentally I’ve been thinking that ASP.NET really makes it tough to get at the ResourceProvider from within an Application. It sure would be nice if we could at runtime get access to the right Provider so we don’t have to guess at which one is running. I haven’t figured out a way to do this short of checking in web.config manually.