0

Code example is reduced to the minimum amount reproducing the issue. Given a custom WPF user control with a list of children defined as such:

[ContentProperty(nameof(FormItems))]
public class FormContainer : ContentControl
{
    static FormContainer()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(FormContainer),
            new FrameworkPropertyMetadata(typeof(FormContainer))
        );
    }

    #region FormItems Dependency Property

    public static readonly DependencyProperty FormItemsProperty = DependencyProperty.Register(
        "FormItems", typeof(ObservableCollection<UIElement>), typeof(FormContainer), 
        new PropertyMetadata(new ObservableCollection<UIElement>()));

    public ObservableCollection<UIElement> FormItems
    {
        get => (ObservableCollection<UIElement>)GetValue(FormItemsProperty);
        set => SetValue(FormItemsProperty, value);
    }

    #endregion

    // ... extra boilerplate removed
}

Which is being rendered in a simple Stack Panel with an ItemsControl help using the following style:

<ControlTemplate x:Key="FormContainerControlTemplate" TargetType="{x:Type uc:FormContainer}">
    <ItemsControl ItemsSource="{TemplateBinding FormItems}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</ControlTemplate>

<Style TargetType="{x:Type uc:FormContainer}">
    <Setter Property="Template" Value="{StaticResource FormContainerControlTemplate}"/>
</Style>

This usage of the control works perfectly:

<controls:FormContainer>
    <Label Content="Something" />
</controls:FormContainer>

This usage crashed with a stackoverflow:

<controls:FormContainer>
    <Label Content="Something" />
    <controls:FormContainer>
        <Label Content="Something else" />
    </controls:FormContainer>
</controls:FormContainer>

I would expected to just get two containers nested with their own controls.

Why does it crashed? What am I missing? And more importantly how can I enable nesting functionality for my custom controls?

Memleak
  • 158
  • 7
  • 1
    you can just derive FormContainer from ItemsControl (and use ItemsPresenter in ControlTemplate) – ASh Sep 12 '22 at 18:16
  • Thank you for the suggestion. Yes I could do that but it does not help me. This is a minimized example that reproduced a problem in a more complicated control that generates children dynamically (among which can be children of the same type). Edit: Also, even with a workaround I am curious on why this happens and how would one handle it. – Memleak Sep 12 '22 at 18:20

2 Answers2

2

DP metadata declaration is incorrect. DP object (FormItemsProperty) is a singletone, so its PropertyMetadata and default value is shared by all intances of FormContainer. Which in case of reference type DP can cause issues.

fix them by assigning a different collection for each instance:

public class FormContainer : ContentControl
{
    static FormContainer()
    {
        DefaultStyleKeyProperty.OverrideMetadata
        (
            typeof(FormContainer),
            new FrameworkPropertyMetadata(typeof(FormContainer))
        );
    }

    public FormContainer()
    {
        SetCurrentValue(FormItemsProperty, new ObservableCollection<UIElement>());
    }

    #region FormItems Dependency Property

    public static readonly DependencyProperty FormItemsProperty = DependencyProperty.Register
    (
        nameof(FormItems),
        typeof(ObservableCollection<UIElement>),
        typeof(FormContainer), 
        new PropertyMetadata(null)
    );

    public ObservableCollection<UIElement> FormItems
    {
        get => (ObservableCollection<UIElement>)GetValue(FormItemsProperty);
        set => SetValue(FormItemsProperty, value);
    }

    #endregion
}
ASh
  • 34,632
  • 9
  • 60
  • 82
  • Feels so stupid now! Yes, thank you, a bad case of a copy-paste where I haven't even read that part. Indeed, it is working fine and is all clear on why happens. – Memleak Sep 12 '22 at 18:38
  • @ASH, Why SetCurrentValue and not SetValue? If I'm not confusing anything, then when revalidating the property (InvalidateProperty), the value set by SetCurrentValue will be lost. – EldHasp Sep 12 '22 at 19:31
  • @EldHasp - https://stackoverflow.com/questions/4230698/whats-the-difference-between-dependency-property-setvalue-setcurrentvalue – ASh Sep 12 '22 at 19:32
  • That's right. It's written exactly the way I think it is. And in this case, you need to use SetValue, because after the InvalidateProperty method, in your example, the property will be reset to null. – EldHasp Sep 12 '22 at 19:39
1

Even though @ASh suggested a working solution, I want to show a more typical way for WPF to implement a collection property of child elements.

        public FormContainer()
        {
            SetValue(FormItemsPropertyKey, new UIElementCollection (this, this));
        }

        private static readonly DependencyPropertyKey FormItemsPropertyKey = DependencyProperty.RegisterReadOnly(
            nameof(FormItems),
            typeof(UIElementCollection),
            typeof(FormContainer));

        public static readonly DependencyProperty FormItemsProperty = FormItemsPropertyKey.DependencyProperty;

        public UIElementCollection FormItems => (UIElementCollection)GetValue(FormItemsProperty);

See more complete example: public UIElementCollection Children

EldHasp
  • 6,079
  • 2
  • 9
  • 24