Expander control for Silverligt 2



This control can be used as a stand alone control, or can be used in a StackPanel as a repetitive control. The control uses a template to format the control and to assign data to intrinsic conrols. The expander control consists of two areas, a header and a footer. The footercan be expanded or collapsed by clicking on a button.

Expander with template


We are now going to have a look at how you can create an expander control that will display a contents area when the user clicks on a button placed to the far right of the header area. 

1.gif
 
Collapsed expander control 

2.gif 
Expanded expander control

The control will be created as a class object and have a template that defines the control appearance. Creating the control with a template makes it easier to redefine the appearance if you want to reuse the control or want potential buyers to be able to change the appearance. All you need to do to change the appearance of the control is to define a new template for the control. The original template will be created in a template file called generic.xaml that must be placed in a folder called Themes.

Creating the Silverlight application

Let's start by creating a new Silverlight application that will host our control.

  1. Open Visual Studio 8 and create a new Silverlight project called ExpanderControl.
  2. Make sure that the Add a new ASP.NET Web project to the solution to host Silverlight option is selected. Make sure the Project Type dropdown is set to ASP.NET Web Application Project, then click OK.
Two projects will be added to the solution. The ExpanderControl project is the Silverlight application project and the ExpanderControl.Web project is the ASP.NET application hosting the Silverlight application. The ExpanderControl.Web project contains one .aspx and one .html page, which one you choose to use as the container is up to you; in this example however we will use the ExpanderControlTestPage.aspx page, so make sure it is set as the start page. You can collapse the ExpanderControl.Web project in the solution explorer; we won't be making any further alterations to it in this example.
The page.axml page is the main page in the Silverlight application, and it should contain the basic framework for hosting Silverlight controls. We will later use this page to test our expander control.

Adding the generic.xaml template file to the Silverlight application.

When we use a replaceable template to define a control we place the original template information in a .xaml document named generic.xaml. This document must be placed in a folder named Themes. This template can then be replaced by user created templates to alter the appearance of the control. You might for instance want to change the background or the button style; to avoid having to create a new control you simply change the template and all the underlying functionality will continue to work without any changes.
  1. Create a new folder named Themes in the ExpanderControl project by right clicking on the ExpanderControl project and select Add-New Folder.
  2. Create the generic.xaml document by right clicking on the Themes folder and select Add-New Item.
  3. Choose XML File in the object list and name it generic.xaml.
  4. Delete the contents in the generic.xaml document. Create a <ResourceDictionary> node and add the following namespaces to it. This node will contain the template definitions for the control as well as styles and setters. The first two namespaces are default namespaces used by Silverlight and the third is the namespace of the ExpanderControl.
     

    <ResourceDictionary

        xmlns="http://schemas.microsoft.com/client/2007"

        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:local="clr-namespace:ExpanderControl">
    </
    ResourceDictionary>
     

  5. Add a style with a TargetType set to Expander. Add a setter with its Property value set to Template. Within this setter we need to create a ControlTemplate node that defines the structure and behavior of the control. The ControlTemplate can be used across multiple instances of the control. Set the ControlTemplates' TargetType to Expander. Add a grid inside the ControlTemplate to act as the canvas for the control. This is the beginning of the template that we will build throughout the example.
     

        <Style TargetType="local:Expander">

            <Setter Property="Template">

                <Setter.Value>

                    <ControlTemplate TargetType="local:Expander">

                        <Grid>

                        </Grid>

                    </ControlTemplate>

                </Setter.Value>

            </Setter>

        </Style>


Adding the Expander class to the project

Because we want to create a control from scratch we won't use the UserControl control, but the ContentCotrol. The content control is the base class of the UserControl and is less specialized. This makes it a great candidate for user defined controls built from the ground up. The UserControl is great when creating more complex composite controls. It is entirely possible to create an expander control with a UserControl, but this example wants to show how to create a ContentControl control governed by a template.
  1. Add a new class named Expander to the ExpanderControl Project by right clicking on the project name and select Add-Class.
  2. Add the following using statement to the class and let the class inherit from the ContentControl class.
  3. Add a region called Constructors and add an empty constructor to the region. Set the DefaultStyleKey property to reference the style for the control. This dependency property is used when themes are used with a control. The style is stored in the generic.xaml document.

     

    #region Constructors

    public Expander()

    {

        DefaultStyleKey = typeof(Expander);

        IsExpanded = false;

    }

    #endregion


Defining default property values in the template

