WPF: How to create UserControl that renders controls passed as content

57 Views Asked by At

I'm trying to create "Expander" usercontrol that facilitates hiding (collapsing) another UI.

Now I have this:

Expander usercontrol - XAML:

    <StackPanel>
        <TextBlock Text="{Binding Caption, ElementName=root}"/>
        <ToggleButton Content="{Binding ToggleText}" IsChecked="{Binding Expanded}"/>
        <ContentControl Visibility="{Binding Expanded, Converter={x:Static root:GlobalConverters.BoolToCollapsedVisibilityConverter}}" Content="{Binding Content, ElementName=root}" />
    </StackPanel>

Expander usercontrol - Code behind:

    public Expander()
    {
        InitializeComponent();
        DataContext = new ViewModels.ExpanderViewModel();
    }
    
    public static readonly DependencyProperty CaptionProperty =
        DependencyProperty.Register("Caption", typeof(string), typeof(Expander), new PropertyMetadata("unkown caption"));
    
    public string Caption
    {
        get => (string)GetValue(CaptionProperty);
        set => SetValue(CaptionProperty, value);
    }
    
    public new static readonly DependencyProperty ContentProperty =
        DependencyProperty.Register("ItemsSource", typeof(object[]), typeof(Expander), new PropertyMetadata(Array.Empty<EmptyContent>()));
    
    public new object[] Content
    {
        get => (object[])GetValue(ContentProperty);
        set => SetValue(ContentProperty, value);
    } 

Expander usage - View:

<DataTemplate DataType="{x:Type viewModels:OrdersListModeViewModel}">
    <StackPanel>
        <controls:Expander Caption="Orders">
            <ItemsControl ItemsSource="{Binding Orders}" />
        </controls:Expander>
    </StackPanel>
</DataTemplate>

I'm facing two issues:

  1. The designer is showing this error screen: Designer error screen I've tried removing obj and bin directories but it didn't help.

  2. I'm getting binding errors within Expander.xaml: Binding errors
    I dont understand why the "controls:Expander" element content doesn't use its own datacontext

2

There are 2 best solutions below

4
BionicCode On

As already suggested, you would have to override the ControlTemplate so that the client of your control can use the Content property to define the actual content you want to host. Then you simply toggle the visibility of the ContentPresenter.

To enable customization, I recommend creating a custom control that extends HeaderedContentControl instead of using a UserControl.
HeaderedContentControl gives you a Header property and a Content property - exactly what you want.

However, because there already is a framework Expander control (that also extends HeaderedContentControl), you should extend Expander instead and add additional functionality to the existing one:

MyExpander.cs
Every control must never depend on its data context. A control must be data context agnostic. If you need external data then you must request it via public dependency properties. Internal elements will bind to those dependency properties to fetch their data.

This way the client of your control has full control over the DataContext and is able to feed the required data via data binding - without any surprises (for example, bindings fail because the control internally changes the DataContext!).

class MyExpander : Expander
{
  static MyExpander()
  {
    // You can remove this if you only need to add behavioral functionality
    // but want to keep the original visuals and visual states.
    FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(
      typeof(MyExpander),
      new FrameworkPropertyMetadata(typeof(MyExpander)));
  }

  // TODO::Add new functionality to the existing Expander
}

Generic.xaml
Example template. Only needed when the default style is explicitly overridden from the static constructor. You can see how the ContentPresenter is collapsed or visible based on the Expander.IsExpanded property:

<Style TargetType="{x:Type MyExpander}">
  <Style.Resources>
    <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
  </Style.Resources>

  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type MyExpander}">
        <Border Background="{TemplateBinding Background}"
                BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}">

          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="Auto" />
              <RowDefinition />
            </Grid.RowDefinitions>

            <TextBlock Grid.Row="0" 
                       Text="{TemplateBinding Header}" />
            <ContenPresenter Grid.Row="1" 
                             Visibility="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsExpanded, Converter={StaticResourtce BooleanToVisibilityConverter}}" />
          </Grid>
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

MainWindow.xaml

<Window>

  <!-- Either inherit or explicitly set the DataContext -->
  <MyExpander DataContext="{Binding ExpanderViewModel}" 
              Header="Expandable content"
              IsExpanded="True">
    <ListBox />
  </MyExpander>
0
Olafvolafka On

So the solution was really trivial and I needed some time to realise. Also some tips in the comments were also helpful.

I've created State property in user control code behind:

public partial class Expander : UserControl
{
    public Expander()
    {
        InitializeComponent();
    }

    public ExpanderViewModel State { get; } = new ExpanderViewModel();
}

Then I made binding to the State property using RelativeSource binding property

<ToggleButton Content="{Binding State.ToggleText,  RelativeSource={RelativeSource AncestorType=UserControl}}" 
              IsChecked="{Binding State.Expanded, RelativeSource={RelativeSource AncestorType=UserControl}}"/>

Thank you for noticing not setting the usercontrol's DataContext.