Creating a Control That Has a Customizable Appearance
Windows Presentation Foundation (WPF) 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 illustration shows a CheckBox that uses a default ControlTemplate and a CheckBox that uses a custom ControlTemplate.
A CheckBox that uses the default control template
A CheckBox that uses a custom control template
If you follow the parts and states model when you create a control, your control's appearance will be customizable. Designer tools such as Blend for Visual Studio 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. The following illustration shows the NumericUpDown
control that is discussed in this topic.
A custom NumericUpDown control
This topic contains the following sections:
Prerequisites
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 Create a template for a control.
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.
Parts and States Model
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 WPF. The following section explains each of these recommendations in detail.
Defining the Visual Structure and Visual Behavior of a Control in a ControlTemplate
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 Create a template for a control.
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 Margin="3"
Background="{TemplateBinding Background}">
<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">
<!--Bind the TextBlock to the Value property-->
<TextBlock Name="TextBlock"
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type src:NumericUpDown}},
Path=Value}"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Name="UpButton"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Name="DownButton"
Grid.Column="1" Grid.Row="1"/>
<Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2"
Stroke="Black" StrokeThickness="1"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
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 its original value. Defining VisualState objects in a ControlTemplate is further discussed in Create a template for a control.
Note
Be sure to set the VisualStateManager.VisualStateGroups attached property on the root FrameworkElement of the ControlTemplate.
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
Using Parts of the ControlTemplate in Code
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:
Set the
x:Name
attribute for each FrameworkElement that you need to reference in code.Define private properties for each FrameworkElement that you need to interact with.
Subscribe to and unsubscribe from any events that your control handles in the FrameworkElement property's set accessor.
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.Check that the FrameworkElement is not
null
before accessing its members. If it isnull
, 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. There is also a property defined, but not shown here, for the other RepeatButton, called DownButtonElement
.
private RepeatButton upButtonElement;
private RepeatButton UpButtonElement
{
get
{
return upButtonElement;
}
set
{
if (upButtonElement != null)
{
upButtonElement.Click -=
new RoutedEventHandler(upButtonElement_Click);
}
upButtonElement = value;
if (upButtonElement != null)
{
upButtonElement.Click +=
new RoutedEventHandler(upButtonElement_Click);
}
}
}
Private m_upButtonElement As RepeatButton
Private Property UpButtonElement() As RepeatButton
Get
Return m_upButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_upButtonElement IsNot Nothing Then
RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
m_upButtonElement = value
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 override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
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 correspond 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.
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
Note that the TextBlock is given a name, but the TextBlock is not in the control contract for NumericUpDown
because the control's logic never references the TextBlock. Elements that are referenced in the ControlTemplate have names, but do not need to be part of the control contract because a new ControlTemplate for the control might not need to reference that element. For example, someone who creates a new ControlTemplate for NumericUpDown
might decide to not indicate that Value
is negative by changing the Foreground. In that case, neither the code nor the ControlTemplate references the TextBlock by name.
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)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
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 returnsfalse
.
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. The NumericUpDown
control also defines the Focused
and UnFocused
states, which tracks the IsFocused property. If you use states that do not naturally correspond to a property of the control, you can define a private property to track the state.
A single method that updates all the states centralizes calls to the VisualStateManager and keeps your code manageable. 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. The control can call UpdateStates
whenever it needs to change its state, regardless of what state changes.
private void UpdateStates(bool useTransitions)
{
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
if (IsFocused)
{
VisualStateManager.GoToState(this, "Focused", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Unfocused", useTransitions);
}
}
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 the Complete Example section in this topic 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:
When the ControlTemplate is applied to the Control.
When a property changes.
When an event occurs.
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 override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
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 static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
NumericUpDown ctl = (NumericUpDown)obj;
int newValue = (int)args.NewValue;
// Call UpdateStates because the Value might have caused the
// control to change ValueStates.
ctl.UpdateStates(true);
// Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(
new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
newValue));
}
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
Dim newValue As Integer = CInt(args.NewValue)
' Call UpdateStates because the Value might have caused the
' control to change ValueStates.
ctl.UpdateStates(True)
' Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
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 override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateStates(true);
}
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
MyBase.OnGotFocus(e)
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.
Providing the Control Contract
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:
A RepeatButton called
UpButton
.A RepeatButton called
DownButton.
The control can be in the following states:
In the
ValueStates
VisualStateGroupPositive
Negative
In the
FocusStates
VisualStateGroupFocused
Unfocused
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 = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
public static readonly DependencyProperty BackgroundProperty;
public static readonly DependencyProperty BorderBrushProperty;
public static readonly DependencyProperty BorderThicknessProperty;
public static readonly DependencyProperty FontFamilyProperty;
public static readonly DependencyProperty FontSizeProperty;
public static readonly DependencyProperty FontStretchProperty;
public static readonly DependencyProperty FontStyleProperty;
public static readonly DependencyProperty FontWeightProperty;
public static readonly DependencyProperty ForegroundProperty;
public static readonly DependencyProperty HorizontalContentAlignmentProperty;
public static readonly DependencyProperty PaddingProperty;
public static readonly DependencyProperty TextAlignmentProperty;
public static readonly DependencyProperty TextDecorationsProperty;
public static readonly DependencyProperty TextWrappingProperty;
public static readonly DependencyProperty VerticalContentAlignmentProperty;
public Brush Background { get; set; }
public Brush BorderBrush { get; set; }
public Thickness BorderThickness { get; set; }
public FontFamily FontFamily { get; set; }
public double FontSize { get; set; }
public FontStretch FontStretch { get; set; }
public FontStyle FontStyle { get; set; }
public FontWeight FontWeight { get; set; }
public Brush Foreground { get; set; }
public HorizontalAlignment HorizontalContentAlignment { get; set; }
public Thickness Padding { get; set; }
public TextAlignment TextAlignment { get; set; }
public TextDecorationCollection TextDecorations { get; set; }
public TextWrapping TextWrapping { get; set; }
public VerticalAlignment VerticalContentAlignment { get; set; }
}
<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 TextAlignmentProperty As DependencyProperty
Public Shared ReadOnly TextDecorationsProperty As DependencyProperty
Public Shared ReadOnly TextWrappingProperty As DependencyProperty
Public Property TextAlignment() As TextAlignment
Public Property TextDecorations() As TextDecorationCollection
Public Property TextWrapping() As TextWrapping
End Class
Complete Example
The following example is the entire ControlTemplate for the NumericUpDown
control.
<!--This is the contents of the themes/generic.xaml file.-->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:VSMCustomControl">
<Style TargetType="{x:Type local:NumericUpDown}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NumericUpDown">
<Grid Margin="3"
Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="ValueStates">
<!--Make the Value property red when it is negative.-->
<VisualState Name="Negative">
<Storyboard>
<ColorAnimation To="Red"
Storyboard.TargetName="TextBlock"
Storyboard.TargetProperty="(Foreground).(Color)"/>
</Storyboard>
</VisualState>
<!--Return the control to its initial state by
return the TextBlock's Foreground to its
original color.-->
<VisualState Name="Positive"/>
</VisualStateGroup>
<VisualStateGroup Name="FocusStates">
<!--Add a focus rectangle to highlight the entire control
when it has focus.-->
<VisualState Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusVisual"
Storyboard.TargetProperty="Visibility" Duration="0">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<!--Return the control to its initial state by
hiding the focus rectangle.-->
<VisualState Name="Unfocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<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">
<!--Bind the TextBlock to the Value property-->
<TextBlock Name="TextBlock"
Width="60" TextAlignment="Right" Padding="5"
Text="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:NumericUpDown}},
Path=Value}"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Name="UpButton"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Name="DownButton"
Grid.Column="1" Grid.Row="1"/>
<Rectangle Name="FocusVisual" Grid.ColumnSpan="2" Grid.RowSpan="2"
Stroke="Black" StrokeThickness="1"
Visibility="Collapsed"/>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
The following example shows the logic for the NumericUpDown
.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
namespace VSMCustomControl
{
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplateVisualState(Name = "Positive", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Negative", GroupName = "ValueStates")]
[TemplateVisualState(Name = "Focused", GroupName = "FocusedStates")]
[TemplateVisualState(Name = "Unfocused", GroupName = "FocusedStates")]
public class NumericUpDown : Control
{
public NumericUpDown()
{
DefaultStyleKey = typeof(NumericUpDown);
this.IsTabStop = true;
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value", typeof(int), typeof(NumericUpDown),
new PropertyMetadata(
new PropertyChangedCallback(ValueChangedCallback)));
public int Value
{
get
{
return (int)GetValue(ValueProperty);
}
set
{
SetValue(ValueProperty, value);
}
}
private static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
NumericUpDown ctl = (NumericUpDown)obj;
int newValue = (int)args.NewValue;
// Call UpdateStates because the Value might have caused the
// control to change ValueStates.
ctl.UpdateStates(true);
// Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(
new ValueChangedEventArgs(NumericUpDown.ValueChangedEvent,
newValue));
}
public static readonly RoutedEvent ValueChangedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
typeof(ValueChangedEventHandler), typeof(NumericUpDown));
public event ValueChangedEventHandler ValueChanged
{
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
protected virtual void OnValueChanged(ValueChangedEventArgs e)
{
// Raise the ValueChanged event so applications can be alerted
// when Value changes.
RaiseEvent(e);
}
private void UpdateStates(bool useTransitions)
{
if (Value >= 0)
{
VisualStateManager.GoToState(this, "Positive", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Negative", useTransitions);
}
if (IsFocused)
{
VisualStateManager.GoToState(this, "Focused", useTransitions);
}
else
{
VisualStateManager.GoToState(this, "Unfocused", useTransitions);
}
}
public override void OnApplyTemplate()
{
UpButtonElement = GetTemplateChild("UpButton") as RepeatButton;
DownButtonElement = GetTemplateChild("DownButton") as RepeatButton;
//TextElement = GetTemplateChild("TextBlock") as TextBlock;
UpdateStates(false);
}
private RepeatButton downButtonElement;
private RepeatButton DownButtonElement
{
get
{
return downButtonElement;
}
set
{
if (downButtonElement != null)
{
downButtonElement.Click -=
new RoutedEventHandler(downButtonElement_Click);
}
downButtonElement = value;
if (downButtonElement != null)
{
downButtonElement.Click +=
new RoutedEventHandler(downButtonElement_Click);
}
}
}
void downButtonElement_Click(object sender, RoutedEventArgs e)
{
Value--;
}
private RepeatButton upButtonElement;
private RepeatButton UpButtonElement
{
get
{
return upButtonElement;
}
set
{
if (upButtonElement != null)
{
upButtonElement.Click -=
new RoutedEventHandler(upButtonElement_Click);
}
upButtonElement = value;
if (upButtonElement != null)
{
upButtonElement.Click +=
new RoutedEventHandler(upButtonElement_Click);
}
}
}
void upButtonElement_Click(object sender, RoutedEventArgs e)
{
Value++;
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Focus();
}
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
UpdateStates(true);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
UpdateStates(true);
}
}
public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);
public class ValueChangedEventArgs : RoutedEventArgs
{
private int _value;
public ValueChangedEventArgs(RoutedEvent id, int num)
{
_value = num;
RoutedEvent = id;
}
public int Value
{
get { return _value; }
}
}
}
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Input
Imports System.Windows.Media
<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 Sub New()
DefaultStyleKeyProperty.OverrideMetadata(GetType(NumericUpDown), New FrameworkPropertyMetadata(GetType(NumericUpDown)))
Me.IsTabStop = True
End Sub
Public Shared ReadOnly ValueProperty As DependencyProperty =
DependencyProperty.Register("Value", GetType(Integer), GetType(NumericUpDown),
New PropertyMetadata(New PropertyChangedCallback(AddressOf ValueChangedCallback)))
Public Property Value() As Integer
Get
Return CInt(GetValue(ValueProperty))
End Get
Set(ByVal value As Integer)
SetValue(ValueProperty, value)
End Set
End Property
Private Shared Sub ValueChangedCallback(ByVal obj As DependencyObject,
ByVal args As DependencyPropertyChangedEventArgs)
Dim ctl As NumericUpDown = DirectCast(obj, NumericUpDown)
Dim newValue As Integer = CInt(args.NewValue)
' Call UpdateStates because the Value might have caused the
' control to change ValueStates.
ctl.UpdateStates(True)
' Call OnValueChanged to raise the ValueChanged event.
ctl.OnValueChanged(New ValueChangedEventArgs(NumericUpDown.ValueChangedEvent, newValue))
End Sub
Public Shared ReadOnly ValueChangedEvent As RoutedEvent =
EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct,
GetType(ValueChangedEventHandler), GetType(NumericUpDown))
Public Custom Event ValueChanged As ValueChangedEventHandler
AddHandler(ByVal value As ValueChangedEventHandler)
Me.AddHandler(ValueChangedEvent, value)
End AddHandler
RemoveHandler(ByVal value As ValueChangedEventHandler)
Me.RemoveHandler(ValueChangedEvent, value)
End RemoveHandler
RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
Me.RaiseEvent(e)
End RaiseEvent
End Event
Protected Overridable Sub OnValueChanged(ByVal e As ValueChangedEventArgs)
' Raise the ValueChanged event so applications can be alerted
' when Value changes.
MyBase.RaiseEvent(e)
End Sub
#Region "NUDCode"
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
Public Overloads Overrides Sub OnApplyTemplate()
UpButtonElement = TryCast(GetTemplateChild("UpButton"), RepeatButton)
DownButtonElement = TryCast(GetTemplateChild("DownButton"), RepeatButton)
UpdateStates(False)
End Sub
Private m_downButtonElement As RepeatButton
Private Property DownButtonElement() As RepeatButton
Get
Return m_downButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_downButtonElement IsNot Nothing Then
RemoveHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
End If
m_downButtonElement = value
If m_downButtonElement IsNot Nothing Then
AddHandler m_downButtonElement.Click, AddressOf downButtonElement_Click
End If
End Set
End Property
Private Sub downButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Value -= 1
End Sub
Private m_upButtonElement As RepeatButton
Private Property UpButtonElement() As RepeatButton
Get
Return m_upButtonElement
End Get
Set(ByVal value As RepeatButton)
If m_upButtonElement IsNot Nothing Then
RemoveHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
m_upButtonElement = value
If m_upButtonElement IsNot Nothing Then
AddHandler m_upButtonElement.Click, AddressOf upButtonElement_Click
End If
End Set
End Property
Private Sub upButtonElement_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
Value += 1
End Sub
Protected Overloads Overrides Sub OnMouseLeftButtonDown(ByVal e As MouseButtonEventArgs)
MyBase.OnMouseLeftButtonDown(e)
Focus()
End Sub
Protected Overloads Overrides Sub OnGotFocus(ByVal e As RoutedEventArgs)
MyBase.OnGotFocus(e)
UpdateStates(True)
End Sub
Protected Overloads Overrides Sub OnLostFocus(ByVal e As RoutedEventArgs)
MyBase.OnLostFocus(e)
UpdateStates(True)
End Sub
#End Region
End Class
Public Delegate Sub ValueChangedEventHandler(ByVal sender As Object,
ByVal e As ValueChangedEventArgs)
Public Class ValueChangedEventArgs
Inherits RoutedEventArgs
Public Sub New(ByVal id As RoutedEvent,
ByVal num As Integer)
Value = num
RoutedEvent = id
End Sub
Public ReadOnly Property Value() As Integer
End Class
See also
.NET Desktop feedback