Home > Software design >  WPF Custom Control with ItemsControl owning itself causes stackoverflow
WPF Custom Control with ItemsControl owning itself causes stackoverflow

Time:09-13

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?

CodePudding user response:

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
}

CodePudding user response:

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

  • Related