To make it easier to use the control we can define default values for a number of properties, such as background, border, corner radius and margins. Let's add these to the template and use them in the control. Open the generic.xaml document and add the following settings. The CornerRadius property determines if the corners of the control should be rounded and how much. The BorderBrush property determines what brush or color should be used for the border of the control. The BorderThickness determines the thickness of the border. The Margin property determines the margin surrounding the control. You can set the Margin to a single value to use the same margin around the control, or you can set four values, one for each side of the control. The Background property is a little bit more complex because we want to use a gradient fill; to accomplish this we use a LinearGradientBrush node inside the Background property. Inside the LinearGradientBrush node we create the individual stops in the gradient fill using GradientStop nodes. Add the new nodes between the Style and the template Setter nodes.


<
Style TargetType="local:Expander">

   <Setter Property="Margin" Value="0"></Setter>

   <Setter Property="BorderBrush" Value="SteelBlue"></Setter>

   <Setter Property="BorderThickness" Value="1"></Setter>

   <Setter Property="CornerRadius" Value="0"></Setter> <!-- Must be defined in the class -->

   <Setter Property="Background">

       <Setter.Value>

           <LinearGradientBrush StartPoint="0.547587,0.322135" EndPoint="0.547587,0.992095">

               <GradientStop Color="#FFFFFFFF" Offset="0"/>

               <GradientStop Color="#FFDCEAF9" Offset="1"/>

           </LinearGradientBrush>

       </Setter.Value>

   </Setter>

   <Setter Property="Template">

Because the CornerRadius property isn't a default property of the ContentsControl we need to add it to the Expander class. Open the Expander.cs class and add a region called Dependency Properties in which you write the following dependency property. A dependency property is a way to expose properties of a control that wouldn't be available otherwise. It can be properties of a child control for instance. A dependency property always consists of two parts; the first part is a static read only dependency property that holds the value, the second part is the actual property that sets the value. The dependency property must be read only because it can only be set once, and it must be static to make it available without first creating an instance of the control.

#region Dependency Properties

public static readonly DependencyProperty CornerRadiusProperty =

    DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(Expander), null);

public CornerRadius CornerRadius

{

    get { return (CornerRadius)GetValue(CornerRadiusProperty); }

    set { SetValue(CornerRadiusProperty, value); }

}
#endregion


Border, Background and radius of the control

Let's use the properties we defined earlier. Open the generic.xaml document and add a Border node inside the grid node. We will use TemplateBinding to connect the properties of the border with the dependency properties we defined earlier. This type of binding can only be used within a ControlTemplate definition in xaml.

Let's also define the grid that will hold the contents of the control. We will need two rows and two columns. In the first row we will need one column to present the header contents and one column to hold the toggle button. In the second row we will span the two columns making it into one column for the detail contents, the contents that will be expanded and collapsed. Height and Width properties can be set to constant values, Auto or star (*). Auto distributes the space evenly based on the size of the content that is within a row or a column. Star (*) means that the height or width of that column or row will receive a weighted proportion of the remaining available space.

<Border

     BorderBrush="{TemplateBinding BorderBrush}"

                        BorderThickness="{TemplateBinding BorderThickness}"

     CornerRadius="{TemplateBinding CornerRadius}"

     Background="{TemplateBinding Background}">

    <Grid x:Name="LayoutRoot">

        <Grid.RowDefinitions>

            <RowDefinition Height="Auto"></RowDefinition>

            <RowDefinition Height="Auto"></RowDefinition>

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="*"></ColumnDefinition>

            <ColumnDefinition Width="50"></ColumnDefinition>

        </Grid.ColumnDefinitions>

    </Grid>

</Border>

A first look at the control

We have written a bunch of code that hasn't yielded any visible output. I bet you're ready to see some results; so with no further ado let's have a sneak preview of the control. To test the control open the Page.xaml document and add the following xaml code inside the Grid node. The StackPanel is a control that can accommodate several other controls and display them horizontally or vertically. You can run the application by pressing F5 on the keyboard.

<Grid x:Name="LayoutRoot" Background="White">

    <StackPanel VerticalAlignment="Top">

        <exp:Expander Margin="2" Height="20"></exp:Expander>

    </StackPanel>
</Grid>
The output should look something like this. 

3.gif
 
Header Content


