(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

Understanding control customization with templates: part #2

(9 votes)
Andrea Boschin
>
Andrea Boschin
Joined Nov 17, 2009
Articles:   91
Comments:   9
More Articles
1 comments   /   posted on Jul 15, 2010
Categories:   Controls , Design

This article is compatible with the latest version of Silverlight.

In the previous part of this article, I've explained the role of templates in Silverlight controls showing how to radically change the appearance of a control without having to change or rewrite its logic. This is an important feature of Silverlight's controls that allow a developer to create an application without concerning about its appearance and leave the designer free of manipulate it without needing to know their intimate logic. Unfortunately there are many times when built-in controls, the SDK and the Toolkit do not contains the control we need for our application. So you have to start writing the control by yourself, and creating a templated control make your work more reusable and customizable for future usages.

Download Source Code

Starting a new control

The set of controls of Silverlight is very wide and cover a lot of needs, so it is hard to find a missing control to show in this article. The inspiration comes from the new generation phones, which often include controls that are very innovative. I decided to try a port of the Special button switch that you have certainly appreciated by accessing an iPhone.

The figure shows two instances of a control, I will call Switch, one in the Checked state (the red one) and the other in the Unchecked state. You can change the state of the control acting two ways: clicking on the coloured area or dragging the thumb with the chevron in the direction you want. The beautiful of the control is that it shows an animation when you make a change in its state scrolling the thumb from a side to the other.

The control, as you can figure out, is a sort of ToggleButton, so we should start its building from this class. I choosed of starting from scratch implementing a ContentControl directly for two reasons. First of all I would want to make the example simple and basic, and secondly I noticed there isn't an easy way to implement the behavior I described using VisualStateManager. So let me start writing code:

 public class Switch : ContentControl
 {
     public Switch()
     {
         this.DefaultStyleKey = typeof(Switch);
     }
 }

In this snippet you see two basic concepts. My class inherits from ContentControl because I need to have some content, like a normal ToggleButton. This content is the label on the side or a more complex structure of objects. When you are starting a new control this is the first choice you have to make: Control (or something inheriting from Control) if the control is a leaf in the VisualTree. ContentControl if it can have a content.

On the other side you can see I given a value to the DefaultStyleKey property. This lets the runtime will search its template in a file we are about to add to the project. The file, called always  generic.xaml, must be in a folder called "Themes" in the root of the project that contains the control. Its purpose is containing the basic templates of all the controls in the assembly. So, Here is the initial generic.xaml

 <ResourceDictionary
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:local="clr-namespace:SilverlightPlayground.ControlTemplates">
  
     <Style TargetType="local:Switch">
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate TargetType="local:Switch">
                     <Grid>
                         <Grid.ColumnDefinitions>
                             <ColumnDefinition Width="Auto" />
                             <ColumnDefinition Width="*" />
                         </Grid.ColumnDefinitions>
                         <Border Grid.Column="1" 
                                 BorderThickness="1" BorderBrush="Black" 
                                 CornerRadius="2" HorizontalAlignment="Stretch">
                             <Grid HorizontalAlignment="Stretch">
                                 <Grid.ColumnDefinitions>
                                     <ColumnDefinition x:Name="OnColumnElement" Width="0*" />
                                     <ColumnDefinition Width="Auto" />
                                     <ColumnDefinition x:Name="OffColumnElement" Width="100*" />
                                 </Grid.ColumnDefinitions>
                                 <Rectangle x:Name="OnBackgroundElement" Grid.Column="0" Fill="Red" />
                                 <Rectangle x:Name="OffBackgroundElement" Grid.Column="2" Fill="Green" />
                                 <Rectangle Grid.Column="1" Fill="#FF617584" />
                                 <Thumb x:Name="ThumbElement" Grid.Column="1" Width="40" />
                             </Grid>
                         </Border>
                         <ContentPresenter Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,5,0" />
                     </Grid>
                 </ControlTemplate>
             </Setter.Value>
         </Setter>
     </Style>
    
 </ResourceDictionary>

This template creates the basic appearance of the control, as you have seen in the previous figure. It misses some important parts, but I will add something else in the following snippets. In the template I have a basic Grid that separates the label from the body of the control. The body is made of another grid with three columns. The size of the first and the last column will be changed to move the thumb from the left to the right or viceversa. In the template I also have a Thumb control, representing an element that can be dragged and a couple of rectangles that give the background color to the visible area.

Now we need to take some references to the elements we need to use for the animation. They are the Columns, the thumb and the coloured rectangles:

 [TemplatePart(Name = Switch.ThumbElementName, Type = typeof(Thumb))]
 [TemplatePart(Name = Switch.OnColumnElementName, Type = typeof(ColumnDefinition))]
 [TemplatePart(Name = Switch.OffColumnElementName, Type = typeof(ColumnDefinition))]
 [TemplatePart(Name = Switch.OnBackgroundElementName, Type = typeof(Shape))]
 [TemplatePart(Name = Switch.OffBackgroundElementName, Type = typeof(Shape))]
 public class Switch : ContentControl
 {
     private const string ThumbElementName = "ThumbElement";
     public Thumb ThumbElement { get; set; }
   
     private const string OnColumnElementName = "OnColumnElement";
     public ColumnDefinition OnColumnElement { get; set; }
   
     private const string OffColumnElementName = "OffColumnElement";
     public ColumnDefinition OffColumnElement { get; set; }
   
     private const string OnBackgroundElementName = "OnBackgroundElement";
     public Shape OnBackgroundElement { get; set; }
   
     private const string OffBackgroundElementName = "OffBackgroundElement";
     public Shape OffBackgroundElement { get; set; }
      
     public Switch()
     {
         this.DefaultStyleKey = typeof(Switch);
     }
   
     public override void OnApplyTemplate()
     {
         this.ThumbElement = this.GetTemplateChild(Switch.ThumbElementName) as Thumb;
         this.ThrowIfMissing(this.ThumbElement, Switch.ThumbElementName);
         this.OffColumnElement = this.GetTemplateChild(Switch.OffColumnElementName) as ColumnDefinition;
         this.ThrowIfMissing(this.OffColumnElement, Switch.OffColumnElementName);
         this.OnColumnElement = this.GetTemplateChild(Switch.OnColumnElementName) as ColumnDefinition;
         this.ThrowIfMissing(this.OnColumnElement, Switch.OnColumnElementName);
         this.OnBackgroundElement = this.GetTemplateChild(Switch.OnBackgroundElementName) as Shape;
         this.OffBackgroundElement = this.GetTemplateChild(Switch.OffBackgroundElementName) as Shape;
      
         this.UpdateVisualState();
      
         base.OnApplyTemplate();
     }
   
     private void UpdateVisualState()
     {
         // update the control appearance here
     }
      
     private void ThrowIfMissing(DependencyObject element, string name)
     {
         if (element == null)
             throw new Exception(string.Format("Missing required element '{0}'", name));
     }
 }

The OnApplyTemplate method is useful for this purpose. It is called in the initial phase of the life of the control and lets the developer to connect some elements of the control I have called "parts" in the previous article. An element is a "part" when it is attached in the OnApplyTemplate method and it is declared with the TemplatePart attribute at the top of the class. This attribute is not used by the runtime but is useful to external tools to detect the parts for design purpose.

Many times a part is optional and can be easily removed without causing an error in the control. Some other times it is required. The control has to handle both the cases. When a part is optional the logic have to gracefully skip it when its value is null. When a part is required the better thing to do is raising an exception during the OnApplyTemplate method, as I did in the ThrowIfMissing method. Remember that the more the parts are optional more your control will be customizable. And also when you choose a class to take a reference to a part choose always the closest to the root of the inheritance hierarchy that has all the methods and property you need to make it working.

Finally, UpdateVisualState is the method where the visual appearance of the control is updated every time a property changes its value. The method is useful every time there is something changed and it needs to be reflected into the UI. In my control, this method will starts an animation to move the thumb. This update is needed only when the IsChecked property (a dependency property) is changed and when the control is initialized to make it respectful of the current value of the same property.

In Silverlight there is not any way to animate a GridLenght property and this is the reason we need to create a custom animation to make the sliding. I create an empty storyboard and I use it as a timer to change the with of the colums, increasing one and decreasing the other. When the thumb has reached its position the animation is stopped:

 private void UpdateVisualState()
 {
     this.SwitchStoryBoard.Begin();
 }
  
 private void SwitchStoryBoard_Completed(object sender, EventArgs e)
 {
     if (!this.IsChecked)
     {
         if (this.OffColumnElement.Width.Value == 100.0)
             this.SwitchStoryBoard.Stop();
         else
         {
             this.OffColumnElement.Width = new GridLength(this.OffColumnElement.Width.Value + 10, GridUnitType.Star);
             this.OnColumnElement.Width = new GridLength(this.OnColumnElement.Width.Value - 10, GridUnitType.Star);
             this.SwitchStoryBoard.Begin();
         }
     }
     else
     {
         if (this.OnColumnElement.Width.Value == 100.0)
             this.SwitchStoryBoard.Stop();
         else
         {
             this.OnColumnElement.Width = new GridLength(this.OnColumnElement.Width.Value + 10, GridUnitType.Star);
             this.OffColumnElement.Width = new GridLength(this.OffColumnElement.Width.Value - 10, GridUnitType.Star);
             this.SwitchStoryBoard.Begin();
         }
     }
 }

Another thing to do, is to bind some properties of the template to the properties of the control. I used the TemplateBinding for the BorderBrush and BorderThickness, then I created two dependency properties in the control, OnBackground e OffBackground, to leave the user free of customizing the colors of the control. I used again the TemplateBinding, same way I did with the other properties.

 <Style TargetType="local:Switch">
     <Setter Property="BorderThickness" Value="1" />
    <Setter Property="OnBackground" Value="#FFFF0000" />
    <Setter Property="OffBackground" Value="#FF008800" />
     <Setter Property="BorderBrush">
        <Setter.Value>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="#FFA3AEB9" Offset="0"/>
                <GradientStop Color="#FF8399A9" Offset="0.375"/>
                <GradientStop Color="#FF718597" Offset="0.375"/>
                <GradientStop Color="#FF617584" Offset="1"/>
            </LinearGradientBrush>
          </Setter.Value>
     </Setter>
     <Setter Property="Template">
         <Setter.Value>
             <ControlTemplate TargetType="local:Switch">
                 <Grid>
                     <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="Auto" />
                         <ColumnDefinition Width="*" />
                     </Grid.ColumnDefinitions>
                     <Border Grid.Column="1" 
                             BorderThickness="{TemplateBinding BorderThickness}" 
                             BorderBrush="{TemplateBinding BorderBrush}" 
                             CornerRadius="2" HorizontalAlignment="Stretch">
                         <Grid HorizontalAlignment="Stretch">
                             <Grid.ColumnDefinitions>
                                 <ColumnDefinition x:Name="OnColumnElement" Width="0*" />
                                 <ColumnDefinition Width="Auto" />
                                 <ColumnDefinition x:Name="OffColumnElement" Width="100*" />
                             </Grid.ColumnDefinitions>
                             <Rectangle x:Name="OnBackgroundElement" Grid.Column="0" Fill="{TemplateBinding OnBackground}" Opacity="1.0" />
                             <Rectangle x:Name="OffBackgroundElement" Grid.Column="2" Fill="{TemplateBinding OffBackground}" Opacity="1.0" />
                             <Rectangle Grid.Column="1" Fill="#FF617584" />
                             <Thumb x:Name="ThumbElement" Grid.Column="1" Width="40" />
                             <Path x:Name="OffChevron" Grid.Column="1" Data="M-5,-5 L6,3 L-5,11 L3,3 z" 
                                   Fill="#FF617584" Margin="14,4" Stretch="Fill" UseLayoutRounding="False" Opacity="1.0" IsHitTestVisible="False" />
                             <Path x:Name="OnChevron" Grid.Column="1" Data="M6.114583,-5 L-2.25,3 L5.8854165,11 L-5,3 z" 
                                   Fill="#FF617584" Margin="14,4" Stretch="Fill" UseLayoutRounding="False" Opacity="0.0" IsHitTestVisible="False" />
                             <Rectangle x:Name="DisabledVisualElement" Grid.Column="0" Grid.ColumnSpan="3" 
                                        Opacity="0.0" Fill="#eeFFFFFF" IsHitTestVisible="False" />
                             <Rectangle x:Name="ContentFocusVisualElement" Grid.ColumnSpan="3" RadiusX="2" RadiusY="2" 
                                        Stroke="#FF6DBDD1" StrokeThickness="1" Opacity="0" IsHitTestVisible="false" />
                         </Grid>
                     </Border>
                     <ContentPresenter Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,5,0" />
                 </Grid>
             </ControlTemplate>
         </Setter.Value>
     </Setter>
 </Style>

Using the Style this way is very comfortable because it lets the developer to define some default values for properties. In the Setter on top of the template I defined some colors, and attributes that the user can change, but they have defaults to prevent unexpected results.

The Visual States

Now that the control works as expected, it is time to make it more customizable. This capability comes from the use of the VisualStateManager that is able to manage some state groups. First of all I added the couple Normal/Disabled but also the Focused and Unfocused status. While the first couple is implemented using a rectangle on top of all the other parts, the Focused status uses another rectangle to create a border around the control.

Another couple of styles, I've implemented, are Checked/Unchecked. Also if these conditions are showed by the sliding thumb the developer can find useful to have these statuses for the purpose of add some other effects. In my case I added a chevron that switches from left to right. Here is the code for the VisualStateManager:

 <VisualStateManager.VisualStateGroups>
     <VisualStateGroup x:Name="CommonStates">
         <VisualState x:Name="Normal" />
         <VisualState x:Name="Disabled" >
             <Storyboard>
                 <DoubleAnimation Duration="0" To="0.5" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="DisabledVisualElement" />
             </Storyboard>
         </VisualState>
     </VisualStateGroup>
     <VisualStateGroup x:Name="FocusStates">
         <VisualState x:Name="Unfocused" />
         <VisualState x:Name="Focused">
             <Storyboard>
                 <DoubleAnimation Duration="0" To="1.0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ContentFocusVisualElement" />
             </Storyboard>
         </VisualState>
     </VisualStateGroup>
     <VisualStateGroup x:Name="CheckStates">
         <VisualState x:Name="Checked">
             <Storyboard>
                 <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="OffChevron"/>
                 <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="OnChevron"/>
             </Storyboard>
         </VisualState>
         <VisualState x:Name="Unchecked">
             <Storyboard>
                 <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="OffChevron"/>
                 <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="OnChevron"/>
             </Storyboard>
         </VisualState>
     </VisualStateGroup>
 </VisualStateManager.VisualStateGroups>

This chunk of code must be inserted in the root element of the ControlTemplate. As you can see, it defines some animations the runtime will show during the transition. The VSM is capable of interpolate all the possible transitions and due to this reason it is possible to not define all the transitions between states.

Once you have defined the states in the markup into the generic.xaml, you have two final things to do. You have to notify the VisualStateManager about the transitions from a state to another and you have to publish the available states using the TemplateState attribute as you already did with parts and the TemplatePart attribute. Starting by the last, the TemplateState attribute enables the graphics tools to know how many statuses they have to handle.

But what really matter is setting the state of the control because it enables the VisualStateManager to apply the defined animations and transitions. In an event handler you can simply call the GoToState static method on the VisualStateManager class. Here is and example:

 private void Switch_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
 {
     this.ThumbElement.IsEnabled = (bool)e.NewValue;
  
     if (this.ThumbElement.IsEnabled != true)
         VisualStateManager.GoToState(this, "Disabled", true);
     else
         VisualStateManager.GoToState(this, "Normal", true);
 }

This code sets the correct state according to the IsEnabled property value. The three arguments of the GoToState method are:

  • The UIElement which you have to change the state
  • The destination state as you have called it in the markup
  • A boolean indicating if the transitions must be used or if the change must be immediate

Template or not template?

The process of creating a templated control may appear hard, but once you have understood how it works you will find there is not any reason to create controls without a template if they have an UI. Having the basic template expressed as xaml markup makes the development process more simple and straightforward then having to create parts in code. The trick, about control reusability, is to think at them always like behaviors and then add a default appearance, but remember not to close any way to future drastic changes.


Subscribe

Comments

  • -_-

    RE: Understanding control customization with templates: part #2


    posted by krk on Jul 25, 2010 05:05

    Thank you for this Interesting and Simplified Write up.

     KRK

Add Comment

Login to comment:
  *      *       

From this series