Creating a WPF Custom Control

This shows an example on how to create a WPF custom control.

Introduction

The .NET Framework provides many out-of-the-box WPF controls that you can use for your application. Most of the time, these controls will suffice in designing the user interface. However, there will be times when you need to create your own control. For example, you will have to create a TimePicker control if you want to specify the time component of a DateTime object. There are two types of controls you can create in WPF, user control and custom control. Creating a user control is just like creating a window and adding contents to it. You can just drag controls to the designer when working with Visual Studio or Expression Blend.

Meanwhile, creating a custom control requires some coding and creating a generic theme for your control. Existing WPF controls are custom controls. You can change how it looks by specifying another theme. You can also override how a control is drawn. Creating a custom control is the choice if you need a control where the theme can be easily changed. Also, you can extend the functionality of a certain control by deriving from it. I would like to discuss in this article the basics you need to know to create a custom control.

Getting Started

I’ve been using the WPF Toolkit for a while and there is a control there for selecting dates, which is called the DatePicker. However, there is no control for selecting time like 8:00 AM. Let’s create a control and name it TimePicker. I’ll be using Visual Studio 2008 to create the control. First, create a project of type WPF Custom Control Library. This creates two files: CustomControl1.cs and Generic.xaml. Let’s rename CustomControl1 to TimePicker. The TimePicker class derives from the Control class and has a static constructor. This constructor specifies the default style key, which is usually the type of the control. This is necessary to find the default style for the control. The Generic.xaml file, which is under the Themes folder, is important. This is where the .NET runtime looks for a control’s theme by default. If you are going to create many custom controls and want to create a folder for each control, make sure you merge your resource dictionaries in the Generic.xaml file (or another file depending on the theme). The following shows the contents of the 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:CustomControlsLib">

    <Style TargetType="{x:Type local:TimePicker}">

        <Setter Property="Template">

            <Setter.Value>

                 <ControlTemplate TargetType="{x:Type local:TimePicker}">

                     <Border Background="{TemplateBinding Background}"

                         BorderBrush="{TemplateBinding BorderBrush}"

                         BorderThickness="{TemplateBinding BorderThickness}">

                     </Border>

                 </ControlTemplate>

            </Setter.Value>

        </Setter>

    </Style>

</ResourceDictionary>

Listing 1. Default TimePicker Style

If we use the control now in an application, we won’t see anything because the TimePicker control is rendered only as a Border with default properties. We have to specify a different template in the Generic.xaml file. Most of the time, a control expects the template to have certain kinds of controls. This is where template parts come into place.

Template Parts

Now let’s think of the controls that will make up our TimePicker control. I think there should be three TextBox controls that correspond to the hour, minute and second components. I’ll use the 24-hour format here. We also need two buttons for incrementing and decrementing a selected component. We will let the user of the control know that the control expects these four controls present in the template. To do this, we must use the TemplatePartAttribute class. The following code listing shows the updated TimePicker class.

[TemplatePart(Name = TimePicker.ElementHourTextBox, Type = typeof(TextBox))]

[TemplatePart(Name = TimePicker.ElementMinuteTextBox, Type = typeof(TextBox))]

[TemplatePart(Name = TimePicker.ElementSecondTextBox, Type = typeof(TextBox))]

[TemplatePart(Name = TimePicker.ElementIncrementButton, Type = typeof(Button))]

[TemplatePart(Name = TimePicker.ElementDecrementButton, Type = typeof(Button))]

public class TimePicker : Control

{

    #region Constants


    private
const string ElementHourTextBox = "PART_HourTextBox";

    private const string ElementMinuteTextBox = "PART_MinuteTextBox";

    private const string ElementSecondTextBox = "PART_SecondTextBox";

    private const string ElementIncrementButton = "PART_IncrementButton";

    private const string ElementDecrementButton = "PART_DecrementButton";


    #endregion


    static
TimePicker()

    {

        DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker), new FrameworkPropertyMetadata(typeof(TimePicker)));

    }

}

Listing 2. Specifying Template Parts

