One of the key design principles for designing controls in WPF is the separation of UI from the actual control implementation (or functionality if you prefer). The aim is to create a control with properties, methods, events, states etc. as needed that does not rely on the specifics of the way the UI displays the control.
I’m going to work through a very simple lookless control to just get an idea how such a control might be implemented.
What’s the control going to be ?
Okay I don’t want anything too large as this is mean’t as a quick and easy to follow post. So the control will be a countdown timer control. A control which takes a time, in seconds and each second decreases the count until it reaches zero at which point it stops.
The control will have the following properties
From – this will hold to countdown “from” value (in seconds). In other words the value we start the countdown at.
Current – this will hold the current value in the countdown, obviously this will reduce from the “From” value down to zero.
IsRunning – this will allow us to know if the countdown timer is running.
Steps
- Create a CountdownControl class and implement the properties described above
- Create a CountdownControl XAML file and create a default style
- Create an alternate UI by overriding the ControlTemplate
Let’s implement this thing
Step 1
So step 1 is to create a new class called CountdownControl, we will derive this from a Control as we’re not looking to extend any existing control for this. So it should look like
public class CountdownControl : Control { }
Nothing exciting there.
As we’re going to countdown from a given value and for this example we’ve assumed the use of seconds only. We’ll use the DispatcherTimer to give us an event every second which will allow us to decrement the current value accordingly. As this isn’t mean’t to be a step by step for coding this I’ll simply point out that we can use the snippets in Visual Studio 2012 (dp and dpp) to create the propeties as follows.
- Create the IsRunning as a dependency property with property changed event
- Create the CurrentProperty as a readonly dependency property
- Create the From as a dependency property with property changed event
- Create a CountdownControl constructor and add the code to created the timer
- create the timer tick event and have it decrement the Current property until it reaches 0 then set the IsRunning to false
- On the OnIsRunningRunningChanged either start or stop the timer depending on the value passed to it
- On the OnFromChanged, set the IsRunning to true if a value greater than 0 is assigned to the From property
And here’s the code
public class CountdownControl : Control { private readonly DispatcherTimer timer; public CountdownControl() { timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromSeconds(1); timer.Tick += TimerTick; } private void TimerTick(object sender, EventArgs e) { if (Current > 0) { SetValue(CurrentPropertyKey, Current - 1); if (Current == 0) { IsRunning = false; } } } public static readonly DependencyProperty IsRunningProperty = DependencyProperty.Register("IsRunning", typeof(bool), typeof(CountdownControl), new FrameworkPropertyMetadata((bool)false, new PropertyChangedCallback(OnIsRunningChanged))); public bool IsRunning { get { return (bool)GetValue(IsRunningProperty); } set { SetValue(IsRunningProperty, value); } } private static void OnIsRunningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { CountdownControl target = (CountdownControl)d; bool oldIsRunning = (bool)e.OldValue; bool newIsRunning = target.IsRunning; target.OnIsRunningChanged(oldIsRunning, newIsRunning); } protected virtual void OnIsRunningChanged(bool oldIsRunning, bool newIsRunning) { if (!DesignerProperties.GetIsInDesignMode(this)) { if (newIsRunning) timer.Start(); else timer.Stop(); } } private static readonly DependencyPropertyKey CurrentPropertyKey = DependencyProperty.RegisterReadOnly("Current", typeof(int), typeof(CountdownControl), new FrameworkPropertyMetadata((int)0)); public static readonly DependencyProperty CurrentProperty = CurrentPropertyKey.DependencyProperty; public int Current { get { return (int)GetValue(CurrentProperty); } } public static readonly DependencyProperty FromProperty = DependencyProperty.Register("From", typeof(int), typeof(CountdownControl), new FrameworkPropertyMetadata((int)0, new PropertyChangedCallback(OnFromChanged))); public int From { get { return (int)GetValue(FromProperty); } set { SetValue(FromProperty, value); } } private static void OnFromChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { CountdownControl target = (CountdownControl)d; int oldFrom = (int)e.OldValue; int newFrom = target.From; target.OnFromChanged(oldFrom, newFrom); } protected virtual void OnFromChanged(int oldFrom, int newFrom) { SetValue(CurrentPropertyKey, newFrom); IsRunning = (newFrom > 0); } }
As can be seen, there’s no UI code here, only properties and functionality yet this will implement a countdown clock.
Note: The user of the DesignerProperties.GetIsInDesignMode is to stop the control counting down when in the XAML designer
Before we move onto the next step, which is to add a default style/look, let’s finish up here by creating the following static constructor
static CountdownControl() { DefaultStyleKeyProperty.OverrideMetadata(typeof(CountdownControl), new FrameworkPropertyMetadata(typeof(CountdownControl))); }
As per a previous post on extending an existing control, the DefaultStyleKeyProperty is here to change the metadata for this class to associate a new style with this specific type.
Step 2
Create a ResourceDictionary named CountdownControl.xaml and add the following code
<Style TargetType="{x:Type Controls:CountdownControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Controls:CountdownControl}"> <Grid> <TextBlock Text="{Binding Current, RelativeSource={RelativeSource TemplatedParent}}" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
There’s not masses to say here except that this will now display the Current property value as it counts down in a TextBlock. The use of the binding instead of TemplateBinding is covered in a previous post, but basically TemplateBinding is lightweight and does not support the type conversion from an int (the current property) to a string. So we have to use the more verbose Binding syntax to solve this.
Lastly, if you are creating this control in it’s own assembly and haven’t already got one, create a Themes folder in your project and add a Generic.xaml ResourceDictionary inside the folder. Then add the following XAML
<ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/SimpleControl;component/CountdownControl.xaml" /> </ResourceDictionary.MergedDictionaries>
Step 3
Now we can use the CountdownControl by simply adding the following
<Controls:CountdownControl From="30"/>
Note: This assumes you’ve a namespace for the CountdownControls aliased as Controls.
If you run the application now it should display a simple text countdown from 30 to zero.
So whilst our lookless code then got a default look, we now want to re-template it to something a little nicer (although still fairly simple).
We’re going to now re-template the control to use a progress bar to show the countdown. So let’s see the new Style for this.
<Style TargetType="{x:Type c:CountdownControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type c:CountdownControl}"> <Grid> <ProgressBar Minimum="0" Maximum="{Binding From, RelativeSource={RelativeSource TemplatedParent}}" Value="{Binding Current, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" Height="20"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
As you can see we’re now also using the From property as well as the Current property. We might have also decided to use the IsRunning with a BooleanToVisibilityConverter to hide the progress bar when it reaches zero.
In Summary
We’ve created a control which began by having no UI and was lookless and also very much testable. We then created a basic default look so we could at least see something when designing with the control. Finally we created a nicer UI for our specific needs.