Introduction
Over the years, I have done mobility applications on Windows Mobile 2003 & 5.0, and I have done globalized/localized applications in ASP, ASP.NET, and Smart Clients. Recently, I came across my first requirement to bring these things together and write a localized, Smart Client application targeted at the Windows Mobile 5.0 platform (utilizing the Microsoft Compact Framework version 2.0). Like most things new on the Compact Framework, it starts out exciting and quickly turns frustrating, as you discover all new areas of the "full" .NET Framework that were sacrificed in order to reduce the footprint of the "compact" version. This article is about the challenges I faced and the solution I used.
Problem Space
In the full .NET Framework, you have the System.Resources.ResourceManager
class, which provides all the built-in functionality you need to easily load resources from external assemblies based on the System.Threading.Thread.CurrentUICulture
. The ResourceManager
is smart enough to find the right assembly, look for the resource and, if it doesn't find the resource, it will "auto-magically" look for your resource in the parent culture, until it either finds a match, or ends up back at the InvariantCulture
(which is basically the unknown or generic culture). Changing the resources used by your user interface is as easy as:
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
The first limitation with the Compact Framework is that the CurrentThread.CurrentUICulture
is not supported. This means that your application will use the culture of the OS (see this article on MSDN for more information). But what if you have a situation where all mobile devices are deployed set for US English (en-US), but some operators may want to use a particular application in Mexican Spanish (es-MX) or French Canadian (fr-CA)? You can't do it with the built-in ResourceManager
unless you use the OS' Control Panel to change the culture settings for the entire device.
Problem #1 is that we need a way to change the CultureInfo of just our application and have that be used by the ResourceManager.
Next, if you have done any globalization/localization work within Visual Studio 2005, you may have found that there is a built-in mechanism for making your forms "Localizable". I'm not going to try and get into a full dissertation on how this works. If you are interested, I would recommend a walk down "Google Lane". In short, you can use the IDE's form properties to view your form in different locales and change certain property values (i.e., Control.Text
) on a per-locale basis. "Under the covers," the IDE creates localized resource assemblies for each locale you support, and will "auto-magically" select the right set of resources at run-time (a-la the ResourceManager
described above).
The second limitation of the Compact Framework is that the mechanism which updates the UI is all tied to the CurrentUICulture
of the current thread, which as we saw above, is never going to change (unless it is done at the device level).
Problem #2 is that we need the ability to update our UI on-the-fly whenever a change is detected in the culture settings.
Design Goals
In addition to solving the above problem, there were certain design goals which I had for the final solution.
- It needed to work (obviously), but in doing so, I didn't want to reinvent the wheel. It needed to use as much of the existing infrastructure as possible. In particular, this meant the logic behind finding/selecting the resource files as well as the fall-back mechanism for missing assemblies or resources.
- It needed to be as "self-contained" and easy to use as possible. I didn't want the developers to get tied up in implementing the mechanism.
- It needed to support the addition of new resource assemblies in the future without any change to the source code or binaries.
The Solution
The solution that I deployed for this comes in two parts. The first is a custom ResourceManager
(implemented as a Singleton), which gives applications the ability to specify the CultureInfo
to be used when looking up resources. The second part is a LocalizedForm
(derived from System.Windows.Forms.Form
) which is designed to be used as the base class for any form that requires localization support. Please refer to the included source code and example project for full details on how this is implemented.
CompactFramework.Utilities.Localization.ResourceManager
The custom ResourceManager
gives your application a way to specify the Assembly
and CultureInfo
of the resources to use. Whenever the CultureInfo
is changed, an event is raised, letting subscribers to the event know that there has been a change. The ResourceManager
also exposes some overloaded helper methods which allow the LocalizedForm
to more easily retrieve resources.
CompactFramework.Utilities.Localization.LocalizedForm
The LocalizedForm
provides a common base-class that can be used to provide the functionality of listening for and responding to the ResourceManager.CultureChanged
event. It will loop through all of its child controls and menus and update their appropriate properties (i.e., Control.Text
, MenuItem.Text
, PictureBox.Image
, etc.) with the appropriate, localized resource.
Menus Suck!
Another of those areas where the Compact Framework is just plain annoying is in working with menu controls. The root of the problem is that the MenuItem
doesn't actually derive from the Control
base-class and, as a result, doesn't actually have a Name
property that you can read at run-time. I played briefly with a Reflection-based solution to this problem, but having recently worked through a Reflection-based hardware abstraction layer for a similar application, I knew that I was going to run into limitations of the Compact Framework as well as performance penalties.
Instead, I placed a little of the implementation burden on the developer in order to improve performance. As a result, the LocalizedForm
base class maintains a Dictionary<MenuItem, string>
which the developer fills as part of the constructor on each form, like so:
this.AddMenuToDictionary(this.menuItem1, "MainMenuLeft");
this.AddMenuToDictionary(this.menuItem2, "MainMenuRight");
The LocalizedForm class uses these strings in lieu of the Control.Name
property when looking for the Text value in the resource assembly.
Two Final "Gotchas"
There are two aspects of using localization that you must be careful to do correctly, or it will simply not work (and not throw any errors). If you don't locate and name your resource files correctly, it won't find them. If you don't name your resources correctly, it won't find them. In either case, the ResourceManager
will simply return its best match or (worst case) will not make any change to the UI.
Location and Name of Your Resource Files
There is one last thing that you have to be sure to do in order for the "built-in" functionality of the underlying ResourceManager
to function properly. When you add these resource files to your project, they must be located inside of a project directory named "Resources" and they must be named Resources.<culture-code>.resx (see the image at the top of this article for an example). If you do not do these two things, the ResourceManager
will not be able to locate your resource files and you will not get any localized updates.
Naming Your Resources
As you can see in the image at the top of the article, this infrastructure utilizes the name of the control, combined with the name of the parent form, to uniquely identify a resource. If you do not follow this naming convention in your resource files, a match will not be found and no change will be made to the UI (and no error thrown).
What's Remaining?
Right now, I handle the basic controls (TextBox
, Label
, Button
, CheckBox
, TabPage
, Form
, RadioButton
, and PictureBox
). Additional controls can certainly be added by updating the LocalizedForm.UpdateControls()
function.
Summary
I hope that this was interesting and/or useful to you. I enjoy doing them, I appreciate feedback (both positive and constructive criticism), and I like seeing what people do with these in their own solutions. If this was useful or interesting to you, please take a moment to rate the article. If you have questions or suggestions, feel free to post them below.