Click here to Skip to main content
Click here to Skip to main content
Go to top

WPF Explorer Bar

, 5 Oct 2008
Rate this:
Please Sign up or sign in to vote.
A WPF explorer bar implementation.

XP_Metallic.pngOdyssey

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;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Specify whether to apply opactiy animation.
    /// <span class="code-SummaryComment"></summary></span>
    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));

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets whether the decorator is expanded or collapsed.
    /// <span class="code-SummaryComment"></summary></span>
    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);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Specify whether to apply animation when IsExpanded is changed.
    /// <span class="code-SummaryComment"></summary></span>

    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));

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the duration for the animation.
    /// <span class="code-SummaryComment"></summary></span>
    public Duration Duration
    {
        get { return (Duration)GetValue(DurationProperty); }
        set { SetValue(DurationProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Duration.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DurationProperty =
        DependencyProperty.Register("Duration", typeof(Duration), 
        typeof(AnimationDecorator), 
        new UIPropertyMetadata(new Duration(new TimeSpan(0,0,0,400))));

    /// <span class="code-SummaryComment"><summary></span>
    /// Perform the animation.
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="expanded"></param></span>
    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;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// A helper value for the current state while in animation.
    /// <span class="code-SummaryComment"></summary></span>
    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));

    /// <span class="code-SummaryComment"><summary></span>
    /// Measures the child element of a
    /// <span class="code-SummaryComment"><see cref="T:System.Windows.Controls.Decorator"/></span>
    /// to prepare for arranging it during the <span class="code-SummaryComment"><see
    /// cref="M:System.Windows.Controls.Decorator.
    ///       ArrangeOverride(System.Windows.Size)"/> pass.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="constraint">An upper limit</span>
    /// <span class="code-SummaryComment"><see cref="T:System.Windows.Size"/> that should not be exceeded.</param></span>
    /// <span class="code-SummaryComment"><returns></span>
    /// The target <span class="code-SummaryComment"><see cref="T:System.Windows.Size"/> of the element.</span>
    /// <span class="code-SummaryComment"></returns></span>

    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;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Arranges the content of a <span class="code-SummaryComment"><see cref="T:System.Windows.Controls.Decorator"/> element.</span>
    /// <span class="code-SummaryComment"></summary></span>
    /// <span class="code-SummaryComment"><param name="arrangeSize">The <see cref="T:System.Windows.Size"/></span>
    /// this element uses to arrange its child content.<span class="code-SummaryComment"></param></span>
    /// <span class="code-SummaryComment"><returns></span>
    /// The <span class="code-SummaryComment"><see cref="T:System.Windows.Size"/> that represents the arranged size</span>
    /// of this <span class="code-SummaryComment"><see cref="T:System.Windows.Controls.Decorator"/> element and its child.</span>
    /// <span class="code-SummaryComment"></returns></span>

    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.

/// <span class="code-SummaryComment"><summary></span>
/// An Expander with animation.
/// <span class="code-SummaryComment"></summary></span>

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)));
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the custom skin for the control.
    /// <span class="code-SummaryComment"></summary></span>
    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);
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the ImageSource for the image in the header.
    /// <span class="code-SummaryComment"></summary></span>
    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

/// <span class="code-SummaryComment"><summary></span>
/// Gets or sets the corner radius for the header.
/// <span class="code-SummaryComment"></summary></span>

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));

/// <span class="code-SummaryComment"><summary></span>
/// Gets or sets the background color of the header on mouse over.
/// <span class="code-SummaryComment"></summary></span>

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));

/// <span class="code-SummaryComment"><summary></span>
/// Gets whether the PressedBackground is not null.
/// <span class="code-SummaryComment"></summary></span>

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));

/// <span class="code-SummaryComment"><summary></span>
/// Gets or sets the background color of the header in pressed mode.
/// <span class="code-SummaryComment"></summary></span>

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); }
}

// Using a DependencyProperty as the backing store
// for HeaderBorderThickness. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HeaderBorderThicknessProperty =
  DependencyProperty.Register("HeaderBorderThickness", 
  typeof(Thickness), typeof(OdcExpander), new UIPropertyMetadata(null));


/// <span class="code-SummaryComment"><summary></span>
/// Gets or sets the foreground color of the header on mouse over.
/// <span class="code-SummaryComment"></summary></span>
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));

/// <span class="code-SummaryComment"><summary></span>
/// Specifies whether to show a elipse with the expanded/collapsed image.
/// <span class="code-SummaryComment"></summary></span>

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:

/// <span class="code-SummaryComment"><summary></span>
/// A helper class to specify the header of an OdcExpander.
/// <span class="code-SummaryComment"></summary></span>

