Creating custom controls

When using WPF, we can generally create the UI that we want using the many techniques already discussed in this book. However, in the cases where we require a totally unique control with both a custom drawn appearance and custom functionality, then we may need to declare a custom control.

Developing custom controls is very different than creating UserControl elements and it can take some time to master this. To start with, we will need to add a new project of type WPF Custom Control Library to declare them in. Also, instead of having a XAML page and a code behind file, we only have the code file. At this point, you may be wondering where we define what our control should look like.

In fact, when defining a custom control, we declare our XAML in a separate file named Generic.xaml, which is added by Visual Studio when we add our controls project. To clarify, the XAML for all of the custom controls that we declare in this project will go into this file. This does not relate to controls that extend the UserControl class and we should not declare those in this project.

This Generic.xaml file gets added into a folder named Themes in the root directory of our WPF Custom Control Library project, as this is where the Framework will look for the default styles of our custom controls. As such, we must declare the UI design of our control in a ControlTemplate and set it to the Template property in a style that targets the type of our control in this file.

The style must be applied to all instances of our control and so the style is defined with the TargetType set, but without the x:Key directive. If you remember, this will ensure that it is implicitly applied to all instances of our control that don't have an alternative template explicitly applied.

A further difference is that we cannot directly reference any of the controls that are defined within the style in the Generic.xaml file. If you recall, when we provided a new template for the built-in controls, we were under no obligation to provide the same controls that were originally used. Therefore, if we tried to access a control from our original template that had been replaced, it would cause an error.

Instead, we generally need to access them by overriding the FrameworkElement.OnApplyTemplate method, which is raised once a template has been applied to an instance of our control. In this method, we should expect that our required control(s) will be missing and ensure that no errors occur if that is the case.

Let's look at a simple example of a custom control that creates a meter that can be used to monitor CPU activity, RAM usage, audio loudness, or any other regularly changing value. We'll first need to create a new project of type WPF Custom Control Library and rename the CustomControl1.cs class that Visual Studio adds for us to Meter.cs.

Note that we can only add a custom control to a project of this type and that when the project is added, Visual Studio will also add our Themes folder and Generic.xaml file, with a style for our control already declared inside it. Let's see the code in the Meter.cs file:

using System; 
using System.Windows; 
using System.Windows.Controls; 
 
namespace CompanyName.ApplicationName.CustomControls 
{ 
  public class Meter : Control 
  { 
    static Meter() 
    { 
      DefaultStyleKeyProperty.OverrideMetadata(typeof(Meter),  
        new FrameworkPropertyMetadata(typeof(Meter))); 
    } 
 
    public static readonly DependencyProperty ValueProperty =  
      DependencyProperty.Register(nameof(Value),  
      typeof(double), typeof(Meter), 
      new PropertyMetadata(0.0, OnValueChanged, CoerceValue)); 
 
    private static object CoerceValue(DependencyObject dependencyObject,
      object value) 
    { 
      return Math.Min(Math.Max((double)value, 0.0), 1.0); 
    } 
 
    private static void OnValueChanged(DependencyObject dependencyObject,
      DependencyPropertyChangedEventArgs e) 
    { 
      Meter meter = (Meter)dependencyObject; 
      meter.SetClipRect(meter); 
    } 
 
    public double Value 
    { 
      get { return (double)GetValue(ValueProperty); } 
      set { SetValue(ValueProperty, value); } 
    } 
 
    public static readonly DependencyPropertyKey clipRectPropertyKey =       
      DependencyProperty.RegisterReadOnly(nameof(ClipRect), typeof(Rect),  
      typeof(Meter), new PropertyMetadata(new Rect())); 
 
    public static readonly DependencyProperty ClipRectProperty =  
      clipRectPropertyKey.DependencyProperty; 
 
    public Rect ClipRect 
    { 
      get { return (Rect)GetValue(ClipRectProperty); } 
      private set { SetValue(clipRectPropertyKey, value); } 
    } 
 
    public override void OnApplyTemplate() 
    { 
      SetClipRect(this); 
    } 
 
    private void SetClipRect(Meter meter) 
    { 
      double barSize = meter.Value * meter.Height; 
      meter.ClipRect =  
        new Rect(0, meter.Height - barSize, meter.Width, barSize); 
    } 
  } 
} 

This is a relatively small class, with only two Dependency Properties and their associated CLR property wrappers and callback handlers. Of particular note is the class's static constructor and the use of the DefaultStyleKeyProperty.OverrideMetadata method.

This is also added by Visual Studio when adding the class and is required to override the type-specific metadata of the DefaultStyleKey Dependency Property when we derive a custom class from the FrameworkElement class.

Specifically, this key is used by the Framework to find the default theme style for our control and so, by passing the type of our class into the OverrideMetadata method, we are telling the Framework to look for a default style for this type in our Themes folder.

If you remember, the theme styles are the last place that the Framework will look for the style of a specific type and declaring styles just about anywhere else in the application will override the default styles defined here.

The first Dependency Property is the main Value property of the control and this is used to determine the size of the visible meter bar. This property defines a default value of 0.0 and attaches the CoerceValue and OnValueChanged callback handlers.

In the CoerceValue handling method, we ensure that the output value always remains between 0.0 and 1.0, as that is the scale that we will be using. In the OnValueChanged handler, we update the value of the other Dependency Property, ClipRect, dependent upon the input value.

To do this, we first cast the dependencyObject input parameter to our Meter type and then pass that instance to the SetClipRect method. In this method, we calculate the relative size of the meter bar and define the Rect element for the ClipRect Dependency Property accordingly.

Next, we see the CLR property wrapper for the Value Dependency Property and then the declaration of the ClipRect Dependency Property. Note that we declare it using a DependencyPropertyKey element, thus making it a read-only property, because it is only for internal use and has no value in being exposed publicly. The actual ClipRect Dependency Property comes from this key element.

After this, we see the CLR property wrapper for the ClipRect Dependency Property and then we come to the aforementioned OnApplyTemplate method. In our case, the purpose of overriding this method is because often, data bound values will be set before the control's template has been applied and so we would not be able to correctly set the size of the meter bar from those values.

Therefore, when the template has been applied and the control has been arranged and sized, we call the SetClipRect method in order to set the Rect element for the ClipRect Dependency Property to the appropriate value. Before this point in time, the Height and Weight properties of the meter instance will be double.NaN (where NaN is short for Not a Number) and cannot be used to size the Rect element correctly.

When this method is called, we can rest assured that the Height and Weight properties of the meter instance will have valid values. Note that had we needed to access any elements from our template, we could have called the FrameworkTemplate.FindName method from this method, on the ControlTemplate object that is specified by our control's Template property.

If we had named a Rectangle element in our XAML PART_Rectangle, we could access it from the OnApplyTemplate method like this:

Rectangle rectangle = Template.FindName("PART_Rectangle", this) as Rectangle; 
if (rectangle != null) 
{ 
  // Do something with rectangle 
} 

Note that we always need to check for null, because the applied template may be a custom template that does not contain the Rectangle element at all. Note also that when we require the existence of a particular element in the template, we can decorate our custom control class declaration with a TemplatePartAttribute, that specifies the details of the required control:

[TemplatePart(Name = "PART_Rectangle", Type = typeof(Rectangle))] 
public class Meter : Control 
{ 
  ... 
} 

This will not enforce anything and will not raise any compilation errors if the named part is not included in a custom template, but it will be used in documentation and by various XAML tools. It helps users of our custom controls to find out which elements are required when they provide custom templates.

Now that we've seen the inner workings of this control, let's take a look at the XAML of the default style of our control in the Generic.xaml file to see how the ClipRect property is used:

<ResourceDictionary  
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  xmlns:CustomControls=
    "clr-namespace:CompanyName.ApplicationName.CustomControls"> 
  <Style TargetType="{x:Type CustomControls:Meter}"> 
    <Setter Property="Template"> 
      <Setter.Value> 
        <ControlTemplate TargetType="{x:Type  
          CustomControls:Meter}"> 
          <ControlTemplate.Resources> 
            <LinearGradientBrush x:Key="ScaleColors"  
              StartPoint="0,1" EndPoint="0,0"> 
              <GradientStop Color="LightGreen" /> 
              <GradientStop Color="Yellow" Offset="0.5" /> 
              <GradientStop Color="Orange" Offset="0.75" />  
              <GradientStop Color="Red" Offset="1.0" /> 
            </LinearGradientBrush> 
          </ControlTemplate.Resources> 
          <Border Background="{TemplateBinding Background}" 
            BorderBrush="{TemplateBinding BorderBrush}"  
            BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True"> <Border.ToolTip> <TextBlock Text="{Binding Value, StringFormat={}{0:P0}}" /> </Border.ToolTip> <Rectangle Fill="{StaticResource ScaleColors}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" SnapsToDevicePixels="True" Name="PART_Rectangle"> <Rectangle.Clip> <RectangleGeometry Rect="{Binding ClipRect, RelativeSource={RelativeSource AncestorType={x:Type CustomControls:Meter}}}" /> </Rectangle.Clip> </Rectangle> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>

When each custom control class is created in a WPF Custom Control Library project, Visual Studio adds an almost empty default style that sets a basic ControlTemplate and targets the type of the class into the Generic.xaml file. We just need to define our custom XAML within this template.

We start by declaring the ScaleColors gradient brush resource within the template. Note that the default value for the Offset property of a GradientStop element is 0 and so we can omit the setting of this property if that is the value that we want it set to. Therefore, when we see a declared GradientStop, like the one with the Color property set to LightGreen, we know its Offset property is set to 0.

Our meter control is basically made up of a Border element that surrounds a Rectangle element. We use TemplateBinding elements to data bind the Background, BorderBrush, and BorderThickness properties of the Border element and set its SnapsToDevicePixels property to True to avoid aliasing.

This enables users of the control to specify the border and background colors of the internal Border element of the meter control from outside the control. We could just as easily have exposed an additional brush property to replace the ScaleColors resource and enable users to define their own meter scale brush.

Note that we couldn't use a TemplateBinding to data bind the Value property in the ToolTip element. This is not because we don't have access to it through the template, but because we need to use the Binding.StringFormat property and the P format specifier to transform our double property value to a percentage value.

If you remember, a TemplateBinding is a lightweight binding and does not offer this functionality. While it is beneficial to use it when we can, this example highlights the fact that we cannot use it in every circumstance.

Finally, we come to the all-important Rectangle element that is responsible for displaying the actual meter bar of our control. The ScaleColors brush resource is used here to paint the background of the rectangle. We set the SnapsToDevicePixels property to true on this element to ensure that the level that it displays is accurate and well-defined.

The magic in this control is formed by the use of the UIElement.Clip property. Essentially, this enables us to provide any type of Geometry element to alter the shape and size of the visible portion of a UI element. The geometry shape that we assign here will specify the visible portion of the control.

In our case, we declare a RectangleGeometry class, whose size and location are specified by its Rect property. We therefore data bind our ClipRect Dependency Property to this Rect property, so that the sizes calculated from the incoming data values are represented by this RectangleGeometry instance, and therefore the visible part of the Rectangle element.

Note that we do this so that the gradient that is painted on the meter bar remains constant and does not change with the height of the bar as its value changes. If we had simply painted the background of the rectangle with the brush resource and adjusted its height, the background gradient would move with the size of the meter bar and spoil the effect.

Therefore, the whole rectangle is always painted with the gradient brush and we simply use its Clip property to just display the appropriate part of it. In order to use it in one of our Views, we'd first need to specify the CustomControls XAML namespace prefix:

xmlns:CustomControls="clr-namespace:CompanyName.ApplicationName.  
  CustomControls;assembly=CompanyName.ApplicationName.CustomControls"

We could then declare a number of them, data bind some appropriate properties to their Value property, and set styles for them, just like any other control:

<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> 
  <StackPanel.Resources> 
    <Style TargetType="{x:Type CustomControls:Meter}"> 
      <Setter Property="Background" Value="Black" /> 
      <Setter Property="BorderBrush" Value="Black" /> 
      <Setter Property="BorderThickness" Value="2" /> 
      <Setter Property="HorizontalAlignment" Value="Center" /> 
      <Setter Property="Width" Value="20" /> 
      <Setter Property="Height" Value="100" /> 
    </Style> 
  </StackPanel.Resources> 
  <CustomControls:Meter Value="{Binding CpuActivity}" /> 
  <CustomControls:Meter Value="{Binding DiskActivity}" Margin="10,0" /> 
  <CustomControls:Meter Value="{Binding NetworkActivity}" /> 
</StackPanel> 

Given some valid properties to data bind to, the preceding example would produce an output similar to the following: