Visualizing data

While there are a number of pre-existing graph controls and third party data visualization controls available in WPF, we can create our own relatively easily. Expressing data in textual terms alone, while generally acceptable, is not optimal. Breaking the norm in an application always makes that application stand out from the rest that strictly adheres to the standard.

As an example, imagine a simple situation, where we have a dashboard that visualizes the number of work tasks that have come in and the number that have been completed. We could just output the numbers in a big, bold font, but that would be the normal kind of output. What about if we visualized each number as a shape, with its size being specified by the number?

Let's reuse our layering techniques from earlier and design some visually appealing spheres, that grow in size depending upon a particular value. To do this, we can create another custom control, with a Value Dependency Property to data bind to. Let's first look at the code of the Sphere class:

using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Media; 
using System.Windows.Shapes; 
using CompanyName.ApplicationName.CustomControls.Enums;
using MediaColor = System.Windows.Media.Color; 
 
namespace CompanyName.ApplicationName.CustomControls 
{ 
  [TemplatePart(Name = "PART_Background", Type = typeof(Ellipse))] 
  [TemplatePart(Name = "PART_Glow", Type = typeof(Ellipse))] 
  public class Sphere : Control 
  { 
    private RadialGradientBrush greenBackground =
      new RadialGradientBrush(new GradientStopCollection() { 
      new GradientStop(MediaColor.FromRgb(0, 254, 0), 0),
      new GradientStop(MediaColor.FromRgb(1, 27, 0), 0.974) }); 
    private RadialGradientBrush greenGlow = 
      new RadialGradientBrush(new GradientStopCollection() { 
      new GradientStop(MediaColor.FromArgb(205, 67, 255, 46), 0),
      new GradientStop(MediaColor.FromArgb(102, 88, 254, 72), 0.426),
      new GradientStop(MediaColor.FromArgb(0, 44, 191, 32), 1) }); 
    private RadialGradientBrush redBackground = 
      new RadialGradientBrush(new GradientStopCollection() { 
      new GradientStop(MediaColor.FromRgb(254, 0, 0), 0),
      new GradientStop(MediaColor.FromRgb(27, 0, 0), 0.974) }); 
    private RadialGradientBrush redGlow = 
      new RadialGradientBrush(new GradientStopCollection() { 
      new GradientStop(MediaColor.FromArgb(205, 255, 46, 46), 0), 
      new GradientStop(MediaColor.FromArgb(102, 254, 72, 72), 0.426),
      new GradientStop(MediaColor.FromArgb(0, 191, 32, 32), 1) }); 
 
    static Sphere() 
    { 
      DefaultStyleKeyProperty.OverrideMetadata(typeof(Sphere),  
        new FrameworkPropertyMetadata(typeof(Sphere))); 
    } 
 
    public static readonly DependencyProperty ValueProperty =  
      DependencyProperty.Register(nameof(Value), typeof(double),  
      typeof(Sphere), new PropertyMetadata(50.0)); 
 
    public double Value 
    { 
      get { return (double)GetValue(ValueProperty); } 
      set { SetValue(ValueProperty, value); } 
    } 
 
    public static readonly DependencyProperty ColorProperty =  
      DependencyProperty.Register(nameof(Color), typeof(SphereColor),  
      typeof(Sphere), new PropertyMetadata(SphereColor.Green,  
      OnColorChanged)); 
 
    public SphereColor Color 
    { 
      get { return (SphereColor)GetValue(ColorProperty); } 
      set { SetValue(ColorProperty, value); } 
    } 
 
    private static void OnColorChanged(DependencyObject  
      dependencyObject, DependencyPropertyChangedEventArgs e) 
    { 
      ((Sphere)dependencyObject).SetEllipseColors(); 
    } 
 
    public override void OnApplyTemplate() 
    { 
      SetEllipseColors(); 
    } 
 