internal class OdcExpanderHeader : ToggleButton
{
    static OdcExpanderHeader()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(OdcExpanderHeader), 
          new FrameworkPropertyMetadata(typeof(OdcExpanderHeader)));
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets whether the expand geometry is not null.
    /// <span class="code-SummaryComment"></summary></span>
    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));


    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the geometry for the collapse symbol.
    /// <span class="code-SummaryComment"></summary></span>
    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;
    }

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the geometry for the expand symbol.
    /// <span class="code-SummaryComment"></summary></span>
    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));

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the corner radius for the header.
    /// <span class="code-SummaryComment"></summary></span>
    public CornerRadius CornerRadius
    {
        get { return (CornerRadius)GetValue(CornerRadiusProperty); }
        set { SetValue(CornerRadiusProperty, value); }
    }

    // Using a DependencyProperty as the backing store for CornerRadius.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty CornerRadiusProperty =
        DependencyProperty.Register("CornerRadius", typeof(CornerRadius), 
        typeof(OdcExpanderHeader), new UIPropertyMetadata(null));

    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets whether to display the ellipse
    /// around the collapse/expand symbol.
    /// <span class="code-SummaryComment"></summary></span>
    public bool ShowEllipse
    {
        get { return (bool)GetValue(ShowEllipseProperty); }
        set { SetValue(ShowEllipseProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ShowEllipse.
    // This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ShowEllipseProperty =
        DependencyProperty.Register("ShowEllipse", typeof(bool), 
        typeof(OdcExpanderHeader), new UIPropertyMetadata(true));


    /// <span class="code-SummaryComment"><summary></span>
    /// Gets or sets the Image to display on the header.
    /// <span class="code-SummaryComment"></summary></span>
    public ImageSource Image
    {
        get { return (ImageSource)GetValue(ImageProperty); }
        set { SetValue(ImageProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Image.
    // This enables animation, styling, binding, etc...
    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.

VistaXP_Metallic.png

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,
 //where theme specific resource dictionaries are located
 //(used if a resource is not found in the page, 
 // or application resource dictionaries)

 ResourceDictionaryLocation.SourceAssembly
 //where the generic resource dictionary is located
 //(used if a resource is not found in the page, 
 // app, or any theme specific resource dictionaries)

)]

(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.

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

Thomas Gerber
Software Developer (Senior)
Germany Germany
MCPD
Enterprise Application Developer 3.5
Windows Developer 3.5
.ASP.NET Developer 3.5
.NET 2.0 Windows Developer
.NET 2.0 Web Developer
.NET 2.0 Enterprise Application Developer
 

MCTS
.NET 3.5 Windows Forms Applications
.NET 3.5 ASP.NET Applications
.NET 3.5, ADO.NET Application Development
.NET 3.5 WCF
.NET 3.5 WPF
.NET 3.5 WF
Microsoft SQL Server 2008, Database Development
.NET 2.0 Windows Applications
.NET 2.0 Web Applications
.NET 2.0 Distributed Applications
SQL Server 2005
Sharepoint Services 3.0 Application Development
Windows Vista Client Configuration

Comments and Discussions

 
GeneralMy vote of 3 PinprofessionalAthari4-Oct-13 1:27 
Question[My vote of 1] wtf PinmemberNilsHult24-Jan-12 1:57 
AnswerRe: [My vote of 1] wtf PinmemberThomas Gerber24-Jan-12 7:52 
GeneralXBAP PinmemberFrankidoze13-Jan-11 6:10 
Hi Thomas,
 
Have you ever tried your Bar in an XBAP app?
I have and it doesn't load any OutlookSection and OdcExpander...
 
Well, if you find anything I'm interested...
 
Cheers!
"Nothing is lost, Nothing is created, Everything is transformed" Lavoisier
 
http://wlwilliamsiv.com

GeneralRe: XBAP PinmemberFrankidoze13-Jan-11 7:08 
GeneralGrate PinmemberMr_Coder14-Jan-10 17:07 
QuestionUsing only the Animation Control PinmemberIlias Bogordos28-Dec-09 23:35 
QuestionBug fix? PinmemberFoxman200012-Aug-09 8:27 
GeneralThank you Pingroupzhujinlong1984091314-Apr-09 17:45 
QuestionBinding PinmemberSteve Fenton12-Feb-09 23:35 
AnswerRe: Binding PinmemberSteve Fenton17-Feb-09 0:23 
GeneralNicely done Pinmemberadityakakrania23-Jan-09 6:50 
GeneralSlight problem re: text wrapping Pinmemberchaiguy133717-Dec-08 19:20 
GeneralRe: Slight problem re: text wrapping Pinmemberchaiguy133718-Dec-08 7:51 
GeneralRe: Slight problem re: text wrapping PinmemberThomas Gerber14-Jan-09 12:06 
GeneralRe: Slight problem re: text wrapping PinmemberDotNetProgramma11-Feb-10 3:23 
GeneralGreat work! Pinmemberchaiguy133717-Dec-08 6:06 
GeneralExcellent work! PinmemberJohn Schroedl8-Oct-08 9:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140916.1 | Last Updated 5 Oct 2008
Article Copyright 2008 by Thomas Gerber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid