Creating Bindable Path Controls in UWP

In this article we’ll show some ways to create a bindable arc segment for UWP. We’ve been using elliptical arc elements relatively often. In most cases these where parts inside of user controls such as the percentage ring, the radial range indicator, and the radial gauge. We noticed that every time we used elliptical arcs, the hosting control’s code rapidly became cluttered with path calculation and drawing routines, making its core functionality unnecessarily hard to debug and extend. So we started looking for ways to abstract the calculation and drawing routines out of the user controls and into some reusable pattern.

Here’s a screenshot of the sample app that was created during the process:
CircleSegment

We targeted to implement the full Elliptical Arc SVG definition, including start and end point, radii, rotation angle, sweep direction, and a large arc indicator. Not only does this provide a mathematical context, it could also allow us to achieve compatibility with the SVG path syntax itself (e.g. for serialization). [Spoiler: We did not implement this in the sample app, because such a parser already exists in the CompositionProToolkit.]

For starters, here’s how a non-bindable elliptical arc path looks like in XAML (straight from the excellent documentation):

<Path Stroke="Black" StrokeThickness="1"  
  Data="M 10,100 A 100,50 45 1 0 200,100" />

XAML solution

The ‘classic’ way to draw an elliptical arc in UWP goes back to WPF, and is also implemented in the user controls that we just mentioned: in your XAML, place an ArcSegment as part of PathFigure in a Path control. The Path provides the Stroke properties (color, thickness, pattern), the PathFigure provides the start position, and the Arcsegment provides the rest.

Here’s how the XAML for a fixed arc segment looks like. It’s the exact same arc as the one in the previous code snippet, but without using the SVG mini-language:

<Path Stroke="Black" StrokeThickness="1">
  <Path.Data>
    <PathGeometry>
      <PathGeometry.Figures>
        <PathFigureCollection>
          <PathFigure StartPoint="10,100">
            <PathFigure.Segments>
              <PathSegmentCollection>
                <ArcSegment Size="100,50" RotationAngle="45" IsLargeArc="True" SweepDirection="CounterClockwise" Point="200,100" />
              </PathSegmentCollection>
            </PathFigure.Segments>
          </PathFigure>
        </PathFigureCollection>
      </PathGeometry.Figures>
    </PathGeometry>
  </Path.Data>
</Path>

This XAML structure looks a lot more binding-friendly than the Data string of the first Path definition. All we need to do is create an appropriate ViewModel for it, preferably one that implements INotifyPropertyChanged and exposes the properties through data types that are easy to bind to. Instead of Point or Vector instances for start position, end position, and radius, the ViewModel exposes numeric properties with the individual coordinates:

public class EllipticalArcViewModel : BindableBase
{
    public int RadiusX
    {
        get { return _radiusX; }
        set
        {
            SetProperty(ref _radiusX, value);
            OnPropertyChanged(nameof(Radius));
        }
    }

    public int RadiusY
    {
        get { return _radiusY; }
        set
        {
            SetProperty(ref _radiusY, value);
            OnPropertyChanged(nameof(Radius));
        }
    }

    public Size Radius => new Size(_radiusX, _radiusY);

    // ...
}

The enumerations for PenLineCap and SweepDirection are accessible through Boolean properties:

public bool IsClockwise
{
    get { return _isClockwise; }
    set
    {
        SetProperty(ref _isClockwise, value);
        OnPropertyChanged(nameof(SweepDirection));
    }
}

public SweepDirection SweepDirection => 
	_isClockwise ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;

Here’s the class diagram for the first iteration of our ‘bindable arc’. The new ViewModel is on the left, and the XAML elements to which we will bind, are on the right:

EllipticalArcViewModel

The hosting page exposes an instance of the ViewModel in its code behind:

private EllipticalArcViewModel _viewModel = new EllipticalArcViewModel
{
    StartPointX = 10,
    StartPointY = 100,
    RadiusX = 100,
    RadiusY = 50,
    RotationAngle = 45,
    EndPointX = 200,
    EndPointY = 100
};

public EllipticalArcViewModel ViewModel => _viewModel;

And in the XAML we added {x:Bind} bindings to the elements:

<Path Stroke="{x:Bind ViewModel.Stroke, Mode=OneWay}"
        Height="400"
        Width="400"
        StrokeThickness="{x:Bind ViewModel.StrokeThickness, Mode=OneWay}"
        StrokeStartLineCap="{x:Bind ViewModel.StrokeLineCap, Mode=OneWay}"
        StrokeEndLineCap="{x:Bind ViewModel.StrokeLineCap, Mode=OneWay}">
    <Path.Data>
        <PathGeometry>
            <PathGeometry.Figures>
                <PathFigureCollection>
                    <PathFigure StartPoint="{x:Bind ViewModel.StartPoint, Mode=OneWay}">
                        <PathFigure.Segments>
                            <PathSegmentCollection>
                                <ArcSegment RotationAngle="{x:Bind ViewModel.RotationAngle, Mode=OneWay}"
                                            IsLargeArc="{x:Bind ViewModel.IsLargeArc, Mode=OneWay}"
                                            SweepDirection="{x:Bind ViewModel.SweepDirection, Mode=OneWay}"
                                            Point="{x:Bind ViewModel.EndPoint, Mode=OneWay}"
                                            Size="{x:Bind ViewModel.Radius, Mode=OneWay}" />
                            </PathSegmentCollection>
                        </PathFigure.Segments>
                    </PathFigure>
                </PathFigureCollection>
            </PathGeometry.Figures>
        </PathGeometry>
    </Path.Data>
</Path>

The ViewModel implements change propagation, so we can use Sliders (or other input controls) to modify its properties, and this will update the XAML elements:

<Slider Header="Rotation Angle"
        Value="{x:Bind ViewModel.RotationAngle, Mode=TwoWay}"
        Maximum="360" />
<ToggleSwitch Header="Sweep Direction"
                IsOn="{x:Bind ViewModel.IsClockwise, Mode=TwoWay}"
                OnContent="Clockwise"
                OffContent="Counterclockwise" />

Here’s how the corresponding page in the sample app looks like. It draws an ArcSegment with bindings to all of its SVG properties. Slider and ToggleSwitch controls allow you to modify the elliptical arc through the ViewModel:
ArcSegment

It’s relatively easy to make this solution more reusable by turning it into a user control. If you’re looking for an example of this, just check the code for the PieSlice and RingSlice controls from WinRT XAML Toolkit.

Composition solution

The last couple of years, we’ve seen an exciting increase in functionality and ease of use of the UWP Composition API. It has come to a point were apps can now relatively easy draw (and/or animate) anything directly in the Visual layer, a layer that is much closer to the fast DirectX drivers than the traditional XAML layer:
layers-win-ui-composition

The Visual Layer allows for fast drawing off the UI thread. Its API was recently enhanced with capabilities to draw paths. This makes it a logical next step in our attempt to create bindable paths.

In this second iteration, we packaged a ‘bindable arc’ as a user control. Its only XAML element is an empty grid which is only there to establish the link between the Visual (composition) Layer and the Framework (XAML) layer. Here’s that XAML:

<UserControl x:Class="XamlBrewer.Uwp.Controls.EllipticalArc">
    <Grid x:Name="Container" />
</UserControl>

Unsurprisingly, the new EllipticalArc control exposes the same list of properties as the EllipticalArcViewModel. Only this time they’re implemented as Dependency Properties:

public sealed partial class EllipticalArc : UserControl
{
    public static readonly DependencyProperty StartPointXProperty =
        DependencyProperty.Register(nameof(StartPointX), 
                                    typeof(int), 
                                    typeof(EllipticalArc), 
                                    new PropertyMetadata(0, new PropertyChangedCallback(Render)));

    public static readonly DependencyProperty StartPointYProperty =
        DependencyProperty.Register(nameof(StartPointY), 
                                    typeof(int), 
                                    typeof(EllipticalArc), 
                                    new PropertyMetadata(0, Render));

    public int EndPointX
    {
        get { return (int)GetValue(EndPointXProperty); }
        set { SetValue(EndPointXProperty, value); }
    }

    public int EndPointY
    {
        get { return (int)GetValue(EndPointYProperty); }
        set { SetValue(EndPointYProperty, value); }
    }

    // ...

}

Here’s the full API:

EllipticalArcControl

Every time a property changes, the Render method is called, to redraw the elliptical arc. It uses classes from the Composition API and Win2D (available through a NuGet package). Here are the key classes and their responsibility in that method:

Class Responsibility
Compositor Creates all Composition objects
Win2D CanvasPathBuilder Holds the path figure definitions
Win2D CanvasGeometry Draws geometric shapes in DirectX
CompositionPath Links the geometric shape to the Visual Layer
CompositionSpriteShape Holds the stroke and fill definitions
ShapeVisual Links the Visual Layer to the Framework Layer

Here’s the full Render method:

private static void Render(DependencyObject d, DependencyPropertyChangedEventArgs args)
{
    var arc = (EllipticalArc)d;
    if (arc.Container.ActualWidth == 0)
    {
        return;
    }

    var root = arc.Container.GetVisual();
    var compositor = Window.Current.Compositor;
    var canvasPathBuilder = new CanvasPathBuilder(new CanvasDevice());

    // Figure
    canvasPathBuilder.BeginFigure(new Vector2(arc.StartPointX, arc.StartPointY));
    canvasPathBuilder.AddArc(
        new Vector2(arc.EndPointX, arc.EndPointY),
        arc.RadiusX,
        arc.RadiusY,
        (float)(arc.RotationAngle * Math.PI / 180),
        arc.IsClockwise ? CanvasSweepDirection.Clockwise : CanvasSweepDirection.CounterClockwise,
        arc.IsLargeArc ? CanvasArcSize.Large : CanvasArcSize.Small);
    canvasPathBuilder.EndFigure(arc.IsClosed ? CanvasFigureLoop.Closed : CanvasFigureLoop.Open);

    // Path
    var canvasGeometry = CanvasGeometry.CreatePath(canvasPathBuilder);
    var compositionPath = new CompositionPath(canvasGeometry);
    var pathGeometry = compositor.CreatePathGeometry();
    pathGeometry.Path = compositionPath;
    var spriteShape = compositor.CreateSpriteShape(pathGeometry);
    spriteShape.FillBrush = compositor.CreateColorBrush(arc.Fill);
    spriteShape.StrokeThickness = (float)arc.StrokeThickness;
    spriteShape.StrokeBrush = compositor.CreateColorBrush(arc.Stroke);
    spriteShape.StrokeStartCap = arc.IsStrokeRounded ? CompositionStrokeCap.Round : CompositionStrokeCap.Flat;
    spriteShape.StrokeEndCap = arc.IsStrokeRounded ? CompositionStrokeCap.Round : CompositionStrokeCap.Flat;

    // Visual
    var shapeVisual = compositor.CreateShapeVisual();
    shapeVisual.Size = new Vector2((float)arc.Container.ActualWidth, (float)arc.Container.ActualHeight);
    shapeVisual.Shapes.Add(spriteShape);
    root.Children.RemoveAll();
    root.Children.InsertAtTop(shapeVisual);
}

Here’s how to use the control on a XAML page:

<controls:EllipticalArc x:Name="EllipticalArc"
                        StartPointX="10"
                        StartPointY="100"
                        RotationAngle="45"
                        RadiusX="100"
                        RadiusY="50"
                        EndPointX="200"
                        EndPointY="100"
                        Height="400"
                        Width="400" />

It supports two-way bindings, so you can update its properties through other controls – or through ViewModels if you prefer an MVVM approach:

<Slider Header="Rotation Angle"
        Value="{Binding RotationAngle, ElementName=EllipticalArc, Mode=TwoWay}"
        Maximum="360" />
<ToggleSwitch Header="Sweep Direction"
                IsOn="{Binding IsClockwise, ElementName=EllipticalArc, Mode=TwoWay}"
                OnContent="Clockwise"
                OffContent="Counterclockwise" />

Here’s how the corresponding page looks like in the sample app – it’s identical to the XAML version:
Composition

Reusable solution

The experiment with the light weight user control inspired us to move one step further, and create an easy-to-use circle segment control as part of a ‘reusable generic bindable path drawing framework for UWP’ (a.k.a. an abstract base class).

This circle segment control is intended for use inside (the template of) other controls – think of percentage ring, radial gauge, pie chart, and doughnut chart. All the common ‘bindable path’ members (such as stroke properties, start position, and closed figure indicator) are factored out into an abstract base class, while the CircleSegment itself only comes with a center, a radius, start end sweep angles. We also added a Boolean property to specify whether to add extra lines to the center and give it a pie slice shape. [We know: mathematically this property should have been called IsSector instead of IsPie.]

Here’s the class diagram for the last ‘bindable arc’ iteration:

CircleSegmentClassDiagram

As in the previous EllipticalArc, the user control’s only XAML element is a Grid to hook the composition drawings to the Visual Tree:

<UserControl
    x:Class="XamlBrewer.Uwp.Controls.CompositionPathHost">
    <Grid x:Name="Host" />
</UserControl>

On top of the property definitions, the control comes with an abstract Render method which is triggered on load and on property changes:

public static readonly DependencyProperty StartPointXProperty =
	DependencyProperty.Register(
		nameof(StartPointX), 
		typeof(double), 
		typeof(CompositionPathHost), 
		new PropertyMetadata(0d, new PropertyChangedCallback(Render)));

