This holiday season, rather than warmth and cheer, I'm filled with cold, hard hatred -- for .resx files.
I've always kinda disliked them, but now that dislike has grown into full-blown hatred. Why now? I'm leading a team of C# developers, all of us using VS2003, to develop a commercial application that must be (a) built and tested against .NET v1.0, and (b) fully, easily localizable. Hence, I have spent countless hours researching the idiosyncrasies of ResX files, and how to work around them -- time that might otherwise have been spent with family or friends, this holiday season.
O ResX file, how do I hate thee? Let me count the ways...
1. I hate the way VS constantly rescrambles the ordering of your elements, which makes reviewing diffs to my source tree ever so painful.
2. I hate the way VS dumps tons of base64 binary goo inside you -- blithely ignoring the .ico and .bmp files that I carefully craft and maintain... you know, the actual files that are checked in and tracked in my source code control system? I hate the way that, when the graphic designer on my team gives me a new set of icons, I can't just check them in and wait for the new build. I have to open up each form in VS, and with dozens of clicks and drags, reassign each one manually.
3. I hate the way you are chock full of data that's tightly dependent on one specific version of the CLR -- once opened and modified by VS2003, I can forget about being able to build you into a down-level project, ever again... not even NAnt is capable of properly back-porting all the version tags in a ResX file to build against v1.0 (yet). Note to readers: some of these version references even appear in the base64 streams (see above)!
4. I hate the way you bloat the size of my compiled binaries, by giving each Form its own fully-serialized copy of my one (1) standard application icon (again, see above). Icons are small, but they add up -- and believe me, sharing the same background image for a splash screen and about box adds up much quicker.
5. I hate the way that some types of resources (ImageLists, for example) aren't even serialized in a fully self-describing way -- only the raw pixel data of the ImageList is serialized in the ResX file; one must parse the corresponding C#/VB source code in order to determine the width, height, and color depth of each image. This makes it very nearly impossible for a mere mortal to write a tool to extract the base64 image data out of you! You are a black hole, ResX -- woe to devs who insert images into you, and expect to be able to extract those resources back out again at a future date...
In general, I hate the fact that you exist at all, ResX -- except that I understand why you're needed: to provide a localized store of text and form-layouts.
However, you are most certainly not needed to provide a store for localized binary image data! Why didn't your designers provide a way for you to point to external image files -- the way RC files have always done, since the dawn of time in classic Win32 programming? What was wrong with that model? I never once heard anyone complain.
(Sigh...) No matter, the CLR is perfectly capable of loading embedded resources from primary or satellite assemblies, without ever needing to go through ResourceManager.
I've developed a checkin-policy for my dev team that forbids the inclusion of any binary data in you, o hated ResX file -- and I've developed a tool to enforce this policy! In my shop, all binary resource files (icons, bitmaps, etc) must be checked in as files, and included as embedded resources so they're picked up in the automated build.
Tip of the Day: if you create copies of your binary resource files
with names that include culture identifiers before the file extension (eg:
SplashBkgrnd.ja-JP.png and SplashBkgrnd.de-DE.png, alongside the
culture-neutral SplashBkgrnd.png) VS will do the right thing and produce
Japanese and German satellite assemblies containing the correspondingly named images! The image resources in these satellite
assemblies will retain the name of the neutral-resource, SplashBkgrnd.png, so
at run-time you can address the resource with a single name -- as long as you're grabbing
it from the right place (eg: look first in the current culture's satellite assembly,
if present, next the
primary assembly), you'll get the right image.
I like to include this small helper class in many of my WinForms projects, to facilitate the loading of embedded resources without ResourceManager. If you've followed my hate-filled rant to this point, then perhaps it'll be of some use to you, too.... Happy Holidays!
using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Reflection; using System.Resources; using System.Globalization; namespace Jitsu.Resources { internal class ResourceLoader { public ResourceLoader() { // Get handle to current assembly, and satellite assem // for current UI culture, if present. CultureInfo curruicult = System.Threading.Thread.CurrentThread.CurrentUICulture; currassem = Assembly.GetExecutingAssembly(); try { satellite = currassem.GetSatelliteAssembly(curruicult); } catch (System.IO.FileNotFoundException) { } } Assembly currassem = null; Assembly satellite = null; public Image TryLoadLocalizedImageResource(string name) { if (satellite == null) return Image.FromStream(currassem.GetManifestResourceStream(name)); try { return Image.FromStream(satellite.GetManifestResourceStream(name)); } catch (MissingManifestResourceException) { return Image.FromStream(currassem.GetManifestResourceStream(name)); } } public Icon TryLoadLocalizedIconResource(string name) { if (satellite == null) return new Icon(currassem.GetManifestResourceStream(name)); try { return new Icon(satellite.GetManifestResourceStream(name)); } catch (MissingManifestResourceException) { return new Icon(currassem.GetManifestResourceStream(name)); } } public void TryLoadLocalizedImageStream( ImageList imageList, string[] names, bool loadAsIcons) { imageList.Images.Clear(); foreach (string name in names) { if (loadAsIcons) { imageList.Images.Add(TryLoadLocalizedIconResource(name)); } else { imageList.Images.Add(TryLoadLocalizedImageResource(name)); } } } } }
