Introduction
The .NET Framework provides reasonably good support for writing multi-lingual Windows Forms applications. Once you have set the
Localizable
property of a form to
true
, then the Visual Studio designer will automatically generate code in the
InitializeComponent
method of the form that loads the localizable properties of the form and its controls from compiled resources. Using the Visual Studio designer, you can also create resources for other languages - simply set the
Language
property of the form and modify the language specific properties of the form and controls. Visual Studio will save these changes into a separate resource file and compile them into a satellite assembly for the language when you build your application.
At runtime when your form is created (and
InitializeComponent
is called), the .NET Framework selects the resources to load based on the current value of the
System.Threading.Thread.CurrentThread.CurrentUICulture
property. If you change this property before your form is created, then the resources for the corresponding culture will be loaded. This means that if you want to change the user interface language once your application is running, you must close any open forms and recreate them (or restart the entire application) to force the resources to be reloaded for the new UI Culture. This is somewhat clumsy and means losing any transient application state stored in the forms. This article provides a
CultureManager
component that allows the localizable resources for open forms and control to be dynamically reloaded using a new UI Culture.
Background
The articles
Instantly Changing Language in the Form and
UICultureChanger Component also provide solutions to this issue. The approach taken in this article differs from these previous articles in the following ways:
- The above articles only handle a subset of standard properties, e.g.
Text
, Location
, Size
, etc. By contrast, the solution presented here can load the resources for any localizable property of a component/control (localizable properties are those marked with the LocalizableAttribute
).
- The
CultureManager
component described here provides a hook (via the UICultureChanged
event) that allows you to execute code after a forms resources have been reloaded. This can be useful, for instance, when the text displayed in a control is generated programmatically - but is still culture dependant.
- The
CultureManager
component allows you to change the UI Culture of all open forms in the application. Alternatively you can change the UICulture
for an individual form allowing you to have two (or more) open forms with different UI Cultures.
- The
CultureManager
component works with forms and controls developed using VB.NET
- The source code for this article is provided under the The Code Project Open License (CPOL). The second article above uses the GNU Lesser General Public License - this prevents inclusion of its source code directly into commercial applications. See the following CodeProject article on Licenses for more information.
How it Works
This section provides an outline of how the
CultureManager
component internal implementation works. If you just want to use the component "as is", you can skip down to
Using the Component.
Basic Architecture
The
CultureManager
is implemented as a component that you place on each form in your application that you want to change culture dynamically. The
CultureManager
component attaches a handler to the
static CultureManager.ApplicationUICultureChanged
event so that it receives notification when the
CultureManager.ApplicationUICulture static
property is changed. The component then uses .NET Reflection to locate localizable resources for the form and reloads these resources using the new UI Culture. This event architecture allows the UI Culture of all open forms in the application to be changed by simply setting the
CultureManager.ApplicationUICulture static
property. Alternatively you can set the
UICulture
of an individual form.
Applying Resources
The
CultureManager.ApplyResources
method (shown below) implements the core logic used to reload the localizable resources for a component.
Collapse | Copy Code
protected virtual void ApplyResources(Type componentType,
Component instance,
CultureInfo culture)
{
System.IO.Stream resourceStream
= componentType.Assembly.GetManifestResourceStream(componentType.FullName
+ ".resources");
if (resourceStream == null) return;
Type parentType = componentType.BaseType;
if (parentType != null)
{
ApplyResources(parentType, instance, culture);
}
ComponentResourceManager resourceManager
= new ComponentResourceManager(componentType);
SortedList<string object,> resources = new SortedList<string object,>();
LoadResources(resourceManager, culture, resources);
Dictionary<string Component,> components = new Dictionary<string Component,>();
Dictionary extenderProviders
= new Dictionary();
bool isVB = IsVBAssembly(componentType.Assembly);
components["$this"] = instance;
FieldInfo[] fields = componentType.GetFields(BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public);
foreach (FieldInfo field in fields)
{
string fieldName = field.Name;
if (isVB)
{
fieldName = fieldName.Substring(1, fieldName.Length - 1);
}
string resourceName = ">>" + fieldName + ".Name";
if (resources.ContainsKey(resourceName))
{
Component childComponent = field.GetValue(instance) as Component;
if (childComponent != null)
{
components[fieldName] = childComponent;
ApplyResources(childComponent.GetType(), childComponent, culture);
if (childComponent is IExtenderProvider)
{
extenderProviders[childComponent.GetType()]
= childComponent as IExtenderProvider;
}
}
}
}
foreach (KeyValuePair<string object,> pair in resources)
{
string resourceName = pair.Key;
object resourceValue = pair.Value;
string[] resourceNameParts = resourceName.Split('.');
string componentName = resourceNameParts[0];
string propertyName = resourceNameParts[1];
if (componentName.StartsWith(">>")) continue;
if (IsExcluded(componentName, propertyName)) continue;
Component component = null;
if (!components.TryGetValue(componentName, out component)) continue;
Control control = component as Control;
if (control != null)
{
switch (propertyName)
{
case "AutoScaleDimensions":
SetAutoScaleDimensions(control as ContainerControl, (SizeF)resourceValue);
continue;
case "Size":
SetControlSize(control, (Size)resourceValue);
continue;
case "Location":
SetControlLocation(control, (Point)resourceValue);
continue;
case "Padding":
case "Margin":
resourceValue = AutoScalePadding((Padding)resourceValue);
break;
case "ClientSize":
if (control is Form && PreserveFormSize) continue;
resourceValue = AutoScaleSize((Size)resourceValue);
break;
}
}
PropertyDescriptor pd
= TypeDescriptor.GetProperties(component).Find(propertyName, false);
if (((pd != null) && !pd.IsReadOnly) &&
((resourceValue == null) || pd.PropertyType.IsInstanceOfType(resourceValue)))
{
pd.SetValue(component, resourceValue);
}
else
{
if (control != null)
{
ApplyExtenderResource(extenderProviders,
control, propertyName, resourceValue);
}
}
}
}
The
ApplyResources
method first checks if there are localizable resources associated with the component type. If there aren't, there is nothing more to do. If there are, then the method recursively calls itself to first apply any localized resources associated with its base type. The base type resources must be applied before any localized resources defined by the derived class.
Next we call
LoadResources
to load the localized resources for this component type into a
SortedList
. This gives us a list of all the localized resources used by the component and enables us to quickly locate resources by name. We use reflection to locate the member variables of the component that may have localized resources and build a lookup table to enable us to quickly locate a child component object given its name. Note that the naming convention used by the VB.NET designer for component member variables requires some special handling for VB.NET components.
Finally we iterate through the list of localizable resources and use reflection to set the properties of the components with some special case handling as discussed below.
Some Special Cases
Setting the
Location
or
Size
property for a component that is anchored or docked can cause unexpected behaviour. The
SetControlSize
and
SetControlLocation
methods contain logic that checks the
Anchor
and
Dock
properties and only updates the components of the location or size that are not controlled by anchoring or docking. The other issue the code addresses is when the screen resolution is different to that used at design time (because the user is using DPI scaling). Normally Windows Forms automatically scales the sizes and locations of controls and forms after they are first created to account for this. Since the sizes and locations in the resources are defined in the design time resolution we need to scale them to match the current screen resolution. The
SetAutoScaleDimensions
method sets the
AutoScaleFactor
based the design time resolution dimensions and the
CurrentAutoScaleDimensions
. Subsequent size, location and padding resources are scaled by this value
Extender Properties
Some localized resources do not correspond to simple properties on the constituent components. Extender components (for example
ToolTips
) provide extended design time properties for other controls on the form. The extender component handles code (and resource) serialization of these properties. Unfortunately there is no reflection mechanism to discover the methods on the extender component that we need to call - or the resource naming convention used for the extender properties. If there is no standard property corresponding to a localized resource, then the
ApplyExtenderResource
method (see below) is called to process the resource using some special case logic.
Collapse | Copy Code
protected virtual void ApplyExtenderResource
(Dictionary extenderProviders,
Control control, string propertyName, object value)
{
IExtenderProvider extender = null;
if (propertyName == "ToolTip")
{
if (extenderProviders.TryGetValue(typeof(ToolTip), out extender))
{
(extender as ToolTip).SetToolTip(control, value as string);
}
}
...
}
We pass the
ApplyExtenderResource
method a lookup table of the components that implement the
IExtenderProvider
interface (indexed by type). This allows the method to locate the extender component associated with the given property and call the appropriate method to set the localized property. The base implementation of the
ApplyExtenderResource
method implements logic for
ToolTip
,
ErrorProvider
and
HelpProvider
extender components. It can be overridden to add support for other custom extender components.
Using the Component
Once you have downloaded the source code and built it, you can add the
CultureManager
component to your Visual Studio toolbox. If you are using the demo solution, then the component should already appear under the "Infralution.Localization Components" tab. To use the component in another project, right click on the toolbox and select "Choose Items..", then select the "Browse" button and locate the
Infralution.Localization.dll assembly that you built earlier.
Place an instance of the component on each form in your application that you want to change culture dynamically. Note that this typically does not need to be all forms in your application, only those that are opened non-modally. If a form is always opened as a modal dialog (using
ShowDialog
) then there is no way for a user to change UI Culture of the application while the form is open.
Add a menu (or other mechanism) to set the
CultureManager.ApplicationUICulture
of the application. This sets the
System.Threading.Thread.CurrentThread.CurrentUICulture
and reloads the localizable resources for each open form that has a
CultureManager
component.
The component also provides some properties and events that you can use for additional control over the UI Culture change process:
ExcludeProperties
- This allows you to specify a list of properties that you don't wish to be reloaded from resources when the UI Culture is changed. For instance, if you add "Enabled
" to this list then the Enabled
property of any controls will not be reloaded from resources. Note that generally you don't need to use this mechanism because Visual Studio will only serialize resources if they have non-default values. So the Enabled
property for a control would not be stored in the form resources unless you had set it to false
in the Visual Studio designer.
PreserveFormSize
- If set to true
then the forms size will not be reloaded from resources.
PreserveFormLocation
- If set to true
then the forms location will not be reloaded from resources.
UICultureChanged
- This event is fired when UI Culture for the form is changed (after the resources have been reloaded). This allows you to execute code to update programmatically generated text.