GridExpander for WPF

When looking for a GridSplitter control in WPF that had the ability to expand and collapse simialar to a docking panel that can be pinned I came accross a post by¬†Shemesh that illustrated how this could be done. Unfortunately however Shemesh’s example is written specifically for Silverlight 4 and doesn’t work for regular WPF pre-4.0. So after some tinkering with the Silverlight example enough to get something working for .NET 3.5 SP1 and the WPF Toolkit February release for the Visual State Manager stuff. Below you can find the two main source files that I’ve fixed, refactored, and reorganized quite a bit.

GridExpander.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace GridExpander
{
    /// <summary>
    /// Specifies different collapse modes of a GridExpander.
    /// </summary>
    public enum GridExpanderDirection
    {
        /// <summary>
        /// The GridExpander cannot be collapsed or expanded.
        /// </summary>
        None = 0,
        /// <summary>
        /// The column (or row) to the right (or below) the
        /// splitter's column, will be collapsed.
        /// </summary>
        Next = 1,
        /// <summary>
        /// The column (or row) to the left (or above) the
        /// splitter's column, will be collapsed.
        /// </summary>
        Previous = 2
    }

    /// <summary>
    /// An updated version of the standard GridExpander control that includes a centered handle
    /// which allows complete collapsing and expanding of the appropriate grid column or row.
    /// </summary>
    [TemplatePart(Name = GridExpander.ElementHandleName, Type = typeof(ToggleButton))]
    [TemplatePart(Name = GridExpander.ElementTemplateName, Type = typeof(FrameworkElement))]
    [TemplateVisualState(Name = "MouseOver", GroupName = "CommonStates")]
    [TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
    [TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
    [TemplateVisualState(Name = "Focused", GroupName = "FocusStates")]
    [TemplateVisualState(Name = "Unfocused", GroupName = "FocusStates")]
    //[TemplateVisualState(Name = "Checked", GroupName = "CheckStates")]
    //[TemplateVisualState(Name = "Unchecked", GroupName = "CheckStates")]
    public class GridExpander : System.Windows.Controls.GridSplitter
    {
        private const string ElementHandleName = "ExpanderHandle";
        private const string ElementTemplateName = "TheTemplate";
        private const string ElementGridExpanderBackground = "GridExpanderBackground";

        private ToggleButton _expanderButton;
        private Rectangle _elementGridExpanderBackground;

        private RowDefinition AnimatingRow;
        private ColumnDefinition AnimatingColumn;

        private GridCollapseOrientation _gridCollapseDirection = GridCollapseOrientation.Auto;
        private GridLength _savedGridLength;
        private double _savedActualValue;
        private double _animationTimeMillis = 200;

        #region Direction dependency property
        /// <summary>
        /// Gets or sets a value that indicates the direction in which the row/colum
        /// will be located that is to be expanded and collapsed.
        /// </summary>
        public GridExpanderDirection Direction
        {
            get { return (GridExpanderDirection)GetValue(DirectionProperty); }
            set { SetValue(DirectionProperty, value); }
        }
        /// <summary>
        /// Identifies the Direction dependency property
        /// </summary>
        public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(
            "Direction",
            typeof(GridExpanderDirection),
            typeof(GridExpander),
            new PropertyMetadata(GridExpanderDirection.None, new PropertyChangedCallback(OnDirectionPropertyChanged)));
        #endregion

        #region HandleStyle dependency property
        ///<summary>
        /// Gets or sets the style that customizes the appearance of the vertical handle
        /// that is used to expand and collapse the GridExpander.
        /// </summary>
        public Style HandleStyle
        {
            get { return (Style)GetValue(HandleStyleProperty); }
            set { SetValue(HandleStyleProperty, value); }
        }
        /// <summary>
        /// Identifies the HandleStyle dependency property
        /// </summary>
        public static readonly DependencyProperty HandleStyleProperty = DependencyProperty.Register(
            "HandleStyle",
            typeof(Style),
            typeof(GridExpander),
            null);
        #endregion

        #region IsAnimated dependency property
        /// <summary>
        /// Gets or sets a value that indicates if the collapse and
        /// expanding actions should be animated.
        /// </summary>
        public bool IsAnimated
        {
            get { return (bool)GetValue(IsAnimatedProperty); }
            set { SetValue(IsAnimatedProperty, value); }
        }
        /// <summary>
        /// Identifies the IsAnimated dependency property
        /// </summary>
        public static readonly DependencyProperty IsAnimatedProperty = DependencyProperty.Register(
            "IsAnimated",
            typeof(bool),
            typeof(GridExpander),
            null);
        #endregion

        #region IsCollapsed dependency property
        /// <summary>
        /// Gets or sets a value that indicates if the target column is
        /// currently collapsed.
        /// </summary>
        public bool IsCollapsed
        {
            get { return (bool)GetValue(IsCollapsedProperty); }
            set { SetValue(IsCollapsedProperty, value); }
        }
        /// <summary>
        /// Identifies the IsCollapsed dependency property
        /// </summary>
        public static readonly DependencyProperty IsCollapsedProperty = DependencyProperty.Register(
            "IsCollapsed",
            typeof(bool),
            typeof(GridExpander),
            new PropertyMetadata(new PropertyChangedCallback(OnIsCollapsedPropertyChanged)));
        #endregion

        #region RowHeightAnimation dependency property
        private double RowHeightAnimation
        {
            get { return (double)GetValue(RowHeightAnimationProperty); }
            set { SetValue(RowHeightAnimationProperty, value); }
        }

        private static readonly DependencyProperty RowHeightAnimationProperty = DependencyProperty.Register(
            "RowHeightAnimation",
            typeof(double),
            typeof(GridExpander),
            new PropertyMetadata(new PropertyChangedCallback(RowHeightAnimationChanged)));
        #endregion

        #region ColWidthAnimation dependency property
        private double ColWidthAnimation
        {
            get { return (double)GetValue(ColWidthAnimationProperty); }
            set { SetValue(ColWidthAnimationProperty, value); }
        }

        private static readonly DependencyProperty ColWidthAnimationProperty = DependencyProperty.Register(
            "ColWidthAnimation",
            typeof(double),
            typeof(GridExpander),
            new PropertyMetadata(new PropertyChangedCallback(ColWidthAnimationChanged)));
        #endregion

        // Define Collapsed and Expanded evenets
        public event EventHandler<EventArgs> Collapsed;
        public event EventHandler<EventArgs> Expanded;

        static GridExpander()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(GridExpander),
                new FrameworkPropertyMetadata(typeof(GridExpander)));
        }

        /// <summary>
        /// Initializes a new instance of the GridExpander class,
        /// which inherits from System.Windows.Controls.GridExpander.
        /// </summary>
        public GridExpander()
        {
            // Set default values
            //DefaultStyleKey = typeof(GridExpander);

            VisualStateManager.GoToState(this, "Checked", false);

            //Direction = GridExpanderDirection.None;
            IsAnimated = true;
            this.LayoutUpdated += delegate { _gridCollapseDirection = GetCollapseDirection(); };

            // All GridExpander visual states are handled by the parent GridSplitter class.
        }

        /// <summary>
        /// This method is called when the tempalte should be applied to the control.
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            _expanderButton = GetTemplateChild(ElementHandleName) as ToggleButton;
            _elementGridExpanderBackground = GetTemplateChild(ElementGridExpanderBackground) as Rectangle;

            // Wire up the Checked and Unchecked events of the VerticalGridExpanderHandle.
            if (_expanderButton != null)
            {
                _expanderButton.Checked += new RoutedEventHandler(GridExpanderButton_Checked);
                _expanderButton.Unchecked += new RoutedEventHandler(GridExpanderButton_Unchecked);
            }

            // Set default direction since we don't have all the components layed out yet.
            _gridCollapseDirection = GridCollapseOrientation.Auto;

            // Directely call these events so design-time view updates appropriately
            OnDirectionChanged(Direction);
            OnIsCollapsedChanged(IsCollapsed);
        }

        /// <summary>
        /// Handles the property change event of the IsCollapsed property.
        /// </summary>
        /// <param name="isCollapsed">The new value for the IsCollapsed property.</param>
        protected virtual void OnIsCollapsedChanged(bool isCollapsed)
        {
            _expanderButton.IsChecked = isCollapsed;
        }

        /// <summary>
        /// Handles the property change event of the Direction property.
        /// </summary>
        /// <param name="direction">The new value for the Direction property.</param>
        protected virtual void OnDirectionChanged(GridExpanderDirection direction)
        {
            if (_expanderButton == null)
            {
                // There is no expander button so don't attempt to modify it
                return;
            }

            // TODO: Use triggers for setting visibility conditionally instead of doing it here
            if (direction == GridExpanderDirection.None)
            {
                // Hide the handles if the Direction is set to None.
                _expanderButton.Visibility = Visibility.Collapsed;
            }
            else
            {
                // Ensure the handle is Visible.
                _expanderButton.Visibility = Visibility.Visible;
            }

        }

        /// <summary>
        /// Raises the Collapsed event.
        /// </summary>
        /// <param name="e">Contains event arguments.</param>
        protected virtual void OnCollapsed(EventArgs e)
        {
            EventHandler<EventArgs> handler = this.Collapsed;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the Expanded event.
        /// </summary>
        /// <param name="e">Contains event arguments.</param>
        protected virtual void OnExpanded(EventArgs e)
        {
            EventHandler<EventArgs> handler = this.Expanded;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Collapses the target ColumnDefinition or RowDefinition.
        /// </summary>
        private void Collapse()
        {
            Grid parentGrid = base.Parent as Grid;
            int splitterIndex = int.MinValue;

            if (_gridCollapseDirection == GridCollapseOrientation.Rows)
            {
                // Get the index of the row containing the splitter
                splitterIndex = (int)base.GetValue(Grid.RowProperty);

                // Determing the curent Direction
                if (this.Direction == GridExpanderDirection.Next)
                {
                    // Save the next rows Height information
                    _savedGridLength = parentGrid.RowDefinitions[splitterIndex + 1].Height;
                    _savedActualValue = parentGrid.RowDefinitions[splitterIndex + 1].ActualHeight;

                    // Collapse the next row
                    if (IsAnimated)
                        AnimateCollapse(parentGrid.RowDefinitions[splitterIndex + 1]);
                    else
                        parentGrid.RowDefinitions[splitterIndex + 1].SetValue(RowDefinition.HeightProperty, new GridLength(0));
                }
                else
                {
                    // Save the previous row's Height information
                    _savedGridLength = parentGrid.RowDefinitions[splitterIndex - 1].Height;
                    _savedActualValue = parentGrid.RowDefinitions[splitterIndex - 1].ActualHeight;

                    // Collapse the previous row
                    if (IsAnimated)
                        AnimateCollapse(parentGrid.RowDefinitions[splitterIndex - 1]);
                    else
                        parentGrid.RowDefinitions[splitterIndex - 1].SetValue(RowDefinition.HeightProperty, new GridLength(0));
                }
            }
            else
            {
                // Get the index of the column containing the splitter
                splitterIndex = (int)base.GetValue(Grid.ColumnProperty);

                // Determing the curent Direction
                if (this.Direction == GridExpanderDirection.Next)
                {
                    // Save the next column's Width information
                    _savedGridLength = parentGrid.ColumnDefinitions[splitterIndex + 1].Width;
                    _savedActualValue = parentGrid.ColumnDefinitions[splitterIndex + 1].ActualWidth;

                    // Collapse the next column
                    if (IsAnimated)
                        AnimateCollapse(parentGrid.ColumnDefinitions[splitterIndex + 1]);
                    else
                        parentGrid.ColumnDefinitions[splitterIndex + 1].SetValue(ColumnDefinition.WidthProperty, new GridLength(0));
                }
                else
                {
                    // Save the previous column's Width information
                    _savedGridLength = parentGrid.ColumnDefinitions[splitterIndex - 1].Width;
                    _savedActualValue = parentGrid.ColumnDefinitions[splitterIndex - 1].ActualWidth;

                    // Collapse the previous column
                    if (IsAnimated)
                        AnimateCollapse(parentGrid.ColumnDefinitions[splitterIndex - 1]);
                    else
                        parentGrid.ColumnDefinitions[splitterIndex - 1].SetValue(ColumnDefinition.WidthProperty, new GridLength(0));
                }
            }

        }

        /// <summary>
        /// Expands the target ColumnDefinition or RowDefinition.
        /// </summary>
        private void Expand()
        {
            Grid parentGrid = base.Parent as Grid;
            int splitterIndex = int.MinValue;

            if (_gridCollapseDirection == GridCollapseOrientation.Rows)
            {
                // Get the index of the row containing the splitter
                splitterIndex = (int)this.GetValue(Grid.RowProperty);

                // Determine the curent Direction
                if (this.Direction == GridExpanderDirection.Next)
                {
                    // Expand the next row
                    if (IsAnimated)
                        AnimateExpand(parentGrid.RowDefinitions[splitterIndex + 1]);
                    else
                        parentGrid.RowDefinitions[splitterIndex + 1].SetValue(RowDefinition.HeightProperty, _savedGridLength);
                }
                else
                {
                    // Expand the previous row
                    if (IsAnimated)
                        AnimateExpand(parentGrid.RowDefinitions[splitterIndex - 1]);
                    else
                        parentGrid.RowDefinitions[splitterIndex - 1].SetValue(RowDefinition.HeightProperty, _savedGridLength);
                }
            }
            else
            {
                // Get the index of the column containing the splitter
                splitterIndex = (int)this.GetValue(Grid.ColumnProperty);

                // Determine the curent Direction
                if (this.Direction == GridExpanderDirection.Next)
                {
                    // Expand the next column
                    if (IsAnimated)
                        AnimateExpand(parentGrid.ColumnDefinitions[splitterIndex + 1]);
                    else
                        parentGrid.ColumnDefinitions[splitterIndex + 1].SetValue(ColumnDefinition.WidthProperty, _savedGridLength);
                }
                else
                {
                    // Expand the previous column
                    if (IsAnimated)
                        AnimateExpand(parentGrid.ColumnDefinitions[splitterIndex - 1]);
                    else
                        parentGrid.ColumnDefinitions[splitterIndex - 1].SetValue(ColumnDefinition.WidthProperty, _savedGridLength);
                }
            }
        }

        /// <summary>
        /// Determine the collapse direction based on the horizontal and vertical alignments
        /// </summary>
        private GridCollapseOrientation GetCollapseDirection()
        {
            if (base.HorizontalAlignment != HorizontalAlignment.Stretch)
            {
                return GridCollapseOrientation.Columns;
            }

            if ((base.VerticalAlignment == VerticalAlignment.Stretch) && (base.ActualWidth <= base.ActualHeight))
            {
                return GridCollapseOrientation.Columns;
            }

            return GridCollapseOrientation.Rows;
        }

        /// <summary>
        /// Handles the Checked event of either the Vertical or Horizontal
        /// GridExpanderHandle ToggleButton.
        /// </summary>
        /// <param name="sender">An instance of the ToggleButton that fired the event.</param>
        /// <param name="e">Contains event arguments for the routed event that fired.</param>
        private void GridExpanderButton_Checked(object sender, RoutedEventArgs e)
        {
            if (IsCollapsed != true)
            {
                // In our case, Checked = Collapsed.  Which means we want everything
                // ready to be expanded.
                Collapse();

                IsCollapsed = true;

                // Deactivate the background so the splitter can not be dragged.
                _elementGridExpanderBackground.IsHitTestVisible = false;
                //_elementGridExpanderBackground.Opacity = 0.5;

                // Raise the Collapsed event.
                OnCollapsed(EventArgs.Empty);
            }
        }

        /// <summary>
        /// Handles the Unchecked event of either the Vertical or Horizontal
        /// GridExpanderHandle ToggleButton.
        /// </summary>
        /// <param name="sender">An instance of the ToggleButton that fired the event.</param>
        /// <param name="e">Contains event arguments for the routed event that fired.</param>
        private void GridExpanderButton_Unchecked(object sender, RoutedEventArgs e)
        {
            if (IsCollapsed != false)
            {
                // In our case, Unchecked = Expanded.  Which means we want everything
                // ready to be collapsed.
                Expand();

                IsCollapsed = false;

                // Activate the background so the splitter can be dragged again.
                _elementGridExpanderBackground.IsHitTestVisible = true;
                //_elementGridExpanderBackground.Opacity = 1;

                // Raise the Expanded event.
                OnExpanded(EventArgs.Empty);
            }
        }

        /// <summary>
        /// The IsCollapsed property porperty changed handler.
        /// </summary>
        /// <param name="d">GridExpander that changed IsCollapsed.</param>
        /// <param name="e">An instance of DependencyPropertyChangedEventArgs.</param>
        private static void OnIsCollapsedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            GridExpander s = d as GridExpander;

            bool value = (bool)e.NewValue;
            s.OnIsCollapsedChanged(value);
        }

        /// <summary>
        /// The DirectionProperty property changed handler.
        /// </summary>
        /// <param name="d">GridExpander that changed IsCollapsed.</param>
        /// <param name="e">An instance of DependencyPropertyChangedEventArgs.</param>
        private static void OnDirectionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            GridExpander s = d as GridExpander;

            GridExpanderDirection value = (GridExpanderDirection)e.NewValue;
            s.OnDirectionChanged(value);
        }

        private static void RowHeightAnimationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as GridExpander).AnimatingRow.Height = new GridLength((double)e.NewValue);
        }

        private static void ColWidthAnimationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as GridExpander).AnimatingColumn.Width = new GridLength((double)e.NewValue);
        }

        /// <summary>
        /// Uses DoubleAnimation and a StoryBoard to animated the collapsing
        /// of the specificed ColumnDefinition or RowDefinition.
        /// </summary>
        /// <param name="definition">The RowDefinition or ColumnDefintition that will be collapsed.</param>
        private void AnimateCollapse(object definition)
        {
            double currentValue;

            // Setup the animation and StoryBoard
            DoubleAnimation gridLengthAnimation = new DoubleAnimation() { Duration = new Duration(TimeSpan.FromMilliseconds(_animationTimeMillis)) };
            Storyboard sb = new Storyboard();

            // Add the animation to the StoryBoard
            sb.Children.Add(gridLengthAnimation);

            if (_gridCollapseDirection == GridCollapseOrientation.Rows)
            {
                // Specify the target RowDefinition and property (Height) that will be altered by the animation.
                this.AnimatingRow = (RowDefinition)definition;
                Storyboard.SetTarget(gridLengthAnimation, this);
                Storyboard.SetTargetProperty(gridLengthAnimation, new PropertyPath("RowHeightAnimation"));

                currentValue = AnimatingRow.ActualHeight;
            }
            else
            {
                // Specify the target ColumnDefinition and property (Width) that will be altered by the animation.
                this.AnimatingColumn = (ColumnDefinition)definition;
                Storyboard.SetTarget(gridLengthAnimation, this);
                Storyboard.SetTargetProperty(gridLengthAnimation, new PropertyPath("ColWidthAnimation"));

                currentValue = AnimatingColumn.ActualWidth;
            }

            gridLengthAnimation.From = currentValue;
            gridLengthAnimation.To = 0;

            // Start the StoryBoard.
            sb.Begin();
        }

        /// <summary>
        /// Uses DoubleAnimation and a StoryBoard to animate the expansion
        /// of the specificed ColumnDefinition or RowDefinition.
        /// </summary>
        /// <param name="definition">The RowDefinition or ColumnDefintition that will be expanded.</param>
        private void AnimateExpand(object definition)
        {
            double currentValue;

            // Setup the animation and StoryBoard
            DoubleAnimation gridLengthAnimation = new DoubleAnimation() { Duration = new Duration(TimeSpan.FromMilliseconds(_animationTimeMillis)) };
            Storyboard sb = new Storyboard();

            // Add the animation to the StoryBoard
            sb.Children.Add(gridLengthAnimation);

            if (_gridCollapseDirection == GridCollapseOrientation.Rows)
            {
                // Specify the target RowDefinition and property (Height) that will be altered by the animation.
                this.AnimatingRow = (RowDefinition)definition;
                Storyboard.SetTarget(gridLengthAnimation, this);
                Storyboard.SetTargetProperty(gridLengthAnimation, new PropertyPath("RowHeightAnimation"));

                currentValue = AnimatingRow.ActualHeight;
            }
            else
            {
                // Specify the target ColumnDefinition and property (Width) that will be altered by the animation.
                this.AnimatingColumn = (ColumnDefinition)definition;
                Storyboard.SetTarget(gridLengthAnimation, this);
                Storyboard.SetTargetProperty(gridLengthAnimation, new PropertyPath("ColWidthAnimation"));

                currentValue = AnimatingColumn.ActualWidth;
            }
            gridLengthAnimation.From = currentValue;
            gridLengthAnimation.To = _savedActualValue;

            // Start the StoryBoard.
            sb.Begin();
        }

        /// <summary>
        /// An enumeration that specifies the direction the GridExpander will
        /// be collapased (Rows or Columns).
        /// </summary>
        private enum GridCollapseOrientation
        {
            Auto,
            Columns,
            Rows
        }
    }
}