    private void SetEllipseColors() 
    { 
      Ellipse backgroundEllipse =  
        GetTemplateChild("PART_Background") as Ellipse; 
      Ellipse glowEllipse = GetTemplateChild("PART_Glow") as Ellipse;
      if (backgroundEllipse != null) backgroundEllipse.Fill = 
        Color == SphereColor.Green ? greenBackground : redBackground; 
      if (glowEllipse != null) glowEllipse.Fill =  
        Color == SphereColor.Green ? greenGlow : redGlow;
    } 
  } 
} 

As this class will declare its own Color property, we start by adding a MediaColor using alias directive, which we'll just use as a shortcut to accessing the methods of the System.Windows.Media.Color class, when declaring the brushes that will be used in the Sphere class. 

From the class declaration, we can see that there are two named parts specified in TemplatePartAttribute attributes. These specify that the two mentioned Ellipse elements are required in our control's template in the Generic.xaml file. Inside the class, we define a number of RadialGradientBrush resources to paint our spheres with.

In the static constructor, we call the OverrideMetadata method to let the Framework know where our control's default style is. We then see the declaration of the Value and Color Dependency Properties, with the Color property's related PropertyChangedCallback hander method.

In this OnColorChanged method, we cast the dependencyObject input parameter to an instance of our Sphere class and call its SetEllipseColors method. In that method, we use the FrameworkElement.GetTemplateChild method to access the two main Ellipse objects from our ControlTemplate element.

Remember that we must always check these objects for null, as our ControlTemplate could have been replaced with one that does not contain these ellipse elements. If they are not null, we set their Fill properties to one of our brush resources using the ternary operator and depending upon the value of our Color property.

One alternative for creating this functionality would be to declare a Dependency Property of type Brush to data bind to each ellipse's Fill property and to set the relevant brush resources to these properties, instead of accessing the XAML elements directly. Before viewing the control's default style, let's see the SphereColor enumeration that is used by the Color property:

namespace CompanyName.ApplicationName.CustomControls.Enums 
{ 
  public enum SphereColor 
  { 
    Green, Red 
  } 
} 

As you can see, this is a simple affair and could be easily extended. Note that this enumeration has been declared within the CustomControls namespace and project, so that the project is self-contained and can be reused in other applications without any external dependencies. Let's take a look at our control's default style from Generic.xaml now:

<Style TargetType="{x:Type CustomControls:Sphere}"> 
  <Setter Property="Template"> 
    <Setter.Value> 
      <ControlTemplate TargetType="{x:Type CustomControls:Sphere}"> 
        <ControlTemplate.Resources> 
          <DropShadowEffect x:Key="Shadow" BlurRadius="10" 
            Direction="270" ShadowDepth="7" Opacity="0.5" /> 
          <LinearGradientBrush x:Key="Reflection" 
            StartPoint="0,0" EndPoint="0,1"> 
            <GradientStop Color="#90FFFFFF" Offset="0.009" />
            <GradientStop Color="#2DFFFFFF" Offset="0.506" /> 
            <GradientStop Offset="0.991" /> 
          </LinearGradientBrush> 
        </ControlTemplate.Resources> 
        <Grid Height="{Binding Value,  
          RelativeSource={RelativeSource TemplatedParent}}"
          Width="{Binding Value,
          RelativeSource={RelativeSource TemplatedParent}}">
          <Grid.RowDefinitions> 
            <RowDefinition Height="5*" /> 
            <RowDefinition Height="2*" /> 
          </Grid.RowDefinitions> 
          <Grid.ColumnDefinitions> 
            <ColumnDefinition Width="*" /> 
            <ColumnDefinition Width="8*" /> 
            <ColumnDefinition Width="*" /> 
          </Grid.ColumnDefinitions> 
          <Ellipse Name="PART_Background" Grid.RowSpan="2"  
            Grid.ColumnSpan="3" Stroke="#FF1B0000"  
            Effect="{StaticResource Shadow}" /> 
          <Ellipse Name="PART_Glow" Grid.RowSpan="2"
            Grid.ColumnSpan="3" /> 
          <Ellipse Grid.Column="1" Margin="0,2,0,0" 
            Fill="{StaticResource Reflection}" /> 
        </Grid> 
      </ControlTemplate> 
    </Setter.Value> 
  </Setter> 
</Style> 

When looking at our control's default template, we can see some of resources defined in the ControlTemplate.Resources section. We first declare a DropShadowEffect element, similar to our previous uses of this class. Next, we define a vertical LinearGradientBrush element, to use as a light reflection layer, in a similar way to our earlier example.

Previously, we saw that the default value of the GradientStop.Offset property is zero and so, we can omit the setting of this property if that is the value that we need to use. In this brush resource, we see that the last GradientStop element has no Color value specified. This is because its default value of this property is Transparent and that is the value that we need to use here.

In the actual markup for our control, we declare three Ellipse objects within a Grid panel. Two of these elements are named and referenced in the control's code, while the third ellipse uses the brush from resources to create the "shine" on top of the other ellipses. The panel's size properties are data bound to the Value Dependency Property, using a TemplatedParent source.

Note that we have used the star-sizing capabilities of the Grid panel to both position and size our ellipse elements, with the exception of the two pixels in the top margin specified on the reflection ellipse. In this way, our control can be any size and the positioning of the various layers will remain visually correct. Note that we could not achieve this by hard coding exact margin values for each element.

Let's see how we could use this in a simple View:

<Grid TextElement.FontSize="28" TextElement.FontWeight="Bold" Margin="20"> 
  <Grid.ColumnDefinitions> 
    <ColumnDefinition /> 
    <ColumnDefinition /> 
  </Grid.ColumnDefinitions> 
  <Grid.RowDefinitions> 
    <RowDefinition /> 
    <RowDefinition Height="Auto" /> 
  </Grid.RowDefinitions> 
  <CustomControls:Sphere Color="Red" Value="{Binding InCount}"  
    VerticalAlignment="Bottom" /> 
  <CustomControls:Sphere Grid.Column="1" Value="{Binding OutCount}"  
    VerticalAlignment="Bottom" /> 
  <TextBlock Grid.Row="1" Text="{Binding InCount}"  
    HorizontalAlignment="Center" Margin="0,10,0,0" /> 
  <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding OutCount}"  
    HorizontalAlignment="Center" Margin="0,10,0,0" /> 
</Grid> 

This is how our example looks when rendered:

As you can see, WPF is very powerful and enables us to create completely original looking controls. However, we can also use it to recreate more commonly seen controls. As an example, let's see how we can create an alternative control to gauge how close we may be to our particular target value:

This example features a semi-circular arc, which is something that does not exist in a form that is usable from XAML, so we'll first create an Arc control to use internally within our Gauge control. Let's see how we can achieve this by adding a new custom control:

using System; 
using System.Windows; 
using System.Windows.Media; 
using System.Windows.Shapes; 
 
namespace CompanyName.ApplicationName.CustomControls 
{ 
  public class Arc : Shape 
  { 
    public static readonly DependencyProperty StartAngleProperty =
      DependencyProperty.Register(nameof(StartAngle), typeof(double),  
      typeof(Arc), new FrameworkPropertyMetadata(180.0, 
FrameworkPropertyMetadataOptions.AffectsRender)); public double StartAngle { get { return (double)GetValue(StartAngleProperty); } set { SetValue(StartAngleProperty, value); } } public static readonly DependencyProperty EndAngleProperty = DependencyProperty.Register(nameof(EndAngle), typeof(double), typeof(Arc), new FrameworkPropertyMetadata(0.0,
FrameworkPropertyMetadataOptions.AffectsRender)); public double EndAngle { get { return (double)GetValue(EndAngleProperty); } set { SetValue(EndAngleProperty, value); } } protected override Geometry DefiningGeometry { get { return GetArcGeometry(); } } private Geometry GetArcGeometry() { Point startPoint = ConvertToPoint(Math.Min(StartAngle, EndAngle)); Point endPoint = ConvertToPoint(Math.Max(StartAngle, EndAngle)); Size arcSize = new Size(Math.Max(0, (RenderSize.Width - StrokeThickness) / 2), Math.Max(0, (RenderSize.Height - StrokeThickness) / 2)); bool isLargeArc = Math.Abs(EndAngle - StartAngle) > 180; StreamGeometry streamGeometry = new StreamGeometry(); using (StreamGeometryContext context = streamGeometry.Open()) { context.BeginFigure(startPoint, false, false); context.ArcTo(endPoint, arcSize, 0, isLargeArc, SweepDirection.Counterclockwise, true, false); } streamGeometry.Transform = new TranslateTransform(StrokeThickness / 2, StrokeThickness / 2); streamGeometry.Freeze(); return streamGeometry; } private Point ConvertToPoint(double angleInDegrees) { double angleInRadians = angleInDegrees * Math.PI / 180; double radiusX = (RenderSize.Width - StrokeThickness) / 2; double radiusY = (RenderSize.Height - StrokeThickness) / 2; return new Point(radiusX * Math.Cos(angleInRadians) + radiusX, radiusY * Math.Sin(-angleInRadians) + radiusY); } } }

Note that we extend the Shape class when creating our Arc class. We do this because it provides us with a wide variety of stroke and fill properties and also the apparatus to render our custom shape from a Geometry object. Additionally, users of our Arc control will also be able to take advantage of the Shape class' transformation abilities through its Stretch and GeometryTransform properties.

To draw our arc, we will use the ArcTo method of the StreamGeometryContext class and with it, we need to specify exact Point values for its start and end. However, in order to reflect the correct value in the size of our arc, it is easier to define it using angle values for its start and end.

Therefore, we add StartAngle and EndAngle Dependency Properties to our Arc class. Note that these two properties are declared with the FrameworkPropertyMetadataOptions.AffectsRender member. This notifies the Framework that changes to these properties need to cause a new rendering pass, so new values will be accurately represented in the control.

After these property declarations, we see the overridden DefiningGeometry property, that enables us to return a Geometry object that defines the shape to be rendered. We simply return the result from the GetArcGeometry method from this property.

In the GetArcGeometry method, we obtain the required start and end Point elements from the ConvertToPoint method, passing in the StartAngle and EndAngle property values. Note that we use the Min and Max methods of the Math class here to ensure that the start point is calculated from the smaller angle and the end point is calculated from the larger angle.

Our arc shape's fill will actually come from the geometric arc's stroke, so we will not be able to add a stroke to it. In WPF, the stroke of a shape with a thickness of one pixel will extend no further than the shape's bounding box. However, at the furthest point, strokes with larger thickness values are rendered so that their center remains on the line of the bounding box also therefore, half of it will extend outside the bounds of the element and half will be rendered within the bounds:

Therefore, we calculate the size of the arc by dividing the RenderSize value minus the StrokeThickness value by two. This will reduce the size of the arc so that it remains totally within the bounds of our control. We make use of the Math.Max method to ensure that the values that we pass to the Size class are never less than zero and avoid exceptions.

When using the ArcTo method, we need to specify a value that determines whether we want to connect our start and end points with a short arc or a long one. Our isLargeArc variable therefore determines whether the two specified angles would produce an arc of more than one hundred and eighty degrees or not.

Next, we create a StreamGeometry object and retrieve a StreamGeometryContext object from its Open method, with which to define our geometric shape. Note that we could equally use a PathGeometry object here, but as we do not need its data binding, animation, or other abilities, we use the more efficient StreamGeometry object instead.

