Click here to Skip to main content
15,867,330 members
Articles / Desktop Programming / Windows Forms
Article

UICultureChanger component

Rate me:
Please Sign up or sign in to vote.
4.67/5 (35 votes)
20 Aug 2006LGPL313 min read 161.9K   2.1K   113   51
Presents a lightweight component that enables you to easily change the culture of your user interface at runtime.

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:

C#
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).

C#
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.

C#
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.

C#
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.

C#
private void ApplyCulture(CultureInfo culture)
{
    // Applies culture to current Thread.

    Thread.CurrentThread.CurrentUICulture = culture;

    // Create a resource manager for this Form
    // and determine its fields via reflection.

    ComponentResourceManager resources = new ComponentResourceManager(this.GetType());
    FieldInfo[] fieldInfos = this.GetType().GetFields(BindingFlags.Instance |
        BindingFlags.DeclaredOnly | BindingFlags.NonPublic);
    
    // Call SuspendLayout for Form and all fields derived from Control, so assignment of 
    // localized text doesn't change layout immediately.

    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);
        }
    }
    
    // If available, assign localized text to Form and fields with Text property.

    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 });
            }
        }
    }
    
    // Call ResumeLayout for Form and all fields
    // derived from Control to resume layout logic.
    // Call PerformLayout, so layout changes due
    // to assignment of localized text are performed.

    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:

    PropertyDefaultDescription
    ApplyTexttrueIndicates whether localized Text values are applied when changing the UI culture.
    ApplySizefalseIndicates 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.
    ApplyLocationfalseIndicates 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.
    ApplyRightToLeftfalseIndicates whether localized RightToLeft values are applied when changing the UI culture.
    ApplyRightToLeftLayoutfalseIndicates 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.
    ApplyToolTipfalseIndicates 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.
    ApplyHelpfalseIndicates whether localized help contents are applied when changing the UI culture.
    PreserveFormSizetrueIndicates whether the Size values of forms are preserved when changing the UI culture. Has no effect unless ApplySize is true.
    PreserveFormLocationtrueIndicates 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.

    C#
    private void ApplyCultureToForm(Form form)
    {
        // Create a resource manager for this Form
        // and determine its fields via reflection.
        // Create and fill a collection, containing
        // all infos needed to apply localized resources.
    
        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)
        {
            // Search for a HelpProvider component in the current form.
    
            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)
            {
                // If available, assign localized help to Form and fields.
    
                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.

    C#
    #if Prior2
        private ArrayList forms;
    #else
        private List<Form> forms;
    #endif

Demo application

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

  1. 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.)
  2. 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.

  3. In the form designer, drag the UICultureChanger component from the Toolbox over to your main form and customize the component.
  4. 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.
  5. 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
  • Initial release.

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.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Software Developer
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHelpProvider Problem Pin
sodus873-Jul-11 9:00
sodus873-Jul-11 9:00 
AnswerRe: HelpProvider Problem Pin
ekekakos22-Nov-16 9:26
professionalekekakos22-Nov-16 9:26 
GeneralA couple problems and their solutions, and an unsolved problem with TreeView, and a suggestion Pin
mjmeans16-Mar-09 17:53
mjmeans16-Mar-09 17:53 
GeneralGreat tip Pin
Mohsen Afshin1-Mar-09 21:14
professionalMohsen Afshin1-Mar-09 21:14 
QuestionProblems with the DLL Pin
MaxxTc6-Jul-08 18:34
MaxxTc6-Jul-08 18:34 
AnswerRe: Problems with the DLL Pin
Stefan Troschuetz25-Jul-08 7:59
Stefan Troschuetz25-Jul-08 7:59 
GeneralUserControl support Pin
eminsenay26-Feb-08 22:38
eminsenay26-Feb-08 22:38 
GeneralUsercontrols Pin
Member 431678810-Jan-08 4:33
Member 431678810-Jan-08 4:33 
GeneralDoesn't update DataGridView-Control Pin
intripoon12-Dec-06 2:36
intripoon12-Dec-06 2:36 
GeneralRe: Doesn't update DataGridView-Control Pin
Stefan Troschuetz12-Dec-06 9:39
Stefan Troschuetz12-Dec-06 9:39 
GeneralExcellent Component Pin
Peter Huber SG28-Nov-06 22:23
mvaPeter Huber SG28-Nov-06 22:23 
GeneralFurther improvements Pin
Phil J Pearson5-Sep-06 11:03
Phil J Pearson5-Sep-06 11:03 
GeneralRe: Further improvements Pin
Stefan Troschuetz5-Sep-06 21:48
Stefan Troschuetz5-Sep-06 21:48 
GeneralRe: Further improvements Pin
Yumashin Alex22-Oct-06 19:41
Yumashin Alex22-Oct-06 19:41 
GeneralRe: Further improvements Pin
Stefan Troschuetz24-Oct-06 21:45
Stefan Troschuetz24-Oct-06 21:45 
GeneralPossible null reference problem fixed Pin
Phil J Pearson5-Sep-06 2:36
Phil J Pearson5-Sep-06 2:36 
GeneralRe: Possible null reference problem fixed Pin
Stefan Troschuetz5-Sep-06 21:41
Stefan Troschuetz5-Sep-06 21:41 
QuestionTitlebar Revered Bug? Pin
Tonster10120-Aug-06 21:16
Tonster10120-Aug-06 21:16 
AnswerRe: Titlebar Revered Bug? Pin
Stefan Troschuetz26-Aug-06 22:40
Stefan Troschuetz26-Aug-06 22:40 
GeneralProblem with Anchor and Dock properties Pin
Yumashin Alex7-Aug-06 0:56
Yumashin Alex7-Aug-06 0:56 
GeneralRe: Problem with Anchor and Dock properties Pin
Stefan Troschuetz7-Aug-06 5:38
Stefan Troschuetz7-Aug-06 5:38 
GeneralRe: Problem with Anchor and Dock properties Pin
Yumashin Alex7-Aug-06 22:35
Yumashin Alex7-Aug-06 22:35 
GeneralAnd one thing more... Pin
Yumashin Alex7-Aug-06 22:37
Yumashin Alex7-Aug-06 22:37 
GeneralRe: Problem with Anchor and Dock properties Pin
Stefan Troschuetz20-Aug-06 4:25
Stefan Troschuetz20-Aug-06 4:25 
GeneralAny property, but different resx files Pin
Ajek4-Jul-06 1:44
Ajek4-Jul-06 1:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.