The convention in naming template parts is to prefix the name with the word PART. Next, we need to override the OnApplyTemplate method of the Control class. We will get the controls from the template and assign them to instance variables, which can be accomplished by using the GetTemplateChild method. This method accepts a string parameter which is the name of the control in the template to be searched. The following code snippet shows the declaration of the instance variables and the implementation of the OnApplyTemplate.

#region Data

private TextBox hourTextBox;

private TextBox minuteTextBox;

private TextBox secondTextBox;

private Button incrementButton;

private Button decrementButton;

#endregion Data

#region Public Methods

public override void OnApplyTemplate()

{

    base.OnApplyTemplate();


    hourTextBox = GetTemplateChild(ElementHourTextBox) as TextBox;

    minuteTextBox = GetTemplateChild(ElementMinuteTextBox) as TextBox;

    secondTextBox = GetTemplateChild(ElementSecondTextBox) as TextBox;

    incrementButton = GetTemplateChild(ElementIncrementButton) as Button;

    decrementButton = GetTemplateChild(ElementDecrementButton) as Button;

}

#endregion Public Methods

Listing 3. Overriding the OnApplyTemplate Method

Now what if the user specified a different template for the control and did not put the expected template parts? There are two things that we can do, throw an exception or just ignore it. We’ll do the latter in this example. However, we’ll need to check if the value of a variable is not null every time we are about to use it. We’ll need to attach event handlers to these controls’ events that we are interested in. For example, we’ll need to attach event handlers to the Click event of the increment and decrement buttons so that we can change the selected time. But before that, we still have to create a property for the selected time.

Dependency Properties

We can either use a normal property or a dependency property for setting or getting the selected time. However, using a dependency property allows us to bind a property using XAML and use it in styles and animations. It also allows property validation and coercion. Most of the time, you will be defining dependency properties when creating a custom control. The following code shows the SelectedTime dependency property. I’ve also added MinTime and MaxTime dependency properties to illustrate property coercion although this might not be necessary in most scenarios.

private const TimeSpan MinValidTime = new TimeSpan(0, 0, 0);

private const TimeSpan MaxValidTime = new TimeSpan(23, 59, 59);

#region Ctor

static TimePicker()

{

    DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker), new FrameworkPropertyMetadata(typeof(TimePicker)));

}

public TimePicker()

{

    SelectedTime = DateTime.Now.TimeOfDay;

}

#endregion Ctor

#region Public Properties

#region SelectedTime

public TimeSpan SelectedTime

{

    get { return (TimeSpan)GetValue(SelectedTimeProperty); }

    set { SetValue(SelectedTimeProperty, value); }

}

public static readonly DependencyProperty SelectedTimeProperty =

    DependencyProperty.Register(

        "SelectedTime",

        typeof(TimeSpan),

        typeof(TimePicker),

        new FrameworkPropertyMetadata(TimePicker.MinValidTime, new PropertyChangedCallback(TimePicker.OnSelectedTimeChanged), new CoerceValueCallback(TimePicker.CoerceSelectedTime)));


#endregion
SelectedTime

#region MinTime

public TimeSpan MinTime

{

    get { return (TimeSpan)GetValue(MinTimeProperty); }

    set { SetValue(MinTimeProperty, value); }

}

public static readonly DependencyProperty MinTimeProperty =

    DependencyProperty.Register(

        "MinTime",

        typeof(TimeSpan),

        typeof(TimePicker),

        new FrameworkPropertyMetadata(TimePicker.MinValidTime, new PropertyChangedCallback(TimePicker.OnMinTimeChanged)),

        new ValidateValueCallback(TimePicker.IsValidTime));

#endregion MinTime

#region MaxTime

public TimeSpan MaxTime

{

    get { return (TimeSpan)GetValue(MaxTimeProperty); }

    set { SetValue(MaxTimeProperty, value); }

}

public static readonly DependencyProperty MaxTimeProperty =

    DependencyProperty.Register(

        "MaxTime",

        typeof(TimeSpan),

        typeof(TimePicker),

        new FrameworkPropertyMetadata(TimePicker.MaxValidTime, new PropertyChangedCallback(TimePicker.OnMaxTimeChanged), new CoerceValueCallback(TimePicker.CoerceMaxTime)),

        new ValidateValueCallback(TimePicker.IsValidTime));

#endregion MaxTime

#endregion Public Properties

Listing 4. Dependency Properties

To define a dependency property, we need to call the static Register method of the DependencyProperty class. The required method parameters are the name of the property, the type of the property and the type of the class containing the property. Optional parameters are property metadata and callback method for validating the value of the property. Using property metadata, we can set a property’s default value, a callback method that is executed when the property’s value is changed, and a callback method used for coercion, among others. Notice that there are constants specifying the minimum and maximum valid time. We will try to limit the values of the MinTime and MaxTime properties since we can also set days in a TimeSpan object. Let’s take a look at the callback methods specified in these dependency properties.

private static object CoerceSelectedTime(DependencyObject d, object value)

{

    TimePicker timePicker = (TimePicker)d;

    TimeSpan minimum = timePicker.MinTime;

    if ((TimeSpan)value < minimum)

    {

        return minimum;

    }

    
    TimeSpan
maximum = timePicker.MaxTime;

    if ((TimeSpan)value > maximum)

    {

        return maximum;

    }

    
    return
value;

}

private static object CoerceMaxTime(DependencyObject d, object value)

{

    TimePicker timePicker = (TimePicker)d;

    TimeSpan minimum = timePicker.MinTime;

    if ((TimeSpan)value < minimum)

    {

        return minimum;

    }

    return value;

}

private static bool IsValidTime(object value)

{

    TimeSpan time = (TimeSpan)value;

    return MinValidTime <= time && time <= MaxValidTime;

}

protected virtual void OnSelectedTimeChanged(TimeSpan oldSelectedTime, TimeSpan newSelectedTime)

{

}

private static void OnSelectedTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

    TimePicker element = (TimePicker)d;

    element.OnSelectedTimeChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);

}

protected virtual void OnMinTimeChanged(TimeSpan oldMinTime, TimeSpan newMinTime)

{

}

private static void OnMinTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

    TimePicker element = (TimePicker)d;

    element.CoerceValue(MaxTimeProperty);

    element.CoerceValue(SelectedTimeProperty);

    element.OnMinTimeChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);

}

protected virtual void OnMaxTimeChanged(TimeSpan oldMaxTime, TimeSpan newMaxTime)

{

}

private static void OnMaxTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

{

    TimePicker element = (TimePicker)d;

    element.CoerceValue(SelectedTimeProperty);

    element.OnMaxTimeChanged((TimeSpan)e.OldValue, (TimeSpan)e.NewValue);

}

Listing 5. Callback Methods

We have attached a corresponding event handler for each PropertyChanged event. Inside an event handler, we will coerce the values of other properties that are dependent on the property that is changed. The MaxTime property is dependent on the MinTime property while the SelectedTime is dependent on the MinTime and MaxTime properties. Meanwhile, the IsValidTime method checks if the MinTime or MaxTime is valid. You can see that there are empty methods in the above code. We can raise an event to notify other classes that a property is changed. But first, we have to know about routed events.

Routed Events

One event that we can define for the TimePicker control is when the selected time changes. The question is whether we are going to define the event as a routed event or not. A routed event is an event wherein it may originate from one control but can be raised on another control. The advantage of defining a routed event is that you can provide the event handling code in another control, which could be useful if you want to centralize your event handling code. You can also utilize features like event triggers and setters.

#region SelectedTimeChangedEvent

public event RoutedPropertyChangedEventHandler<TimeSpan> SelectedTimeChanged

{

    add { base.AddHandler(SelectedTimeChangedEvent, value); }

    remove { base.RemoveHandler(SelectedTimeChangedEvent, value); }

}

public static readonly RoutedEvent SelectedTimeChangedEvent =

    EventManager.RegisterRoutedEvent(

        "SelectedTimeChanged",

        RoutingStrategy.Bubble,

        typeof(RoutedPropertyChangedEventHandler<TimeSpan>),

        typeof(TimePicker));


#endregion
SelectedTimeChangedEvent