We enter the arc's start point in the BeginFigure method and the remaining parameters in the ArcTo method. Note that we call these methods on our StreamGeometryContext object from within a using statement to ensure that it is closed and disposed of properly, once we are finished with it.

Next, we apply a TranslateTransform element to the Transform property of the StreamGeometry object in order to shift the arc so that it is fully contained within our control. Without this step, our arc would stick out of the bounding box of our control to the upper left, by the amount of half of the StrokeThickness property value.

Once we have finished manipulating our StreamGeometry object, we call its Freeze method, which makes it unmodifiable and rewards us with additional performance benefits. We'll find out more about this in Chapter 11, Improving Application Performance, but for now, let's continue looking through this example.

Finally, we get to the ConvertToPoint method, which converts the values of our two angle Dependency Properties into two-dimensional Point objects. Our first job is to convert each angle from degrees into radians, as the methods of the Math class that we need to use require radian values.

Next, we calculate the two radii of our arc using half of the RenderSize value minus the StrokeThickness property value, so that the size of the arc does not exceed the bounding box of our Arc control. Finally, we perform some basic trigonometry using the Math.Cos and Math.Sin methods when calculating the Point element to return.

That completes our simple Arc control and so now, we can utilize this new class in our Gauge control. We'll need to create another new custom control for it, so let's first see the properties and code in our new Gauge class:

using System.Windows; 
using System.Windows.Controls; 
 
namespace CompanyName.ApplicationName.CustomControls 
{ 
  public class Gauge : Control 
  { 
    static Gauge() 
    { 
      DefaultStyleKeyProperty.OverrideMetadata (typeof(Gauge),  
        new FrameworkPropertyMetadata(typeof(Gauge))); 
    } 
 
    public static readonly DependencyPropertyKey valueAnglePropertyKey = 
      DependencyProperty.RegisterReadOnly(nameof(ValueAngle),  
      typeof(double), typeof(Gauge), new PropertyMetadata(180.0)); 
 
    public static readonly DependencyProperty ValueAngleProperty =  
      valueAnglePropertyKey.DependencyProperty; 
 
    public double ValueAngle 
    { 
      get { return (double)GetValue(ValueAngleProperty); } 
      private set { SetValue(valueAnglePropertyKey, value); } 
    } 
 
    public static readonly DependencyPropertyKey  
      rotationAnglePropertyKey = DependencyProperty.RegisterReadOnly( 
      nameof(RotationAngle), typeof(double), typeof(Gauge),
      new PropertyMetadata(180.0)); 
 
    public static readonly DependencyProperty RotationAngleProperty =  
      rotationAnglePropertyKey.DependencyProperty; 
 
    public double RotationAngle 
    { 
      get { return (double)GetValue(RotationAngleProperty); } 
      private set { SetValue(rotationAnglePropertyKey, value); } 
    } 
 
    public static readonly DependencyProperty ValueProperty =  
      DependencyProperty.Register(nameof(Value), typeof(double),  
      typeof(Gauge), new PropertyMetadata(0.0, OnValueChanged)); 
 
    private static void OnValueChanged(DependencyObject  
      dependencyObject, DependencyPropertyChangedEventArgs e) 
    { 
      Gauge gauge = (Gauge)dependencyObject; 
      if (gauge.MaximumValue == 0.0)  
        gauge.ValueAngle = gauge.RotationAngle = 180.0; 
      else if ((double)e.NewValue > gauge.MaximumValue) 
      { 
        gauge.ValueAngle = 0.0; 
        gauge.RotationAngle = 360.0; 
      } 
      else 
      { 
        double scaledPercentageValue =  
          ((double)e.NewValue / gauge.MaximumValue) * 180.0; 
        gauge.ValueAngle = 180.0 - scaledPercentageValue; 
        gauge.RotationAngle = 180.0 + scaledPercentageValue; 
      } 
    } 
 
