Stefan Zuefeldt
Stefan Zuefeldt

Reputation: 191

WPF Custom Control - How to properly loop through and display child content

Attempting to write a custom control which permits a collection of children content. The end goal is to have some functional filtering of the child elements based on custom attached dependency properties; however, before getting that far, any attempts I've made to collect and re-display child UIElements have ended in exceptions during XAML parsing/display.

I have a class CustomFilter : Control which has an associated default style in a nearby xaml file. It has a dependency property for Children, which currently collect the child elements in a UIElementCollection.

I believe my problem may be how I am attempting to render the collection of children. The only way I know of looping through content in XAML involves using an ItemsControl, to which I am passing my Children collection as the ItemsSource, which feels rather backwards (using UIElements as data models?)

Afterwards, we attempt to render the elements via a content presenter inside of the ItemTemplate. I've seen in many other examples which render control children using content presenters, so I expect this is probably at least partially correct (though all only using single elements).

The sample class

  [ContentProperty(nameof(Children))]
  public class CustomFilter : Control
  {
    static CustomFilter()
    {
      DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomFilter), new FrameworkPropertyMetadata(typeof(CustomFilter)));
    }

    public CustomFilter()
    {
      Children = new UIElementCollection(this, this);
    }

    public static readonly DependencyProperty ChildrenProperty = DependencyProperty.Register(nameof(Children), typeof(UIElementCollection), typeof(CustomFilter), new FrameworkPropertyMetadata(RefreshFilter));
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    public UIElementCollection Children
    {
      get { return (UIElementCollection)GetValue(ChildrenProperty); }
      private set { SetValue(ChildrenProperty, value); }
    }
  }

The sample template

  <Style TargetType="{x:Type local:CustomFilter}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type local:CustomFilter}">
          <StackPanel>
            <ItemsControl ItemsSource="{TemplateBinding Children}">
              <ItemsControl.ItemTemplate>
                <DataTemplate>
                  <StackPanel>
                    <ContentPresenter Content="{Binding}"/>
                  </StackPanel>
                </DataTemplate>
              </ItemsControl.ItemTemplate>
            </ItemsControl>
          </StackPanel>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>

The use case

  <local:CustomFilter>
    <TextBlock Text="X"/>
    <TextBlock Text="Y"/>
    <TextBlock Text="Z"/>
  </local:CustomFilter>

Attempting to run use this control whatsoever causes it to fail when it reaches the ItemsControl, without reaching the ItemTemplate, with the following exception:

ArgumentException: Specified Visual is already a child of another Visual or the root of a CompositionTarget.

I've attempted to create a wrapper class for the Children, passing one child to each instance of the wrapper, and binding to that as an ItemsSource - it successfully loops through the ItemTemplate, but attempting to use the ContentPresenter there on the wrapped child element provides me with the following exception instead:

ArgumentException: Must disconnect specified child from current parent Visual before attaching to new parent Visual.

Upvotes: 2

Views: 1059

Answers (1)

mm8
mm8

Reputation: 169380

Why are you using an UIElementCollection instead of a List<UIElement>? You are not supposed to bind to an UIElementCollection in a template.

Your example should work if you define a List<UIElement> dependency property:

[ContentProperty(nameof(Children))]
public class CustomFilter : Control
{
    static CustomFilter()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomFilter), new FrameworkPropertyMetadata(typeof(CustomFilter)));
    }

    public CustomFilter()
    {
        SetValue(ChildrenPropertyKey, new List<UIElement>());
    }

    private static readonly DependencyPropertyKey ChildrenPropertyKey =
            DependencyProperty.RegisterReadOnly(
              nameof(Children),
              typeof(List<UIElement>),
              typeof(CustomFilter),
              new FrameworkPropertyMetadata(new List<UIElement>())
            );
    public static readonly DependencyProperty ChildrenProperty =
        ChildrenPropertyKey.DependencyProperty;

    public List<UIElement> Children
    {
        get { return (List<UIElement>)GetValue(ChildrenProperty); }
    }
}

Upvotes: 1

Related Questions