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

1

6

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!

Amyamyas answered 14/3, 2018 at 16:47 Comment(10)
Provide a little more code,are you sure your are applying the correct resources to the correct controls? Han's code uses the Form's ResourceManager on all the controlsPrelacy
Hard to guess what the problem might be, looks to me you are asking for opposite features. You ought to call SuspendLayout() before ApplyResources, ResumeLayout() afterwards to get automatic layout. Like InitializeComponent() does. Give us a concrete example if that doesn't help. And fix that Controls.Clear() bug, calling the Dispose() method for controls you remove is a rock-hard requirement. In itself odd btw, re-creating the control is already enough to not need ApplyResources for it.Ionium
Could you please provide a minimal reproducible example, I'm keen to have a play and see if I can get it working, its a bit hard to repro from your instructions.Coal
@GeorgeVovos yes I'm sure the correct resource file is being applied, otherwise I wouldn't have this problem as the translations and size/location wouldn't get modified. The User Control's ComponentResourceManager is loaded and used in the call to ApplyResourcesAmyamyas
As Jeremy said ,you should probably provide a minimal example .Shouldn't be too difficultPrelacy
@HansPassant yes, I tried the whole spiel from InitializeComponent, incl. Suspend and ResumeLayout but it doesn't work. I'm holding a reference to the instantiated User Controls in a dictionary, so I'm not re-creating them; does that still suffer from the bug you mention? I'd seen that in another answer of yours but thought Dispose would be called on them when the application exits. The controls hold state as one navigates between them but I suppose I could live with recreating them on culture change if the layout doesn't work. Would appreciate more detail on you see these as opposite featuresAmyamyas
@JeremyThompson Unfortunately, a WinForms "Minimum, Complete, and Verifiable example" is rather lengthy for the site's question format. I've added the salient functions to the question, and created a repo with a full example which reproduces the behavior at dropbox.com/sh/6vwpfseey5c6mfv/AADdRTs7ak4XeqapAbcLCVNua?dl=0Amyamyas
Downvoter: would love to hear your input as to how to improve the question. Thanks!Amyamyas
@GeorgeVovos I've added additional code and provided a minimal example which reproduces the behavior (see prievious comment to Jeremy). Thanks!Amyamyas
@GeorgeVovos I agree, I'm unfortunately unable to access Github due to proxy restrictions. Dropbox should do vs. OneDrive, unless I'm missing something? I've also updated the question with visuals to illustrate the behaviorAmyamyas
C
2

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.

Consociate answered 25/3, 2018 at 17:13 Comment(3)
This seems to work on the sample application, thanks. Unfortunately it does introduce flickering and the transitional (wrong) positioning of the label is visible for a fraction of a second, even with Suspend and ResumeLayout. Is there anything that can be done to ameliorate that side effect? Also, could you explain how it's able to determine the new locale when no CultureInfo argument is passed to your ApplyResources extension method?Amyamyas
If you pass null culture to ApplyResources method of ComponenResourceManager, it uses CurrentUICulture. So I've made it optional which fits the common usage.Consociate
What about flickering, I was suspecting it could happen, but have no reproducible test. One thing you could try is to put SuspendLayout(); at the beginning of the static void ApplyResources(ComponentResourceManager resourceManager, Control target, ... method and ResumeLayout(false); at the end. And modify the main (form) call to be this.ApplyResources(); this.PerformLayout(); Not sure if that would help though, just an idea.Consociate

© 2022 - 2024 — McMap. All rights reserved.