Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Re-apply layout of a dynamically added UserControl after calling ApplyResources

In a WinForms application, a Panel is used as a placeholder to display a single User Control as a navigation strategy: whenever the user wishes to navigate to a given area, the respective User Control is added to the Panel. Simplified:

contentPanel.Controls.Clear();
userControl.Dock = DockStyle.Fill;
contentPanel.Controls.Add(userControl);

As a result of a requirement that is out of my control, the Form must support switching the language dynamically. This is implemented and working fine using Hans Passant's answer, with a modification to use the User Control's Resource Manager, which correctly gets and applies the localized text to controls.

After applying the resources from the User Control's respective resource file, however, the layout resulting from DockStyle.Fill is lost for the User Control's constituent controls that are not themselves set to have a DockStyle.Fill. This has the effect that controls no longer stretch to fill the available area, and are limited to the original size defined in the designer/resource file. Note that the Dock property of the User Control is still set correctly to DockStyle.Fill after applying the resources.

I created an example application which illustrates/reproduces the problem: the form below has a panel to which a user control is added dynamically and set to DockStyle.Fill. The user control has a label which is anchored top left on the Default locale and top right in the German locale. I would expect the form to snap the label which is anchored to the right against the right margin of the form, but the size of the user control is reset to the value at design time. View source code.

If I start the form on the German locale, the label is correctly laid out against the right edge of the form:

enter image description here

What I'd like to happen is that the layout is retained after calling ApplyResources. Of course I could simply make a copy of the controls' Location and Size properties (as suggested in another answer to the same question mentioned above) but unfortunately values of these properties differ between locales. So, after the localized string and positioning are applied, how can the User Control be directed to layout all its controls anew?

What I've tried

  • By looking into InitializeComponent(), I've tried calling PerformLayout() to the Panel container, the User Control, and the Form to no avail.
  • Adding SuspendLayout() and ResumeLayout(true) before and after the call to ApplyResources, also without success.

Additional implementation details

  • References to instantiated User Controls are kept in a private dictionary in the Main Form. When navigation for that control is raised, the previous user control is removed and the existing reference added with the snippet above.
  • Reacting to the user event of changing the language:

    protected virtual void OnChangeCulture(CultureInfo newCulture)
    {
        System.Threading.Thread.CurrentThread.CurrentCulture = newCulture;
        System.Threading.Thread.CurrentThread.CurrentUICulture = newCulture;
    
        SuspendLayout();
        ComponentResourceManager resources = new ComponentResourceManager(this.GetType());
        ApplyResources(resources, this, newCulture);
        ResumeLayout(true);
    }
    
  • Applying the resources to all controls in the form:

    private void ApplyResources(ComponentResourceManager resourceMgr, Component target, CultureInfo culture)
    {
        //Since target can be a Control or a Component, get their name and children (OMITTED) in order to apply the resources and recurse
        string name;
        IEnumerable<Component> children;
    
        //Have the resource manager apply the resources to the given target
        resourceMgr.ApplyResources(target, name, culture);
    
        //iterate through the collection of children and recursively apply resources
        foreach (Component c in children)
        {
            //In the case of user controls, they have their own ResourceManager with the translated strings, so get it and use it instead
            if (c is UserControl)
                resourceMgr = new ComponentResourceManager(c.GetType());
    
            //recursively apply resources to the child
            this.ApplyResources(resourceMgr, c, culture);
        }
    }
    

Many thanks in advance for any pointers!

like image 302
user5877732 Avatar asked Mar 14 '18 16:03

user5877732


1 Answers

I can suggest the following custom extension method:

using System.ComponentModel;
using System.Globalization;

namespace System.Windows.Forms
{
    public static partial class Extensions
    {
        public static void ApplyResources(this Control target, CultureInfo culture = null)
        {
            ApplyResources(new ComponentResourceManager(target.GetType()), target, "$this", culture);
        }

        static void ApplyResources(ComponentResourceManager resourceManager, Control target, string name, CultureInfo culture = null)
        {
            // Preserve and reset Dock property
            var dock = target.Dock;
            target.Dock = DockStyle.None;
            // Reset Anchor property
            target.Anchor = AnchorStyles.Top | AnchorStyles.Left;
            // Have the resource manager apply the resources to the given target
            resourceManager.ApplyResources(target, name, culture);
            // Iterate through the collection of children and recursively apply resources
            foreach (Control child in target.Controls)
            {
                if (child is UserControl)
                    ApplyResources(child, culture);
                else
                    ApplyResources(resourceManager, child, child.Name, culture);
            }
            // Restore Dock property
            target.Dock = dock;
        }
    }
}

The essential changes are two.

First, since the stored location/sizes are relative to the container design size (before being docked), we preserve the Dock property, reset it to None during the ApplyResources calls to the control and its children, and finally restore it to the current value.

This basically resolves the issue with right anchor. However, since Windows Forms designer doesn't save property values having default values, and the default value for Anchor property is AnchorStyles.Top | AnchorStyles.Left, it's not stored and hence is not set correctly (when going from German to English in your sample).

So the second fix is to simply reset it to its default value before ApplyResources call.

The usage is simple:

protected virtual void OnChangeCulture(CultureInfo newCulture)
{
    System.Threading.Thread.CurrentThread.CurrentCulture = newCulture;
    System.Threading.Thread.CurrentThread.CurrentUICulture = newCulture;

    SuspendLayout();
    this.ApplyResources(); // <--
    ResumeLayout(true);
}

Note that SuspendLayout and ResumeLayout calls are not essential - it works with or without them. They are used to eventually prevent flickering, which doesn't happen in your example.

like image 133
Ivan Stoev Avatar answered Nov 02 '22 02:11

Ivan Stoev