Changing CurrentUICulture at runtime in a Localizable Form in WinForms

后端 未结 3 2041
囚心锁ツ
囚心锁ツ 2020-12-12 03:26

I have been searching about how to change the language of a Form that has the Localizable attribute set to true.

https://msdn.microsoft.com/en-us/librar

相关标签:
3条回答
  • 2020-12-12 03:40

    After some tries, I have realized that several things are failing.

    I have to say also that all the given help in this question is greatly appreciated, and also that the LocalizedForm snippet is very useful.

    The first issue is that I have realized is that the controls that are under a level of more than 1 in the child hierarchy with this solution doesn't work.

    And iterating over all the controls is a expensive task. Maybe mine is worst but has less iterations (because I only seach for Controls with Text)

        /// <summary>
        /// Current culture of this form
        /// </summary>
        [Browsable(false)]
        [Description("Current culture of this form")]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public CultureInfo Culture
        {
            get { return this.culture; }
            set
            {
                if (this.culture != value)
                {
                    ResourceSet resourceSet = new ComponentResourceManager(GetType()).GetResourceSet(value, true, true);
                    IEnumerable<DictionaryEntry> entries = resourceSet
                        .Cast<DictionaryEntry>()
                        .Where(x => x.Key.ToString().Contains(".Text"))
                        .Select(x => { x.Key = x.Key.ToString().Replace(">", "").Split('.')[0]; return x; });
    
                    foreach (DictionaryEntry entry in entries)
                    {
                        if (!entry.Value.GetType().Equals(typeof(string))) return;
    
                        string Key = entry.Key.ToString(),
                               Value = (string) entry.Value;
    
                        try
                        {
                            Control c = Controls.Find(Key, true).SingleOrDefault();
                            c.Text = Value;
                        }
                        catch
                        {
                            Console.WriteLine("Control {0} is null in form {1}!", Key, GetType().Name);
                        }
                    }
    
                    this.culture = value;
                    this.OnCultureChanged();
                }
            }
        }
    

    What I do is the following, first, I search for the ResourceManager, take care! Because here is the second issue and is that if you use CultureInfo.CreateSpecificCulture instead CultureInfo.GetCultureInfo in some cases a new culture will be created and default values will be returned (Form1.resx values instead of Form1.es.resx values (for example)).

    Once we have loaded all the value from the resx file, we iterate over all of them, and we delete the double >> (it appears in some cases) and we get the name of those Controls that only have declared the Text attribute.

    The next step is find the Control and replace its Text...

    Well, I have a little mess with the derived classes, that's why I created a try-catch system, because, Controls.Find search in all the Derived Classes I would prefer to be a little bit more specific but I don't know how... (That's why I created this question)

    With this we haven't to save any object because we won't clear and recreate them.

    The main problem here wasn't the way I was doing this, because it was correct. The problem is that Assemblies merged do weird things when you call for example this:

    ResourceSet resourceSet = new ComponentResourceManager(GetType()).GetResourceSet(value, true, true);

    The value will be the default... Something like that this Culture doesn't exist, and is because, the merged Assembly can't find the resx file.

    So, I will try AppDomain.CurrentDomain.AssemblyResolve that @ScottChamberlain has suggested to me. Or ILRepack maybe.

    Any help for optimization or ideas (in comments) of why this doesn't work will be appreciated!

    0 讨论(0)
  • 2020-12-12 03:52

    MVVM (Model - View - ViewModel) approach have some benefits which can be useful in your case.

    Create new resource files for languages which you will use for localization. Working with form's own resource files can be little bid tricky because it regenerated every time you make change in the designer - so I think own resource file will be easier to maintain and even share with other forms and even projects.

    LocalizationValues.resx // (default english), set Access Modifier to "Internal" or "Public"
        "Title": "Title"
        "Description": "Description"
    
    LocalizationValues.es.resx
        "Title": "Título"
        "Description": "Descripción"
    

    Visual Studio generate static class LocalizationValues with properties as keys of .resx file. So "Title" can be accessed as LocalizationValues.Title

    Create "viewmodel" class which represents all texts you are using in localization. Class should implements INotifyPropertyChanged interface.

    public class LocalizationViewModel : INotifyPropertyChanged
    {
        public string Title
        {
            get
            {
                return LocalizationValues.Title;
            }
        }
    
        public string Description
        {
            get
            {
                return LocalizationValues.Description;
            }
        }
    
        public void SetLanguage(string language)
        {
            var culture = new CultureInfo(language);
            Thread.CurrentThread.CurrentUICulture = culture;
    
            // This is important, 
            // By raising PropertyChanged you notify Form to update bounded controls
            NotifyAllPropertyChanged();
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected void NotifyAllPropertyChanged()
        {
            // Passing empty string as propertyName
            // will indicate that all properties of viewmodel have changed
            NotifyPropertyChanged(string.Empty);
        }
    
        protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    

    Then in Form bind viewmodel to controls

    public partial class YourForm : Form
    {
        private LocalizationViewModel _viewmodel;
    
        public YourForm()
        {
            InitializeComponent();
    
            _viewmodel = new LocalizationViewModel();
    
            // Bound controls to correspondent viewmodel's properties
            LblTitle.DataBindings.Add("Text", _viewmodel, "Title", true);
            LblDescription.DataBindings.Add("Text", _viewmodel, "Description", true);
        }
    
        // Menu buttons to change language
        private void SpanishToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _viewmodel.SetLanguage("es");
        }
    
        private void EnglishToolStripMenuItem_Click(object sender, EventArgs e)
        {
            _viewmodel.SetLanguage("en");
        }
    }
    

    Approach above will provide more benefits then only updating controls. You get clearly separated parts of your application, which can be tested independently from each other.

    0 讨论(0)
  • 2020-12-12 03:54

    There are different solutions to solve the problem, including the MVVM that mentioned in other answers. But you can consider some other options as well.

    Call ApplyResource on all controls

    You can set the currunt UI culture of the current thread and the call ApplyResource on all controls. To do so you need to create a method to return all controls, then just call ApplyResource on all controls, for examle:

    private void englishToolStripMenuItem_Click(object sender, EventArgs e)
    {
        SetCulture("en-US");
    }
    private void persianToolStripMenuItem_Click(object sender, EventArgs e)
    {
        SetCulture("fa-IR");
    }
    public void SetCulture(string cultureName)
    {
        System.Threading.Thread.CurrentThread.CurrentUICulture =
           System.Globalization.CultureInfo.GetCultureInfo(cultureName);
        var resources = new System.ComponentModel.ComponentResourceManager(this.GetType());
        GetChildren(this).ToList().ForEach(c => {
            resources.ApplyResources(c, c.Name);
        });
    }
    public IEnumerable<Control> GetChildren(Control control)
    {
        var controls = control.Controls.Cast<Control>();
        return controls.SelectMany(ctrl => GetChildren(ctrl)).Concat(controls);
    }
    

    Creating a Text Localization Extender Component

    You also can create an extender component that can be used at design-time as well as run-time and assign some resource file and resource keys to controls. This way you can simply switch between languages by changing the current UI culture of the current thread. Just to see an example of the idea, take a look at this post:

    • How to set text to a control from resource file in design time?
    0 讨论(0)
提交回复
热议问题