[This topic is pre-release documentation and is subject to change in future releases of Microsoft Silverlight.]
Creating Custom Controls for Silverlight
Introduction
This walkthrough shows you how to create a custom, templatable control. A templatable control enables application developers who use your control to change its appearance without writing code or changing its functionality. To make a control templatable, you need to separate the control's visual structure and visual behavior from its logic. This walkthrough creates a templatable control by using the techniques discussed in Creating a Templatable Control.
Prerequisites
The following tools are required to complete this walkthrough and are available from the Silverlight download site.
Control UI and Object Model
Creating a control involves two general tasks: defining the UI and defining the logic of the control. The UI is primarily defined in XAML. The logic is primarily defined in code. The logic sometimes needs to reference the UI. For example, you might name parts in XAML, and then get references to the parts in the code.
This walkthrough demonstrates making a NumericUpDown control, which enables a user to increase or decrease its Value property by clicking on its buttons. Although the code and XAML are typically compiled into a library, which is then used by other applications or projects, in this walkthrough, the application and custom control are in the same project.
Creating a Custom Control and Application for Silverlight
To create your application and custom control
-
Create a Silverlight project in Visual Studio called CustomControlWalkthrough. (For instructions about how to create a Silverlight project, see Creating an Application for Silverlight.)
-
On the View menu, click Solution Explorer.
-
Right-click the project CustomControlWalkthrough. Select Add and click New Item.
-
In the left pane, select the item that corresponds to the language you are using (Visual Basic, Visual C#, and so forth). In the right pane, select Class.
-
In the text box labeled Name, enter NumericUpDown and click OK.
-
On the View menu, click Solution Explorer.
-
Right-click the project CustomControlWalkthrough. Select Add and click New Item.
-
In the left pane, select the item that corresponds to the language you are using (Visual Basic, Visual C#, and so forth). In the right pane, select XML File.
-
In the text box labeled Name, enter generic.xaml (this must be the name of the file) and click OK.
Defining the UI
The UI of your custom control is defined in a ControlTemplate, which is defined in generic.xaml.
To create the UI for your control
-
Make the NumericUpDown control inherit from the Control class.
cs
public class NumericUpDown : Control
{
}
-
Replace the contents of generic.xaml with the following XAML. This is the ControlTemplate for the NumericUpDown control, and consists of two buttons and a TextBlock inside of a grid.
note
If did not name the Visual Studio CustomControlWalkthrough, you must substitute the name of your project in the line xmlns:src="clr-namespace:CustomControlWalkthrough;assembly=CustomControlWalkthrough".
cs
<ResourceDictionary
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:CustomControlWalkthrough;assembly=CustomControlWalkthrough"
>
<Style TargetType="src:NumericUpDown">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="src:NumericUpDown">
<Grid x:Name="RootElement" Margin="3">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!--Elements that defines the control's visual
structure.-->
<Border BorderThickness="1" BorderBrush="Gray"
Margin="7,2,2,2" Grid.RowSpan="2"
Background="#E0FFFFFF"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<TextBlock x:Name="TextElement"
TextAlignment="Center" Padding="5"/>
</Border>
<RepeatButton Content="Up" Margin="2,5,5,0"
Background="#FF333233"
x:Name="UpButtonElement"
Grid.Column="1" Grid.Row="0"/>
<RepeatButton Content="Down" Margin="2,0,5,5"
Background="#FF333233"
x:Name="DownButtonElement"
Grid.Column="1" Grid.Row="1"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Now that you have created the UI for your control, you can add it to your Silverlight-based application and test it.
To test your control
-
Add the following XAML as an attribute to the root <UserControl> element in Page.xaml. Replace CustomControlWalkthrough if your project has a different name.
xmlns:src="clr-namespace:CustomControlWalkthrough"
-
Add the following XAML between the <Grid> tags in Page.xaml.
<src:NumericUpDown Width="100" Height="60"/>
-
On the Debug menu, click Start Debugging.
You should be able to see the NumericUpDown control, but you might have noticed that the control doesn't do anything. The next step is to add the control's logic to implement its functionality.
Implementing the Functionality of the Control
You will define logic of your control in the class file NumericUpDown. The NumericUpDown control increases its Value by one when the user clicks Up and decreases when the user clicks Down. In this section, you will create the Value property and change Value when the user clicks a button.
In Silverlight, it is recommended that you create properties as dependency properties, so that they can participate in styling, binding, and animation. For more information, see Dependency Properties Overview.
To define the Value property as a dependency property
-
Add the following code to the NumericUpDown class. This example does the following:
-
Defines a field called ValueProperty as a DependencyProperty by calling Register(String, Type, Type, PropertyChangedCallback). When you call Register(String, Type, Type, PropertyChangedCallback), you define the name of the property, the type of the property, the type that owns the property, and a PropertyChangedCallback that is called when the property changes.
-
Defines a method called ValueChangedCallback as the PropertyChangedCallback. You will add the logic to this method later in this walkthrough.
-
Defines the Value property, which is the same name that is used to register the dependency property. The get and set accessors call GetValue(DependencyProperty) and SetValue(DependencyProperty, Object), respectively, which is the recommended way to get and set dependency properties in Silverlight.
cs
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(NumericUpDown),
new PropertyChangedCallback(ValueChangedCallback));
private static void ValueChangedCallback(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
// Add animation and event logic here.
}
public int Value
{
get
{
return (int)GetValue(ValueProperty);
}
set
{
SetValue(ValueProperty, value);
}
}
Now that the Value property is defined, you need to subscribe to the Click event for the buttons, update Value in the Click event handlers, and put Value in the TextBlock. To do these things, the control's code needs to reference parts that are in its ControlTemplate. Remember that the primary goal for using a ControlTemplate is to allow others to customize the appearance of your control by creating a new ControlTemplate. To create a robust control that will function even if parts are missing from the ControlTemplate, you should create private properties for each part and subscribe to events in the properties' set accessors.
To reference the elements that are defined in the ControlTemplate in your code.
-
Add the following properties to the NumericUpDown class. This example defines the properties UpButtonElement, DownButtonElement, TextElement, and RootElement, which correspond to the elements that are defined in the ControlTemplate.
The UpButtonElement and DownButtonElementset accessors do the following:
cs
private RepeatButton upButtonElement;
private RepeatButton downButtonElement;
private TextBlock textElement;
private FrameworkElement rootElement;
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 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);
}
}
}
private TextBlock TextElement
{
get { return textElement; }
set
{
textElement = value;
if (textElement != null)
{
textElement.Text = Value.ToString();
}
}
}
private FrameworkElement RootElement
{
get
{
return rootElement;
}
set
{
rootElement = value;
}
}
-
Add the following method to the NumericUpDown class. This method searches for the elements in the ControlTemplate and assigns them to the appropriate property so that the control has a reference to the element.
cs
protected override void OnApplyTemplate()
{
RootElement = (FrameworkElement)GetTemplateChild("RootElement");
TextElement = (TextBlock)GetTemplateChild("TextElement");
UpButtonElement = (RepeatButton)GetTemplateChild("UpButtonElement");
DownButtonElement = (RepeatButton)GetTemplateChild("DownButtonElement");
}
Now that the control has references to the buttons and TextBlock that are defined in the ControlTemplate, you can change Value and when the user clicks a button and show the Value in TextBlock.
To change Value when the user clicks a button
-
Add the following Click event handlers to the NumericUpDown class. These handlers increase and decrease Value when the user clicks UpButtonElement and DownButtonElement, respectively.
cs
void upButtonElement_Click(object sender, RoutedEventArgs e)
{
Value++;
}
void downButtonElement_Click(object sender, RoutedEventArgs e)
{
Value--;
}
-
Add the following to the ValueChangedCallback method that you defined when you defined the Value property. ValueChangedCallback is called whenever the Value property is changed. This code updates the Text property
cs
NumericUpDown ctl = (NumericUpDown)obj;
int oldValue = (int)args.OldValue;
int newValue = (int)args.NewValue;
// Update the TextElement to the new value.
if (ctl.TextElement != null)
{
ctl.TextElement.Text = newValue.ToString();
}
Now when you run your application and click one of the buttons, you can see the value of the NumericUpDown control change.
Adding Properties that affect your Control’s appearance
Many controls provide properties that affect the appearance of the control. This enables the application developer to set properties to slightly change the control’s appearance without creating a new ControlTemplate. When the application developer wants to change the appearance in more drastic ways, a new ControlTemplate needs to be created.
When you provide a property that affects the appearance of your control, you should bind it to an element in your ControlTemplate by using the TemplateBinding M arkup E xtension. This binds a property that is in the ControlTemplate to the property of your control. This section creates a Background property for the NumericUpDown control and uses the TemplateBinding Markup Extension markup extension to bind it to the Grid in the ControlTemplate.
To create a property and bind it to the ControlTemplate
-
Add the following to the NumericUpDown class. This defines the Background dependency property.
-
Replace the first <Grid> tag in the ControlTemplate with the following XAML.
<Grid x:Name="RootElement" Margin="3"
Background="{TemplateBinding Background}">
-
Replace the <src:NumericUpDown> element with the following in page.xaml.
cs
<src:NumericUpDown Background="Blue" Width="100" Height="60"/>
Now when you run the application, the NumericUpDown control is that color specified. You can change the Background property to change the control’s color without creating a new ControlTemplate.
Changing the Appearance of the Control When Its State Changes
Many controls' appearance slightly changes to indicate that it is in a certain state. For example, a button might appear darker to indicate that the user clicked it. You should define the state parts in the ControlTemplate so that people who customize the appearance of your control can also customize its appearance according to its state. A state is defined by a Storyboard.
This section shows you how to create two states for the NumericUpDown control: the Negative State, and the Normal State. The NumericUpDown control is in the Negative State when Value is a negative number. The control is in the Normal State whenever it is not in another state; in this case, the NumericUpDown control is in the NormalState when Value is 0 or positive. All custom controls should have a Normal State.
To create use the Negative and Normal States
-
Add the following XAML after the <Grid> opening element, before the <Grid.RowDefinithions> element, in the generic.xaml file. The Negative State sets the Foreground of the TextBlock, TextElement, to red. The Normal State sets Foreground to black.
cs
<Grid.Resources>
<!--Elements that define the Control's state.-->
<Storyboard x:Key="Negative State">
<ColorAnimation BeginTime="00:00:00" To="Red" Duration="0:0:0"
Storyboard.TargetName="TextElement"
Storyboard.TargetProperty="(Foreground).(SolidBruch.Color)"/>
</Storyboard>
<Storyboard x:Key="Normal State">
<ColorAnimation BeginTime="00:00:00" To="Black" Duration="0:0:0"
Storyboard.TargetName="TextElement"
Storyboard.TargetProperty="(Foreground).(SolidBruch.Color)"/>
</Storyboard>
</Grid.Resources>
-
Add the following to the NumericUpDown class. This example creates the properties NegativeState and NormalState.
cs
private Storyboard negativeState;
private Storyboard normalState;
private Storyboard currentState;
private Storyboard NegativeState
{
get { return negativeState; }
set { negativeState = value; }
}
private Storyboard NormalState
{
get { return normalState; }
set { normalState = value; }
}
-
Add the following to the ApplyTemplate() method in the NumericUpDown class. Notice that the example gets the Resources of RootElement to set the NegativeState and NormalState properties. You must be sure that you added the storyboards to the grid's resources in the first step of this procedure for this to work correctly.
cs
if (RootElement != null)
{
NormalState = (Storyboard)RootElement.Resources["Normal State"];
NegativeState = (Storyboard)RootElement.Resources["Negative State"];
}
-
Add the following to the NumericUpDown class. The GoToState method starts the new state's storyboard and then stops the state's storyboard (if there is one). It is recommended that you always start and stop storyboards in you custom control in this order. The DoValueAnimation method begins the NegativeState storyboard when Value goes from positive to negative, and begins the NormalState storyboard when Value goes from negative to positive.
cs
// If Value changes from positive to negative,
// begin the Negative State.
// If Value changes from negative to positive,
// begin the Normal State.
private void DoValueAnimation(int oldValue, int newValue)
{
if (oldValue < 0 && newValue >= 0)
{
GoToState(NormalState);
}
if (oldValue >= 0 && newValue < 0)
{
GoToState(NegativeState);
}
}
private void GoToState(Storyboard state)
{
if (state != null)
{
// Begin new state storyboard
state.Begin();
}
if (currentState != null)
{
// Stop old state storyboard.
currentState.Stop();
}
currentState = state;
}
-
Add the following to the ValueChangedCallback method that you previously defined.
cs
// Initiate the animation according to the value;
ctl.DoValueAnimation(oldValue, newValue);
Now when you run the application and change Value from positive to negative (and vice versa), the value will appear in different colors.
Adding and Raising Events in Your Control
Often, a control raises an event to notify applications about user interaction or that some other action has occurred. For example, a Click event occurs when a user clicks a button. A natural event to add to the NumericUpDown class is a ValueChanged event that notifies the application whenever Value changes.
note
This is different from the PropertyChangedCallback you defined earlier. The PropertyChangedCallback notifies the control that Value has changed, but it doesn't notify applications.
To add the ValueChanged event to your control.
-
Add the following delegate and class to the NumericUpDown.cs file (before or after the NumericUpDown class--not inside it). The ValueChangedEventArgs contains the Value of the NumericUpDown control, and ValueChangedEventHandler defines the signature for the event's event handlers.
cs
public delegate void ValueChangedEventHandler(object sender, ValueChangedEventArgs e);
public class ValueChangedEventArgs : EventArgs
{
private int _value;
public ValueChangedEventArgs(int num)
{
_value = num;
}
public int Value
{
get { return _value; }
}
}
-
Add the following event and method to the NumericUpDown class. The OnValueChanged method raises the event if anyone has subscribed to it.
cs
public event ValueChangedEventHandler ValueChanged;
protected virtual void OnValueChanged(ValueChangedEventArgs e)
{
ValueChangedEventHandler handler = ValueChanged;
if (handler != null)
{
handler(this, e);
}
}
-
Add the following to the ApplyTemplate() method in the NumericUpDown class. This creates the ValueChangedEeventArgs and calls OnValueChanged whenever the property changes.
cs
// Raise the ValueChanged event.
ValueChangedEventArgs e = new ValueChangedEventArgs(newValue);
ctl.OnValueChanged(e);
Now applications can subscribe to the ValueChanged event to be notified whenever the Value property changes.
Publishing the Control Contract for Your Control
Now that you've creating a custom control that uses a ControlTemplate, you need to state which parts your control's code expects to find in the ControlTemplate. You do this by adding the TemplatePartAttribute to the control for each part.
To state which parts the control expects to find in the ControlTemplate.
-
Add the following directly above the declaration of the NumericUpDown class.
cs
[TemplatePart(Name = "RootElement", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "TextElement", Type = typeof(TextBlock))]
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "Negative State", Type = typeof(Storyboard))]
[TemplatePart(Name = "Normal State", Type = typeof(Storyboard))]
The parts that the control expects to find in the ControlTemplate and the properties that affect the appearance of the control make up the control contract. The following example shows the control contract for the NumericUpDown control.
cs
[TemplatePart(Name = "RootElement", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "TextElement", Type = typeof(TextBlock))]
[TemplatePart(Name = "UpButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "DownButtonElement", Type = typeof(RepeatButton))]
[TemplatePart(Name = "Negative State", Type = typeof(Storyboard))]
[TemplatePart(Name = "Normal State", Type = typeof(Storyboard))]
public class NumericUpDown : Control
{
public static readonly DependencyProperty BackgroundProperty;
public Brush Background { get; set; }
}