WPF Custom Controls

Applying templates

This article was originally published by developmentor
DevelopMentor

 

Updating the CurrentTime

Normal digital clocks show the second-by-second progression of time via a digital display. This control already has a user interface element for showing the current time—it just needs to be wired up so as to display the current time. Fortunately, that’s very easy to do. Remember that the CurrentTime is a dependency property, so it may be databound to user interface elements. In this example, the code simply defines a method that will be fired periodically (via a timer we’ll install in just a minute). When the timer fires off, the AlarmClockControl will modify the CurrentTime property, which will be reflected by any user interface elements bound to it (we’ll bind the CurrentTime to the control’s current time TextBlock in just a minute after the template has been applied).

This is also a good time to check to see if the requested AlarmTime is equal to or later than the CurrentTime-- and if the alarm is armed the control should sound the alarm and raise the AlarmEvent. This example creates an instance of the Windows Media Player and plays a WAV file when the alarm goes off. You could also interoperate with the Win32 MessageBeep method for simplicity.

protected void RingAlarm()
{
    SoundPlayer sp = new SoundPlayer(@"c:\windows\media\tada.wav");
    sp.Play();
    FireAlarm();
}
public void OnDisplayTimerTick(object o, EventArgs args)
{
    this.CurrentTime = DateTime.Now;

    if (this.AlarmSet == true)
    {
        if (DateTime.Now.Ticks > this.AlarmTime.Ticks)
        {
            RingAlarm();
        }
    }
}

Setting the Alarm Time

An alarm clock isn’t complete unless the user has a way of setting the alarm. In the case of this control, the alarm time can be set by invoking a small dialog that hosts the Windows Forms DateTimePicker. The dialog is set up as a regular Window. It uses the WindowsFormsHost control from the WindowsFormsIntegration assembly, and the Windows Forms DateTimePicker control. The dialog also has two buttons: one representing OK and the other one representing Cancel. The Buttons are wired up so that when the user selects OK, the dialog returns true as the DialogResult and closes the dialog. The Cancel handler sets the DialogResult to false and closes the dialog.

The definition of the dialog looks like this, and is married to the DateTimeDlg class defined within the control’s assembly.

<Window x:Class="AlarmClockControlLibrary.DateTimeDlg"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
    xmlns:wfi="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"                
    Title="DateTimeDlg" Height="160" Width="312" ResizeMode="NoResize">
    <StackPanel Height="112">
        <wfi:WindowsFormsHost Margin="5" Width="200" Height="35">
            <wf:DateTimePicker x:Name="dtp" Format="Short"/>
        </wfi:WindowsFormsHost>
        <Grid Height="72">
            <Grid.ColumnDefinitions>
                <ColumnDefinition></ColumnDefinition>
                <ColumnDefinition ></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="60"></RowDefinition>
            </Grid.RowDefinitions>
            <Button Padding="10" Margin="10" Click="Button_Click">Cancel</Button>
            <Button Padding="10" Margin="10" x:Name="buttonOkay" Grid.Column="1" Click="buttonOkay_Click">OK</Button>
        </Grid>
    </StackPanel>
</Window>

Here’s the DateTimeDlg definition. It simply exposes the AlarmTime as a normal .NET property. It has buttons for the OK and Cancel buttons that set the dialog’s DialogResult property appropriately.

public partial class DateTimeDlg : Window
{

    DateTime alarmTime;

    public DateTime AlarmTime
    {
        get { return alarmTime; }
        set
        {
            alarmTime = value;
            this.dtp.Value = alarmTime;
        }
    }

    public DateTimeDlg()
    {
        InitializeComponent();

        dtp.Format =
            System.Windows.Forms.DateTimePickerFormat.Custom;
        dtp.CustomFormat = "ddd MMM dd yyyy hh:mm:ss";
        dtp.ShowUpDown = false;

    }

    private void buttonOkay_Click(object sender, RoutedEventArgs e)
    {
        alarmTime = this.dtp.Value;
        this.DialogResult = true;
        this.Close();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        this.DialogResult = false;
        this.Close();
    }
}

Invoking the dialog is simply a matter of showing the dialog using ShowDialog and checking the result when the dialog is done. This is done in a RoutedEvent handler that will be wired up to the control’s button used for setting the alarm time (after the template has been applied—coming up).

void OnShowSetAlarmDlg(object sender, RoutedEventArgs ea)
{
    DateTimeDlg dateTimeDlg = new DateTimeDlg();

    dateTimeDlg.AlarmTime = this.AlarmTime;
    if (dateTimeDlg.ShowDialog() == true)
    {
        this.AlarmTime = dateTimeDlg.AlarmTime;
    }

}

Applying the Template

Once all the infrastructure has been set up, the final step is to wire the template up to the control. This is done automatically via the virtual function Control.OnApplyTemplate. In this case, there are a few databindings and event handlers to set up. OnApplyTemplate is a good place to wire these things up because you can be sure that the control’s visual tree is set up by this time. In the case of the AlarmClockControl, the timer is started, the CurrentTime property is bound to the current timer TextBlock element, the AlarmSet property is bound to the alarm set checkbox, and the AlarmTime property is bound to the TextBlock in the button that sets the alarm time.

Notice that the Control has a member named Template—this represents the ControlTemplate set by the Style from the Generic.xaml file. The Template has a method named FindName which retrieves a reference to the specified user interface element. Remember the elements within the ControlTemplate named “PART_CURRENTDATETIME”, “PART_CHECKBOXALARMSET”, “PART_SETALARMBUTTON”, and “PART_SETALARMBUTTONTEXTPANE”? The following code uses FindName to retrieve the current time TextBlock, the CheckBox for setting the alarm, the button for setting the alarm, and the TextBlock within the alarm-setting button that shows the current alarm time. These user interface elements are bound to the dependency properties within the AlarmClockControl, and the handler for displaying the alarm-setting dialog is wired up to the alarm-setting button. Finally, it makes sense to start the timer here, as well.

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    Button bSetAlarmDlg =
        (Button)this.Template.FindName("PART_SETALARMBUTTON", this);

    bSetAlarmDlg.Click += OnShowSetAlarmDlg;

    displayTimer = new DispatcherTimer();
    displayTimer.Interval = new TimeSpan(0, 0, 0, 0, 250);
    displayTimer.Tick += OnDisplayTimerTick;

    displayTimer.Start();

    // Set up a databinding between the checkbox in the template and the flag...
    CheckBox cbAlarmSet = (CheckBox)this.Template.FindName("PART_CHECKBOXALARMSET", this);
    Binding bindingAlarmSet = new Binding();
    bindingAlarmSet.Source = this;
    bindingAlarmSet.Path = new PropertyPath("AlarmSet");
    cbAlarmSet.SetBinding(CheckBox.IsCheckedProperty, bindingAlarmSet);

    TextBlock tbAlarmSetButtonTextPane =
        (TextBlock)this.Template.FindName("PART_SETALARMBUTTONTEXTPANE", this);
    Binding bindingSetAlarmButtonTextPane = new Binding();
    bindingSetAlarmButtonTextPane.Source = this;
    bindingSetAlarmButtonTextPane.Path = new PropertyPath("AlarmTime");
    tbAlarmSetButtonTextPane.SetBinding(TextBlock.TextProperty, bindingSetAlarmButtonTextPane);

    TextBlock tbCurrentTime =
        (TextBlock)this.Template.FindName("PART_CURRENTDATETIME", this);
    Binding bindingCurrentTime = new Binding();
    bindingCurrentTime.Source = this;
    bindingCurrentTime.Path = new PropertyPath("CurrentTime");
    tbCurrentTime.SetBinding(TextBlock.TextProperty, bindingCurrentTime);
}

Hosting the Control

A control is of little use unless it’s hosted somewhere. Because this is a WPF control, it may be hosted anywhere standard WPF controls may be hosted. In this example, it’s simply hosted within the logical tree of normal WPF window. The following XAML shows how the alarm clock control is hosted like any other control.

<Window x:Class="CustomControlORama.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:alarmClock="clr-namespace:AlarmClockControlLibrary;assembly=AlarmClockControlLibrary"
    Title="Window1" Height="300" Width="437">
    <StackPanel>
        <alarmClock:AlarmClockControl></alarmClock:AlarmClockControl>
    </StackPanel>
</Window>

What’s Missing

Though this example may seem somewhat involved at first glance (with the separate code behind and DataTemplate definition, and all the dependency properties), there are actually a few things still missing. For example, most controls take great pains to support the standard Windows theming (for example supporting Aero, Luna, Windows Classic, etc. themes). This requires creating a separate control template within the correctly-named XAML file under the Themes folder (remember—the folder created by Visual Studio that contains the Generic.xaml file). In addition, the control must advertise its support for various themes using the assembly-wide ThemeInfo attribute.

There’s also no design-time support for this control. WPF supports various designer aspects of the way WPF interacts with Visual Studio. These include custom adorners, tools, property editors, and custom designers.

Finally, there are a couple of other features that you may implement for the alarm—like a snooze button sets the AlarmTime to be 10 minutes more than the CurrentTime. You might also include a way for the client to configure the current time display color.

Conclusion

WPF completely redefines how a Windows program renders itself. No longer is an individual control responsible for rendering its content onto a device context. Instead, a WPF program defines a logical tree of controls and framework elements (usually via some XAML). In turn, the individual controls each define their own visual tree—which is composed of various other elements responsible for defining the various parts of the control.

WPF supports a wide variety of methods for working with controls including:

  1. Being able to define anything as content
  2. Defining DataTemplates
  3. Replacing parts of the visual tree
  4. Combining controls via User Controls

If one of these solutions doesn’t quite suite your goals, you may define your own custom control from scratch.

Writing a custom WPF control requires creating a basic default style and a Control-derived class to go with it. The default style sets the control template and defines the default appearance of the control. It’s here that you define the control’s visual tree. The control should use dependency properties to expose properties that are supposed to be databound by the client. Events exposed by the control should be exposed as RoutedEvents so that they play well with the rest of the WPF application’s event routing. Finally, the control should support the standard Windows XP and Vista themes if it plans to play well with the other controls declared by the host.

You might also like...

Comments

About the author

George Shepherd United Kingdom

George Shepherd has been programming Windows since the 2.0 days back in the late '80s. When MFC came out in the early 1990s, George co-authored the definitive reference for MFC, MFC Internals (1...

Interested in writing for us? Find out more.

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“In order to understand recursion, one must first understand recursion.”