Introduction
.NET framework provides a rather good support for writing multilingual applications. Localized resources are stored in separate resource files that are compiled into separate DLLs. When the application is started, resources for selected localized settings are loaded from the appropriate DLL.
To see how this actually works, you should sneak into the code that Visual Studio automatically generates. When a Localizable
property of a form is set to true
, Windows Forms Designer modifies the code inside InitializeComponent
method, adding an instance of ResourceManager
class and modifying properties' set methods; all localizable properties of controls are set calling ResourceManager
�s GetObject
or GetString
methods, making ResourceManager
responsible for loading appropriate localized resources. These methods load corresponding values for the language set by the current thread�s culture. It is worth noting that not only captions displayed are localized, but also other settings for individual controls, such as position, size and visibility. This is useful in cases when controls are positioned differently for different languages.
The simplest way to change the language of the form is to set application thread UI language before InitializeComponent
is called in the form�s constructor. Obviously, this requires the application to be restarted in order to change the language of the UI. Namely, InitializeComponent
not only loads resources, but also initializes all controls on the form. Calling it for the second time in the same application run would create a new set of controls that are appended to the existing collection. These new controls will not be visible since they are covered with originally created controls, except if their positions and/or sizes differ. Moreover, reloading resources will reset the content of controls like textboxes.
And what if you want to change the UI language without restarting the application and keeping the changes the user has made? One possible solution is provided in this article.
Background
The basic idea is to rescan all properties of the parent form and containing controls and set their localizable values to a newly selected culture. The simplest approach would be to obtain the list of all properties using reflection. However, this �unselective� approach may cause unintended changes. For example, we usually do not want to change Text
property of the TextBox
control, but want to update this property for the Label
control. Also, reloading Location
property for the parent form will very probably reposition it. Therefore, properties are reloaded selectively, the list of properties being hard-coded.
We start with the parent form, reloading its localizable properties and then recurring containing controls. The procedure is repeated for each control, recurring its containing controls if they exist:
private void ChangeFormLanguage(Form form) {
form.SuspendLayout();
Cursor.Current = Cursors.WaitCursor;
ResourceManager resources = new ResourceManager(form.GetType());
form.Text = resources.GetString("$this.Text", m_cultureInfo);
ReloadControlCommonProperties(form, resources);
ToolTip toolTip = GetToolTip(form);
RecurControls(form, resources, toolTip);
ScanNonControls(form, resources);
form.ResumeLayout();
}
ReloadControlCommonProperties
method reloads the hard-coded list of properties that are common to the majority of controls:
protected virtual void ReloadControlCommonProperties(Control control,
ResourceManager resources) {
SetProperty(control, "AccessibleDescription", resources);
SetProperty(control, "AccessibleName", resources);
SetProperty(control, "BackgroundImage", resources);
SetProperty(control, "Font", resources);
SetProperty(control, "ImeMode", resources);
SetProperty(control, "RightToLeft", resources);
SetProperty(control, "Size", resources);
if (!(control is System.Windows.Forms.Form)) {
SetProperty(control, "Anchor", resources);
SetProperty(control, "Dock", resources);
SetProperty(control, "Enabled", resources);
SetProperty(control, "Location", resources);
SetProperty(control, "TabIndex", resources);
SetProperty(control, "Visible", resources);
}
if (control is ScrollableControl) {
ReloadScrollableControlProperties((ScrollableControl)control, resources);
if (control is Form) {
ReloadFormProperties((Form)control, resources);
}
}
}
SetProperty
method reloads a value of the property for which a name is passed:
private void SetProperty(Control control, string propertyName,
ResourceManager resources) {
PropertyInfo propertyInfo = control.GetType().GetProperty(propertyName);
if (propertyInfo != null) {
string controlName = control.Name;
if (control is Form)
controlName = "$this";
object resObject = resources.GetObject(controlName + "." +
propertyName, m_cultureInfo);
if (resObject != null)
propertyInfo.SetValue(control, Convert.ChangeType(resObject,
propertyInfo.PropertyType), null);
}
}
First, it checks if a property with this name exists for the control. If GetProperty
method returns a non-null value (which means that the property does exist), ResourceManager
tries to get the value of the resource; if this value is successfully obtained, the corresponding property is changed.
The method utilizes reflection to make setting property generic. Casting to the appropriate type is implemented by static ChangeType
method of the Convert
class.
The alternative to this generic approach would be to replace each call to SetProperty
method with an appropriate code with hard-coded casts. Although such approach would result in faster code execution, it has some pitfalls. For example, there are properties of the same name in different classes that are of a different type: Appearance
property in TabControl
is of TabAppearance
enumeration type, while in CheckBox
it is of Appearance
enumeration type. Therefore, such approach would require more extensive type checking and would be more error-prone.
RecurControls
method scans all items in Controls
collection, reloading its properties and recurring for containing controls. Since any containing control may have a localized text associated to it, we have to pass the reference to the parent form�s ToolTip
object too.
private void RecurControls(Control parent,
ResourceManager resources, ToolTip toolTip) {
foreach (Control control in parent.Controls) {
ReloadControlCommonProperties(control, resources);
ReloadControlSpecificProperties(control, resources);
if (toolTip != null)
toolTip.SetToolTip(control, resources.GetString(control.Name +
".ToolTip", m_cultureInfo));
if (control is UserControl)
RecurUserControl((UserControl)control);
else {
ReloadTextForSelectedControls(control, resources);
ReloadListItems(control, resources);
if (control is TreeView)
ReloadTreeViewNodes((TreeView)control, resources);
if (control.Controls.Count > 0)
RecurControls(control, resources, toolTip);
}
}
}
Since containing control can be of any type, besides properties common to all controls, we also have to check properties specific to some controls, which is done in the ReloadControlSpecificProperties
method given below:
protected virtual void
ReloadControlSpecificProperties(System.Windows.Forms.Control control,
System.Resources.ResourceManager resources) {
SetProperty(control, "ImageIndex", resources);
SetProperty(control, "ToolTipText", resources);
SetProperty(control, "IntegralHeight", resources);
SetProperty(control, "ItemHeight", resources);
SetProperty(control, "MaxDropDownItems", resources);
SetProperty(control, "MaxLength", resources);
SetProperty(control, "Appearance", resources);
SetProperty(control, "CheckAlign", resources);
SetProperty(control, "FlatStyle", resources);
SetProperty(control, "ImageAlign", resources);
SetProperty(control, "Indent", resources);
SetProperty(control, "Multiline", resources);
SetProperty(control, "BulletIndent", resources);
SetProperty(control, "RightMargin", resources);
SetProperty(control, "ScrollBars", resources);
SetProperty(control, "WordWrap", resources);
SetProperty(control, "ZoomFactor", resources);
}
As can be noted, RecurControls
method calls itself recursively for any containing child.
If containing control is UserControl
, a new ResourceManager
has to be initialized for it to possibly load resources from the external DLL. Also, a ToolTip
object of the UserControl
must be obtained to pass reference to RecurControls
method:
private void RecurUserControl(UserControl userControl) {
ResourceManager resources = new ResourceManager(userControl.GetType());
ToolTip toolTip = GetToolTip(userControl);
RecurControls(userControl, resources, toolTip);
}
Some UI components, like MenuItem
s, StatusBarPanel
s and ColumnHeader
s in ListView
s, are not contained in Controls
collections. These components are direct members of the parent form, so we are accessing them using reflection (the code below is somewhat simplified):
protected virtual void ScanNonControls(Form form, ResourceManager resources) {
FieldInfo[] fieldInfo = form.GetType().GetFields(BindingFlags.NonPublic
| BindingFlags.Instance | BindingFlags.Public);
for (int i = 0; i < fieldInfo.Length; i++) {
object obj = fieldInfo[i].GetValue(form);
string fieldName = fieldInfo[i].Name;
if (obj is MenuItem) {
MenuItem menuItem = (MenuItem)obj;
menuItem.Enabled = (bool)(resources.GetObject(fieldName +
".Enabled", m_cultureInfo));
}
if (obj is StatusBarPanel) {
StatusBarPanel panel = (StatusBarPanel)obj;
panel.Alignment =
(HorizontalAlignment)(resources.GetObject(fieldName +
".Alignment", m_cultureInfo));
}
if (obj is ColumnHeader) {
ColumnHeader header = (ColumnHeader)obj;
header.Text = resources.GetString(fieldName + ".Text", m_cultureInfo);
header.TextAlign =
(HorizontalAlignment)(resources.GetObject(fieldName +
".TextAlign", m_cultureInfo));
header.Width = (int)(resources.GetObject(fieldName + ".Width", m_cultureInfo));
}
if (obj is ToolBarButton) {
ToolBarButton button = (ToolBarButton)obj;
button.Enabled = (bool)(resources.GetObject(fieldName +
".Enabled", m_cultureInfo));
}
}
}
Using the code
The procedure described is implemented as a FormLanguageSwitchSingleton
class with two public methods: ChangeCurrentThreadUICulture
and ChangeLanguage
, the latter having two overloaded implementations. Class is implemented in System.Globalization
namespace.
There are two possible scenarios for the use of FormLanguageSwitchSingleton
:
- Current thread�s culture is changed by calling
ChangeCurrentThreadUICulture
method first, followed by call to ChangeLanguage
method, like: CultureInfo newCulture = new CultureInfo("de");
FormLanguageSwitchSingleton.Instance.ChangeCurrentThreadUICulture(newCulture);
FormLanguageSwitchSingleton.Instance.ChangeLanguage(this);
- The overloaded version of
ChangeLanguage
method that accepts additional CultureInfo
argument is called only: CultureInfo newCulture = new CultureInfo("de");
FormLanguageSwitchSingleton.Instance.ChangeLanguage(this, newCulture);
In the second scenario, only forms currently opened will be �translated� to the culture provided; all subsequently opened forms will use the culture of the current application thread. The reader may observe the difference between the two scenarios in the TestMDIApp provided as a sample for download; checking/unchecking the option in the Change Language dialog:
The following methods of the singleton class are made virtual
to allow users to override them:
ReloadTextForSelectedControls
; current implementation does this for AxHost
, ButtonBase
, GroupBox
, Label
, ScrollableControl
, StatusBar
, TabControl
, ToolBar
control types.
ReloadControlCommonProperties
(implementation given above);
ReloadControlSpecificProperties
(c.f. above);
ScanNonControls
(c.f. above);
ReloadListItems
- reloads items in ComboBox
and ListBox
controls. Moreover, if items are not sorted, item(s) selection is kept.
FormLanguageSwitchSingleton
is compiled to a DLL. To use the class, just append the corresponding class library into the reference list. Examples of use are given in two test samples provided with project source.
Points of Interest
Items
in ListBox
, ComboBox
and DomainUpDown
, as well as TreeNodes
in TreeView
are reloaded by ReloadListBoxItems
, ReloadComboBoxItems
, ReloadUpDownItems
and ReloadTreeViewNodes
methods respectively. However, it is important to note that there are no references to individual items/nodes. They are loaded as nameless objects in AddRange
methods of Items
/Nodes
properties of the corresponding method, e.g.:
this.listBox.Items.AddRange(new object[]
{
resources.GetString("listBox.Items.Items"),
resources.GetString("listBox.Items.Items1"),
resources.GetString("listBox.Items.Items2")
}
);
As seen from the code above, the name of an item is created from the control name, followed by two �Items� strings, a numeric index being appended to the second string for all items except the first one. Therefore, these names have to be created dynamically:
private void ReloadItems(string controlName, IList list, int itemsNumber,
System.Resources.ResourceManager resources) {
string resourceName = controlName + ".Items.Items";
list.Clear();
list.Add(resources.GetString(resourceName, m_cultureInfo));
for (int i = 1; i < itemsNumber; i++)
list.Add(resources.GetString(resourceName + i, m_cultureInfo));
}
History
- ver. 1.0 - initial release (December 6, 2004).
- ver. 1.1 - some bug fixes, including those noticed by Gregory Bleiker and Piotr Sielski (March 21, 2005).
- ver. 1.2 - obtaining resources made safer through call of GetSafeValue method. It looks for selected language localized resource first; if not found, value for default language is returned. Also (hopefully) fixed "Ambiguous match found" error.