protected virtual void OnSelectedTimeChanged(TimeSpan oldSelectedTime, TimeSpan newSelectedTime)

{

    RoutedPropertyChangedEventArgs<TimeSpan> e = new RoutedPropertyChangedEventArgs<TimeSpan>(oldSelectedTime, newSelectedTime);

    e.RoutedEvent = SelectedTimeChangedEvent;

    base.RaiseEvent(e);

}

Listing 6. SelectedTimeChanged Routed Event

Defining a routed event is similar to defining a dependency property. I guess the routing strategy needs a bit of explanation. This can be set to Tunnel, Bubble or Direct. A bubbling event is an event that travels from the control that defines the event upwards the tree of controls. A tunneling event is the reverse of a bubbling event. Most of the tunneling events’ names are prefixed with the word Preview. A tunneling event is useful when you do not want the corresponding bubbling event executed. This can be done by marking the event as handled. Direct routed events do not travel but make use of event triggers and setters.

User Interaction

Now we need to specify how the user will interact with our control. We’ll need to hook up event handlers for the events of the different controls that make up our TimePicker control. For example, we need to attach event handlers for the Click event of the increment and decrement buttons. We also need to create a variable that will store the currently selected TextBox, whether it is the hourTextBox or minuteTextBox. This will determine if the hour or minute component of the selected time is to be changed when the user clicks on the increment or decrement button, which is demonstrated in the following code.

#region Public Methods

public override void OnApplyTemplate()

{

    base.OnApplyTemplate();


    hourTextBox = GetTemplateChild(ElementHourTextBox) as TextBox;

    if (hourTextBox != null)

    {

        hourTextBox.IsReadOnly = true;

        hourTextBox.GotFocus += SelectTextBox;

    }

    minuteTextBox = GetTemplateChild(ElementMinuteTextBox) as TextBox;

    if (minuteTextBox != null)

    {

        minuteTextBox.IsReadOnly = true;

        minuteTextBox.GotFocus += SelectTextBox;

    }

    secondTextBox = GetTemplateChild(ElementSecondTextBox) as TextBox;

    if (secondTextBox != null)

    {

        secondTextBox.IsReadOnly = true;

        secondTextBox.GotFocus += SelectTextBox;

    }

    incrementButton = GetTemplateChild(ElementIncrementButton) as Button;

    if (incrementButton != null)

    {

        incrementButton.Click += IncrementTime;

    }


    decrementButton = GetTemplateChild(ElementDecrementButton) as Button;

    if (decrementButton != null)

    {

        decrementButton.Click += DecrementTime;

    }

}

#endregion Public Methods

#region Private Methods

private void SelectTextBox(object sender, RoutedEventArgs e)

{

    selectedTextBox = sender as TextBox;

}

private void IncrementTime(object sender, RoutedEventArgs e)

{

    IncrementDecrementTime(1);

}

private void DecrementTime(object sender, RoutedEventArgs e)

{

    IncrementDecrementTime(-1);

}

private void IncrementDecrementTime(int step)

{

    if (selectedTextBox == null)

    {

        selectedTextBox = hourTextBox;

    }

    TimeSpan time;

    if (selectedTextBox == hourTextBox)

    {

        time = SelectedTime.Add(new TimeSpan(step, 0, 0));

    }

    else if (selectedTextBox == minuteTextBox)

    {

        time = SelectedTime.Add(new TimeSpan(0, step, 0));

    }

    else

    {

        time = SelectedTime.Add(new TimeSpan(0, 0, step));

    }

    SelectedTime = time;

}

#endregion

Listing 7. Defining the User Interaction Logic

You will have to attach event handlers using code, not XAML, when creating a custom control. Now let’s create the default template. The following XAML code shows the default template found in Generic.xaml.

