Geeks With Blogs
Imran Shaik Silverlight Quintessential Rambling

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.

2008-10-25_141920 2008-10-25_141938

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). 

2008-10-25_142351 2008-10-25_142446 2008-10-25_142514

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.

2008-10-25_210935

Add another folder and name it "NumericUpDown", this is where we'll be building our custom control.

2008-10-25_142528 2008-10-25_142541 2008-10-25_142558

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.

  1. Is a Numeric Only TextBox with 2 buttons for increasing and decreasing the value.
  2. Will have a Minimum and Maximum, along with an actual Value.
  3. TextBox should only take numeric input along with Up/Down buttons
  4. Optionally control can be made to use only Up/Down instead of directly entering the value in the TextBox.
  5. When the value is changed, the control will raise an event to notify anyone listening, with its own EventArgs.
  6. 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.

2008-10-26_092756

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

Creative Commons License
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#)

Posted on Friday, October 31, 2008 12:48 AM Silverlight | Back to top


Comments on this post: Developing Custom Controls in Silverlight 2

# re: Developing Custom Controls in Silverlight 2
Requesting Gravatar...
In your example, you do not use

[TemplatePart(Name = "UpButtonElement", Type = typeof(Button))]

Without defining the control part attributes, what is lost?
Left by Dan Hickman on Dec 04, 2008 4:20 AM

# re: Developing Custom Controls in Silverlight 2
Requesting Gravatar...
can u explain DependencyProperty in details.
Left by harish on Dec 15, 2008 6:32 AM

# re: Developing Custom Controls in Silverlight 2
Requesting Gravatar...
Harish,

Comming your way
Left by Imran on Dec 17, 2008 9:54 AM

# re: Developing Custom Controls in Silverlight 2
Requesting Gravatar...
Ok After developing custom control what?..how to use it...
Left by Gopala on Aug 03, 2009 11:12 AM

# re: Developing Custom Controls in Silverlight 2
Requesting Gravatar...
Great tutorial but I believe there are some faults. (The source is complete though :))

you mis the line:
private bool _ValueChanged= false;

and the 'void' is missing in this line:
public delegate void NumericBoxChangedHandler(object sender, NumericBoxChangedArgs e);

And because this is an beginner lesson, could you maybe add a section how to test and use this control?

Thanks for taking the time to create this tutorial!


Left by Bertram on Dec 22, 2009 6:38 PM

# re: Developing Custom Controls in Silverlight 2
Requesting Gravatar...
Hey Imran,Really a great blog..
Can u tell me how can i bind data to a datagrid if i define it in generic.xaml
and also how to store a VSM for any control thru blend to generix.xaml
Left by Vishal on Mar 13, 2011 7:35 PM

Your comment:
 (will show your gravatar)


Copyright © Imran Shaik | Powered by: GeeksWithBlogs.net