Search code examples
c#wpfwpf-controlscustom-controlsstack-overflow

WPF Custom Control with ItemsControl owning itself causes stackoverflow


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?


Solution

  • 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
    }