65.9K
CodeProject is changing. Read more.
Home

Custom Expander Control for WPF

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.32/5 (4 votes)

Feb 7, 2017

CPOL

1 min read

viewsIcon

17065

downloadIcon

389

A WPF custom control for grouping data similar to build-in expander

Introduction

This is a very simple custom expander control. The control is called "GroupExpander".

Background

I don't like the built-in expander control in WPF. Although I can change the control template to give a new look and feel, I thought it will be quicker to create a custom one.

Using the Code

I have a default WPF project "WpfApplication1". This will create a MainWindow.xaml and cs by default. Add a custom control "GroupExpander". This will give us a GroupExpander.cs and a Generic.xaml. Create a Header property in GroupExpander control and rest of the stuff is done in XAML in Generic.xaml file. I have used "WpfApplication1" as namespace all across. I have coded the GroupExpander control to match my existing UI. But the XAML for template is available. Feel free to change.

The code for GroupExpander control is as follows:

public class GroupExpander : ContentControl
{
  public string Header
  {
    get { return (string)GetValue(HeaderProperty); }
    set { SetValue(HeaderProperty, value); }
  }
  public static readonly DependencyProperty HeaderProperty = 
       DependencyProperty.Register("Header", typeof(string), typeof(GroupExpander), 
       new PropertyMetadata(string.Empty));
  static GroupExpander()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupExpander), 
      new FrameworkPropertyMetadata(typeof(GroupExpander)));
  }
}

In the generic.xaml file, we are defining the control template for the GroupExpander control. We have a Toggle button to control the visibility of the content presenter. The template of the toggle button has been changed. This has a textblock to show the header, a polyline to draw angle indicator and a line separator. The XAML in Generic.xaml file to format the control:

<Style TargetType="{x:Type local:GroupExpander}">
  <Setter Property="SnapsToDevicePixels" Value="True" />
  <Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type local:GroupExpander}">
    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <ToggleButton Grid.Row="0" 
    Name="PART_ToggleButton" IsChecked="True" 
    OverridesDefaultStyle="True">
      <ToggleButton.Style>
        <Style TargetType="{x:Type ToggleButton}">
          <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
              <Grid HorizontalAlignment="Stretch" 
              VerticalAlignment="Stretch" Margin="1,0,1,0">
              <Grid.Triggers>
                <EventTrigger RoutedEvent="UIElement.MouseEnter">
                  <BeginStoryboard>
                    <BeginStoryboard.Storyboard>
                      <Storyboard Storyboard.TargetName="PART_Rectangle" 
                                  Storyboard.TargetProperty="Opacity">
                        <DoubleAnimation To="1" Duration="0:0:0.01" />
                      </Storyboard>
                    </BeginStoryboard.Storyboard>
                  </BeginStoryboard>
                </EventTrigger>
                <EventTrigger RoutedEvent="UIElement.MouseLeave">
                  <BeginStoryboard>
                    <BeginStoryboard.Storyboard>
                      <Storyboard Storyboard.TargetName="PART_Rectangle" 
                                  Storyboard.TargetProperty="Opacity">
                        <DoubleAnimation To="0" Duration="0:0:0.1" />
                      </Storyboard>
                    </BeginStoryboard.Storyboard>
                  </BeginStoryboard>
                </EventTrigger>
              </Grid.Triggers>
              <Rectangle x:Name="PART_Rectangle" 
              Fill="#EEE" Opacity="0" />
              <DockPanel Background="Transparent" Margin="0,5,0,10">
                <TextBlock DockPanel.Dock="Left" 
                FontWeight="Bold" Margin="0,4,0,0" 
                           Text="{Binding Path=Header,
                           RelativeSource={RelativeSource AncestorType=
                           {x:Type local:GroupExpander}}}"/>
                <Grid DockPanel.Dock="Right" Margin="0,4,0,0" 
                VerticalAlignment="Center">
                  <Polyline Points="0,0 4,4 8,0" 
                  Stroke="Black" StrokeThickness="1">
                    <Polyline.Style>
                      <Style TargetType="{x:Type Polyline}">
                        <Setter Property="Visibility" Value="Visible"/>
                        <Style.Triggers>
                          <DataTrigger Binding="{Binding ElementName=PART_ToggleButton, 
                                       Path=IsChecked }" Value="True">
                            <Setter Property="Visibility" Value="Collapsed"/>
                          </DataTrigger>
                        </Style.Triggers>
                      </Style>
                    </Polyline.Style>
                  </Polyline>
                  <Polyline Points="0,4 4,0 8,4" 
                  Stroke="Black" StrokeThickness="1">
                    <Polyline.Style>
                      <Style TargetType="{x:Type Polyline}">
                        <Setter Property="Visibility" Value="Collapsed"/>
                        <Style.Triggers>
                          <DataTrigger Binding="{Binding ElementName=PART_ToggleButton, 
                                       Path=IsChecked }" Value="True">
                            <Setter Property="Grid.Visibility" Value="Visible"/>
                          </DataTrigger>
                        </Style.Triggers>
                      </Style>
                    </Polyline.Style>
                  </Polyline>
                </Grid>
                <Separator Margin="4,4,4,0" />
              </DockPanel>
            </Grid>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </ToggleButton.Style>
    </ToggleButton>
    <Grid Grid.Row="1" x:Name="PART_Grid" Visibility="Collapsed" 
          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
          Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
        <ContentPresenter Content="{Binding RelativeSource=
                          {RelativeSource TemplatedParent},Path=Content}" />
    </Grid>
   </Grid>
   <ControlTemplate.Triggers>
     <DataTrigger Binding="{Binding ElementName=PART_ToggleButton,Path=IsChecked}" 
                  Value="True">
       <Setter TargetName="PART_Grid" 
       Property="Visibility" Value="Visible"/>
     </DataTrigger>
   </ControlTemplate.Triggers>
  </ControlTemplate>
  </Setter.Value>
 </Setter>
</Style>

Once you have compiled the project, you will be able to use the control in MainWindow. I have put some dummy control inside, just to give you an idea. Insert the following XAML inside the <window> tag. The XAML for MainWindow is as given below:

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
  <local:GroupExpander Header="Single Customer" Grid.Row="0">
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition/>
        <ColumnDefinition/>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
      </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Text="ID"/>
        <TextBox Grid.Row="0" Grid.Column="1" Text="101"/>
        <TextBlock Grid.Row="1" Grid.Column="0" Text="Name"/>
        <TextBox Grid.Row="1" Grid.Column="1" Text="Bob Wang"/>
        <Button Grid.Row="2" Content="Save"/>
      </Grid>
  </local:GroupExpander>
  <local:GroupExpander Grid.Row="1" Header="All Customers">
    <ListBox>
      <ListBoxItem>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="101"/>
          <TextBlock Text="Bob Wang"/>
        </StackPanel>
      </ListBoxItem>
      <ListBoxItem>
        <StackPanel Orientation="Horizontal">
          <TextBlock Text="102"/>
          <TextBlock Text="Tim Bret"/>
        </StackPanel>
      </ListBoxItem>
    </ListBox>
  </local:GroupExpander>
</Grid>