Introduction
Creating custom controls is not as difficult as they actually appear, but before we go creating custom controls lets first look at the difference between Custom Controls (CC) and Custom User-Controls (CU).
Simply put Custom Controls (CC) are skinable, themable and reusable controls that once created can be used by simply loading the assembly in any project, where are Custom User-Controls are user controls that can be reused but they can't be skinned or themed. Technically they are both difference Custom Controls inherits from (System.Windows.Controls) Controls whereas User Controls inherits from (System.Windows.Controls.UserControl) UserControl. All controls that are used in Silverlight (eg., Button, TextBlock, TextBox) and UserControl is also a Control.
Lets just start by creating a custom control and we can discuss the technicalities where they arise. I'll do this in both C# and VB.NET.
Note: This was my first custom control I built on Silverlight 2 about couple of weeks ago, and I wrote this blog last week as well. I wanted to first introduce concepts like binding and dependency properties before I publish this, but since Microsoft have released Silverlight Toolkit and included NumericUpDown, this is purely academic now. But do enjoy and learn how to create Custom Controls. But the basic concepts are still going to follow.
Developing a Custom Control
In this example we'll create a custom control for NumericUpDown. Its a very simple control that can be easily built, I'll be using Microsoft Visual Studio 2008 and Microsoft Expression Blend 2 (SP1), we don't really need Expression Blend but sometimes it saves time using Expression Blend.
First, we'll set up and prepare to built a custom control and to test it.
1. Setup:
In Visual Studio Start a new Project with Silverlight Application Template and name it (I named it Silverlight Control Library), then select ASP.NET Web Application Project for testing.
Now the project for testing is ready and we need a new Silverlight Class Library Template project within the solution, so add new project and Name it (I named it Controls), this is where we'll be building our custom controls, by default you'll have a Class1.vb or Class1.cs file in the project delete the file, and add a new folder and rename it "themes" (The name of this folder is important, so keep it like it is).
Now add an empty xaml file in that folder and name it "generic.xaml" (Again the name is important), add the following XAML tags in the the xaml file, this is the basic shell we'll always need in building custom controls.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
<Style>
</Style>
</ResourceDictionary>
It is very important that you remember to make the generic.xaml build action as Resource, this is to make sure that the template is packed in the same assembly as the control otherwise the template wont be available.
Add another folder and name it "NumericUpDown", this is where we'll be building our custom control.
2: Preparing
Now that the setup is complete we can start building the control but first we need to focus and gather facts about the custom control.
- Is a Numeric Only TextBox with 2 buttons for increasing and decreasing the value.
- Will have a Minimum and Maximum, along with an actual Value.
- TextBox should only take numeric input along with Up/Down buttons
- Optionally control can be made to use only Up/Down instead of directly entering the value in the TextBox.
- When the value is changed, the control will raise an event to notify anyone listening, with its own EventArgs.
- The Control should allow access and Binding on basic properties like IsEnabled, Foreground, Font Size etc.
3: Building
First, we'll add a Class file in "NumericUpDown" folder and name it "NumericBox", this will be the name of the control, but like I explained before all controls should inherit from Control, second you'll have to tell the compiler where default style for the control is, in order to do this we'll set the DefaultStyleKey value, so in constructor.
C#
public class NumericBox: Control
{
public NumericBox()
{
DefaultStyleKey = typeof(NumericBox);
}
}
VB.NET
Public Class NumericBox
Inherits Control
Public Sub New()
DefaultStyleKey = GetType(NumericBox)
End Sub
End Class
Now the constructor will look for default style in generic.xaml (under themes folder) for any style referencing to current assembly.
Now we'll expose some properties externally for anyone to bind, you should be aware that any external property you want to bind to the control should be a DependencyProperty, any property (Attached Property / Dependency Property) can be binded but the property that it is binding to should be a DependencyProperty, so to keep this control bindable we'll have to expose Dependency Properties, If you have problem following this, leave a message and I'll try to explain DependencyProperty in detail.
Now, we already know that we need to expose three properties Minimum, Maximum and Value, in order for us to able to check if the values entered are valid we'll check them every time the any property is changed by using PropertyChangedCallback. (We'll implement Property Callbacks later)
C#
#region Dependency Properties
public int Minimum
{
get { return (int)GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(int), typeof(NumericBox),
new PropertyMetadata(0, new PropertyChangedCallback(MinimumChanged)));
private static void MinimumChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
}
public int Maximum
{
get { return (int)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(int), typeof(NumericBox),
new PropertyMetadata(10, new PropertyChangedCallback(MaximumChanged)));
private static void MaximumChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
}
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(int), typeof(NumericBox),
new PropertyMetadata(0, new PropertyChangedCallback(ValueChanged)));
private static void ValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
}
#endregion
VB.NET
#Region "Dependency Properties"
Public Property Minimum()
Get
Return GetValue(MinimumProperty)
End Get
Set(ByVal value)
SetValue(MinimumProperty, value)
End Set
End Property
Public Shared MinimumProperty As DependencyProperty = _
DependencyProperty.Register("Minimum", GetType(Integer), GetType(NumericBox), _
New PropertyMetadata(0, New PropertyChangedCallback(AddressOf MinimumValueChanged)))
Private Shared Sub MinimumValueChanged(ByVal sender As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
End Sub
Public Property Maximum()
Get
Return GetValue(MaximumProperty)
End Get
Set(ByVal value)
SetValue(MaximumProperty, value)
End Set
End Property
Public Shared MaximumProperty As DependencyProperty = _
DependencyProperty.Register("Maximum", GetType(Integer), GetType(NumericBox), _
New PropertyMetadata(10, New PropertyChangedCallback(AddressOf MaximumValueChanged)))
Private Shared Sub MaximumValueChanged(ByVal sender As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
End Sub
Public Property Value()
Get
Return GetValue(ValueProperty)
End Get
Set(ByVal value)
SetValue(ValueProperty, value)
End Set
End Property
Public Shared ValueProperty As DependencyProperty = _
DependencyProperty.Register("Value", GetType(Integer), GetType(NumericBox), _
New PropertyMetadata(0, New PropertyChangedCallback(AddressOf ValueChanged)))
Private Shared Sub ValueChanged(ByVal sender As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
End Sub
#End Region
Now we have three Dependency Properties exposed, we are setting the default values here but its a good practice to also set them in Style, so we'll go to generic.xaml (after compiling the project) and add a reference to current assembly and then add them to the style.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
xmlns:local="clr-namespace:Controls.NumericUpDown;assembly=Controls.NumericUpDown">
<Style TargetType="local:NumericBox">
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="10"/>
<Setter Property="Value" Value="0"/>
</Style>
</ResourceDictionary>
Now that you understand how to expose properties, we'll first finish the job with Dependency Properties by validating the values.
C#
private static void MinimumChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
NumericBox NB = sender as NumericBox;
int val = (int)e.NewValue;
if (NB == null)
return;
if (val > NB.Maximum)
NB.Minimum = (int)e.OldValue;
if (NB.Value < val)
NB.Value = val;
}
private static void MaximumChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
NumericBox NB = sender as NumericBox;
int val = (int)e.NewValue;
if (NB == null)
return;
if (val < NB.Minimum)
NB.Maximum = (int)e.OldValue;
}
private static void ValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
NumericBox NB = sender as NumericBox;
int val = (int)e.NewValue;
if (NB == null)
return;
if (val < NB.Minimum)
{
NB.Value = NB.Minimum;
NB._ValueChanged = false;
return;
}
if (val > NB.Maximum)
{
NB.Value = NB.Maximum;
NB._ValueChanged = false;
return;
}
NB._ValueChanged = true;
}
VB.NET
Private Shared Sub MinimumValueChanged(ByVal sender As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim NB As NumericBox = CType(sender, NumericBox)
Dim Val As Integer = CType(e.NewValue, Integer)
If NB Is Nothing Then Return
If Val >= NB.Maximum Then NB.Minimum = CType(e.OldValue, Integer)
If Val > NB.Value Then NB.Value = Val
End Sub
Private Shared Sub MaximumValueChanged(ByVal sender As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim NB As NumericBox = CType(sender, NumericBox)
Dim Val As Integer = CType(e.NewValue, Integer)
If NB Is Nothing Then Return
If Val <= NB.Minimum Then NB.Maximum = CType(e.OldValue, Integer)
If Val < NB.Value Then NB.Value = Val
End Sub
Private Shared Sub ValueChanged(ByVal sender As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
Dim NB As NumericBox = CType(sender, NumericBox)
Dim Val As Integer = CType(e.NewValue, Integer)
If NB Is Nothing Then Return
If Val < NB.Minimum Then
NB.Value = NB.Minimum
NB._ValueUpdated = False
Return
End If
If Val > NB.Maximum Then
NB.Value = NB.Maximum
NB._ValueUpdated = False
Return
End If
NB._ValueUpdated = True
End Sub
Now that basic logic is in place we'll finish with the basic structure, for this we need to define template in generic.xaml to provide the building blocks of the control (i.e., the look and feel of the control).
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
xmlns:local="clr-namespace:Controls.NumericUpDown;assembly=Controls.NumericUpDown">
<Style TargetType="local:NumericBox">
<Setter Property="Minimum" Value="0"/>
<Setter Property="Maximum" Value="10"/>
<Setter Property="Value" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NumericBox">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="NumericTextBox" Grid.Column="0" Grid.ColumnSpan="2" IsTabStop="True"
IsEnabled="{TemplateBinding IsEnabled}"
Foreground="{TemplateBinding Foreground}"
Background="{TemplateBinding Background}"
Visibility="{TemplateBinding Visibility}"
Text="{TemplateBinding Value}"
/>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button x:Name="ValUp" Grid.Row="0" Margin="0,1,1,0" IsTabStop="False"
IsEnabled="{TemplateBinding IsEnabled}"
Background="{TemplateBinding Background}"
Visibility="{TemplateBinding Visibility}"
>
<Path Fill="{TemplateBinding Foreground}"
Data="F1 M 4.81721,-3.05176e-005L 9.63441,8.3436L 2.49481e-006,8.3436L 4.81721,-3.05176e-005 Z "/>
</Button>
<Button x:Name="ValDown" Grid.Row="1" Margin="0,0,1,1" IsTabStop="False"
IsEnabled="{TemplateBinding IsEnabled}"
Background="{TemplateBinding Background}"
Visibility="{TemplateBinding Visibility}"
>
<Path Fill="{TemplateBinding Foreground}"
Data="F1 M 4.81721,8.34363L 9.63441,0L 2.49481e-006,0L 4.81721,8.34363 Z "/>
</Button>
</Grid>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Note that we have binded all external properties to both the TextBox or Buttons, and pay attention that the Buttons and the TextBox have names, we use these names in our control class to access these objects to access events associated with them, if for some reason the names in the control templates change (user defined template/Skinned template), the whole control will fall apart, and there is no simple way to deal with it (we can deal with this by accessing the root element and checking its children, but that is a different story)
Now we have to access four events, TextBox.TextChanged, TextBox.KeyDown, ButtonUp.Click and ButtonDown.Click to do this we'll overload/override "OnApplyTemplate" Method, it has to be noted that you can't access the template from constructor, even though we set the DefaultKeyStyle in constructor so the only way to safely access the objects and capture events is to do it when the template is applied, you can get a child from the template using "GetTemplateChild" method.
C#
private TextBox TBxNum;
private Button ButUp;
private Button ButDw;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
TBxNum = base.GetTemplateChild("NumericTextBox") as TextBox;
ButUp = base.GetTemplateChild("ValUp") as Button;
ButDw = base.GetTemplateChild("ValDown") as Button;
if (TBxNum == null)
return;
if (ButUp == null)
return;
if (ButDw == null)
return;
TBxNum.TextChanged += new TextChangedEventHandler(TBxNum_TextChanged);
TBxNum.KeyDown += new KeyEventHandler(TBxNum_KeyDown);
ButUp.Click += new RoutedEventHandler(ButUp_Click);
ButDw.Click += new RoutedEventHandler(ButDw_Click);
}
VB.NET
Private TBxNum As TextBox
Private ButUp As Button
Private ButDw As Button
Public Overloads Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
TBxNum = CType(MyBase.GetTemplateChild("NumericTextBox"), TextBox)
ButUp = CType(MyBase.GetTemplateChild("ValUp"), Button)
ButDw = CType(MyBase.GetTemplateChild("ValDown"), Button)
If TBxNum Is Nothing Then Return
If ButUp Is Nothing Then Return
If ButDw Is Nothing Then Return
AddHandler TBxNum.TextChanged, AddressOf TBxNum_TextChanged
AddHandler TBxNum.KeyDown, AddressOf TBxNum_KeyDown
AddHandler ButUp.Click, AddressOf ButUp_Click
AddHandler ButDw.Click, AddressOf ButDw_Click
End Sub
We need to capture Button.Click events to Up/Down the Value, but we are defining TextBox.KeyDown event to restrict the entry of non-numeric keys in the text box, and we'll update Value when the Value is typed in the TextBox, thats why we are capturing the TextBox.TextChanged Event, although we are already restricting the keyboard entry I still prefer to check if the Value entered in the TextBox is a number, and we can update the Value in one procedure.
C#
void TBxNum_TextChanged(object sender, TextChangedEventArgs e)
{
if (Single.IsNaN(System.Convert.ToSingle(TBxNum.Text)))
throw new NotFiniteNumberException(TBxNum.Text);
else
UpdateValue((int)System.Convert.ToInt64(TBxNum.Text));
}
void TBxNum_KeyDown(object sender, KeyEventArgs e)
{
if (((e.Key >= Key.D0 && e.Key <= Key.D9) || (e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9) || e.Key == Key.Back))
e.Handled = false;
else
{
e.Handled = true;
}
}
void ButUp_Click(object sender, RoutedEventArgs e)
{
UpdateValue(Value + 1);
}
void ButDw_Click(object sender, RoutedEventArgs e)
{
UpdateValue(Value - 1);
}
void UpdateValue(int val)
{
_ValueChanged = false;
Value=int;
if(_ValueChanged)
{
TBxNum.Text = Value.ToString();
}
}
VB.NET
Private Sub TBxNum_TextChanged(ByVal sender As Object, ByVal e As TextChangedEventArgs)
If (Single.IsNaN(System.Convert.ToSingle(TBxNum.Text))) Then
Throw New NotFiniteNumberException(TBxNum.Text)
Else
UpdateValue(CType(TBxNum.Text, Integer))
End If
End Sub
Private Sub TBxNum_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
If ((e.Key >= Key.D0 And e.Key <= Key.D9) OrElse (e.Key >= Key.NumPad0 And e.Key <= Key.NumPad9) _
OrElse e.Key = Key.Back) Then e.Handled = False Else e.Handled = True
End Sub
Private Sub ButUp_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
UpdateValue(Value + 1)
End Sub
Private Sub ButDw_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
UpdateValue(Value - 1)
End Sub
Private Sub UpdateValue(ByVal val As Integer)
_ValueUpdated = False
Value = val
If _ValueUpdated Then
TBxNum.Text = val
End If
End Sub
Now the control is up and ready for testing(don't forget to add reference to this assembly), you can make any changes to the source code as you see it fit, the only thing left is to raise an event when the value changes in case anyone is listening. For this we'll build a custom EventArgs to pass the changed value.
So add another file in the folder and name it "NumericBoxChangedArgs.cs" or "NumericBoxChangedArgs.vb" as shown below.
Now, the NumericBoxChangedArgs class inheirts EventArgs and have a readonly property and a constructor to set the Value changed, it also have a delegate event handler with NumbericBoxChangedArgs signature.
C#
public delegate NumericBoxChangedHandler(object sender, NumericBoxChangedArgs e);
public class NumericBoxChangedArgs : EventArgs
{
private readonly int _Value;
public NumericBoxChangedArgs(int val)
{
_Value = val;
}
public int Value
{
get
{
return _Value;
}
}
}
VB.NET
Namespace NumericUpDown
Public Delegate Sub NumericBoxChangedHandler(ByVal sender As Object, ByVal e As NumericBoxChangedArgs)
Public Class NumericBoxChangedArgs
Inherits EventArgs
Private ReadOnly _val As Integer
Public Sub New(ByVal val As Integer)
_val = val
End Sub
Public ReadOnly Property Value() As Integer
Get
Return _val
End Get
End Property
End Class
End Namespace
Now we just declare the delegate locally and raise event in it in UpdateValue procedure.
C#
public event NumericBoxChangedHandler NumericBoxChanged;
void UpdateValue(int val)
{
_ValueChanged = false;
Value=int;
if(_ValueChanged)
{
TBxNum.Text = Value.ToString();
NumericBoxChangedArgs NArgs = new NumericBoxChangedArgs(Value);
NumericBoxChanged(this, NArgs);
}
}
VB.NET
Public Event NumericBoxChanged As NumericBoxChangedHandler
Private Sub UpdateValue(ByVal val As Integer)
_ValueUpdated = False
Value = val
If _ValueUpdated Then
TBxNum.Text = Value
Dim NArgs As New NumericBoxChangedArgs(Value)
RaiseEvent NumericBoxChanged(Me, NArgs)
End If
End Sub
All the points mentioned while preparing the control are now achieved (I have added couple of more Dependency Properties to the source code (IncrementStep, IncrementOnly). Our Custom Numeric Up/Down Box is now complete, you can download the source code for this custom control below, if you make any modifications to the source code, please let me know.
Licence
Silverlight NumericUpDown Control by Imran Shaik is licensed under a Creative Commons Attribution-Non-Commercial-Share Alike 2.0 UK: England & Wales License.
Based on a work at www.geekswithblogs.net.
Download Source Code/Binary
Version 1.0 (Both VB.NET & C#)