Building a custom UWP control with XAML and the Composition API

In this article we’ll build a custom UWP control that will be drawn partly by the XAML engine and partly by the Composition API. We’ll start from scratch … well almost. We’ll build yet another new version of the Modern Radial Gauge. Here’s how it will look like. The ‘old’ full-XAML gauge is on the left, the new version is on the right:

CompositionGauge_Comparison

The design of this radial gauge is timeless, thanks to Arturo Toledo. Unfortunately its implementations are not as timeless. The ‘old’ gauge goes back to early versions of Windows 8 and the continuous upgrades to more recent platforms have stretched its limits. Literally. Over the last couple of years, device screens have improved and the XAML engine was adapted to run on more and more platforms. We noticed that on some screens, the rotations and translations of the XAML elements are starting to suffer from rounding errors. As a result the radial gauge does not look crisp anymore in every size and resolution. The screenshot proves why it’s time for a new version, and why we could use some help from the Composition API:

CompositionGauge_Errors

Anatomy of a Radial Gauge

A gauge is an indicator that displays a value within a range – so it needs to have things like Minimum, Maximum, Value, and probably a Unit. A radial gauge is a gauge that looks like an analog clock – so it comes with things like a Scale, Ticks, and a Needle.

Here’s an overview of the important UI parts of the Modern Radial Gauge:

CompositionGauge_Anatomy

Here’s the list of properties that are defined in the actual control:

  • Minimum: minimum value on the scale (double, default 0)
  • Maximum: maximum value on the scale (double, default 100)
  • Value: the value to represent (double, default 0)
  • ValueStringFormat: StringFormat to apply to the displayed value (string)
  • Unit: unit measure to display (string)
  • TickSpacing: spacing -in value units- between ticks (int)
  • NeedleBrush: color of the needle (SolidColorBrush)
  • TickBrush: color of the outer ticks (SolidColorBrush)
  • ScaleWidth: thickness of the scale in pixels – relative to the control’s default size (double, default 26)
  • ScaleBrush: background color of the scale (Brush)
  • ScaleTickBrush: color of the ticks on the scale (SolidColorBrush)
  • TrailBrush: color of the trail following the needle (Brush)
  • ValueBrush: color of the value text (Brush)
  • UnitBrush: color of the unit measure text (Brush)

Using the gauge on a page should be as easy as the following one-liner:

<controls:RadialGauge Value="{Binding Temperature}" Unit="°C" />

But the user of the control should be able to fully customize the looks – like this:

<controls:RadialGauge Value="85"
                        Unit="bottles of beer on the wall"
                        TickBrush="Transparent"
                        ScaleTickBrush="Transparent"
                        NeedleBrush="{StaticResource DarkBrown}"
                        TrailBrush="{StaticResource DarkBrown}"
                        UnitBrush="{StaticResource VeryDarkBrown}"
                        ValueBrush="{StaticResource DarkBrown}"
                        Margin="4">
    <controls:RadialGauge.ScaleBrush>
        <SolidColorBrush Color="#FFFFD9AA"
                            Opacity=".6" />
    </controls:RadialGauge.ScaleBrush>
</controls:RadialGauge>

On top of that, the control is templatable: a developer can hook the gauge to a custom template (provided or not by a designer) as long as some requirements are met.

Project Structure

In Visual Studio 2015, we created a class library and added a new item of the type ‘templated control’. The RadialGauge class inherits from Control, and comes with a Style definition the Themes/Generic.xaml file. Here’s how that looks like in the IDE:

CompositionGauge_VS

Control template

The generic.xaml file is a XAML ResourceDictionary that contains the default style with the ControlTemplate for the custom control – it can be overridden. We defined the radial gauge control as a 200 by 200 pixels grid, inside a ViewBox. All internal calculations are conveniently done against that 200×200 grid, but the ViewBox will stretch the gauge to any size:

<Style TargetType="local:RadialGauge">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:RadialGauge">
                <Viewbox>
                    <Grid x:Name="PART_Container"
                            Height="200"
                            Width="200">

Here’s how to decide what elements to put in the style. For the elements that are drawn by the Composition API, it’s simple: there’s no markup available for them (yet?), so they will always be drawn in code-behind. Any rectangle that is filled with a solid color or an image is a candidate for rendering through the Composition API and hence will not appear in the control template. For the radial gauge these are the ticks inside and outside the scale, and the needle.

If you’re not into DirectX then the remaining parts will be drawn in XAML. For these elements you still have the choice. You can declare them in the style, or program them in code-behind. For the radial gauge, the XAML parts are [the border of] two ArcSegment instances and the text blocks that display the value and the unit.

Here’s a visual overview of the radial gauge parts by technology:

CompositionGauge_Parts 

We can not define the arc segment fully in XAML: the width of the background scale is configurable, and the end point of the trail depends on the current value. So both arc segments need to be drawn programmatically. The corresponding Path elements are declared in the XAML control template, but only with their color properties set – no path or dimensions. The Stroke and StrokeThickness are bound through a TemplateBinding to properties that are defined in the class itself.

Here’s the XAML load of the control template:

<!-- Scale -->
<Path Name="PART_Scale"
        Stroke="{TemplateBinding ScaleBrush}"
        StrokeThickness="{TemplateBinding ScaleWidth}" />

<!-- Trail -->
<Path Name="PART_Trail"
        Stroke="{TemplateBinding TrailBrush}"
        StrokeThickness="{TemplateBinding ScaleWidth}" />

<!-- Value and Unit -->
<StackPanel VerticalAlignment="Bottom"
            HorizontalAlignment="Center">
    <TextBlock Name="PART_ValueText"
                Foreground="{TemplateBinding ValueBrush}"
                FontSize="20"
                FontWeight="SemiBold"
                Text="{TemplateBinding Value}"
                TextAlignment="Center"
                Margin="0 0 0 2" />
    <TextBlock Foreground="{TemplateBinding UnitBrush}"
                FontSize="16"
                TextAlignment="Center"
                Text="{TemplateBinding Unit}"
                Margin="0" />
</StackPanel>

Observe that all named elements follow the PART_Xxx naming convention which is typical for templated controls.

Class Definition

The C# class definition is decorated with TemplatePart attributes for those same named elements. Again, this is just a convention. It indicates to the template (re-)designer that the code will break when these parts are not provided by the template:

[TemplatePart(Name = ScalePartName, Type = typeof(Path))]
[TemplatePart(Name = TrailPartName, Type = typeof(Path))]
[TemplatePart(Name = ValueTextPartName, Type = typeof(TextBlock))]
public class RadialGauge : Control
{

Dependency Properties

The class definition also contains the properties that are exposed by the gauge: the Minimum, Maximum and current Value, and a set of colors. These are implemented as Dependency Properties, which makes them automagically available for data binding, animation, and change notification. A dependency property registration declares a name, a type, and optionally a default value and a change event handler.

Here’s part of the Value property definition:

public static readonly DependencyProperty ValueProperty = 
DependencyProperty.Register(
    "Value",
    typeof(double),
    typeof(RadialGauge),
    new PropertyMetadata(0.0, OnValueChanged));

public double Value
{
    get { return (double)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

private static void OnValueChanged(DependencyObject d, 
DependencyPropertyChangedEventArgs e)
{
    // Redraw trail, rotate needle, and update value text.
    // ...
}

The rest of the properties have similar declarations. They’re all generated by the same built-in code snippet: ‘propdp’.

Initial look

At runtime the OnApplyTemplate override is the first place where we have programmatic access to the fully templated control. All XAML elements are available there. If necessary, we can now refine the initial look. We will draw the scale and the ticks, and also the needle in its initial position. For each part, we call GetTemplateChild() to get a reference to the element in the template. It’s good practice to check if the part actually exists. After all, as a custom control developer we only provide a default template. We’re never sure of the actual template that will be applied.

Here’s the code for drawing the scale – we’ll spare you the details of the ArcSegment drawing routine:

var scale = this.GetTemplateChild(ScalePartName) as Path;
if (scale != null)
{
    var pg = new PathGeometry();
    var pf = new PathFigure();
    // ArcSegment details omitted.
    // ...
    pf.Segments.Add(seg);
    pg.Figures.Add(pf);
    scale.Data = pg;
}

So far for the XAML part, let’s move to the Composition API. In that same OnApplyTemplate() method we draw the ticks outside and inside the scale, and the needle. For an introduction to drawing with the Composition API, please check this article. The code presented there draws an analog clock with ticks and hands, so it is extremely similar to the radial gauge. With the Composition API you’ll always need

Here’s the code that draws the needle. The ticks and scale ticks are drawn the same way – they’re all just colored rectangles:

var container = this.GetTemplateChild(ContainerPartName) as Grid;
_root = container.GetVisual();
_compositor = _root.Compositor;

_needle = _compositor.CreateSpriteVisual();
_needle.Size = new Vector2(NeedleWidth, NeedleHeight);
_needle.Brush = _compositor.CreateColorBrush(NeedleBrush.Color);
_needle.CenterPoint = new Vector3(NeedleWidth / 2, NeedleHeight, 0);
_needle.Offset = new Vector3(100 - NeedleWidth / 2, 100 - NeedleHeight, 0);
_root.Children.InsertAtTop(_needle);

At the end of OnApplyTemplate() the gauge is properly initialized and ready to shine.

Changing the value

When the Value property changes, it fires the event handler that was registered in the dependency property definition. That handler is a static method –as imposed by the dependency property infrastructure- so you can’t use ‘this’  in it. The reference to the gauge comes in as a parameter of the type DependencyObject, so you have to cast it. I don’t remember why I called the corresponding variable ‘c’  in the following code, but it’s actually a common practice to call it ‘_this’, or ‘that’.

Here’s the code to update needle’s rotation, draw the trail, and update the text to its new value. The pattern is always the same: get a reference to the UI element from the control template, check if it’s really there, and then update it:

private static void OnValueChanged(DependencyObject d)
{
    RadialGauge c = (RadialGauge)d;
    if (!Double.IsNaN(c.Value))
    {
        var middleOfScale = 100 - ScalePadding - c.ScaleWidth / 2;
        c.ValueAngle = c.ValueToAngle(c.Value);

        // Needle
        if (c._needle != null)
        {
            c._needle.RotationAngleInDegrees = (float)c.ValueAngle;
        }

        // Trail
        var trail = c.GetTemplateChild(TrailPartName) as Path;
        if (trail != null)
        {
            if (c.ValueAngle > MinAngle)
            {
                trail.Visibility = Visibility.Visible;
                var pg = new PathGeometry();
                var pf = new PathFigure();
                // ArcSegment drawing omitted.
                // ...
                pf.Segments.Add(seg);
                pg.Figures.Add(pf);
                trail.Data = pg;
            }
            else
            {
                trail.Visibility = Visibility.Collapsed;
            }
        }

        // Value Text
        var valueText = c.GetTemplateChild(ValueTextPartName) as TextBlock;
        if (valueText != null)
        {
            valueText.Text = c.Value.ToString(c.ValueStringFormat);
        }
    }
}

In a production version of this gauge, it would make sense to also listen to changes in other properties. If you want to use this gauge to follow up the daily evolution of a stock quote, then the Minimum and Maximum values will change at runtime. This version assumes that all properties except Value are assigned in the XAML, and do not change after that.

Cleaning up the code

In the code snippets, we omitted the beef of the calculations. I admit that in earlier versions these calculations were hard to understand, and hence hard to maintain. In this version of the gauge, we took the time to finally factor out all relevant parameters, and defined them as constants with a decent name: 

private const double MinAngle = -150.0;
private const double MaxAngle = 150.0;
private const float TickHeight = 18.0f;
private const float TickWidth = 5.0f;
private const float ScalePadding = 23.0f;
private const float ScaleTickWidth = 2.5f;
private const float NeedleWidth = 5.0f;
private const float NeedleHeight = 100.0f;

Not only does this make the calculations easier to understand and more maintainable, it also reveals good candidate dependency properties for future versions of the control. I definitely want to experiment with a scale from –180 to +90 degrees, like the ones in this car:

A5_gauges

[For the record: this is *not* my car – my speedometer stops at 280 and I have more fuel]

Testing the looks

The SquareOfSquares control is an ideal container to check the looks of the radial gauge with different settings for size, colors, and scale width. Here’s how the gauge looks like in a variety of partially random configurations:

CompositionGauge_Square

Of course it also makes sense to create a gallery with some representative looks, like this:

CompositionGauge_Gallery

You may consider the SquareOfSquares and the Gallery as a kind of ‘visual unit tests’.

Keep it Universal

Since the control is meant to be Universal, we must test it on all relevant device families. Here’s how the pages of the sample app look like on the phone:

CompositionGauge_Comparison_Phone CompositionGauge_Parts_Phone CompositionGauge_Square_Phone CompositionGauge_Gallery_Phone

Source Code

The upgraded version of the Modern Radial Gauge looks crisp again in any size and resolution. Its Visual Tree is a lot smaller, and the needle rotation is not done by the XAML engine anymore. That should result in better performance. That may be important e.g. in a dashboard with a huge amount of gauge instances. Last but not least, we identified some useful enhancements for future versions.

All source code is available. The sample app and all related controls live here on GitHub.

Enjoy!

Advertisements

6 thoughts on “Building a custom UWP control with XAML and the Composition API

  1. michelleof7

    Fantastic! Your intent might not have been to explain template controls and dependency properties, but your clear and concise explanations serve to help solidy those as well as your primary concepts. I read and similarly appreciated your ‘clock’ posting as well. [Please] Keep up the good work!

    Liked by 1 person

    Reply
  2. michelleof7

    I read your article “Using SQLite on the Universal Windows Platform” and downloaded/ran your code (UWP-SQLite-Sample). Great article and sample!!

    QUESTION: When I run your sample app, I notice that when it calls ‘Insert’ (or ‘InsertOrReplace’), the result code is always ‘1’, (Example: after the Dal.cs line 59 “var i = db.InsertOrReplace(person);” executes, i is set to ‘1’).
    According to Sqlite doc, ‘1’ is an error (SQLITE_ERROR). However, the insert operation ‘appears’ to complete successfully (per inspection of the database tables, etc.).
    I then isolated the functionality into as simple an app as possible, and got the same results (result code ‘1’ even though the operation appeared to work fine).

    Do you see this same behavior? Any advice?
    Thanks!

    Like

    Reply
    1. xamlbrewer Post author

      Unfortunately the underlying SQLite.Net-PCL library is not well documented.

      It is common for insert and delete methods to return the number of records affected, so the “1” may mean “I successfully inserted 1 row”.

      Like

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s