Export (0) Print
Expand All

Creating a New Control by Creating a ControlTemplate

Silverlight

Silverlight gives you the ability to create a control whose appearance can be customized. For example, you can change the appearance of a CheckBox beyond what setting properties will do by creating a new ControlTemplate. The following link shows the difference between a CheckBox that uses a default ControlTemplate and a CheckBox that uses a custom ControlTemplate.

Run this sample

If you follow the parts and states model when you create a control, your control's appearance will be customizable. Designer tools such as Microsoft Expression Blend support the parts and states model, so when you follow this model your control will be customizable in those types of applications. This topic discusses the parts and states model and how to follow it when you create your own control. This topic uses an example of a custom control, NumericUpDown, to illustrate the philosophy of this model. The NumericUpDown control displays a numeric value, which a user can increase or decrease by clicking on the control's buttons. For the complete example of the NumericUpDown control, see How to: Create a New Control by Creating a ControlTemplate. To view a running sample of the NumericUpDown control, click the following link.

Run this sample

This topic contains the following sections.

This topic assumes that you know how to create a new ControlTemplate for an existing control, are familiar with what the elements on a control contract are, and understand the concepts discussed in Customizing the Appearance of an Existing Control by Using a ControlTemplate.

Note Note:

To create a control that can have its appearance customized, you must create a control that inherits from the Control class or one of its subclasses other than UserControl. A control that inherits from UserControl is a control that can be quickly created, but it does not use a ControlTemplate and you cannot customize its appearance.

The parts and states model specifies how to define the visual structure and visual behavior of a control. To follow the parts and states model, you should do the following:

  • Define the visual structure and visual behavior in the ControlTemplate of a control.

  • Follow certain best practices when your control's logic interacts with parts of the control template.

  • Provide a control contract to specify what should be included in the ControlTemplate.

When you define the visual structure and visual behavior in the ControlTemplate of a control, application authors can change the visual structure and visual behavior of your control by creating a new ControlTemplate instead of writing code. You must provide a control contract that tells application authors which FrameworkElement objects and states should be defined in the ControlTemplate. You should follow some best practices when you interact with the parts in the ControlTemplate so that your control properly handles an incomplete ControlTemplate. If you follow these three principles, application authors will be able to create a ControlTemplate for your control just as easily as they can for the controls that ship with Silverlight.  The following section explains each of these recommendations in detail.

When you create your custom control by using the parts and states model, you define the control's visual structure and visual behavior in its ControlTemplate instead of in its logic. The visual structure of a control is the composite of FrameworkElement objects that make up the control. The visual behavior is the way the control appears when it is in a certain state. For more information about creating a ControlTemplate that specifies the visual structure and visual behavior of a control, see Customizing the Appearance of an Existing Control by Using a ControlTemplate.

In the example of the NumericUpDown control, the visual structure includes two RepeatButton controls and a TextBlock. If you add these controls in the code of the NumericUpDown control--in its constructor, for example--the positions of those controls would be unalterable. Instead of defining the control's visual structure and visual behavior in its code, you should define it in the ControlTemplate. Then an application developer to customize the position of the buttons and TextBlock and specify what behavior occurs when Value is negative because the ControlTemplate can be replaced.

The following example shows the visual structure of the NumericUpDown control, which includes a RepeatButton to increase Value, a RepeatButton to decrease Value, and a TextBlock to display Value.


<ControlTemplate TargetType="src:NumericUpDown">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>

      <Border BorderThickness="1" BorderBrush="Gray" 
            Margin="7,2,2,2" Grid.RowSpan="2" 
            Background="#E0FFFFFF"
            VerticalAlignment="Center" 
            HorizontalAlignment="Stretch">
        <TextBlock x:Name="TextBlock" TextAlignment="Center" Padding="5"
             Foreground="{TemplateBinding Foreground}"/>

      </Border>
    <RepeatButton Content="Up" Margin="2,5,5,0"
                  x:Name="UpButton"
                  Grid.Column="1" Grid.Row="0"
                  />
    <RepeatButton Content="Down" Margin="2,0,5,5"
                  x:Name="DownButton"
                  Grid.Column="1" Grid.Row="1"/>
  </Grid>