GridExpander.Generic.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:Rgs.Presentation.Wpf;assembly="
                    xmlns:vsm="http://schemas.microsoft.com/wpf/2008/toolkit">

    <!-- GridExpander Handle Style -->
    <Style x:Key="ExpanderHandleStyle"
           TargetType="ToggleButton">
        <Setter Property="Cursor"
                Value="Hand" />
        <Setter Property="IsChecked"
                Value="False" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ToggleButton">
                    <Grid x:Name="Root">

                        <!-- VSM -->
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:00.2"
                                                      To="MouseOver" />
                                    <VisualTransition GeneratedDuration="0:0:00.2"
                                                      To="Normal" />
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Normal">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="BackgroundBorder"
                                                         Storyboard.TargetProperty="Opacity"
                                                         Duration="00:00:00.3"
                                                         To="0.4" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="MouseOver">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="BackgroundBorder"
                                                         Storyboard.TargetProperty="Opacity"
                                                         Duration="00:00:00.3"
                                                         To="1" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>

                        <Grid HorizontalAlignment="Stretch"
                              VerticalAlignment="Stretch">
                            <!-- Background -->
                            <Border x:Name="BackgroundBorder"
                                    CornerRadius="3"
                                    BorderBrush="#FF000000"
                                    BorderThickness="0"
                                    HorizontalAlignment="Stretch"
                                    VerticalAlignment="Stretch"
                                    Background="Gray"
                                    Opacity="0" />

                            <!-- Vertical Handle Icon -->
                            <Path Data="M0,0 L0,6 L3,3 Z"
                                  Fill="{TemplateBinding Foreground}"
                                  HorizontalAlignment="Center"
                                  VerticalAlignment="Center"
                                  DataContext="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:GridExpander}}}"
                                  x:Name="ArrowShape"
                                  RenderTransformOrigin="0.5,0.5">
                                <Path.Style>
                                    <Style TargetType="{x:Type Path}">
                                        <Setter Property="RenderTransform">
                                            <Setter.Value>
                                                <RotateTransform Angle="0" />
                                            </Setter.Value>
                                        </Setter>
                                        <Style.Triggers>

                                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type ToggleButton}}, Path=IsChecked}"
                                                         Value="True">
                                                <DataTrigger.EnterActions>
                                                    <BeginStoryboard>
                                                        <Storyboard>
                                                            <DoubleAnimation Storyboard.TargetProperty="RenderTransform.Angle"
                                                                             By="180"
                                                                             Duration="00:00:00" />
                                                        </Storyboard>
                                                    </BeginStoryboard>
                                                </DataTrigger.EnterActions>
                                                <DataTrigger.ExitActions>
                                                    <BeginStoryboard>
                                                        <Storyboard>
                                                            <DoubleAnimation Storyboard.TargetProperty="RenderTransform.Angle"
                                                                             By="-180"
                                                                             Duration="00:00:00" />
                                                        </Storyboard>
                                                    </BeginStoryboard>
                                                </DataTrigger.ExitActions>
                                            </DataTrigger>

                                            <MultiDataTrigger>
                                                <MultiDataTrigger.Conditions>
                                                    <Condition Binding="{Binding Path=Direction}"
                                                               Value="Previous" />
                                                    <Condition Binding="{Binding Path=VerticalAlignment}"
                                                               Value="Stretch" />
                                                </MultiDataTrigger.Conditions>
                                                <MultiDataTrigger.Setters>
                                                    <Setter Property="RenderTransform">
                                                        <Setter.Value>
                                                            <RotateTransform Angle="180" />
                                                        </Setter.Value>
                                                    </Setter>
                                                </MultiDataTrigger.Setters>
                                            </MultiDataTrigger>

                                            <MultiDataTrigger>
                                                <MultiDataTrigger.Conditions>
                                                    <Condition Binding="{Binding Path=Direction}"
                                                               Value="Previous" />
                                                    <Condition Binding="{Binding Path=HorizontalAlignment}"
                                                               Value="Stretch" />
                                                </MultiDataTrigger.Conditions>
                                                <MultiDataTrigger.Setters>
                                                    <Setter Property="RenderTransform">
                                                        <Setter.Value>
                                                            <RotateTransform Angle="-90" />
                                                        </Setter.Value>
                                                    </Setter>
                                                </MultiDataTrigger.Setters>
                                            </MultiDataTrigger>

                                            <MultiDataTrigger>
                                                <MultiDataTrigger.Conditions>
                                                    <Condition Binding="{Binding Path=Direction}"
                                                               Value="Next" />
                                                    <Condition Binding="{Binding Path=VerticalAlignment}"
                                                               Value="Stretch" />
                                                </MultiDataTrigger.Conditions>
                                                <MultiDataTrigger.Setters>
                                                    <Setter Property="RenderTransform">
                                                        <Setter.Value>
                                                            <RotateTransform Angle="0" />
                                                        </Setter.Value>
                                                    </Setter>
                                                </MultiDataTrigger.Setters>
                                            </MultiDataTrigger>

                                            <MultiDataTrigger>
                                                <MultiDataTrigger.Conditions>
                                                    <Condition Binding="{Binding Path=Direction}"
                                                               Value="Next" />
                                                    <Condition Binding="{Binding Path=HorizontalAlignment}"
                                                               Value="Stretch" />
                                                </MultiDataTrigger.Conditions>
                                                <MultiDataTrigger.Setters>
                                                    <Setter Property="RenderTransform">
                                                        <Setter.Value>
                                                            <RotateTransform Angle="90" />
                                                        </Setter.Value>
                                                    </Setter>
                                                </MultiDataTrigger.Setters>
                                            </MultiDataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </Path.Style>
                            </Path>
                        </Grid>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!-- GridExpander Preview -->
    <Style x:Key="GridExpanderPreviewStyle"
           TargetType="Control">
        <Setter Property="Background"
                Value="#FF868686" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Control">
                    <Grid x:Name="Root"
                          Opacity=".5">

                        <!-- Background -->
                        <Rectangle Fill="{TemplateBinding Background}" />

                        <Grid x:Name="TheTemplate"
                              Width="6">
                            <!-- Just show the faint gray grid splitter rectangle with no other details -->
                        </Grid>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!-- GridExpander -->
    <Style x:Key="{x:Type local:GridExpander}"
           TargetType="{x:Type local:GridExpander}">
        <Setter Property="Background"
                Value="Transparent" />
        <Setter Property="IsTabStop"
                Value="true" />
        <Setter Property="PreviewStyle"
                Value="{StaticResource GridExpanderPreviewStyle}" />
        <Setter Property="HandleStyle"
                Value="{StaticResource ExpanderHandleStyle}" />
        <Setter Property="HorizontalAlignment"
                Value="Center" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:GridExpander">
                    <Grid x:Name="Root"
                          IsHitTestVisible="{TemplateBinding IsEnabled}">

                        <!-- VSM -->
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="MouseOver" />
                                <VisualState x:Name="Disabled">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="Root"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="0.5"
                                                         Duration="0" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="FocusStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0" />
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Unfocused" />
                                <VisualState x:Name="Focused">
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="FocusVisual"
                                                         Storyboard.TargetProperty="Opacity"
                                                         To="1"
                                                         Duration="0" />
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>

                        <!-- Background -->
                        <Rectangle x:Name="GridExpanderBackground"
                                   Fill="{TemplateBinding Background}"
                                   StrokeThickness="0" />

                        <!-- Element Template -->
                        <Grid x:Name="TheTemplate"
                              HorizontalAlignment="Stretch"
                              VerticalAlignment="Stretch"
                              MaxWidth="50"
                              MaxHeight="50">
                            <!-- GridExpander Handle -->
                            <ToggleButton x:Name="ExpanderHandle"
                                          Grid.Row="1"
                                          IsHitTestVisible="True"
                                          Style="{TemplateBinding HandleStyle}"
                                          RenderTransformOrigin="0.5,0.5">
                            </ToggleButton>
                        </Grid>

                        <!-- Focus Visual -->
                        <Rectangle x:Name="FocusVisual"
                                   Stroke="#FF6DBDD1"
                                   StrokeThickness="1"
                                   Opacity="0"
                                   IsHitTestVisible="false" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

About these ads

6 responses to “GridExpander for WPF

  1. Hi,

    Do you have any examples of this control in use? I’m having a heck of a time trying to even get an example implementation done.

    Cheers,

    John

    • Yes, I have a similar concept currently in production code. I can’t share the exact code for obvious reasons but I can assure you that, bugs aside, the general concept works as described.

  2. Thanks Mate, It worked great for me. saved lots of time. however I remarked the TemplateVisualState attributes as I had conflict in namespaces in class file and wasn’t a big deal for me.. good work.

  3. Hi kainhart,

    I’m trying to implement this on an application and it send me no errors but when i add the component into a row inside a grid it doesnt seems to work at all. What are the main configuration i need to do to have it working?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s