<ResourceDictionary

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:CustomControlsLib">

    <ControlTemplate

        x:Key="TimePickerButtonTemplate"

        TargetType="ButtonBase">

        <Border

            x:Name="ContentContainer"

            Background="{TemplateBinding Background}"

            BorderBrush="{TemplateBinding BorderBrush}"

            BorderThickness="1"

            SnapsToDevicePixels="True">

            <ContentPresenter

                 x:Name="Content"

                 RecognizesAccessKey="True"

                 Content="{TemplateBinding Content}"

                 ContentTemplate="{TemplateBinding ContentTemplate}"

                 ContentStringFormat="{TemplateBinding ContentStringFormat}"

                 Margin="{TemplateBinding Padding}"

                 HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

                 VerticalAlignment="{TemplateBinding VerticalContentAlignment}"

                 SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />

        </Border>

    <ControlTemplate.Triggers>

        <Trigger Property="IsMouseOver" Value="True">

            <Setter

                 TargetName="ContentContainer"

                 Property="Background"

                 Value="LightBlue">

            </Setter>

        </Trigger>

        <Trigger Property="Button.IsDefaulted" Value="True">

            <Setter

                 TargetName="ContentContainer"

                 Property="Background"

                 Value="LightBlue">

            </Setter>

        </Trigger>

        <Trigger Property="ButtonBase.IsPressed" Value="True">

            <Setter

                 TargetName="Content"

                 Property="RenderTransform">

                 <Setter.Value>

                     <TranslateTransform Y="0.5"/>

                 </Setter.Value>

            </Setter>

        </Trigger>

        <Trigger Property="ToggleButton.IsChecked" Value="True">

            <Setter

                 TargetName="Content"

                 Property="RenderTransform">

                 <Setter.Value>

                     <TranslateTransform Y="0.5"/>

                 </Setter.Value>

            </Setter>

        </Trigger>

        <Trigger Property="UIElement.IsEnabled" Value="False">

            <Setter Property="TextElement.Foreground">

                 <Setter.Value>

                     <DynamicResource ResourceKey="{x:Static SystemColors.GrayTextBrushKey}" />

                 </Setter.Value>

            </Setter>

        </Trigger>

    </ControlTemplate.Triggers>

</ControlTemplate>


<
Style x:Key="{x:Type local:TimePicker}" TargetType="{x:Type local:TimePicker}">

    <Setter Property="Width" Value="100"/>

    <Setter Property="Height" Value="23"/>

    <Setter Property="BorderBrush" Value="Black"/>

    <Setter Property="BorderThickness" Value="0.5"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="{x:Type local:TimePicker}">

                 <Border

                     Background="{TemplateBinding Background}"

                     BorderBrush="{TemplateBinding BorderBrush}"

                     BorderThickness="{TemplateBinding BorderThickness}">

                     <Grid>

                         <Grid.ColumnDefinitions>

                              <ColumnDefinition Width="*"/>

                              <ColumnDefinition Width="Auto"/>

                         </Grid.ColumnDefinitions>


                         <
StackPanel

                              Orientation="Horizontal"

                              DataContext="{TemplateBinding local:TimePicker.SelectedTime}">

                              <TextBox

                                  x:Name="PART_HourTextBox"

                                  Text="{Binding Hours, Mode=OneWay, StringFormat=00}"

                                  BorderBrush="{x:Null}"

                                  Width="23"/>

                              <TextBlock

                                  Text=":"

                                  VerticalAlignment="Center"/>

                              <TextBox

                                  x:Name="PART_MinuteTextBox"

                                  Text="{Binding Minutes, Mode=OneWay, StringFormat=00}"

                                  BorderBrush="{x:Null}"

                                  Width="23"/>

                              <TextBlock

                                  Text=":"

                                  VerticalAlignment="Center"/>

                              <TextBox

                                  x:Name="PART_SecondTextBox"

                                  Text="{Binding Seconds, Mode=OneWay, StringFormat=00}"

                                  BorderBrush="{x:Null}"

                                  Width="23"/>

                         </StackPanel>

                         
                          <
Grid

                              Grid.Column="1">

                              <Grid.RowDefinitions>

                                  <RowDefinition Height="*"/>

                                  <RowDefinition Height="*"/>

                              </Grid.RowDefinitions>


                              <
Button

                                  x:Name="PART_IncrementButton"

                                  Margin="1,1,1,0"

                                  Width="20"

                                  Template="{StaticResource TimePickerButtonTemplate}">
                                  <