Now that the basic outline of the control is finished we will continue by adding a header content area. This area can contain any type of information; we will however use it to display text. There are two steps to accomplish this task. First we need to add a new dependency property that holds the header content. Secondly we need to define the header content as a ContentPersenter in the template. Let's start with the dependency property. Open the Expander.cs class and add the following dependency property to the Dependency Properties region. The first parameter of the Register method is the name of the dependency property, the second parameter is the type of object that the property can hold, the third parameter is the control type, and the fourth parameter can contain additional information.


public
static readonly DependencyProperty HeaderContentProperty =

    DependencyProperty.Register("HeaderContent", typeof(object),

    typeof(Expander), null);

public object HeaderContent

{

get { return (object)GetValue(HeaderContentProperty); }

set { SetValue(HeaderContentProperty, value); }
}

Once the property has been added to the Expander.cs class we can add it to the innermost grid node in the generic.xaml document, below the grid row and column definitions. Note that we use TemplateBinding to display the contents of the dependency property.

<ContentPresenter x:Name="HeaderContent" Content="{TemplateBinding

    HeaderContent}" Margin="5"></ContentPresenter>

If you switch to the Page.xaml document and replace the Height property with the HeaderContent property and assign it the text "Header content" and compile the solution you should see the following result.

<exp:Expander Margin="2" HeaderContent="Header content"></exp:Expander>

4.gif 

Collapsible content area

Let's continue by adding the collapsible contents area, this is the part of the control that can be hidden or visible. In the finished control the user will be able to collapse or expand this region by clicking on a button.

We need to add a TemplatePart that defines the Content attribute. This attribute exists by default; we only need to make it visible through the Expander control. We will define this attribute as FrameworkElement; this means that it can store any type of framework element, such as TextBlock, TextBox, List controls, and many more. To add an attribute to the class simply add the attribute immediately before the class definition in the Expander.cs class.

[TemplatePart(Name = "Content", Type = typeof(FrameworkElement))]

public class Expander : ContentControl

Switch to the generic.xaml document. Add a ContentPresenter node below the ContentPresenter already present in the template. Set the Grid.Row property to 1 and the Grid.ColumnSpan to 2; this will ensure that the content will be displayed in the second row and that the content and span both columns. Name it Content using the x:Name attribute and use TemplateBindning to get the value stored in the TemplatePart with the same name. Set the margin to 5 to get some distance to the Header contents.

<ContentPresenter Grid.Row="1" Grid.ColumnSpan="2" x:Name="Content" Content="{TemplateBinding Content}" Margin="5"></ContentPresenter>

Switch to the Page.xaml document and add the Content attribute giving it a value of "Content area". The output should look something like this.

<exp:Expander Margin="2" HeaderContent="Header content" Content="Content area"></exp:Expander>

5.gif 

Creating the expand/collapse button

Let's start by adding a button to the second column of the first row. Later we will alter the buttons' appearance making it more appealing, by changing its template. We will also expose the button through an attribute that we will call ExpandCollapseButton; this will make it available through the Expand class and make it possible to alter the button appearance through a new template. Once the button is visually ready we will continue by creating the methods and events needed to control the button and its animation.

Switch to the Expander.cs class and add a new attribute named ExpandCollapseButton of type ToggleButton.

[TemplatePart(Name = "ExpandCollapseButton", Type = typeof(ToggleButton))]

[TemplatePart(Name = "Content", Type = typeof(FrameworkElement))]

public class Expander : ContentControl

Now switch to the generic.xaml document and add a ToggleButton node named ExpandCollapseButton to the template; add it between the two ContentPresenter nodes.

<ContentPresenter x:Name="HeaderContent" Content="{TemplateBinding

    HeaderContent}" Margin="5"></ContentPresenter>

<ToggleButton Grid.Column="1" Grid.Row="0" x:Name="ExpandCollapseButton"

     Margin="3">

</ToggleButton>

<ContentPresenter Grid.Row="1" Grid.ColumnSpan="2" x:Name="Content"

    Content="{TemplateBinding Content}" Margin="5"></ContentPresenter>

The control should look something like this. Note the button in the right corner.

6.gif 

Let's continue by making the button a little more appealing to the eye. Let's make it round. To achieve this we need to alter the button template in the generic.xaml document; we do this by adding a ToggleButton.Template node inside the ToggleButton node. Inside the template node we need to override the existing control template, we do this by adding a ControlTemplate node inside the ToggleButton.Template node. Inside the ControlTemplate node we create an Elipse node inside a Grid node.

<ToggleButton Grid.Column="1" Grid.Row="0" x:Name="ExpandCollapseButton"

          Margin="3">

    <ToggleButton.Template>

        <ControlTemplate>

            <Grid>

                <Ellipse Stroke="#FFA9A9A9" Fill="AliceBlue" Width="19"

                                       Height="19"></Ellipse>

            </Grid>

        </ControlTemplate>

    </ToggleButton.Template>

</ToggleButton>

If you switch to the Page.xaml page the updated control should look something like this. Note that the button in the right corner is round.

7.gif 

The next step is to create an arrow on the button. This arrow will later be made to rotate 180° and expand or collapse the content region when the button is clicked. Switch to the generic.xaml document and add the following node to the template below the Ellipse node. The Path member is used to draw a series of connected lines and curves. The Data dependency property sets a Geometry object that specifies the shape to be drawn. Stroke sets the Brush that is used to paint the outline of the shape. StrokeThickness sets the outline width. HorizontalAlignment and VerticalAlignment determine the alignment of the control when used in a parent element.

<Path Data="M1,1.5L4.5,5 8,1.5" Stroke="#FF666666" StrokeThickness="2" HorizontalAlignment="Center" VerticalAlignment="Center"></Path>

If you switch to the Page.xaml document and update the control it should now have a round button with a downward pointing arrow illustrating that the content area is visible.

8.gif 

Animating the button

Let's make the button animated to really illustrate that something is happening when the button is clicked. Switch to the generic.xaml document. To pull this off we need to add a ToggleButton.RenderTransform node to the ToggleButton node; place it below the ToggleButton.Template node. We also need to add the RenderTransformOrigin to the ToggleButton node. The RenderTransformOrigin is a Point object that defines the center point declared by the RenderTransform object, relative to the bounds of the element. The value is set between 0 and 1. A value of (0.5, 0.5) will cause the transform to be centered on the element. If you leave out tis attribute the transform will behave strangely.

<ToggleButton Grid.Column="1" Grid.Row="0" x:Name="ExpandCollapseButton"

    Margin="3" RenderTransformOrigin="0.5,0.5">

...

     <ToggleButton.RenderTransform>

          <RotateTransform x:Name="RotateButtonTransform"></RotateTransform>

     </ToggleButton.RenderTransform>

</ToggleButton>

Now switch to Expander.cs class. Add a region named Fields and add a private ToggleButton container named btnExpandOrCollapse. This container will hold a reference to the button in the control. Add a field named contentElement of type FrameworkElement that will hold the contents of the Content area. The state is stored in a field of VisualState type named collapsedState.

#region Fields

private ToggleButton btnExpandOrCollapse;

private FrameworkElement contentElement;

private VisualState collapsedState;

#endregion

Now we have to write a method called ChangeVisualState that will change the visual state of the button. This method is dependent on two view states that must be declared with attributes on the class. The two states are named Collapsed and Expanded. Start by define the two states.

[TemplateVisualState(Name = "Expanded", GroupName = "ViewStates")]

[TemplateVisualState(Name = "Collapsed", GroupName = "ViewStates")]

public class Expander : ContentControl

Next add a dependency property to the Dependency Properties region called IsExpandedProperty that will hold the value stating if the control is in its expanded or collapsed state.

public static readonly DependencyProperty IsExpandedProperty =

    DependencyProperty.Register("IsExpanded", typeof(bool), typeof(Expander), new PropertyMetadata(true));

public bool IsExpanded

{

    get { return (bool)GetValue(IsExpandedProperty); }

    set

    {

        SetValue(IsExpandedProperty, value);

        if (btnExpandOrCollapse != null) btnExpandOrCollapse.IsChecked = IsExpanded;

    }

}

Next create a region called Methods and write the ChangeVisualState method in that region. The VisualStateManagers' GoToState method is used to alter the state of a control. The useTransitions parameter tells the visual state manager if transitions such as a time interval will be used.

#region Methods

private void ChangeVisualState(bool useTransitions)

{

   //  Apply the current state from the ViewStates group.

   if (IsExpanded)

   {

       VisualStateManager.GoToState(this, "Expanded", useTransitions);

   }

   else

   {

       VisualStateManager.GoToState(this, "Collapsed", useTransitions);

   }

}

#endregion

Next add a public method called ExpandOrCollapse that can be called to alter the state of the control.

public void ExpandOrCollapse(bool useTransitions)

{

   IsExpanded = !IsExpanded;

   ChangeVisualState(useTransitions);

}


Create a region named Event Methods and add an event method called btnExpandOrCollapse_Click in the event method, call the ExpandCollapse method with its parameter value set to true.

#region Event Methods

 private void btnExpandOrCollapse_Click(object sender, RoutedEventArgs e)

 {

     ExpandOrCollapse(true);

 }

#endregion

Now override the OnApplyTemplate method and get the template definition for the ExpandCollapseButton and create an event handler for the buttons' Click event.

#region Overridden Methods

public override void OnApplyTemplate()

{

    base.OnApplyTemplate();

   btnExpandOrCollapse = GetTemplateChild("ExpandCollapseButton") as ToggleButton;

    if (btnExpandOrCollapse != null)

    {

        btnExpandOrCollapse.Click += btnExpandOrCollapse_Click;

    }

    ChangeVisualState(false);

}

#endregion

Now we need to add a VisualStateGroup, for the two states, in the generic.xaml document. These two states will be used from the Expander class through the Expanded and Collapsed attributes defined on the class.

Add a VisualStateManager.VisualStateGroups node directly under the outermost Grid node. Create a VisualStateGroup node inside the VisualStateManager.VisualStateGroups node and add two VisualState nodes, one called Expanded and one called Collapsed. Create a Storyboard node inside each of the two VisualState nodes. Create a DoubleAnimation node inside each of the two StoryBoard nodes and give them the same StoryBoard.TargetName, RoteteButtonTransform.

<ControlTemplate TargetType="local:Expander">

    <Grid>

        <VisualStateManager.VisualStateGroups>

           <VisualStateGroup x:Name="ViewStates">

               <VisualState x:Name="Expanded">

                   <Storyboard>

                       <DoubleAnimation

                       Storyboard.TargetName="RotateButtonTransform"

                       Storyboard.TargetProperty="Angle" Duration="0"

                       To="180"></DoubleAnimation>

                   </Storyboard>

               </VisualState>

               <VisualState x:Name="Collapsed">

                   <Storyboard>

                       <DoubleAnimation

                          Storyboard.TargetName="RotateButtonTransform"

                          Storyboard.TargetProperty="Angle" Duration="0"

                          To="0"></DoubleAnimation>

                   </Storyboard>

               </VisualState>

           </VisualStateGroup>

                   </VisualStateManager.VisualStateGroups>

Now, for the final touch, we will set the IsExpanded property value to false in the constructor.

public Expander()

{

    DefaultStyleKey = typeof(Expander);

     IsExpanded = false;

}

Expanding and collapsing the content area

This is the final step towards finishing the control. The only thing left for us to do is to make the content area toggle between expanded and collapsed.
Let's start by adding some code to the ChangeVisualState method. We need to check if the IsExpanded property is true and if the contentElement not is null, if that is the case then we set the Visibility property of the contnentElement equal to Visibility.Visible. If on the other hand the IsExpanded property is false, the collapsedState is null and the contentElement is not null then we set the Visibility property of the contnentElement equal to Visibility.Collapsed.


private
void ChangeVisualState(bool useTransitions)

{

    //  Apply the current state from the ViewStates group.

    if (IsExpanded)

    {

        if (contentElement != null) contentElement.Visibility =

           Visibility.Visible;

        VisualStateManager.GoToState(this, "Expanded", useTransitions);

    }

    else

    {

        VisualStateManager.GoToState(this, "Collapsed", useTransitions);

        if (collapsedState == null)

        {

           // There is no state animation, so just hide the content

           //region immediately.

           if (contentElement != null) contentElement.Visibility =

               Visibility.Collapsed;

        }

    }
}

Next we add an event method called collapsedStoryboard_Completed to the Event Methods region. This method will be called when a storyboard finishes its work and in it we will set the Visibility property of the contentElement to Visibility.Collapsed.


private
void collapsedStoryboard_Completed(object sender, EventArgs e)

{

    contentElement.Visibility = Visibility.Collapsed;
}

Next we add some code to the overridden OnApplyTemplate method.Firstly we need to get the contetns of the content area. Next we check it the contentElement is open, and if so we collapse that control and attach an event handler to the states' storyboard.


public
override void OnApplyTemplate()

{

    base.OnApplyTemplate();

    contentElement = GetTemplateChild("Content") as FrameworkElement;

    if (contentElement != null)

    {

        collapsedState = GetTemplateChild("Collapsed") as VisualState;

        if ((collapsedState != null) && (collapsedState.Storyboard != null))

        {

           collapsedState.Storyboard.Completed +=

               collapsedStoryboard_Completed;

        }
    }

Now start the application by pressing F5 on the keyboard. The control should now be expanding and collapsing its contents area when the button is clicked.