Changing the Culture During Execution


In some applications, it might be helpful to allow the user to switch between different cultures while the application is running. This kind of application is great to demo because it provides immediate visual feedback on the act of changing the language. Figure 4.5 shows an application that offers a menu of languages.

Figure 4.5. Windows Forms Application Offering a Choice of Languages


Figure 4.6 shows the same application after the user has selected Spanish as the language of choice.

Figure 4.6. Windows Forms Application after Selecting Spanish


To achieve this result, we need to solve two problems. First, we need to be able to get a list of all forms in the application. Second, we need to be able to reapply the new resources to each form.

The first problem is easy to solve if you are using the .NET Framework 2.0. The System.Windows.Forms.Application class has a static property called OpenForms, which is a FormCollection. To solve our problem, we create a new class that looks like this:

 public class ChangeFormCulture {     public static void ChangeAllForms(string culture)     {         FormCollection forms = Application.OpenForms;         foreach (Form form in forms)         {            ChangeForm(form, culture);         }     } } 


The static ChangeAllForms method iterates through each form in the Application.OpenForms collection, calling the ChangeForm method.

Getting a List of Open Forms in the .NET Framework 1.1

The Application.OpenForms property is new in the .NET Framework 2.0, so it is not available in the .NET Framework 1.1. However, with reflection, we can do anything (almost). The .NET Framework 1.1 does maintain a collection of open forms, but it doesn't expose it. The collection is maintained in the System.Windows.Forms.Application.ThreadWindows.windows private field. To access this field, we need to construct a new ThreadWindows instance. The windows property contains an array of IntPtrs. The array has gaps in it, so not all of the elements are used. The following OpenForms property returns an array of Form objects by getting the value of the windows field, removing the gaps, and converting the IntPtrs to Form objects.


 public static Form[] OpenForms {     get     {         Module MSCorLibModule = typeof(Form).BaseType.Module;         Type threadWindowsType =             MSCorLibModule.GetType(             "System.Windows.Forms.Application+ThreadWindows");         ConstructorInfo constructorInfo =             threadWindowsType.GetConstructor(             BindingFlags.Instance |             BindingFlags.NonPublic,             null, CallingConventions.HasThis,             new Type[] {typeof(Control),             typeof(Boolean)},             null);         if (constructorInfo == null)             return null;         Object threadWindow = constructorInfo.Invoke(             new object[] {null, (Object) false});         FieldInfo windowsFieldInfo =             threadWindowsType.GetField("windows",             BindingFlags.NonPublic |             BindingFlags.Instance);         if (windowsFieldInfo == null)             return null;         IntPtr[] windows = (IntPtr[])             windowsFieldInfo.GetValue(threadWindow);         ArrayList forms = new ArrayList();         foreach(IntPtr window in windows)         {             Control control =                 Control.FromHandle(window);             if (control != null)                 forms.Add((Form) control);         }         return (Form[]) forms.ToArray(typeof(Form));     } } 


The second problem, applying the new resources to each form, has two solutions, each with pros and cons. The first solution is simply to use ComponentResource Manager to apply the new resources to each control. The ChangeForm method shows how to do this:

 public static void ChangeForm(Form form, string culture) {     Thread.CurrentThread.CurrentUICulture =         new System.Globalization.CultureInfo(culture);     ComponentResourceManager resourceManager =         new ComponentResourceManager(form.GetType());     // apply resources to each control     foreach (Control control in form.Controls)     {         resourceManager.ApplyResources(control, control.Name);     }     // apply resources to the form     int X = form.Location.X;     int Y = form.Location.Y;     resourceManager.ApplyResources(form, "$this");     form.Location = new Point(X, Y);     ApplyMenuResources(resourceManager, form); } 


ChangeForm starts by changing the CurrentUICulture to the desired culture. It then gets a ComponentResourceManager for the given form and iterates through every control, reapplying its resources. It then saves the form's position, applies the new resources to the form, and restores the form's position. So far, so good. At the end of the method is a call to ApplyMenuResources. The problem with MenuItems is that they are components, not controls; as such, their Name property returns an empty string. The name is essential because it is the key by which the corresponding resource entries are found. The form's InitializeComponent method gets around this limitation because it knows the name of each component on the form and can generate code that uses a literal string:

 resources.ApplyResources(this.menuItem1, "menuItem1"); 


To load the resources for the menu item, we need to find the menu item's name. The ApplyMenuResources method uses reflection to iterate through all the private fields on the form, looking for fields that have a type of MenuItem. Having found such a field, the name of the corresponding resources can be found from the name of the field, and ComponentResourceManager.ApplyResources can be called:

 private static void ApplyMenuResources(     ComponentResourceManager resourceManager, Form form) {     if (form.Menu != null)     {         FieldInfo[] fieldInfos = form.GetType().GetFields(             BindingFlags.Instance | BindingFlags.NonPublic);         foreach (FieldInfo fieldInfo in fieldInfos)         {             if (fieldInfo.FieldType ==                 typeof(System.Windows.Forms.MenuItem))             {                 MenuItem menuItem =                     (MenuItem)fieldInfo.GetValue(form);                 resourceManager.ApplyResources(                     menuItem, fieldInfo.Name);             }         }     } } 


This solution works well, but it does not work with all components. Some components inject additional code into the form's serialization code (in the Initialize Component method). Because the InitializeComponent method is not called when the form's culture is changed, this code will not be executed. Whether this is important depends upon the nature of the injected code. If it is important, you need the second solution to this problem. The ChangeFormUsingInitializeComponent method uses reflection to get a MethodInfo object for the form's private Initialize Component method. It deletes all the controls on the form, saves the form's position, invokes the InitializeComponent method, and restores the form's position.

 public static void ChangeFormUsingInitializeComponent(     Form form, string culture) {     // get the form's private InitializeComponent method     MethodInfo initializeComponentMethodInfo =         form.GetType().GetMethod("InitializeComponent",         BindingFlags.Instance | BindingFlags.NonPublic);     if (initializeComponentMethodInfo != null)     {         // the form has an InitializeComponent method         // that we can invoke         // save all controls         List<Control> controls = new List<Control>();         foreach (Control control in form.Controls)         {             controls.Add(control);         }         // remove all controls         foreach (Control control in controls)         {             form.Controls.Remove(control);         }         int X = form.Location.X;         int Y = form.Location.Y;         Thread.CurrentThread.CurrentUICulture =             new System.Globalization.CultureInfo(culture);         // call the InitializeComponent method to add back controls         initializeComponentMethodInfo.Invoke(             form, new object[] { });         form.Location = new Point(X, Y);     } } 


If you are using the .NET Framework 1.1, you can use an ArrayList instead of the generic List collection (i.e., List<Control>) in this example.


So in this strategy, all the controls are destroyed and then rebuilt. This solves the original problem of ensuring that injected code is run, but it creates two new problems: The current values of controls are lost (e.g., TextBox.Text, CheckBox.Checked, ListBox.SelectedIndex), and the controls' events might fire, affecting the behavior or appearance of the form. You must decide which evil you can live with.

With our solution in place, each language menu item (e.g., English, French, Spanish, German) needs only to call the ChangeAllForms method, passing the correct culture:

 ChangeFormCulture.ChangeAllForms("es"); 





.NET Internationalization(c) The Developer's Guide to Building Global Windows and Web Applications
.NET Internationalization: The Developers Guide to Building Global Windows and Web Applications
ISBN: 0321341384
EAN: 2147483647
Year: 2006
Pages: 213

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net