TextBlock

                                      Text="p"

                                      FontFamily="Wingdings 3"

                                      FontSize="6"

                                      HorizontalAlignment="Center"

                                      VerticalAlignment="Center"/>

                              </Button>

                              <Button

                                  x:Name="PART_DecrementButton"

                                  Grid.Row="1"

                                  Margin="1,1,1,1"

                                  Width="20"

                                  Template="{StaticResource TimePickerButtonTemplate}">

                                  <TextBlock

                                      Text="q"

                                      FontFamily="Wingdings 3"

                                      FontSize="6"

                                      HorizontalAlignment="Center"

                                      VerticalAlignment="Center"/>

                              </Button>

                         </Grid>

                     </Grid>

                 </Border>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

</ResourceDictionary>

Listing 8. TimePicker Default Template

Aside from the default style for your control, you can also add other things like brushes, other control templates, etc. in the resource dictionary. The first entry in the resource dictionary is the control template for the TimePicker buttons. Here, a Button’s appearance is defined as a Border control having a ContentPresenter inside it. If a Button’s content is a property of the Button class, how does the ContentPresenter get the content so that it can display it? This can be solved by using TemplateBinding. You can use TemplateBinding if you want to access the properties of the control where the template will be applied. This is equivalent to using Binding and setting the Source property to RelativeSource.TemplatedParent. For example, the Content property of the ContentPresenter can also be set like the following: Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}". However, take note that you must set the TargetType property of the ControlTemplate. In our example, it is set to ButtonBase. You can also use the more verbose {x:Type ButtonBase}.

Another important thing to know is how to add triggers to your template. You can define here how the control will look like when it is focused, when the mouse is over it, when it is disabled, etc. You need to supply the Property and Value properties of a Trigger. The Property can be set to any dependency property of the control where the template is applied. If the value of the dependency property is equal to the value specified in the Value property of the Trigger, then the corresponding setters are executed. Optionally, you can also set the SourceName property just in case you want to specify a dependency property of a different control inside the template. For example, instead of the Button, we may want to know if the Border’s IsMouseOver property is set to true. As for the Setter, it lets you set a value of a target control’s dependency property. There are actually other types of triggers and setters that you might encounter like DataTrigger and EventSetter among others.

Let’s see the default style applied to the TimePicker control. You can see that the TargetType of the style is set to local:TimePicker. By convention, the word local is used as the identifier of the local namespace, or the namespace containing the control. We haven’t set the resource key of the style so that the style gets applied to all TimePicker controls, unless the style is overridden. However, you can still specify a resource key and have the style applied to all TimePicker controls. The resource key must be the type of the control. In this case, the resource key would be assigned as follows: x:Key=”{x:Type local:TimePicker}”. You might notice that I set the DataContext property of a StackPanel using TemplateBinding. Aside from the reason that the SelectedTime is used by the controls in the StackPanel and saves us from typing more code, we can’t use TemplateBinding to bind to a property of a property. For example, {TemplateBinding SelectedTime.Hours} will not work because WPF will treat SelectedTime as the type and Hours as the property. Using {TemplateBinding local:TimePicker.SelectedTime.Hours} will not work either. Now let’s use the TimePicker control in an application.

<Window

    x:Class="CustomControlsDemo.MainWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:controls="clr-namespace:CustomControlsLib;assembly=CustomControlsLib"

    Title="MainWindow" Height="300" Width="300">

    <Grid>

        <controls:TimePicker

            SelectedTime="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=Time}"

            MinTime="8:00:00"

            MaxTime="17:00:00"/>

    </Grid>

</Window>

Listing 9. Using the TimePicker Control

Using your custom control is just like using an out-of-the-box WPF control. Just remember to add a reference to your project and the correct namespace to use it.



Figure 1. The TimePicker Control

The TimePicker control here is just created for learning purposes and could not be used in production. The Visual Studio 2008 solution is available here. If you want to use a TimePicker control, I suggest you use Marlon Grech’s Avalon Controls Library located at http://marlongrech.wordpress.com/avalon-controls-library/. These library contains other great WPF controls as well.

By Michael Detras   Popularity  (29123 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog