Twitter

Facebook

Umbraco Certified

RAR Recommended

Twitter

Thu 17 May 12 @ 7:06JetBrains dotPeek 1.0 has just been released http://t.co/q0toNbzY

Wed 16 May 12 @ 8:53Grew up in the 1980's? Take a trip down memory lane http://t.co/nFPqtLtH #1980s #memories

Tue 15 May 12 @ 8:30What a cool idea for a piggy bank http://t.co/IN08jLi2

Follow Us On Twitter

nopCommerce Solution Provider

Microsoft Partner Network

Archive

View All

Creating a WPF User Control

Tuesday, September 20, 2011

I am going to show you how to create and use a user control in WPF, also known as  composite control, using Visual Studio 2010.

The control I am going to create will be used within a scheduling application, and it is used to specify the hour and minute in each day that the scheduler becomes active. When the scheduling application was first written, the user had to specify the hour and day in two separate text boxes, like this:

 

image

 

Furthermore, the original application contained no validation for user input on those text boxes, so users were free to input values that were not numbers between 1 and 24 or 1 and 60, or not even numbers at all.

Create the User Control

In Visual Studio 2010, select the appropriate project in your solution, right-click, choose "Add" then "New Item". From the list in the Add New Item dialog, select "User Control (WPF)". Provide a name for the control: "HourMinutePicker.xaml:

 

image

We will want to access our new control from the Visual Studio Toolbox. To add the new control to the Toolbox, first build your assembly. Then right-click on the Toolbox and select "Choose Items…". The "Choose Toolbox Items" dialog will open. Select the "WPF Components" tab. Now click the "Browse" button and browse to the compiled assembly that contains the control. In my example the control is part of the executable so I browse to the bin folder and select my exe file. My control is then listed and I can select it:

 

image

 

Hint: to see your new control in the Toolbox, you may have to right-click on the Toolbox and select "Show All".

Obviously you don't have to add your new control to the Toolbox - you can still use it just by referencing it through its namespace in your xaml.

For a more detailed description of adding WPF controls to the Toolbox see: http://geekswithblogs.net/RockStarCoder/archive/2009/07/15/adding-your-wpf-control-to-the-toolbox.aspx

 

Add the Constituent Controls

A user control is also known as a composite control because it may contain other controls, known as constituent controls. Our hour minute picker will contain some constituent controls, including two text boxes to enter the hour and minute respectively, plus some custom spinners to allow the user to increment or decrement the values (a more sophisticated solution would create another user control to encapsulate these time element, and we would re-use this control twice within our control).

Double-click on the xaml file component of your new control to open the designer. Open the Visual Studio Toolbox. From the Toolbox drag three controls onto your designer surface:

  • A text box for the hour component
  • A text box for the minute component
  • A label to act as a separator between the hour and minute values. Give it a Content of ":"

Use the Designer or edit the xaml to adjust the various heights and widths so that your control looks nicely arranged, for example like this:

image

 

We will not allow the user to type values into the HourMinutePicker. This avoids some difficulties, notably the fact that if we try and validate as the user enters text then we don't know when they entered '4' whether that's a valid reference to 4 am, or 4 pm, or whether they are about to type another digit such as '2' to create an invalid hour ('42'). Our control is relatively unsophisticated so we only allow them to change values by clicking on "spinner" arrows within the control. So disable the user input in the hour and minute text boxes by setting their "readonly" flags to "true" .

Add the "uparrow" and "downarrow" images to the solution.

uparrow

downarrow

 

Position them in the user control to produce spinners:

image

 

Add a Property

Our user control needs a property to represent the hour and minute, so we will create a DateTime property named HourMinute.

The User Control as a Binding Target

We want our user control to be a binding target. This means that we can bind another object to it and the framework will take care of updating that data object with the values entered in the control by the user. Similarly our user control will reflect the value of the data object, so for example when the control is opened it displays the hour and minute from the bound object.

We are going to bind our control to a DateTime member in an instance of the class "Task". In the data binding scenario this means that our user control is a binding target and therefore its property to be bound must be a "DependencyProperty". The process of defining dependency properties using the HourMinutePicker as an example is described here:

/blog/2011/9/20/wpf-user-controls-turn-your-user-control-into-a-data-binding-target/

The User Control as a Binding Source

Our user control also needs to act as a binding source. This is because we want to be able to bind our hour and minute text boxes as binding targets to the hour and minute contained within our HourMinute property. This means our user control's HourMinute property is the data source in the binding scenario. To support data binding source behaviour the user control must implement the INotifyPropertyChanged interface. That means updates to the HourMinute property can be notified to the appropriate text controls.

public partial class HourMinutePicker : UserControl, INotifyPropertyChanged

 

We then implement the INotifyPropertyChanged  interface:

protected void OnPropertyChanged(string name)
{
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
    {
        handler(this, new PropertyChangedEventArgs(name));
    }
}

 

The HourMinute property must raise the change notification:

OnPropertyChanged("HourMinute");

 

The user control must set up the binding between its HourMinute property and the text boxes. This can be done in xaml or in the C#. Here it is in C#, called in the constructor of the user control:

var hourBinding = new Binding();
 hourBinding.Source = this;
 hourBinding.Path = new PropertyPath("HourMinute.Hour");
 hourBinding.Mode = BindingMode.OneWay;
 textBoxHour.SetBinding(TextBox.TextProperty, hourBinding);

 var minuteBinding = new Binding();
 minuteBinding.Source = this;
 minuteBinding.Path = new PropertyPath("HourMinute.Minute");
 minuteBinding.Mode = BindingMode.OneWay;
 textBoxMinute.SetBinding(TextBox.TextProperty, minuteBinding);

 

The binding is one-way because we just want the text boxes to reflect the value of the property (the value itself is controlled via the spinners).

The User Control as Both Target and Source

Combining the changes required for supporting the binding target and binding source behaviours results in these property definitions:

public static DependencyProperty HourMinuteProperty = DependencyProperty.Register(
    "HourMinute", typeof(DateTime), typeof(HourMinutePicker), 
    new PropertyMetadata(OnHourMinChanged));

private DateTime _hourMinute = new DateTime(1900, 1, 1);

static void OnHourMinChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
    if (obj != null && obj is HourMinutePicker)
    {
       (obj as HourMinutePicker).UpdateTime(e);
    }
}

private void  UpdateTime(DependencyPropertyChangedEventArgs e)
{
    _hourMinute = (DateTime) e.NewValue;
}

public DateTime HourMinute
{
    get
    {
       return (DateTime) GetValue(HourMinuteProperty);
    }
    set
    {
       SetValue(HourMinuteProperty, value);

       OnPropertyChanged("HourMinute");
    }
}

Implement Command Handling

Now we need to capture user mouse clicks on the up and down arrows. We will use routed commands, so in the code for the user control (HourMinutePicker.xaml.cs) we declare 4 static routed commands:

public static RoutedCommand HourUpTask = new RoutedCommand();
public static RoutedCommand MinuteUpTask = new RoutedCommand();
public static RoutedCommand HourDownTask = new RoutedCommand();
public static RoutedCommand MinuteDownTask = new RoutedCommand();
We can associate these commands with the spinner images (up and down arrows) in the user control's xaml:
<Image …>
            <Image.InputBindings>
                <MouseBinding Gesture="LeftClick" Command="{x:Static Controls:HourMinutePicker.HourDownTask}"  />
            </Image.InputBindings>
</Image>

Now we can bind to these commands in the constructor of our user control:
this.CommandBindings.Add(new CommandBinding(HourUpTask, ExecuteHourDownTask, CanExecuteHourMinute));
this.CommandBindings.Add(new CommandBinding(MinuteUpTask, ExecuteMinuteDownTask, CanExecuteMinuteTask));
this.CommandBindings.Add(new CommandBinding(HourDownTask, ExecuteHourUpTask,CanExecuteHourMinute));
this.CommandBindings.Add(new CommandBinding(MinuteDownTask, ExecuteMinuteUpTask, CanExecuteMinuteTask));

 

Note that each command references two methods: an execute method and a "can execute" method. The "can execute" method is called to determine whether the command processing should proceed for this handler. For our user control we do not need to validate so we can always return true. This is because an advantage of using our spinner inputs with a DateTime property is that the DateTime class will automatically roll the values round the hour and minute boundaries as we add or subtract values.  The user cannot enter an invalid character. Therefore we just need a single "can execute" command that returns true.  We need four execute methods:

public void ExecuteHourDownTask(object sender, ExecutedRoutedEventArgs e)
{
     HourMinute = _hourMinute.AddHours(-1);            
}

public void ExecuteHourUpTask(object sender, ExecutedRoutedEventArgs e)
{
     HourMinute = _hourMinute.AddHours(1);
}

public void ExecuteMinuteDownTask(object sender, ExecutedRoutedEventArgs e)
{
     HourMinute = _hourMinute.AddMinutes(-1);
}
public void ExecuteMinuteUpTask(object sender, ExecutedRoutedEventArgs e)
{
     HourMinute = _hourMinute.AddMinutes(1);
}

public void CanExecuteHourMinute(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = true;
}

The "execute" tasks are self-explanatory, they simply amend the appropriate hour or minute field in the required direction.

Using the User Control

The user control can now be used. We will embed it in the xaml for a dialog that updates the properties of a Task object. Our user control manages the hour and minute for the scheduling details of the task:

<Controls:HourMinutePicker HourMinute="{Binding Task.ScheduledHourMin, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />

Note that we have declared our control and bound it to the "ScheduledHourMin" property of our Task object. You can set the attributes of the control such as its border thickness and colour.

 

Complete Source Code

xaml

<UserControl x:Class="FtpSync.Controls.HourMinutePicker"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:Controls="clr-namespace:FtpSync.Controls" mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             Name="HMPicker"
             >
    <Border Height="29" Width="73" BorderThickness="1" BorderBrush="Black">
    <Grid Height="19" Width="66">
            <Grid.RowDefinitions>
                <RowDefinition Height="10*" />
                <RowDefinition Height="9*" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
            <ColumnDefinition Width="11*"/>
            <ColumnDefinition Width="14*"/>
            <ColumnDefinition Width="65*" />
        </Grid.ColumnDefinitions>
        <Label Content=":" Height="25" HorizontalAlignment="Left" Margin="8.5,-5,0,0" Name="label1" VerticalAlignment="Top" DataContext="{Binding}" Width="Auto" Grid.Column="2" d:LayoutOverrides="HorizontalAlignment" Grid.RowSpan="2" />
        <TextBox Height="18" Name="textBoxMinute" VerticalAlignment="Top" Grid.Column="2" BorderThickness="0" Grid.RowSpan="2" Margin="19.001,1,-0.333,0" />
        <TextBox Height="19" Name="textBoxHour" VerticalAlignment="Top" IsEnabled="True" IsReadOnly="True" Grid.ColumnSpan="2" BorderThickness="0" Grid.RowSpan="2" Margin="0,1,-10.667,0" d:LayoutOverrides="GridBox" />
        <Image HorizontalAlignment="Left" Height="4" Margin="3.5,1,0,0" Source="/FtpSync;component/Images/downarrow.png" Stretch="Fill" VerticalAlignment="Top" Width="7" Grid.Column="2">
            <Image.InputBindings>
                <MouseBinding Gesture="LeftClick" Command="{x:Static Controls:HourMinutePicker.HourUpTask}"  />
            </Image.InputBindings>
        </Image>
        <Image HorizontalAlignment="Left" Height="4" Margin="3.5,5,0,0" Source="/FtpSync;component/Images/uparrow.png" Stretch="Fill" VerticalAlignment="Top" Width="7" Grid.Column="2" Grid.Row="1">
            <Image.InputBindings>
                <MouseBinding Gesture="LeftClick" Command="{x:Static Controls:HourMinutePicker.HourDownTask}"  />
            </Image.InputBindings>
        </Image>
        <Image HorizontalAlignment="Left" Margin="41.5,1,0,0" Source="/FtpSync;component/Images/downarrow.png" Stretch="Fill" Width="7" Grid.Column="2" Height="4" VerticalAlignment="Top">
            <Image.InputBindings>
                <MouseBinding Gesture="LeftClick" Command="{x:Static Controls:HourMinutePicker.MinuteUpTask}"  />
            </Image.InputBindings>
        </Image>
        <Image HorizontalAlignment="Left" Height="4" Margin="41,5,0,0" Source="/FtpSync;component/Images/uparrow.png" Stretch="Fill" VerticalAlignment="Top" Width="7" Grid.Column="2" Grid.Row="1">
            <Image.InputBindings>
                <MouseBinding Gesture="LeftClick" Command="{x:Static Controls:HourMinutePicker.MinuteDownTask}"  />
            </Image.InputBindings>
        </Image>      
    </Grid>
    </Border>
</UserControl>


C#

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;

namespace FtpSync.Controls
{
   public partial class HourMinutePicker : UserControl, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public static RoutedCommand HourUpTask = new RoutedCommand();
        public static RoutedCommand MinuteUpTask = new RoutedCommand();
        public static RoutedCommand HourDownTask = new RoutedCommand();
        public static RoutedCommand MinuteDownTask = new RoutedCommand();

        public static DependencyProperty HourMinuteProperty = DependencyProperty.Register(
            "HourMinute", typeof(DateTime), typeof(HourMinutePicker), 
            new PropertyMetadata(OnHourMinChanged));

       private DateTime _hourMinute = new DateTime(1900, 1, 1);

       static void OnHourMinChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (obj != null && obj is HourMinutePicker)
            {
               (obj as HourMinutePicker).UpdateTime(e);
            }
        }

       private void  UpdateTime(DependencyPropertyChangedEventArgs e)
        {
            _hourMinute = (DateTime) e.NewValue;
        }

       public DateTime HourMinute
        {
            get
            {
               return (DateTime) GetValue(HourMinuteProperty);
            }
            set
            {
               SetValue(HourMinuteProperty, value);

               OnPropertyChanged("HourMinute");
            }
        }

       public HourMinutePicker()
        {
            InitializeComponent();

            this.CommandBindings.Add(new CommandBinding(HourUpTask, ExecuteHourDownTask,
                                            CanExecuteHourMinute));
            this.CommandBindings.Add(new CommandBinding(MinuteUpTask, ExecuteMinuteDownTask,
                                CanExecuteMinuteTask));
            this.CommandBindings.Add(new CommandBinding(HourDownTask, ExecuteHourUpTask,
                                CanExecuteHourMinute));
            this.CommandBindings.Add(new CommandBinding(MinuteDownTask, ExecuteMinuteUpTask,
                                CanExecuteMinuteTask));

            var hourBinding = new Binding();
            hourBinding.Source = this;
            hourBinding.Path = new PropertyPath("HourMinute.Hour");
            hourBinding.Mode = BindingMode.OneWay;
            textBoxHour.SetBinding(TextBox.TextProperty, hourBinding);

            var minuteBinding = new Binding();
            minuteBinding.Source = this;
            minuteBinding.Path = new PropertyPath("HourMinute.Minute");
            minuteBinding.Mode = BindingMode.OneWay;
            textBoxMinute.SetBinding(TextBox.TextProperty, minuteBinding);
        }

       public void CanExecuteHourMinute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true;
        }

       public void ExecuteHourDownTask(object sender, ExecutedRoutedEventArgs e)
        {
            HourMinute = _hourMinute.AddHours(-1);            
        }

       public void ExecuteHourUpTask(object sender, ExecutedRoutedEventArgs e)
        {
            HourMinute = _hourMinute.AddHours(1);
        }

       public void ExecuteMinuteDownTask(object sender, ExecutedRoutedEventArgs e)
        {
            HourMinute = _hourMinute.AddMinutes(-1);
        }

       public void ExecuteMinuteUpTask(object sender, ExecutedRoutedEventArgs e)
        {
            HourMinute = _hourMinute.AddMinutes(1);
        }

       public void CanExecuteMinuteTask(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (_hourMinute.Minute >= 0 && _hourMinute.Minute <= 59);
        }

       protected void OnPropertyChanged(string name)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(name));
            }
        }
    }
}