Sunday, October 2, 2011

WPF Collapsible Panel

In this article I have describe way to create a collapsible panel in WPF. This is one of the most common questions asked by many developers. Collapsible Panel can be used by user to collapse the content which he doesn't want to use and see only core information and hence keep the screen clean.
WPFCollapsiblePanel
This sample inherits the Expander control which is available in WPF. It support panel collapse in all the four directions (left, right, top and bottom).
public class CollapsiblePanel : Expander
{
static CollapsiblePanel()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CollapsiblePanel), new FrameworkPropertyMetadata(typeof(CollapsiblePanel)));
}
}

Here is the XAML for CollapsiblePanel

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib" 
xmlns:local="clr-namespace:CollapsiblePanelDemo">

<local:MultiplyConverter x:Key="multiplyConverter" />

<Style x:Key="ExpanderHeaderFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Border>
<Rectangle SnapsToDevicePixels="true" Margin="0" Stroke="Black" StrokeDashArray="1 2" StrokeThickness="1" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="ExpanderUpHeaderStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.LayoutTransform>
<RotateTransform Angle="90"/>
</Grid.LayoutTransform>


<Path x:Name="trapazoid" Fill="{Binding Path=Background, RelativeSource={RelativeSource FindAncestor, AncestorType=local:CollapsiblePanel}}"  HorizontalAlignment="Center"   VerticalAlignment="Center">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0" >
<PathFigure.Segments>

<ArcSegment Size="25,25" RotationAngle="45" IsLargeArc="False" SweepDirection="Clockwise" Point="0,48"/>

</PathFigure.Segments>
</PathFigure >
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>



<Path x:Name="arrow" Data="M 1,1.5 L 9,9 L 1,16 1, 1" HorizontalAlignment="Center" SnapsToDevicePixels="false" Fill="{Binding Path=BorderBrush, RelativeSource={RelativeSource FindAncestor, AncestorType=local:CollapsiblePanel}}" StrokeThickness="2" VerticalAlignment="Center"/>

</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true">
<Setter Property="Data" TargetName="arrow" Value="M 9,1 L 1,8 L 9,16, 9,1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="ExpanderLeftHeaderStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">



<Path x:Name="trapazoid" Fill="{Binding Path=Background, RelativeSource={RelativeSource FindAncestor, AncestorType=local:CollapsiblePanel}}"  HorizontalAlignment="Center"   VerticalAlignment="Center">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0" >
<PathFigure.Segments>

<ArcSegment Size="25,25" RotationAngle="45" IsLargeArc="False" SweepDirection="Clockwise" Point="0,48"/>

</PathFigure.Segments>
</PathFigure >
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
<Path x:Name="arrow" Data="M 1,1.5 L 9,9 L 1,16 1, 1" HorizontalAlignment="Center" SnapsToDevicePixels="false" Fill="{Binding Path=BorderBrush, RelativeSource={RelativeSource FindAncestor, AncestorType=local:CollapsiblePanel}}" StrokeThickness="2" VerticalAlignment="Center"/>

</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true">
<Setter Property="Data" TargetName="arrow" Value="M 9,1 L 1,8 L 9,16, 9,1"/>
</Trigger>

</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="ExpanderRightHeaderStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.LayoutTransform>
<RotateTransform Angle="180"/>
</Grid.LayoutTransform>

<Path x:Name="trapazoid" Fill="{Binding Path=Background, RelativeSource={RelativeSource FindAncestor, AncestorType=local:CollapsiblePanel}}"  HorizontalAlignment="Center"   VerticalAlignment="Center">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0" >
<PathFigure.Segments>

<ArcSegment Size="25,25" RotationAngle="45" IsLargeArc="False" SweepDirection="Clockwise" Point="0,48"/>

</PathFigure.Segments>
</PathFigure >
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
<Path x:Name="arrow" Data="M 1,1.5 L 9,9 L 1,16 1, 1" HorizontalAlignment="Center" SnapsToDevicePixels="false" Fill="{Binding Path=BorderBrush, RelativeSource={RelativeSource FindAncestor, AncestorType=local:CollapsiblePanel}}" StrokeThickness="2" VerticalAlignment="Center"/>

</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true">
<Setter Property="Data" TargetName="arrow" Value="M 9,1 L 1,8 L 9,16, 9,1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style x:Key="ExpanderDownHeaderStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Padding="{TemplateBinding Padding}">
<Grid Background="Transparent" SnapsToDevicePixels="False">
<Grid.LayoutTransform>
<RotateTransform Angle="-90"/>
</Grid.LayoutTransform>


<Path x:Name="trapazoid" Fill="{Binding Path=Background, RelativeSource={RelativeSource FindAncestor, AncestorType=local:CollapsiblePanel}}"  HorizontalAlignment="Center"   VerticalAlignment="Center">
<Path.Data>
<PathGeometry>
<PathGeometry.Figures>
<PathFigure StartPoint="0,0" >
<PathFigure.Segments>

<ArcSegment Size="25,25" RotationAngle="45" IsLargeArc="False" SweepDirection="Clockwise" Point="0,48"/>

</PathFigure.Segments>
</PathFigure >
</PathGeometry.Figures>
</PathGeometry>
</Path.Data>
</Path>
<Path x:Name="arrow" Data="M 1,1.5 L 9,9 L 1,16 1, 1" HorizontalAlignment="Center" SnapsToDevicePixels="false" Fill="{Binding Path=BorderBrush, RelativeSource={RelativeSource FindAncestor, AncestorType=local:CollapsiblePanel}}" StrokeThickness="2" VerticalAlignment="Center"/>

