

Introduction
This is a WPF Explorer Bar similar to the Explorer bar in Windows XP. An explorer bar usually contains one ore more collapsible panels, as shown above.
Using the code
WPF offers very nice animation features to allow almost anything you can imagine. Unfortunately, for a generic panel that supports animation while expanding/collapsing, it's not just as simple as defining DoubleAnimation to the Height property. Although it would work for ScaleTransform.ScaleY, the effect would be different to what we see in XP. Therefore, I use a custom Decorator control, which, in a few words, is a panel that can contain only one child. The AnimationDecorator has an IsExpanded property that specifies whether the decorator is expanded or collapsed. To perform the animation, I added a helper property named YOffset that will be animated. YOffset has a range from 0 to the ActualHeight of the decorator, and is used at ArrangeOverride and MeasureOverride to perform the animation. The code looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace Odyssey.Controls
{
public class AnimationDecorator : Decorator
{
static AnimationDecorator()
{
}
public AnimationDecorator()
: base()
{
ClipToBounds = true;
}
public bool OpacityAnimation
{
get { return (bool)GetValue(OpacityAnimationProperty); }
set { SetValue(OpacityAnimationProperty, value); }
}
public static readonly DependencyProperty OpacityAnimationProperty =
DependencyProperty.Register("OpacityAnimation",
typeof(bool),
typeof(AnimationDecorator),
new UIPropertyMetadata(true));
public bool IsExpanded
{
get { return (bool)GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.Register("IsExpanded",
typeof(bool),
typeof(AnimationDecorator),
new PropertyMetadata(true,IsExpandedChanged));
public static void IsExpandedChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
AnimationDecorator expander = d as AnimationDecorator;
bool expanded = (bool)e.NewValue;
expander.DoAnimate(expanded);
}
public DoubleAnimation HeightAnimation
{
get { return (DoubleAnimation)GetValue(HeightAnimationProperty); }
set { SetValue(HeightAnimationProperty, value); }
}
public static readonly DependencyProperty HeightAnimationProperty =
DependencyProperty.Register("HeightAnimation",
typeof(DoubleAnimation),
typeof(AnimationDecorator),
new UIPropertyMetadata(null));
public Duration Duration
{
get { return (Duration)GetValue(DurationProperty); }
set { SetValue(DurationProperty, value); }
}
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(Duration),
typeof(AnimationDecorator),
new UIPropertyMetadata(new Duration(new TimeSpan(0,0,0,400))));
private void DoAnimate(bool expanded)
{
if (Child != null)
{
if (YOffset > 0) YOffset = 0;
if (-YOffset > Child.DesiredSize.Height)
YOffset = -Child.DesiredSize.Height;
DoubleAnimation animation = HeightAnimation;
if (animation == null)
{
animation = new DoubleAnimation();
animation.DecelerationRatio = 0.9;
animation.Duration = Duration;
}
animation.From = null;
animation.To = expanded ? 0 : -Child.DesiredSize.Height;
this.BeginAnimation(AnimationDecorator.YOffsetProperty, animation);
if (OpacityAnimation)
{
animation.From = null;
animation.To = expanded ? 1 : 0;
this.BeginAnimation(Control.OpacityProperty, animation);
}
}
else
{
YOffset = int.MinValue;
}
}
protected void SetYOffset(bool expanded)
{
YOffset = expanded ? 0 : -Child.DesiredSize.Height;
}
internal Double YOffset
{
get { return (Double)GetValue(YOffsetProperty); }
set { SetValue(YOffsetProperty, value); }
}
public static readonly DependencyProperty YOffsetProperty =
DependencyProperty.Register("YOffset",
typeof(Double), typeof(AnimationDecorator),
new FrameworkPropertyMetadata(0.0,
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.AffectsArrange
| FrameworkPropertyMetadataOptions.AffectsMeasure));
protected override Size MeasureOverride(Size constraint)
{
if (Child == null) return new Size(0, 0);
Child.Measure(new Size(Double.PositiveInfinity,
Double.PositiveInfinity));
Size size = new Size();
size.Width = DesiredSize.Width;
size.Height = Child.DesiredSize.Height;
Double h = size.Height + YOffset;
if (h < 0) h = 0;
size.Height = h;
if (Child != null) Child.IsEnabled = h > 0;
return size;
}
protected override Size ArrangeOverride(Size arrangeSize)
{
if (Child == null) return arrangeSize;
Size size = new Size();
size.Width = arrangeSize.Width;
size.Height = Child.DesiredSize.Height;
Point p = new Point(0, YOffset);
Child.Arrange(new Rect(p, size));
Double h = Child.DesiredSize.Height + YOffset;
if (h < 0) h = 0;
size.Height = h;
return size;
}
}
}
The OdcExpander itself contains various properties to customize the skin of the control. Actually, I intended to keep some properties internal, such as MouseOverHeaderForeground that specifies the foreground color of the header on mouse-over, or PressedHeaderBackground, etc., but this wouldn't allow you to easily apply custom skins since you would have to completely describe the control template instead of just modifying some properties.
public class OdcExpander : HeaderedContentControl
{
static OdcExpander()
{
MarginProperty.OverrideMetadata(
typeof(OdcExpander),
new FrameworkPropertyMetadata(new Thickness(10, 10, 10, 2)));
FocusableProperty.OverrideMetadata(typeof(OdcExpander),
new FrameworkPropertyMetadata(false));
DefaultStyleKeyProperty.OverrideMetadata(typeof(OdcExpander),
new FrameworkPropertyMetadata(typeof(OdcExpander)));
}
public static string Skin { get; set; }
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
ApplySkin();
}
public void ApplySkin()
{
if (!string.IsNullOrEmpty(Skin))
{
Uri uri = new Uri(Skin, UriKind.Absolute);
ResourceDictionary skin = new ResourceDictionary();
skin.Source = uri;
this.Resources = skin;
}
}
public Brush HeaderBorderBrush
{
get { return (Brush)GetValue(HeaderBorderBrushProperty); }
set { SetValue(HeaderBorderBrushProperty, value); }
}
public static readonly DependencyProperty HeaderBorderBrushProperty =
DependencyProperty.Register("HeaderBorderBrush",
typeof(Brush), typeof(OdcExpander),
new UIPropertyMetadata(Brushes.Gray));
public Brush HeaderBackground
{
get { return (Brush)GetValue(HeaderBackgroundProperty); }
set { SetValue(HeaderBackgroundProperty, value); }
}
public static readonly DependencyProperty HeaderBackgroundProperty =
DependencyProperty.Register("HeaderBackground",
typeof(Brush), typeof(OdcExpander),
new UIPropertyMetadata(Brushes.Silver));
public bool IsMinimized
{
get { return (bool)GetValue(MinimizedProperty); }
set { SetValue(MinimizedProperty, value); }
}
public static readonly DependencyProperty MinimizedProperty =
DependencyProperty.Register("IsMinimized",
typeof(bool), typeof(OdcExpander),
new UIPropertyMetadata(false, IsMinimizedChanged));
public static void IsMinimizedChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
OdcExpander expander = d as OdcExpander;
RoutedEventArgs args = new RoutedEventArgs((bool)e.NewValue ?
MinimizedEvent : MaximizedEvent);
expander.RaiseEvent(args);
}
public ImageSource Image
{
get { return (ImageSource)GetValue(ImageProperty); }
set { SetValue(ImageProperty, value); }
}
public static readonly DependencyProperty ImageProperty =
DependencyProperty.Register("Image",
typeof(ImageSource), typeof(OdcExpander),
new UIPropertyMetadata(null));
public bool IsExpanded
{
get { return (bool)GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
public event RoutedEventHandler Expanded
{
add { AddHandler(ExpandedEvent, value); }
remove { RemoveHandler(ExpandedEvent, value); }
}
public event RoutedEventHandler Collapsed
{
add { AddHandler(CollapsedEvent, value); }
remove { RemoveHandler(CollapsedEvent, value); }
}
public event RoutedEventHandler Minimized
{
add { AddHandler(MinimizedEvent, value); }
remove { RemoveHandler(MinimizedEvent, value); }
}
public event RoutedEventHandler Maximized
{
add { AddHandler(MaximizedEvent, value); }
remove { RemoveHandler(MaximizedEvent, value); }
}
#region dependency properties and routed events definition
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.Register(
"IsExpanded",
typeof(bool),
typeof(OdcExpander),
new UIPropertyMetadata(true, IsExpandedChanged));
public static void IsExpandedChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
OdcExpander expander = d as OdcExpander;
RoutedEventArgs args = new RoutedEventArgs((bool)e.NewValue ?
ExpandedEvent : CollapsedEvent);
expander.RaiseEvent(args);
}
public static readonly RoutedEvent ExpandedEvent =
EventManager.RegisterRoutedEvent(
"ExpandedEvent",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(OdcExpander));
public static readonly RoutedEvent CollapsedEvent =
EventManager.RegisterRoutedEvent(
"CollapsedEvent",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(OdcExpander));
public static readonly RoutedEvent MinimizedEvent =
EventManager.RegisterRoutedEvent(
"MinimizedEvent",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(OdcExpander));
public static readonly RoutedEvent MaximizedEvent =
EventManager.RegisterRoutedEvent(
"MaximizedEvent",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(OdcExpander));
#endregion
public CornerRadius CornerRadius
{
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register("CornerRadius", typeof(CornerRadius),
typeof(OdcExpander), new UIPropertyMetadata(null));
public Brush MouseOverHeaderBackground
{
get { return (Brush)GetValue(MouseOverHeaderBackgroundProperty); }
set { SetValue(MouseOverHeaderBackgroundProperty, value); }
}
public static readonly DependencyProperty MouseOverHeaderBackgroundProperty =
DependencyProperty.Register("MouseOverHeaderBackground",
typeof(Brush), typeof(OdcExpander), new UIPropertyMetadata(null));
public bool HasPressedBackground
{
get { return (bool)GetValue(HasPressedBackgroundProperty); }
set { SetValue(HasPressedBackgroundProperty, value); }
}
public static readonly DependencyProperty HasPressedBackgroundProperty =
DependencyProperty.Register("HasPressedBackground",
typeof(bool), typeof(OdcExpander), new UIPropertyMetadata(false));
public Brush PressedHeaderBackground
{
get { return (Brush)GetValue(PressedHeaderBackgroundProperty); }
set { SetValue(PressedHeaderBackgroundProperty, value); }
}
public static readonly DependencyProperty PressedHeaderBackgroundProperty =
DependencyProperty.Register("PressedHeaderBackground",
typeof(Brush), typeof(OdcExpander),
new UIPropertyMetadata(null, PressedHeaderBackgroundPropertyChangedCallback));
public static void PressedHeaderBackgroundPropertyChangedCallback(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
OdcExpander expander = (OdcExpander)d;
expander.HasPressedBackground = e.NewValue != null;
}
public Thickness HeaderBorderThickness
{
get { return (Thickness)GetValue(HeaderBorderThicknessProperty); }
set { SetValue(HeaderBorderThicknessProperty, value); }
}
public static readonly DependencyProperty HeaderBorderThicknessProperty =
DependencyProperty.Register("HeaderBorderThickness",
typeof(Thickness), typeof(OdcExpander), new UIPropertyMetadata(null));
public Brush MouseOverHeaderForeground
{
get { return (Brush)GetValue(MouseOverHeaderForegroundProperty); }
set { SetValue(MouseOverHeaderForegroundProperty, value); }
}
public static readonly DependencyProperty MouseOverHeaderForegroundProperty =
DependencyProperty.Register("MouseOverHeaderForeground",
typeof(Brush), typeof(OdcExpander), new UIPropertyMetadata(null));
public bool ShowEllipse
{
get { return (bool)GetValue(ShowEllipseProperty); }
set { SetValue(ShowEllipseProperty, value); }
}
public static readonly DependencyProperty ShowEllipseProperty =
DependencyProperty.Register("ShowEllipse", typeof(bool),
typeof(OdcExpander), new UIPropertyMetadata(false));
}
To simplify the design of the header of the OdcExpander, I wrote a helper control named OdcExpanderHeader, and added some properties for skinning:
internal class OdcExpanderHeader : ToggleButton
{
static OdcExpanderHeader()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(OdcExpanderHeader),
new FrameworkPropertyMetadata(typeof(OdcExpanderHeader)));
}
public bool HasExpandGeometry
{
get { return (bool)GetValue(HasExpandGeometryProperty); }
set { SetValue(HasExpandGeometryProperty, value); }
}
public static readonly DependencyProperty HasExpandGeometryProperty =
DependencyProperty.Register("HasExpandGeometry", typeof(bool),
typeof(OdcExpanderHeader), new UIPropertyMetadata(false));
public Geometry CollapseGeometry
{
get { return (Geometry)GetValue(CollapseGeometryProperty); }
set { SetValue(CollapseGeometryProperty, value); }
}
public static readonly DependencyProperty CollapseGeometryProperty =
DependencyProperty.Register("CollapseGeometry",
typeof(Geometry), typeof(OdcExpanderHeader),
new UIPropertyMetadata(null));
public static void CollapseGeometryChangedCallback(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
OdcExpanderHeader eh = d as OdcExpanderHeader;
eh.HasExpandGeometry = e.NewValue != null;
}
public Geometry ExpandGeometry
{
get { return (Geometry)GetValue(ExpandGeometryProperty); }
set { SetValue(ExpandGeometryProperty, value); }
}
public static readonly DependencyProperty ExpandGeometryProperty =
DependencyProperty.Register("ExpandGeometry", typeof(Geometry),
typeof(OdcExpanderHeader), new UIPropertyMetadata(null,
CollapseGeometryChangedCallback));
public CornerRadius CornerRadius
{
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register("CornerRadius", typeof(CornerRadius),
typeof(OdcExpanderHeader), new UIPropertyMetadata(null));
public bool ShowEllipse
{
get { return (bool)GetValue(ShowEllipseProperty); }
set { SetValue(ShowEllipseProperty, value); }
}
public static readonly DependencyProperty ShowEllipseProperty =
DependencyProperty.Register("ShowEllipse", typeof(bool),
typeof(OdcExpanderHeader), new UIPropertyMetadata(true));
public ImageSource Image
{
get { return (ImageSource)GetValue(ImageProperty); }
set { SetValue(ImageProperty, value); }
}
public static readonly DependencyProperty ImageProperty =
DependencyProperty.Register("Image", typeof(ImageSource),
typeof(OdcExpanderHeader), new UIPropertyMetadata(null));
}
Note that I added some properties like HasExpandGeometry. Such properties help to conditionaly define the XAML file, as follows:
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="True"/>
<Condition Property="HasExpandGeometry" Value="True"/>
</MultiTrigger.Conditions>
<Setter TargetName="path" Property="Data"
Value="{Binding RelativeSource={RelativeSource
TemplatedParent},Path=ExpandGeometry}"/>
</MultiTrigger>
In this case, the Multitrigger checks the IsChecked property of the OdcExpanderHelper (which is derived from ToggleButton) together with the HasExpandedGeometry, and only if both values are true, a setter changes the Data of the Path control.
Themes
The OdcExpander supports themes that depend on the current theme of the OS. Therefore, the OdcExpander looks different on Vista and XP.