A visual behavior of the NumericUpDown control is that the value is in a red font if it is negative. If you change the Foreground of the TextBlock in code when the Value is negative, the NumericUpDown will always show a red negative value. You specify the visual behavior of the control in the ControlTemplate by adding VisualState objects to the ControlTemplate. The following example shows the VisualState objects for the Positive and Negative states. Positive and Negative are mutually exclusive (the control is always in exactly one of the two), so the example puts the VisualState objects into a single VisualStateGroup. When the control goes into the Negative state, the Foreground of the TextBlock turns red. When the control is in the Positive state, the Foreground returns to it original value. Defining VisualState objects in a ControlTemplate is further discussed in Customizing the Appearance of an Existing Control by Using a ControlTemplate.


<vsm:VisualStateGroup x:Name="ValueStates">

  <!--Make the Value property red when it is negative.-->
  <vsm:VisualState x:Name="Negative">
    <Storyboard>
      <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(SolidBruch.Color)"/>
    </Storyboard>

  </vsm:VisualState>

  <!--Return the control to its initial state by
      return the TextBlock's Foreground to its 
      original color.-->
  <vsm:VisualState x:Name="Positive"/>
</vsm:VisualStateGroup>


A ControlTemplate author might omit FrameworkElement or VisualState objects, either purposefully or by mistake, but your control's logic might need those parts to function properly. The parts and states model specifies that your control should be resilient to a ControlTemplate that is missing FrameworkElement or VisualState objects. Your control should not throw an exception or report an error if a FrameworkElement, VisualState, or VisualStateGroup is missing from the ControlTemplate. This section describes the recommended practices for interacting with FrameworkElement objects and managing states.

Anticipate Missing FrameworkElement Objects

When you define FrameworkElement objects in the ControlTemplate, your control's logic might need to interact with some of them. For example, the NumericUpDown control subscribes to the buttons' Click event to increase or decrease Value and sets the Text property of the TextBlock to Value. If a custom ControlTemplate omits the TextBlock or buttons, it is acceptable that the control loses some of its functionality, but you should be sure that your control does not cause an error. For example, if a ControlTemplate does not contain the buttons to change Value, the NumericUpDown loses that functionality, but an application that uses the ControlTemplate will continue to run.

The following practices will ensure that your control responds properly to missing FrameworkElement objects:

  1. Set the x:Name attribute for each FrameworkElement that you need to reference in code.

  2. Define private properties for each FrameworkElement that you need to interact with.

  3. Subscribe to and unsubscribe from any events that your control handles in the FrameworkElement property's set accessor.

  4. Set the FrameworkElement properties that you defined in step 2 in the OnApplyTemplate method. This is the earliest that the FrameworkElement in the ControlTemplate is available to the control. Use the x:Name of the FrameworkElement to get it from the ControlTemplate.

  5. Check that the FrameworkElement is not null before accessing its members. If it is null, do not report an error.

The following examples show how the NumericUpDown control interacts with FrameworkElement objects in accordance with the recommendations in the preceding list.

In the example that defines the visual structure of the NumericUpDown control in the ControlTemplate, the RepeatButton that increases Value has its x:Name attribute set to UpButton. The following example declares a property called UpButtonElement that represents the RepeatButton that is declared in the ControlTemplate. The set accessor first unsubscribes to the button's Click event if UpDownElement is not null, then it sets the property, and then it subscribes to the Click event. Properties are also defined, but not shown here, for the other RepeatButton, called DownButtonElement, and the TextBlock, called TextProperties.


Private m_upButtonElement As RepeatButton

Private Property UpButtonElement() As RepeatButton
    Get
        Return m_upButtonElement
    End Get

    Set(ByVal repeatBtn As RepeatButton)
        If m_upButtonElement IsNot Nothing Then
            RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
        m_upButtonElement = repeatBtn

        If m_upButtonElement IsNot Nothing Then
            AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
        End If
    End Set
End Property


The following example shows the OnApplyTemplate for the NumericUpDown control. The example uses the GetTemplateChild method to get the FrameworkElement objects from the ControlTemplate. Notice that the example guards against cases where GetTemplateChild finds a FrameworkElement with the specified name that is not of the expected type. It is also a best practice to ignore elements that have the specified x:Name but are of the wrong type.


Public Overloads Overrides Sub OnApplyTemplate()
    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
    TextElement = TryCast(GetTemplateChild("TextBlock"), TextBlock)

    UpdateStates(False)
End Sub


When the Value of the NumericUpDown control changes, the control must update the Text of the TextBlock. The following example shows part of the PropertyChangedCallback, ValueChangedCallback, for the Value dependency property. ValueChangeCallback is called whenever Value changes. The example checks whether TextBlock is null before accessing the Text property to be sure that the control does not try to set the Text property if TextElement is null.


    Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject, _
                        ByVal args As DependencyPropertyChangedEventArgs)

        Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
        Dim newValue As Integer = args.NewValue

        ' Update the TextElement to the new value. 
        If ctl.TextElement IsNot Nothing Then
            ctl.TextElement.Text = newValue.ToString()
        End If



...


End Sub


By following the practices that are shown in the previous examples, you ensure that your control will continue to run when the ControlTemplate is missing a FrameworkElement.

Use the VisualStateManager to Manage States

The VisualStateManager keeps track of the states of a control and performs the logic necessary to transition between states. When you add VisualState objects to the ControlTemplate, you add them to a VisualStateGroup and add the VisualStateGroup to the VisualStateManager.VisualStateGroups attached property so that the VisualStateManager has access to them.

The following example repeats the previous example that shows the VisualState objects that corresponds to the Positive and Negative states of the control. The Storyboard in the Negative VisualState turns the Foreground of the TextBlock red. When the NumericUpDown control is in the Negative state, the storyboard in the Negative state begins. Then the Storyboard in the Negative state stops when the control returns to the Positive state. The Positive VisualState does not need to contain a Storyboard because when the Storyboard for the Negative stops, the Foreground returns to its original color.


<vsm:VisualStateGroup x:Name="ValueStates">

  <!--Make the Value property red when it is negative.-->
  <vsm:VisualState x:Name="Negative">
    <Storyboard>
      <ColorAnimation To="Red"
                      Storyboard.TargetName="TextBlock" 
                      Storyboard.TargetProperty="(Foreground).(SolidBruch.Color)"/>
    </Storyboard>

  </vsm:VisualState>

  <!--Return the control to its initial state by
      return the TextBlock's Foreground to its 
      original color.-->
  <vsm:VisualState x:Name="Positive"/>
</vsm:VisualStateGroup>


The control's logic is responsible for changing the control's state. The following example shows that the NumericUpDown control calls the GoToState method to go into the Positive state when Value is 0 or greater, and the Negative state when Value is less than 0.


If Value >= 0 Then
    VisualStateManager.GoToState(Me, "Positive", useTransitions)
Else
    VisualStateManager.GoToState(Me, "Negative", useTransitions)
End If


The GoToState method performs the logic necessary to start and stop the storyboards appropriately. When a control calls GoToState to change its state, the VisualStateManager does the following:

  • If the VisualState that the control is going to has a Storyboard, the storyboard begins. Then, if the VisualState that the control is coming from has a Storyboard, the storyboard ends.

  • If the control is already in the state that is specified, GoToState takes no action and returns true.

  • If state that is specified doesn't exist in the ControlTemplate of control, GoToState takes no action and returns false.

Best Practices for Working with the VisualStateManager

It is recommended that you do the following to maintain your control's states:

  • Use properties to track its state.

  • Create a helper method to transition between states.

The NumericUpDown control uses its Value property to track whether it is in the Positive or Negative state. In addition to the Positive and Negative states, the NumericUpDown control defines the Focused and UnFocused states, which occur when the NumericUpDown has focus, or does not have focus, respectively. The following example shows that the NumericUpDown control defines a private property to track whether the control has focus and sets that property according to whether the control has focus.


Private isFocused As Boolean

Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    isFocused = True
    UpdateStates(True)
End Sub

Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
    MyBase.OnLostFocus(e)
    isFocused = False
    UpdateStates(True)
End Sub


The following example shows the NumericUpDown control's helper method, UpdateStates. When Value is greater than or equal to 0, the Control is in the Positive state. When Value is less than 0, the control is in the Negative state. When isFocused is true, the control is in the Focused state; otherwise, it is in the Unfocused state.