</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true">
<Setter Property="Data" TargetName="arrow" Value="M 9,1 L 1,8 L 9,16, 9,1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

<Style TargetType="{x:Type local:CollapsiblePanel}">
<Setter Property="Background" Value="LightGray"/>
<Setter Property="BorderBrush" Value="White"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Expander}">
<Border SnapsToDevicePixels="true">
<DockPanel>

<ToggleButton x:Name="HeaderSite" ContentTemplate="{TemplateBinding HeaderTemplate}" ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" Content="{TemplateBinding Header}" DockPanel.Dock="Top" Foreground="{TemplateBinding Foreground}" FontWeight="{TemplateBinding FontWeight}" FocusVisualStyle="{StaticResource ExpanderHeaderFocusVisual}" FontStyle="{TemplateBinding FontStyle}" FontStretch="{TemplateBinding FontStretch}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Margin="0" MinWidth="0" MinHeight="0" Padding="{TemplateBinding Padding}" Style="{StaticResource ExpanderDownHeaderStyle}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>


<Grid  
x:Name="ExpandSiteContainer"  
Visibility="Visible"  
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"  
Margin="{TemplateBinding Padding}"  
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"  
DockPanel.Dock="Bottom"                                        
>

<Grid.Tag>
<sys:Double>0.0</sys:Double>
</Grid.Tag>
<ScrollViewer  
VerticalScrollBarVisibility="Hidden"  
HorizontalScrollBarVisibility="Hidden"  

>
<ContentPresenter  
x:Name="ExpandSite"  
Focusable="false"  
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"  VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 
>
</ContentPresenter>
</ScrollViewer>
</Grid>
</DockPanel>
</Border>
<ControlTemplate.Triggers>

<Trigger Property="IsExpanded" Value="true">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName ="ExpandSiteContainer"   
Storyboard.TargetProperty ="Tag"  
To="1.0" Duration ="0:0:0.45" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName ="ExpandSiteContainer"   
Storyboard.TargetProperty ="Tag"  
To="0" Duration ="0:0:0.45"/>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>

<Trigger Property="ExpandDirection" Value="Down">
<Setter Property="Height" TargetName="ExpandSiteContainer">
<Setter.Value>
<MultiBinding Converter="{StaticResource multiplyConverter}">
<Binding Path="ActualHeight" ElementName="ExpandSite"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="ExpandDirection" Value="Right">
<Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Right"/>
<Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Left"/>
<Setter Property="Style" TargetName="HeaderSite" Value="{StaticResource ExpanderRightHeaderStyle}"/>
<Setter Property="Width" TargetName="ExpandSiteContainer">
<Setter.Value>
<MultiBinding Converter="{StaticResource multiplyConverter}">
<Binding Path="ActualWidth" ElementName="ExpandSite"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="ExpandDirection" Value="Up">
<Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Top"/>
<Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Bottom"/>
<Setter Property="Style" TargetName="HeaderSite" Value="{StaticResource ExpanderUpHeaderStyle}"/>
<Setter Property="Height" TargetName="ExpandSiteContainer">
<Setter.Value>
<MultiBinding Converter="{StaticResource multiplyConverter}">
<Binding Path="ActualHeight" ElementName="ExpandSite"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="ExpandDirection" Value="Left">
<Setter Property="DockPanel.Dock" TargetName="ExpandSite" Value="Left"/>
<Setter Property="DockPanel.Dock" TargetName="HeaderSite" Value="Right"/>
<Setter Property="Style" TargetName="HeaderSite" Value="{StaticResource ExpanderLeftHeaderStyle}"/>
<Setter Property="Width" TargetName="ExpandSiteContainer">
<Setter.Value>
<MultiBinding Converter="{StaticResource multiplyConverter}">
<Binding Path="ActualWidth" ElementName="ExpandSite"/>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
</MultiBinding>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>

</Style>
</ResourceDictionary>


Following is the example of using CollapsiblePanel control in WPF.

<my:CollapsiblePanel Grid.Row="2" Grid.Column="1" Header="Toolbar Demo" ExpandDirection="Down" x:Name="Expander2" BorderBrush="Yellow" Background="{StaticResource ToggleButtonBrush}" IsExpanded="False">
<my:CollapsiblePanel.Content>
<Image Source="/CollapsiblePanelDemo;component/Images/Koala.jpg" Height="240"  Stretch="Fill"   />
</my:CollapsiblePanel.Content>
</my:CollapsiblePanel>

You can download complete source code from following link
Download Source Code

5 comments:

  1. Hello,
    unfortunately i can´t access live skydrive. could you please post the "multiplyConverter" also?
    thanks in advance...

    christian

    ReplyDelete
  2. Code for MultiplyConverter

    public class MultiplyConverter : IMultiValueConverter
    {
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
    double result = 1.0;
    for (int i = 0; i < values.Length; i++)
    {
    if (values[i] is double)
    result *= (double)values[i];
    }

    return result;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
    throw new Exception("Not implemented");
    }
    }

    ReplyDelete
  3. Its great...
    But i want one more thing in it.
    Can't it be expanded more by dragging? I mean when we click on the expander button it is expanded to a fixed length, i want to increase the length by dragging although the length remain same when the expander button is pressed.

    ReplyDelete
    Replies
    1. You can add a GridSplitter to expand more.

      Delete