    public double Value 
    { 
      get { return (double)GetValue(ValueProperty); } 
      set { SetValue(ValueProperty, value); } 
    } 
 
    public static readonly DependencyProperty MaximumValueProperty =  
      DependencyProperty.Register(nameof(MaximumValue), typeof(double),  
      typeof(Gauge), new PropertyMetadata(0.0)); 
 
    public double MaximumValue 
    { 
      get { return (double)GetValue(MaximumValueProperty); } 
      set { SetValue(MaximumValueProperty, value); } 
    } 
 
    public static readonly DependencyProperty TitleProperty =  
      DependencyProperty.Register(nameof(Title), typeof(string),  
      typeof(Gauge), new PropertyMetadata(string.Empty)); 
 
    public string Title 
    { 
      get { return (string)GetValue(TitleProperty); } 
      set { SetValue(TitleProperty, value); } 
    } 
  } 
} 

As usual, we start by overriding the metadata of the DefaultStyleKeyProperty for our control type in the static constructor, to help the Framework find where its default style is defined. We then declare the internal, read-only ValueAngle and RotationAngle Dependency Properties and the regular public Value, MaximumValue, and Title Dependency Properties.

We declare a PropertyChangedCallback hander for the Value property, and, in that method, we first cast the dependencyObject input parameter to an instance of our Gauge class. If the value of the MaximumValue property is zero, then we simply set both of the ValueAngle and RotationAngle properties to 180.0, which results in the arc and needle being displayed in their start positions, on the left.

If the new value of the data bound Value property is more than the value of the MaximumValue property, then we make the arc and needle display in their end, or full, positions to the right. We do this by setting the ValueAngle property to 0.0 and the RotationAngle property to 360.0.

If the new value of the Value property is valid, then we calculate the scaledPercentageValue variable. We do this by first dividing the new value by the value of the MaximumValue property, to get the percentage of the maximum value. We then multiply that figure by 180.0, because our gauge covers a range of one hundred and eighty degrees.

We then subtract the scaledPercentageValue variable value from 180.0 for the ValueAngle property and add it to 180.0 for the RotationAngle property. This is because the ValueAngle property is used by our arc and needs to be between 180.0 and 0.0, and the RotationAngle property is used by our gauge needle and needs to be between 180.0 and 360.0.

This will soon be made clearer, so let's now see how we use these properties and the Arc control in our Gauge control's default style from the Generic.xaml file:

<Style TargetType="{x:Type CustomControls:Gauge}"> 
  <Setter Property="Template"> 
    <Setter.Value> 
      <ControlTemplate TargetType="{x:Type CustomControls:Gauge}"> 
        <Grid Background="{Binding Background,  
          RelativeSource={RelativeSource TemplatedParent}}"> 
          <Grid Margin="{Binding Padding,  
            RelativeSource={RelativeSource TemplatedParent}}"> 
            <Grid.RowDefinitions> 
              <RowDefinition Height="Auto" /> 
              <RowDefinition /> 
              <RowDefinition Height="Auto" /> 
            </Grid.RowDefinitions> 
            <TextBlock Text="{Binding Title,  
              RelativeSource={RelativeSource TemplatedParent}}"  
              HorizontalAlignment="Center" /> 
            <Canvas Grid.Row="1" Width="300" Height="150"
              HorizontalAlignment="Center" Margin="0,5">
              <CustomControls:Arc Width="300" Height="300"
                StrokeThickness="75"  Stroke="#FF444444" />
              <CustomControls:Arc Width="300" Height="300"
                StrokeThickness="75" Stroke="OrangeRed" StartAngle="180"
                EndAngle="{Binding AngleValue, 
                RelativeSource={RelativeSource TemplatedParent}}" /> 
              <Path Canvas.Left="150" Canvas.Top="140"  
                Fill="White" StrokeThickness="5" Stroke="White" 
                StrokeLineJoin="Round" Data="M0,0 L125,10, 0,20Z"
                Stretch="Fill" Width="125" Height="20"> 
                <Path.RenderTransform> 
                  <RotateTransform Angle="{Binding RotationAngle,
                    RelativeSource={RelativeSource TemplatedParent}}"
                    CenterX="0" CenterY="10" /> 
                </Path.RenderTransform> 
              </Path> 
            </Canvas> 
            <TextBlock Grid.Row="2" Text="{Binding Value, StringFormat=N0,
              RelativeSource={RelativeSource TemplatedParent}}"
              HorizontalAlignment="Center" FontWeight="Bold" /> 
          </Grid> 
        </Grid> 
      </ControlTemplate> 
    </Setter.Value> 
  </Setter> 