Private Sub UpdateStates(ByVal useTransitions As Boolean)
    If Value >= 0 Then
        VisualStateManager.GoToState(Me, "Positive", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Negative", useTransitions)
    End If

    If isFocused Then
        VisualStateManager.GoToState(Me, "Focused", useTransitions)
    Else
        VisualStateManager.GoToState(Me, "Unfocused", useTransitions)
    End If

End Sub


If you pass a state name to GoToState when the control is already in that state, GoToState does nothing, so you don't need to check for the control's current state. For example, if Value changes from one negative number to another negative number, the storyboard for the Negative state is not interrupted and the user will not see a change in the control.

The VisualStateManager uses VisualStateGroup objects to determine which state to exit when you call GoToState. The control is always in one state for each VisualStateGroup that is defined in its ControlTemplate and only leaves a state when it goes into another state from the same VisualStateGroup. For example, the ControlTemplate of the NumericUpDown control defines the Positive and Negative VisualState objects in one VisualStateGroup and the Focused and Unfocused VisualState objects in another. (You can see the Focused and Unfocused VisualState defined in How to: Create a New Control by Creating a ControlTemplate.) When the control goes from the Positive state to the Negative state, or vice versa, the control remains in either the Focused or Unfocused state.

There are three typical places where the state of a control might change:

The following examples demonstrate updating the state of the NumericUpDown control in these cases.

You should update the state of the control in the OnApplyTemplate method so that the control appears in the correct state when the ControlTemplate is applied. The following example calls UpdateStates in OnApplyTemplate to ensure that the control is in the appropriate states. For example, suppose that you create a NumericUpDown control, and then set its Foreground to green and Value to -5. If you do not call UpdateStates when the ControlTemplate is applied to the NumericUpDown control, the control is not in the Negative state and the value is green instead of red. You must call UpdateStates to put the control in the Negative state.


Public Overloads Overrides Sub OnApplyTemplate()
    UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
    DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
    TextElement = TryCast(GetTemplateChild("TextBlock"), TextBlock)

    UpdateStates(False)
End Sub


You often need to update the states of a control when a property changes. The following example shows the entire ValueChangedCallback method. Because ValueChangedCallback is called when Value changes, the method calls UpdateStates in case Value changed from positive to negative or vice versa. It is acceptable to call UpdateStates when Value changes but remains positive or negative because in that case, the control will not change states.


Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject, _
                    ByVal args As DependencyPropertyChangedEventArgs)

    Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
    Dim newValue As Integer = args.NewValue

    ' Update the TextElement to the new value. 
    If ctl.TextElement IsNot Nothing Then
        ctl.TextElement.Text = newValue.ToString()
    End If

    ' Call UpdateStates because the Value might have caused the 
    ' control to change ValueStates. 
    ctl.UpdateStates(True)

    ' Raise the ValueChanged event so applications can be alerted 
    ' when Value changes. 
    Dim e As New ValueChangedEventArgs(newValue)
    ctl.OnValueChanged(e)
End Sub


You might also need to update states when an event occurs. The following example shows that the NumericUpDown calls UpdateStates on the Control to handle the GotFocus event.


Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
    MyBase.OnGotFocus(e)
    isFocused = True
    UpdateStates(True)
End Sub


The VisualStateManager helps you manage your control's states. By using the VisualStateManager, you ensure that your control correctly transitions between states. If you follow the recommendations described in this section for working with the VisualStateManager, your control's code will remain readable and maintainable.

You provide a control contract so that ControlTemplate authors will know what to put in the template. A control contract has three elements:

  • The visual elements that the control's logic uses.

  • The states of the control and the group each state belongs to.

  • The public properties that visually affect the control.

Someone that creates a new ControlTemplate needs to know what FrameworkElement objects the control's logic uses, what type each object is, and what its name is. A ControlTemplate author also needs to know the name of each possible state the control can be in, and which VisualStateGroup the state is in.

Returning to the NumericUpDown example, the control expects the ControlTemplate to have the following FrameworkElement objects:

The control can be in the following states:

To specify what FrameworkElement objects the control expects, you use the TemplatePartAttribute, which specifies the name and type of the expected elements. To specify the possible states of a control, you use the TemplateVisualStateAttribute, which specifies the state's name and which VisualStateGroup it belongs to. Put the TemplatePartAttribute and TemplateVisualStateAttribute on the class definition of the control.

Any public property that affects the appearance of your control is also a part of the control contract.

The following example specifies the FrameworkElement object and states for the NumericUpDown control.


<TemplatePart(Name:="TextElement", Type:=GetType(TextBlock))> _
<TemplatePart(Name:="UpButtonElement", Type:=GetType(RepeatButton))> _
<TemplatePart(Name:="DownButtonElement", Type:=GetType(RepeatButton))> _
<TemplateVisualState(Name:="Positive", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Negative", GroupName:="ValueStates")> _
<TemplateVisualState(Name:="Focused", GroupName:="FocusedStates")> _
<TemplateVisualState(Name:="Unfocused", GroupName:="FocusedStates")> _
Public Class NumericUpDown
    Inherits Control

    Public Shared ReadOnly BackgroundProperty As DependencyProperty
    Public Shared ReadOnly BorderBrushProperty As DependencyProperty
    Public Shared ReadOnly BorderThicknessProperty As DependencyProperty
    Public Shared ReadOnly FontFamilyProperty As DependencyProperty
    Public Shared ReadOnly FontSizeProperty As DependencyProperty
    Public Shared ReadOnly FontStretchProperty As DependencyProperty
    Public Shared ReadOnly FontStyleProperty As DependencyProperty
    Public Shared ReadOnly FontWeightProperty As DependencyProperty
    Public Shared ReadOnly ForegroundProperty As DependencyProperty
    Public Shared ReadOnly HorizontalContentAlignmentProperty As DependencyProperty
    Public Shared ReadOnly PaddingProperty As DependencyProperty
    Public Shared ReadOnly TextAlignmentProperty As DependencyProperty
    Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
    Public Shared ReadOnly TextWrappingProperty As DependencyProperty
    Public Shared ReadOnly VerticalContentAlignmentProperty As DependencyProperty


    Public Property Background() As Brush
        Get
        End Get

        Set(ByVal value As Brush)
        End Set
    End Property

    Public Property BorderBrush() As Brush
        Get
        End Get

        Set(ByVal value As Brush)
        End Set
    End Property

    Public Property BorderThickness() As Thickness
        Get
        End Get

        Set(ByVal value As Thickness)
        End Set
    End Property

    Public Property FontFamily() As FontFamily
        Get
        End Get

        Set(ByVal value As FontFamily)
        End Set
    End Property

    Public Property FontSize() As Double
        Get
        End Get

        Set(ByVal value As Double)
        End Set
    End Property

    Public Property FontStretch() As FontStretch
        Get
        End Get

        Set(ByVal value As FontStretch)
        End Set
    End Property

    Public Property FontStyle() As FontStyle
        Get
        End Get

        Set(ByVal value As FontStyle)
        End Set
    End Property

    Public Property FontWeight() As FontWeight
        Get
        End Get

        Set(ByVal value As FontWeight)
        End Set
    End Property

    Public Property Foreground() As Brush
        Get
        End Get

        Set(ByVal value As Brush)
        End Set
    End Property

    Public Property HorizontalContentAlignment() As HorizontalAlignment
        Get
        End Get

        Set(ByVal value As HorizontalAlignment)
        End Set
    End Property

    Public Property Padding() As Thickness
        Get
        End Get

        Set(ByVal value As Thickness)
        End Set
    End Property

    Public Property TextAlignment() As TextAlignment
        Get
        End Get

        Set(ByVal value As TextAlignment)
        End Set
    End Property

    Public Property TextDecorations() As TextDecorationCollection
        Get
        End Get

        Set(ByVal value As TextDecorationCollection)
        End Set
    End Property

    Public Property TextWrapping() As TextWrapping
        Get
        End Get

        Set(ByVal value As TextWrapping)
        End Set
    End Property

    Public Property VerticalContentAlignment() As VerticalAlignment
        Get
        End Get

        Set(ByVal value As VerticalAlignment)
        End Set
    End Property
End Class


Community Additions

ADD
Show:
© 2014 Microsoft