Contents
Introduction
A month ago, I started localizing a Windows application I was developing. There are tons of information regarding this topic on MSDN, so after a while, I could run my application with an English or German user interface. Unfortunately, the desired UI culture had to be assigned at the very beginning of my program. But, I was looking for a way to also change the culture of the user interface at runtime, e.g., after clicking on a specific menu item. After an unsuccessful search on MSDN and the rest of the internet, I decided to do it on my own.
Localization and assignment of a UI culture
At the beginning, I took a close look at how localization works when starting my application. And, it works pretty simple. Inside the form designer generated InitializeComponent
method, a ComponentResourceManager
instance is created, which provides access to (localized) resources considering the culture of the current thread, and a fallback mechanism if no resources are available for this culture (more details can be found on MSDN). Following this, its ApplyResources
method gets called for all the UI controls located on the form as well as the Form
instance itself to assign the corresponding resources. The calls for the UI controls thereby have a fixed pattern, with the control itself as the first and its name as the second parameter. The following example lines are copied from the InitializeComponent
method of the demo application:
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources =
new System.ComponentModel.ComponentResourceManager(typeof(Form1));
...
resources.ApplyResources(this.label1, "label1");
...
resources.ApplyResources(this, "$this");
...
}
To start the application with a specific UI culture for which resources are available, we have to add the following line before the InitializeComponent
method gets called (the example applies to German culture).
Thread.CurrentThread.CurrentUICulture = new CultureInfo("de");
Developing the change functionality
To switch between cultures, I created two menu items for English and German, and defined event handlers for their Click
events. Inside of these, I call a method named ApplyCulture
, which should encapsulate the change of the UI culture and pass an appropriate CultureInfo
object. Here, I'm going to describe the development of the ApplyCulture
method.
At first, the method assigns the passed CultureInfo
object to the CurrentUICulture
property of the current thread, because, as described above, this is used by ComponentResourceManager
to determine which resources should be used.
In a first attempt, I simply cleared the ControlCollection
of my form, and afterwards, called its InitializeComponent
method, so the user interface is constructed again considering the changed UI culture. This solution successfully changed the culture of the user interface, but I regarded it as being pretty rude. Also, there were some drawbacks, e.g., changes to the Enabled
property can get lost, or the initial size of a form gets reassigned.
private void ApplyCulture(CultureInfo culture)
{
Thread.CurrentThread.CurrentUICulture = culture;
this.Controls.Clear();
this.InitializeComponent();
}
To be more elegant, I decided to only call the ComponentResourceManager.ApplyResources
method for all the UI controls, as done inside the InitializeComponent
method. Because these calls have a fixed pattern and the Visual Studio designer defines a field for all of them, this can easily be done via Reflection. Information about all non-public, non-inherited instance fields of the form are retrieved and filtered for the UI controls (fields whose type is derived from Component
). Finally, the ApplyResources
method of a created ComponentResourceManager
is called for the filtered UI controls, and the field itself as well as its name are passed as parameters. Although this second solution changed the culture of the user interface more elegantly, there were still the same drawbacks, like the possible loss of changes to the Enabled
property.
private void ApplyCulture(CultureInfo culture)
{
Thread.CurrentThread.CurrentUICulture = culture;
ComponentResourceManager resources =
new ComponentResourceManager(this.GetType());
FieldInfo[] fieldInfos = this.GetType().GetFields(BindingFlags.Instance |
BindingFlags.DeclaredOnly | BindingFlags.NonPublic);
for (int index = 0; index < fieldInfos.Length; index++)
{
if (fieldInfos[index].FieldType.IsSubclassOf(typeof(Component)))
{
resources.ApplyResources(fieldInfos[index].GetValue(this),
fieldInfos[index].Name);
}
}
}
To avoid these drawbacks, the final solution replaced the calls to the ComponentResourceManager.ApplyResources
method. It only loads the localized text of the UI controls by calling the ComponentResourceManager.GetString
method and passing a string with the format "[name of the UI control].Text" (e.g., "label1.Text"). If the returned string isn't null
, it's assigned to the Text
property of the UI control. Because this solution requires the existence of a Text
property, the reflected fields of the form are now filtered by reflecting if they have such a property. If the form and the contained UI controls auto-size, the change of the user interface culture can cause a nervous resizing, because each assignment of localized text to a UI control can change the layout. To avoid this effect, the layout logic is halted for the form and all its fields that are derived from the Control
class before changing the culture. Afterwards, the layout logic is resumed and layout changes are performed. Thereby, again, reflection is used to call the methods Control.SuspendLayout
and Control.ResumeLayout
on all the fields that are derived from Control
.
private void ApplyCulture(CultureInfo culture)
{
Thread.CurrentThread.CurrentUICulture = culture;
ComponentResourceManager resources = new ComponentResourceManager(this.GetType());
FieldInfo[] fieldInfos = this.GetType().GetFields(BindingFlags.Instance |
BindingFlags.DeclaredOnly | BindingFlags.NonPublic);
this.SuspendLayout();
for (int index = 0; index < fieldInfos.Length; index++)
{
if (fieldInfos[index].FieldType.IsSubclassOf(typeof(Control)))
{
fieldInfos[index].FieldType.InvokeMember("SuspendLayout",
BindingFlags.InvokeMethod, null,
fieldInfos[index].GetValue(this), null);
}
}
String text = resources.GetString("$this.Text");
if (text != null)
this.Text = text;
for (int index = 0; index < fieldInfos.Length; index++)
{
if (fieldInfos[index].FieldType.GetProperty("Text", typeof(String)) != null)
{
text = resources.GetString(fieldInfos[index].Name + ".Text");
if (text != null)
{
fieldInfos[index].FieldType.InvokeMember("Text",
BindingFlags.SetProperty, null,
fieldInfos[index].GetValue(this), new object[] { text });
}
}
}
for (int index = 0; index < fieldInfos.Length; index++)
{
if (fieldInfos[index].FieldType.IsSubclassOf(typeof(Control)))
{
fieldInfos[index].FieldType.InvokeMember("ResumeLayout",
BindingFlags.InvokeMethod, null,
fieldInfos[index].GetValue(this), new object[] { false });
}
}
this.ResumeLayout(false);
this.PerformLayout();
}
Developing the UICultureChanger component
The feedback on the first version of this article showed me that I'm not the only one who needs the presented functionality, but also that it's not yet sufficient for everyone. There are needs to change the UI culture of multiple forms, and to apply not only localized text but also sizes or locations. So, I decided to enhance the change functionality and put it into a component to improve its usage.
-
Customize which localized resources are applied
The UICultureChanger
component supports the application of localized text, sizes, locations, tooltips, and help contents. Which of these resources are applied can be customized through the following properties:
Property |
Default |
Description |
ApplyText |
true |
Indicates whether localized Text values are applied when changing the UI culture. |
ApplySize |
false |
Indicates whether localized Size values are applied when changing the UI culture. If sizes are applied, the Anchor settings will be taken into account in the following way:
- If a UI control is bound to the left and right container edges, its width will be preserved.
- If a UI control is bound to the top and bottom container edges, its height will be preserved.
|
ApplyLocation |
false |
Indicates whether localized Location values are applied when changing the UI culture. If locations are applied, the Anchor settings will be taken into account in the following way:
- If a UI control is bound to the right but not the left container edge, its X-coordinate will be preserved.
- If a UI control is bound to the bottom but not the top container edge, its y-coordinate will be preserved.
|
ApplyRightToLeft |
false |
Indicates whether localized RightToLeft values are applied when changing the UI culture. |
ApplyRightToLeftLayout |
false |
Indicates whether localized RightToLeftLayout values are applied when changing the UI culture. RightToLeftLayout properties are not available in .NET Framework versions prior 2.0, so this property isn't available in these versions too. |
ApplyToolTip |
false |
Indicates whether localized tooltips and ToolTipText values are applied when changing the UI culture. ToolTipText properties are not available in .NET Framework versions prior 2.0. |
ApplyHelp |
false |
Indicates whether localized help contents are applied when changing the UI culture. |
PreserveFormSize |
true |
Indicates whether the Size values of forms are preserved when changing the UI culture. Has no effect unless ApplySize is true . |
PreserveFormLocation |
true |
Indicates whether the Location values of forms are preserved when changing the UI culture. Has no effect unless ApplyLocation is true . |
In case you didn't localize everything (e.g., just text and sizes), only set the appropriate properties to true
to optimize the performance of changes to the UI culture. The value of all properties can easily be changed in the form designer.
-
Multiple form support
The UICultureChanger
component supports changing the UI culture of multiple forms, which is extremely useful, for example, in MDI applications. The component contains a List
to collect all forms whose UI culture should be changed, and exposes the AddForm
and RemoveForm
methods to allow "access" to the collection. AddForm
not only adds the passed form to the collection, but also registers the component to the FormClosed
event, so after being closed, the form can automatically be removed from the collection. This way, we aren't required to explicitly call the RemoveForm
method unless there is another reason to exclude a form from the UI culture changes than being closed. The form which hosts the UICultureChanger
component is automatically added to the collection inside the form designer generated InitializeComponent
method. To achieve the insertion of the necessary method call, I've had to write a custom CodeDomSerializer
, which is defined as a nested type inside the component type. This was a bit tricky as I've been new to this, but also very interesting. I'm not going into details here, because it would be a bit off topic, but if you're interested, take a look at the commented source code.
-
Enhanced change functionality
The basic concept of the change functionality is still the same as in the final version presented above. The UICultureChanger
component exposes an ApplyCulture
method that takes a CultureInfo
object, and at first, assigns this to the the CurrentUICulture
property of the current thread. Afterwards, it iterates over the form collection, and passes each form to the new ApplyCultureToForm
method that processes the application of localized resources.
To allow an equal treatment of the form and its fields during this process, the method initially creates a List
of custom ChangeInfo
objects which encapsulate all the information needed to apply localized resources to either the form or one of its fields. These information are the field names, or "$this" in the case of the form to retrieve localized resources, and an object reference and a Type
object to apply the retrieved values via Reflection.
Afterwards, Reflection is used to call SuspendLayout
on all derivatives of the Control
type, and localized text, sizes, and locations are applied. Besides the usage of the ChangeInfo
's collection, there are only some other minor changes to this part, so I've excluded it from the code snippet below. The application of localized sizes and locations is pretty much the same as the application of text, which was presented above.
In contrast, the application of tooltips and help contents requires some extra work, as these aren't properties of the form and its UI controls, but are passed to a ToolTip
or HelpProvider
component. For both, the procedure is very similar, so the code snippet below only shows the application of help contents. At first, the ChangeInfo
's collection gets parsed for a HelpProvider
component. If found, it gets extracted from the collection, and its own resources are applied by lazily calling ApplyResources
. Afterwards, the method iterates over the collection, filters for derivatives of the Control
type as only these can have help content, and retrieves the localized content as usual. But, instead of setting a property via Reflection, now, a method of the found HelpProvider
is called, and the retrieved content as well as the current Control
derivative are passed in. This gets repeated for HelpKeyword
, HelpNavigator
, HelpString
, and ShowHelp
, whereby the ComponentResourceManager.GetObject
method has to be used to retrieve HelpNavigator
and ShowHelp
values, and the returned object has to be checked for the correct type.
Finally, ResumeLayout
is called on all derivatives of the Control
type, and Form.PerformLayout
is executed, so layout changes due to assignment of localized resources are performed.
private void ApplyCultureToForm(Form form)
{
ComponentResourceManager resources =
new ComponentResourceManager(form.GetType());
FieldInfo[] fields = form.GetType().GetFields(BindingFlags.Instance |
BindingFlags.DeclaredOnly | BindingFlags.NonPublic);
List<ChangeInfo> changeInfos = new List<ChangeInfo>(fields.Length + 1);
changeInfos.Add(new ChangeInfo("$this", form, form.GetType()));
for (int index = 0; index < fields.Length; index++)
{
changeInfos.Add(new ChangeInfo(fields[index].Name,
fields[index].GetValue(form),
fields[index].FieldType));
}
changeInfos.TrimExcess();
...
if (this.applyHelp)
{
HelpProvider helpProvider = null;
for (int index = 1; index < changeInfos.Count; index++)
{
if (changeInfos[index].Type == typeof(HelpProvider))
{
helpProvider = (HelpProvider)changeInfos[index].Value;
resources.ApplyResources(helpProvider, changeInfos[index].Name);
changeInfos.Remove(changeInfos[index]);
break;
}
}
if (helpProvider != null)
{
String text;
object helpNavigator, showHelp;
for (int index = 0; index < changeInfos.Count; index++)
{
if (changeInfos[index].Type.IsSubclassOf(typeof(Control)))
{
text = resources.GetString(changeInfos[index].Name +
".HelpKeyword");
if (text != null)
{
helpProvider.SetHelpKeyword(
(Control)changeInfos[index].Value, text);
}
helpNavigator = resources.GetObject(changeInfos[index].Name +
".HelpNavigator");
if (helpNavigator != null && helpNavigator.GetType() ==
typeof(HelpNavigator))
{
helpProvider.SetHelpNavigator(
(Control)changeInfos[index].Value,
(HelpNavigator)helpNavigator);
}
text = resources.GetString(changeInfos[index].Name +
".HelpString");
if (text != null)
{
helpProvider.SetHelpString(
(Control)changeInfos[index].Value, text);
}
showHelp = resources.GetObject(changeInfos[index].Name +
".ShowHelp");
if (showHelp != null && showHelp.GetType() == typeof(bool))
{
helpProvider.SetShowHelp(
(Control)changeInfos[index].Value, (bool)showHelp);
}
}
}
}
}
...
}
Compile for .NET Framework versions prior 2.0
The UICultureChanger
component employs some useful features that are new in the .NET Framework version 2.0, like Generics or the Form.FormClosed
event, and therefore, it couldn't be compiled for prior framework versions. Beginning with version 2.1 of the component, this restriction is by-passed through the use of preprocessor directives and conditional compilation.
At the beginning of the code file, the definition of the symbol Prior2
is added. Wherever new features of the .NET Framework version 2.0 are used, preprocessor directives are added, which exclude this code from compilation if the Prior2
symbol is defined, and instead include corresponding constructs that are supported by prior .NET Framework versions. By default, the definition of the Prior2
symbol is commented out, so the UICultureChanger
component employs the new features. Simply remove the comment delimiters, or define the Prior2
symbol using project settings, if you want to compile for .NET Framework versions prior 2.0.
The following example shows the declaration of the collection that will take the forms whose UI culture should be changed and whose type is either the new generic List
or the well-known ArrayList
.
#if Prior2
private ArrayList forms;
#else
private List<Form> forms;
#endif
Demo application
The demo application is a simple MDI application that shows the capabilities of the UICultureChanger
component. The parent form hosts an instance of the component, and allows you to customize it at runtime through the UICultureChanger menu entry. The text of menu items is localized, and the parent form contains a HelpProvider
component that opens a localized topic, if no child form is open and F1 gets pressed. Furthermore, the RightToLeft
property is localized, and the parent form is sizable, so you can see the effects of the component's PreserveFormSize
property.
The child forms contain some labels and buttons whose text, sizes, locations, and/or tooltips are localized as well as the necessary ToolTip
component. To show the effects of the PreserveFormLocation
property, the child forms have a manual start position, which gets reapplied if PreserveFormLocation
is false
. Furthermore, the RightToLeft
and also the RightToLeftLayout
properties are localized.
Using the component
- Localize your application, which is described by MSDN. To have UI controls with dynamic content that isn't affected by changes to the user interface culture, delete their text in the form designer. (The demo application, for example, contains a label that shows the application startup time and doesn't get changed.)
- Add the
UICultureChanger
component to your project using one of the following possibilities:
- In the Solution Explorer, right-click on the References entry of your project, select the "Add Reference..." option, browse for UICultureChanger.dll, and click OK.
- In the Solution Explorer, right-click on the Solution entry, select the "Add Existing Project..." option, browse for the UICultureChanger.csproj file, and click OK.
- In the Solution Explorer, right-click on the entry of your project, select the "Add Existing Item..." option, browse for the UICultureChanger.cs file, and click OK.
- In the Solution Explorer, right-click on the References entry of your project, select the "Add Reference..." option, select System.Design.dll, and click OK.
Build your project.
- In the form designer, drag the
UICultureChanger
component from the Toolbox over to your main form and customize the component.
- Provide a way to choose between different cultures (e.g., with menu items, as done in the demo application), and call the
ApplyCulture
method on the generated member variable of the UICultureChanger
component.
- If you want to change the UI culture of multiple forms (e.g., in an MDI application), for each form, call the
AddForm
method on the generated member variable of the UICultureChanger
component.
Version history
2.4 |
- Application of localized
Size and Location values considers Anchor settings.
|
2.3 |
- Fixed bug that the text of a
RichTextBox was changed although its Text property is set to an empty string in the form designer.
|
2.2 |
- Added support for application of localized
ToolTipText values.
|
2.1 |
- Added support for application of localized
RightToLeft and RightToLeftLayout values.
- Added option to compile for .NET Framework versions prior 2.0.
|
2.0 |
- Implemented as a component.
- Supports changing the UI culture of multiple forms.
- Supports application of localized tooltips, help contents,
Text , Size , and Location values.
|
1.0 |
|
Copyright notice
UICultureChanger is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.