

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 ResourceDictionary
s 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.