</Style> 

We start our default style as usual, by specifying the type of our control in both the style and the control template. Inside the template, we have two Grid panels and data bind the Background property of the outer panel and the Margin property of the inner panel to properties of our templated control, so that users can set them externally.

We then define three rows in our inner panel. The control's Title property is data bound to a horizontally centered TextBlock element in the first row. In the second row, we declare a horizontally centered Canvas panel that contains two of our new Arc controls and a Path object.

The first Arc control is gray and represents the background track that the Arc that represents our Gauge control's Value property sits on. The second Arc control is colored OrangeRed and displays the current value of our Gauge control's Value property, by data binding its EndAngle property to the AngleValue Dependency Property of the Gauge control.

Note that the angles in our Arc control follow the common Cartesian coordinate system, with an angle of zero degrees falling to the right and increasing values moving anti-clockwise. Therefore, to draw a semi-circular arc from left to right, we start with an angle of 180 degrees and end at 0 degrees, as demonstrated by the background arc in our Gauge control.

Furthermore, our Arc controls have the same width and height values, but as we don't need their lower halves, we crop them using the height of the canvas panel. The Path object represents the gauge needle in our control and is painted white.

We set the StrokeLineJoin property to the Round value in order to curve the three corners, where the lines of the needle path meet. Note that the needle is positioned exactly half way across the width of the canvas and ten pixels above the bottom, to enable its center line to lie along the bottom of the canvas.

Rather than declaring PathFigure and LineSegment objects to define the needle, we have used the shorthand notation inline in the Data property. The M specifies that we should move to (or start from) point 0,0, the L specifies that we want to draw a line to point 125,10 and then from there to point 0,20, and the Z means that we want to close the path by joining the first and last points.

We then set the width and height of the path to the same values that were declared within Data property. Now, the essential part of enabling this needle to point to the relevant position to reflect the data bound Value property, is the RotateTransform object that is applied to the path's RenderTransform property. Note that its center point is set to be the center of the bottom of the needle, as that is the point that we want to rotate from.

As the RotateTransform object rotates clockwise with increasing Angle values, we cannot reuse the AngleValue Dependency Property with it. Therefore, in this particular example, we define the needle pointing to the right and use a range of 180.0 to 360.0 degrees in the RotationAngle read-only Dependency Property with the transform object to match the position of the value arc.

At the end of the example, we see another horizontally centered TextBlock, element that outputs the current, unaltered value of the data bound Value Dependency Property. Note that we use the StringFormat value of N0 to remove the decimal places from the value before displaying it.

That completes our new Gauge control and so, all we need to do now is see how we can use it:

<CustomControls:Gauge Width="400" Height="300"  
  MaximumValue="{Binding InCount}" Value="{Binding OutCount}"  
  Title="Support Tickets Cleared" Foreground="White" FontSize="34" 
  Padding="10" /> 

We could extend our new Gauge control to make it more usable in several ways. We could add a MinimumValue Dependency Property to enable its use with value ranges that do not start at zero, or we could expose further properties to enable users to color, size, or further customize the control. Alternatively, we could rewrite it to enable it to be any size, instead of hard coding sizes as we did previously.