Proceed with Caution: Strongly Typed Resources Ahead

Visual Studio 2005 made using embedded resources a much more integrated experience than it was in Visual Studio 2003. But what I didn’t know is that there are some important things to be aware of when referencing these resources. A leaky abstraction strikes again!

The Problem

Even though it “seems” like a static resource, accessed via a static property, should create an instance once and only once, it’s not true. Consider adding and using an image resource – for example, a “lock” image that could be shown in the rows of a DataGridView to represent a boolean value:

image

One way to translate the data type of a bound object to an “expected” data type for a DataGridView column is to handle the CellFormatting event. You might be tempted to write the code to reference your strongly typed “lock” image resource like this:

private static Image _blankIcon = new Bitmap(16, 16);
private void transactionDataGridView_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
    if ((e.ColumnIndex == Locked.Index)
        && (e.DesiredType == typeof(Image))
        && (e.Value is bool))
    {
        bool locked = (bool)e.Value;
        e.Value = (locked) ? Properties.Resources.LockIcon : _blankIcon;
        e.FormattingApplied = true;
    }
}

But you could very easily encounter a Win32Exception indicating that you are out of memory. Why? The short answer is that every reference to the property ends up creating a new instance of the resource. With images, movies, or sounds, that can end up using a lot of memory. Notice that the code generated for accessing the resource doesn’t perform any caching, and in fact two instances of the returned object are logically the same, but are not the same reference. The following is true: “Properties.Resources.LockIcon != Properties.Resources.LockIcon”.

internal static System.Drawing.Bitmap LockIcon {
    get {
        object obj = ResourceManager.GetObject("LockIcon", resourceCulture);
        return ((System.Drawing.Bitmap)(obj));
    }
}

The Solution

It’s simple, really, but you just have to know to do it. The only real solution is to be intelligent about accessing these resources such that as few instances as possible are created. 99% of the time that means creating a global static variable that is initialized with the resource, and all your code accesses your static variable. This can be time consuming to maintain, if you have a lot of resources, but it’s definitely worth it. The change to the CellFormatting event handler is a simple one.

private static Image _lockIcon = Properties.Resources.LockIcon;
private static Image _blankIcon = new Bitmap(16, 16);
private void transactionDataGridView_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
    if ((e.ColumnIndex == Locked.Index)
        && (e.DesiredType == typeof(Image))
        && (e.Value is bool))
    {
        bool locked = (bool)e.Value;
        e.Value = (locked) ? _lockIcon : _blankIcon;
        e.FormattingApplied = true;
    }
}

One possibility that could increase maintainability is to write your own Visual Studio custom tool that generates the strongly-typed resource class (the .Designer.cs file). The custom tool could create the standard resource access code but also create a 2nd class that ends in “Cache”, for example, and manages a static reference to each resource – as mentioned in this MSDN forum post. An article on The Code Project gives an example implementation of a custom tool. I downloaded it with the intention of doing exactly what I mentioned, but there’s quite a bit of code there – and it’s a lot less trivial than I had hoped. In addition, everybody would have to install this custom tool on their own machine, and would have to manually run it on resource files. Non-optimal. Oh, and I’m lazy. :)

kick it on DotNetKicks.com

This entry was posted on Sunday, October 14th, 2007 at 4:23 pm and is filed under programming, tips and tricks. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

4 Responses to “Proceed with Caution: Strongly Typed Resources Ahead”

  1. mike Says:

    Yeah, I really thought those were static resources. That’s pretty bad and non-intuitive like you pointed out.

  2. leppie Says:

    The information you are providing is incorrect. The ResourceManager contains a ResourceSet which in turn contains a Hashtable. The Hashtable is populated when a ResourceSet is created. When GetObject is called on the ResourceManager, the already created value will be retrieved from the ResourceSet’s Hashtable, and will NOT be recreated.

    This can be clearly seen by looking at the classes in Reflector.

  3. aaron Says:

    Thanks leppie

    While what you say is true, your explanation still fails to explain why the following line of code (using my example above) asserts:
    Debug.Assert(Properties.Resources.LockIcon == Properties.Resources.LockIcon);

    The same references are not equal, therefore the same object is not being returned.

    Maybe if the resx file contained the base-64 encoded bits of the image it might return the same instance, but look at the resx file for the particular resource – here’s mine for the example:
    <data name=”LockIcon” type=”System.Resources.ResXFileRef, System.Windows.Forms”>
    <value>..\Resources\lock16.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
    </data>

    The ResXFileRef is just a container that uses a TypeConverter to return the appropriate type it points to.

    I don’t have enough time tonight to trace through exactly what’s going on in the ResourceManager (and below) to know what’s happening – it’ll be nice to get my hands on the source when they release it with .NET 3.5! :)

  4. leppie Says:

    You are right too! I investigated a bit further. It seems the RuntimeResourceSet (internal class) does not perform any caching, while it’s base class ResourceSet, does cache, which is what you would expect.

    Not sure if this a bug, perhaps :)