public double StartPointX
{
    get { return (double)GetValue(StartPointXProperty); }
    set { SetValue(StartPointXProperty, value); }
}

// ...

private void CompositionPathHost_Loaded(object sender, RoutedEventArgs e)
{
    Render(this, null);
}

protected static void Render(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var path = (CompositionPathHost)d;
    if (path.Container.ActualWidth == 0)
    {
        return;
    }

    path.Render();
}

protected abstract void Render();

The new CircleSegment control inherits from this abstract base class, and adds its own properties;

public class CircleSegment : CompositionPathHost
{
  public static readonly DependencyProperty CenterPointYProperty =
      DependencyProperty.Register(
		nameof(CenterPointY), 
		typeof(double), 
		typeof(CircleSegment), 
		new PropertyMetadata(0d, Render));

  public double CenterPointX
  {
    get { return (double)GetValue(CenterPointXProperty); }
    set { SetValue(CenterPointXProperty, value); }
  }

  // ...

}

And of course provides its own implementation of the Render method. It’s a specialized version of the one in EllipticalArc that’s based on a more convenient AddArc overload with a center point and start and sweep angles. The start point coordinates of the underlying figure are determined by the IsPie property. The figure starts at the circle’s center for a pie slice shape (sector), or at the beginning of the arc for a simple segment:

protected override void Render()
{
    var root = Container.GetVisual();
    var compositor = Window.Current.Compositor;
    var canvasPathBuilder = new CanvasPathBuilder(new CanvasDevice());
    if (IsStrokeRounded)
    {
        canvasPathBuilder.SetSegmentOptions(CanvasFigureSegmentOptions.ForceRoundLineJoin);
    }
    else
    {
        canvasPathBuilder.SetSegmentOptions(CanvasFigureSegmentOptions.None);
    }

    // Figure
    if (IsPie)
    {
        StartPointX = CenterPointX;
        StartPointY = CenterPointY;
    }
    else
    {
        StartPointX = Radius * Math.Cos(StartAngle * Degrees2Radians) + CenterPointX;
        StartPointY = Radius * Math.Sin(StartAngle * Degrees2Radians) + CenterPointY;
    }

    canvasPathBuilder.BeginFigure(new Vector2((float)StartPointX, (float)StartPointY));

    canvasPathBuilder.AddArc(
        new Vector2((float)CenterPointX, (float)CenterPointY),
        (float)Radius,
        (float)Radius,
        (float)(StartAngle * Degrees2Radians),
        (float)(SweepAngle * Degrees2Radians));

    canvasPathBuilder.EndFigure(IsClosed || IsPie ? CanvasFigureLoop.Closed : CanvasFigureLoop.Open);

    // Path
    var canvasGeometry = CanvasGeometry.CreatePath(canvasPathBuilder);
    var compositionPath = new CompositionPath(canvasGeometry);
    var pathGeometry = compositor.CreatePathGeometry();
    pathGeometry.Path = compositionPath;
    var spriteShape = compositor.CreateSpriteShape(pathGeometry);
    spriteShape.FillBrush = compositor.CreateColorBrush(Fill);
    spriteShape.StrokeThickness = (float)StrokeThickness;
    spriteShape.StrokeBrush = compositor.CreateColorBrush(Stroke);
    spriteShape.StrokeStartCap = IsStrokeRounded ? CompositionStrokeCap.Round : CompositionStrokeCap.Flat;
    spriteShape.StrokeEndCap = IsStrokeRounded ? CompositionStrokeCap.Round : CompositionStrokeCap.Flat;

    // Visual
    var shapeVisual = compositor.CreateShapeVisual();
    shapeVisual.Size = new Vector2((float)Container.ActualWidth, (float)Container.ActualHeight);
    shapeVisual.Shapes.Add(spriteShape);
    root.Children.RemoveAll();
    root.Children.InsertAtTop(shapeVisual);
}

Please observe we switched from integers to doubles for all coordinate properties. The calculation of the starting point revealed the rounding errors when using integers.

For a screenshot of the corresponding page in the sample app, scroll back to the beginning of this article. It’s basically the same as all the previous ones, only with better code behind. We also brought up the old SquareOfSquares control to host some CircleSegment instances with different sizes and random properties:

SquareOfSquares

Last but not least, we added an inspiring gallery page to demonstrate some potential usages of the control:

Gallery

All the code

The sample app lives here on GitHub.

Enjoy!

Advertisements

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 )

Google+ photo

You are commenting using your Google+ 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 )

Connecting to %s