The theme is only different when a OdcExpander is inside a ExplorerBar; otherwise, it always uses the same generic style. For instance, the part for the XP metallic snippet looks like:
<Style TargetType="{x:Type local:ExplorerBar}">
<Setter Property="Background" Value="#FFBDBAD6"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsPresenter/>
</ScrollViewer>
</Border>
<ControlTemplate.Resources>
<Style TargetType="{x:Type local:OdcExpander}">
<Setter Property="HeaderBorderThickness" Value="0"/>
<Setter Property="HeaderBackground"
Value="{StaticResource HeaderBackgroundBrush}"/>
<Setter Property="Background" Value="{StaticResource ExpanderBg}"/>
<Setter Property="MouseOverHeaderBackground"
Value="{StaticResource HeaderBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="White"/>
<Setter Property="MouseOverHeaderForeground"
Value="{StaticResource HighlightHeaderTextBrush}"/>
<Setter Property="CornerRadius" Value="6,6,0,0"/>
<Setter Property="ShowEllipse" Value="True"/>
</Style>
</ControlTemplate.Resources>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
About Themes
To allow a control to have Windows based themes, you need to specify a custom theme for each Windows theme. Each theme must reside in the Themes folder of the source control as a ResourceDictionary. I implemented the following themes:
- Generic.xaml (the default style)
- Classic.xaml (the style for classic Windows themes)
- Luna.NormalColor.xaml (the style for the blue XP theme)
- Luna.Metallic.xaml (the style for the silver XP theme)
- Luna.Homestead.xaml (the style for the olive XP theme)
- Aero.NormalColor.xaml (the style for the Vista theme)
Each ResourceDictionary is optional. For instance, if you don't specify the Luna.Homestead.xaml, the style falls back to Classic.xaml, if available; otherwise, to Generic.xaml.
But, that's still not all. When you create your first themable control, you'll wonder why it is only using the generic.xaml and never the customized dictionaries. To finally enable theming, you need to modify the AssemblyInfo.cs as follows:
[assembly: ThemeInfo(
ResourceDictionaryLocation.SourceAssembly,
ResourceDictionaryLocation.SourceAssembly
)]
(For further information, please read the documentation for ThemeInfo.)
Drawback
When you create custom controls, Visual Studio automatically creates a default template for the new control in Generic.xaml. Thus, all styles for all controls of a control library would reside in one XAML. This can become very confusing. But fortunately, it is possible to merge various ResourceDictionarys together. So, I created a folder for each control that contains all the possible styles (generic, Classic, Luna, etc.). Each ResourceDictionary for each theme is now merged with the base ResourceDictionary, as follows:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/Odyssey;Component/Themes/Expander/Luna.HomeStead.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
In Luna.Homestead.xaml, the Style for the Expander is added using MergedDictionaries. So, it is easy to add styles for other controls by adding its ResourceDictionary to the MergedDictionaries block.
History
The ExplorerBar and OdcExpander is part of the Odyssey class library that I'm currently developing for free. It also contains the BreadcrumbBar which was already introduced on CodeProject.