Monthly Archives: October 2016

A recipe for printing in UWP MVVM apps

This article describes a framework that facilitates printing from a XAML MVVM UWP app. Most UWP printing samples are limited to easy scenarios, like generating and printing one or more screenshots, or printing fixed content with paging. A lot of apps require a lot more than that. This article describes a recipe for printing that deals with challenges like

  • data binding,
  • paging,
  • printing any type of content,
  • page numbering,
  • different page sizes, and
  • different page orientation.

This framework is used in real life UWP apps and successfully prints things like route descriptions including maps, bills of material including tables and gauges, and product catalogs with texts and images.

Here are some screenshots from the sample app. The app contains a ListView and a GridView. The page itself scrolls vertically, but the GridView items are displayed in rows:

View_1

 

View_2

Both item controls are bound to collections in the view model. In a real-life app the content would come from a service or a database, so you don’t know upfront how many items there will be. You also don’t know how long the text in the descriptions would be. So for printing the content of this page, a list of screenshots or a separate page per list item would definitely not suffice. You probably want to rearrange the content in a strictly vertical style, and enable paging. Well, this is exactly what this article is about.

Here’s a screenshot of print dialog that is populated from the app:

Print_dialog

Printing in UWP apps

The printing framework that is presented in this article, comes with three classes: PrintServiceProvider, PrintPage, and PrintServiceEventArgs. These classes work together with the existing UWP PrintDocument and PrintManager classes that are used in Window’s print dialogs an preview pages:

API

Let’s first take a look at the two built-in classes:

The PrintManager class orchestrates the printing flow between your UWP app and the operating system. Apps that want to print must get a PrintManager instance through GetForCurrentView, and register an event handler for PrintTaskRequested. In the Windows 8 days this would enable the print charm, but nowadays you need to provide a button or menu item yourself, and open the print dialog with ShowPrintUIAsync.

The PrintDocument hosts the set of XAML elements that is sent to the printer. These elements are grouped into pages. With AddPage you add an element to this list, with AddPagesComplete you indicate that you provided all the pages and are ready to print. PrintDocument also serves the content of the page previews in the printer dialog. You’ll have to hook event handlers to Paginate and GetPreviewPage to deliver these.

For a straightforward project that shows how to communicate with PrintManager and PrintDocument, please check the Microsoft Official UWP Printing Sample.

Let’s now dive into our own framework.

Why RichTextBlock is your best friend

This UWP MVVM printing framework relies heavily on the RichTextBlock control, for two reasons:

  • RichTextBlock has the capability to overflow into another block (of type RichTextBlockOverflow). This capabilty was originally introduced to allow developers to create the column layout of the typical horizontally scrolling Windows 8 apps. This printing framework uses this capability to allow paging, by letting rich text overflow to new print pages.
  • RichTextBlock can contain more than just text. A RichTextBlock has a Blocks collection with Paragraph instances. Those paragraphs are not limited to  just text (e.g. in Run instances) but they can contain any XAML control (Maps, Gauges, Images) as long as it’s wrapped in a InlineUIContainer.

Here’s how the recipe works:

For every page in your app that you want to print, you need to develop a corresponding print report. That print report is a page that contains more or less the same controls as the page on screen (it needs to be bindable to the same viewmodel), but it’s organized for printing (vertically), and it has to have a RichtTextBlock as main element.

At runtime, when you request to print a page, its corresponding print report is instantiated and populated with the current viewmodel. Then the PrintServiceProvider recursively walks through the content of that print report to flatten it into a list of paragraphs. These paragraphs are inserted in PrintPage instances – a class provided by the framework. Each print page contains a RichTextBlockOverflow control. As long as this control has Overflow itself, a new print page is added to the list. Finally these pages are presented to the built-in PrintManager

Printing_UWP

The rendering of the paragraphs has to be done inside the visual tree – otherwise their height will be always zero. Therefor the printing framework requires the calling page to provide a Canvas called PrintRoot. Make sure to position this Canvas outside the working screen and/or make it completely transparent so that it does not interfere with your UI:

<Grid>
    <!-- Required for the print service provider -->
    <Canvas x:Name="printingRoot"
            Opacity="0"
            Margin="-2000 0 0 0" />

    <!-- Actual Page Content -->
    <ScrollViewer VerticalScrollMode="Auto">
    <!-- ... -->
    </ScrollViewer>
</Grid>

One PrintPage to rule them all

The framework comes with a standard PrintPage that can host any content, so you can reuse it in all apps  (you might want to change the logo). Here’s the XAML of that page. It contains

  • a header that displays a logo and a title,
  • a footer that displays the page number,
  • an invisible TextBlock (it’s in a Grid Row with Height 0), and most importantly
  • a RichTextBlockOverflow that covers the whole content of the page:
<Grid x:Name="printableArea">

    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="0" />
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <!-- Header -->
    <Grid Grid.Row="0"
            Margin="0 0 0 8">
        <StackPanel Orientation="Horizontal">
            <Image Source="ms-appx:///Assets/StoreLogo.png"
                    Height="32" />
            <TextBlock Text="XAML Brewer UWP Printing Sample"
                        Margin="8 0 0 0"
                        FontSize="16"
                        VerticalAlignment="Center" />
        </StackPanel>
        <TextBlock Name="title"
                    Foreground="Black"
                    FontSize="16"
                    HorizontalAlignment="Right"
                    Text="" />
    </Grid>

    <RichTextBlock x:Name="textContent"
                    FontSize="18"
                    Grid.Row="1"
                    OverflowContentTarget="{Binding ElementName=textOverflow}" />

    <RichTextBlockOverflow x:Name="textOverflow"
                            Grid.Row="2" />

    <!-- Footer -->
    <Grid Grid.Row="3"
            Grid.Column="0"
            Margin="0 8 0 0">
        <StackPanel Orientation="Horizontal">
            <TextBlock FontSize="16"
                        Text="© 2016 by XAML Brewer"
                        VerticalAlignment="Bottom" />
        </StackPanel>
        <TextBlock Name="pageNumber"
                    Foreground="Black"
                    FontSize="16"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Bottom"
                    Text="-#-" />
    </Grid>
</Grid>

The code-behind of the page just exposes its main elements, and provides a helper method to add a new Paragraph to its content:

using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Documents;

public partial class PrintPage
{
    public PrintPage()
    {
        InitializeComponent();
    }

    public PrintPage(RichTextBlockOverflow textLinkContainer)
        : this()
    {
        if (textLinkContainer == null) 
	throw new ArgumentNullException(nameof(textLinkContainer));
        textLinkContainer.OverflowContentTarget = textOverflow;
    }

    internal Grid PrintableArea => printableArea;

    internal RichTextBlock TextContent => textContent;

    internal RichTextBlockOverflow TextOverflow => textOverflow;

    internal void AddContent(Paragraph block)
    {
        textContent.Blocks.Add(block);
    }
}

Rolling your own XAML Report

As already mentioned: when you want to print a more of less complex XAML page, you have to provide a printer friendly version of it. These are the only requirements:

  • it should inherit from Page (that’s just to give you Visual Studio design support), and
  • its root control should be a RichTextBlock, and
  • it should be able to data-bind to the same viewmodel as the page on the screen.

Here’s an example of the print page in the sample app. We decomposed the ListView’s template into paragraphs and runs, but that is not really necessary. For the gridview from the original page we reused the full data template with Grid, Image, Border and TextBlock controls:

<RichTextBlock>
    <Paragraph TextAlignment="Right">
        <Run Text="La Commedia dell'arte"
                Foreground="{StaticResource TitlebarBackgroundBrush}"
                FontSize="28" />
    </Paragraph>
    <Paragraph>
        <LineBreak />
        <Run Text="Types"
                FontSize="20" />
    </Paragraph>
    <Paragraph>
        <InlineUIContainer>
            <ItemsControl ItemsSource="{Binding Types}"
                            Margin="0">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <RichTextBlock>
                            <Paragraph>
                                <LineBreak />
                                <Run Text="{Binding Name}"
                                        FontSize="16"
                                        FontWeight="Bold" />
                                <LineBreak />
                                <Run Text="{Binding Description}" />
                            </Paragraph>
                        </RichTextBlock>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </InlineUIContainer>
    </Paragraph>
    <Paragraph>
        <LineBreak />
        <Run Text="Characters"
                FontSize="20" />
        <LineBreak />
    </Paragraph>
    <Paragraph>
        <InlineUIContainer>
            <ItemsControl ItemsSource="{Binding Characters}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <!-- Any MaxHeight will help. -->
                        <Grid MaxHeight="7000">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="120" />
                                <ColumnDefinition Width="*" />
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="auto" />
                                <RowDefinition Height="*" />
                                <RowDefinition Height="auto" />
                            </Grid.RowDefinitions>
                            <Border BorderThickness="2"
                                    BorderBrush="{StaticResource SplitViewBackgroundBrush}"
                                    Width="100"
                                    VerticalAlignment="Top"
                                    Grid.Row="1"
                                    Margin="10">
                                <Image Source="{Binding Image}" />
                            </Border>
                            <TextBlock Text="{Binding Name}"
                                        FontSize="16"
                                        FontWeight="Bold"
                                        Grid.ColumnSpan="2" />
                            <TextBlock Text="{Binding Description}"
                                        TextWrapping="WrapWholeWords"
                                        TextTrimming="CharacterEllipsis"
                                        Margin="10"
                                        Grid.Column="1"
                                        Grid.Row="1" />
                            <TextBlock Text="{Binding PrimaryComicTrait}"
                                        TextAlignment="Right"
                                        TextWrapping="WrapWholeWords"
                                        FontWeight="SemiBold"
                                        Margin="10 0 10 10"
                                        Grid.ColumnSpan="2"
                                        Grid.Row="2" />
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </InlineUIContainer>
    </Paragraph>
</RichTextBlock>

Printing as a MVVM Service

Here’s the public API for the PrintService:

Title Property Gets or sets the title that will appear on top of the printed pages.
RegisterForPrinting Method Informs Windows that you have something to print. The OS may react to this by enabling charms or menu items.
Print Method Opens the Print Dialog.
UnregisterForPrinting Method Informs Windows that you have nothing to print. The OS may react to this by disabling charms or menu items.
StatusChanged Event Occurs when the print service wants to inform you about its status.

The PrintService code is spread over two partial class files:

  • PrintServiceProvider.Contracts hosts all communication with the built-in PrintManager and PrintDocument classes, while
  • PrintServiceProvider.Core hosts the fun bit: flattening the print report content into a list of paragraphs, and rendering these paragraphs to assign their height to that they get distributed over the different print pages.

Let’s discuss some of the core methods.

Flattening the content

The print report has a RichtTextBlock as its main control. The printing framework uses the following recursive routine to flatten the content of that control into the list of paragraphs that will be presented to the print pages. Each Paragraph element in the Blocks collection of the report’s RichTextBlock is examined. When it contains an ItemsControl, that control is rendered separately – not to measure it, but to read its ItemsSource and create new paragraphs:

// Flatten content from user print page to a list of paragraphs, and move these to the print template.
var userPrintPageContent = userPrintPage.Content as RichTextBlock;
if (userPrintPageContent == null)
{
    OnStatusChanged("Configuration error: print page's main panel is not a RichTextBlock.", EventLevel.Error);
    return;
}

while (userPrintPageContent.Blocks.Count > 0)
{
    var paragraph = userPrintPageContent.Blocks.First() as Paragraph;
    userPrintPageContent.Blocks.Remove(paragraph);

    var container = paragraph.Inlines[0] as InlineUIContainer;
    if (container != null)
    {
        var itemsControl = container.Child as ItemsControl;
        if (itemsControl?.Items != null)
        {
            // Render the paragraph (only to read the ItemsSource).
            Render(paragraph);

            // Transform each item to a paragraph, render separately, and add to the page.
            foreach (var item in itemsControl.Items)
            {
                var itemParagraph = new Paragraph();
                var inlineContainer = new InlineUIContainer();
                var element = (itemsControl.ContainerFromItem(item) as ContentPresenter).ContentTemplate.LoadContent() as UIElement;
                var frameworkElement = element as FrameworkElement;
                frameworkElement.DataContext = item;
                inlineContainer.Child = element;
                itemParagraph.Inlines.Add(inlineContainer);
                itemParagraph.LineHeight = Render(itemParagraph);
                _firstPrintPage.AddContent(itemParagraph);
            }
        }
        else
        {
            // Place the paragraph in a new textblock, and measure it.
            var actualHeight = Render(paragraph);

            // Apply line height to trigger overflow.
            paragraph.LineHeight = actualHeight;

            _firstPrintPage.AddContent(paragraph);
        }
    }
    else
    {
        _firstPrintPage.AddContent(paragraph);
    }
}

Rendering a Paragraph

Here’s the routine that renders a paragraph. The paragraph needs to be plugged into the PrintingRoot on the Visual Tree to get a ‘real’ size. There it is submitted to InvalidateMeasure and UpdateLayout calls, and finally the paragraph is removed from the Visual Tree.

// Renders a single paragraph and returns its height.
private double Render(Paragraph paragraph)
{
    var blockToMeasure = new RichTextBlock();
    blockToMeasure.Blocks.Add(paragraph);
    PrintingRoot.Children.Clear();
    PrintingRoot.Children.Add(blockToMeasure);
    PrintingRoot.InvalidateMeasure();
    PrintingRoot.UpdateLayout();
    blockToMeasure.Blocks.Clear();

    return blockToMeasure.ActualHeight;
}

The measured height is assigned it to the paragraph’s LineHeight, because that is the property that RichTextBlock in the print page looks at to trigger the overflow to the next page.

Admiring the result

Here are some screenshots from the print dialog in the sample app. Observe the successful dynamic page breaking:

Print_p1 Print_p2

Why RichTextBlock is NOT your best friend

The printing framework depends on the overflow capabilities or the RichTextBlock control. The size of its content –and whether or not there is overflow- is determined by the XAML layout system. This system calculates the size and positioning of all XAML elements through measure and arrange steps. This is the result of a recursive discussion between a panel and its children. The layout system is based on Voodoo Magic heuristics. In the fuzzy communication between all XAML elements, RichTextBlock is definitely not the most assertive: it will rapidly adapt (obey) to constraints and shrink its own size and hence the size of its content. Here’s an example of an incomplete paragraph (of course: this behavior was stimulated by the TextTrimming value of the encapsulated TextBlock):

Print_p3

For getting decent print results you can –and should- help the layout system by adding useful XAML properties like MinWidth and MinHeight in the print report’s constituents.

The curse of the Printer Driver

I’m not sure if this is this is related to the previous remark, but it looks like the printer drives also influences the process. Here’s the second page of the print preview (in landscape) on Microsoft’s Print-to_PDF driver. There’s nothing wrong with it:

Print_landscape_success

Here’s that same page (with similar page size and orientation) again, now displayed by the XPS driver. For one reason or another, the layout system decided not to overflow after two correctly rendered paragraphs, and to shrink the third one to a one-liner (and yes: a MinHeight on the image –almost- solves the problem):

Print_landscape_fail

To closely monitor such issues, I added the StatusChanged event and the PrintServiceEventArgs class to the printing framework. The status change event not only reports configuration problems (like forgetting the PrintingRoot canvas on the main page), but it can also be used for tracing the printing process. It exposes a lot of the internal processing.

Here’s my personal StatusChanged event handler, it writes the informational messages to the log, but notifies the developer or the user with a toast for real errors:

private void PrintServiceProvider_StatusChanged(
	object sender, 
	PrintServiceEventArgs e)
{
    switch (e.Severity)
    {
        case EventLevel.Informational:
            Log.Info(e.Message);
            break;
        default:
            Log.Error(e.Message);
            Toast.Show(e.Message, "ms-appx:///Assets/Toasts/Printer.png");
            break;
    }
}

This gives me tons of discrete information while debugging …

Tracing

… and a noticeable message in case of a real error:

Toast

It’s Universal

Not all devices are capable of printing. Fortunately the PrintManager class is able to detect that with the IsSupported method:

if (!PrintManager.IsSupported())
{
    OnStatusChanged(
	"Sorry, printing is not supported on this device.", 
	EventLevel.Error);
}

When I tested this on my phone, I discovered that the Windows phone *can* actually print. Here’s the sample app’s print dialog running on my Lumia 950:

wp_ss_20161020_0001

It’s for free

The source code lives here on GitHub. The sample app has the entire reusable printing framework in its Services/Printing folder.

Enjoy!

Advertisements

A lap around the UWP Community Toolkit Radial Gauge control

The XAML and Composition API Radial Gauge control that I developed a while ago is now part of to the UWP Community Toolkit on GitHub. Its code was cleaned up, thanks to peer pressure from fellow MVP’s. At the same time the gauge’s functionality was extended, based on community feedback. V1.1 of the UWP Community Toolkit was just released, here is the new official Radial Gauge documentation.

The new Radial Gauge comes with a lot more configurable properties. Its default look and feel and its basic constituents did not change however:

compositiongauge_anatomy

Properties

Here’s the alphabetic list of public dependency properties. The properties that are tagged with an asterisk are brand new:

IsInteractive* bool, default false Determines whether the control accepts changing its Value through interaction.
MaxAngle* int, default 150 The stop angle of the scale, in degrees.
Maximum double, default 100 The maximum value on the scale.
MinAngle* int, default -150 The start angle of the scale, in degrees.
Minimum double, default 0 The minimum value on the scale.
NeedleBrush SolidColorBrush, default Red The color of the needle.
NeedleLength* double, default 100 The length of the needle, in percentage of the gauge radius.
NeedleWidth* double, default 5 The width of the needle, in percentage of the gauge radius.
ScaleBrush Brush, default solid DarkGray The background color of the scale.
ScaleTickBrush Brush, default solid Black The color of the ticks on the scale.
ScaleTickWidth* double, default 2.5 The scale tick width, in percentage of the gauge radius.
ScaleWidth double, default 26 The thickness of the scale in pixels, in percentage of the gauge radius.
StepSize* double, default 0 The rounding interval for the Value.
TickBrush SolidColorBrush, default White The color of the outer ticks.
TickLength* double, default 18 The outer tick length, in percentage of the gauge radius.
TickSpacing int, default 10 The spacing between ticks, in Value units.
TickWidth* double, default 5 The outer tick width, in percentage of the gauge radius.
TrailBrush Brush, default solid Orange The color of the trail following the needle. 
Unit string The unit measure to display.
UnitBrush Brush, default solid White The color of the unit measure text. 
Value double, default 0 The value to represent.
ValueBrush Brush, default solid White The color of the value text.
ValueStringFormat string, default ‘N0’ (integer values) The StringFormat to apply to the displayed value.

Each of these dependency properties has a PropertyMetadata with a default value (so you don’t have to provide one) and a PropertyChangedCallback (so any change at runtime is reflected immediately in the gauge’s looks and behavior).

There are three such callbacks:

  • OnValueChanged: called when the value changed – redraws the trail and rotates the needle
  • OnInteractivityChanged: called when IsInteractive changed (see further)
  • OnScaleChanged: called when the scale needs to be redrawn
  • OnFaceChanged: called when the needle and ticks need to be redrawn

The control is responsive: whenever a property value is changed, one (or more) of these callbacks is executed.

What’s new in Radial Gauge v1.1?

Height and Width of face elements are configurable.

In the previous versions of the Radial Gauge, the height and width of needle and ticks was fixed to give the control its ‘chubby’ Windows 8 look. To customize the gauge, you could only play with its colors:

compositiongauge_gallery

The new version allows you to go for a thinner look and feel (and more):

Gauge_Widths

Here’s part of the XAML for the first gauge from the previous screenshot. You can now specify the width of all constituents:

<controls:RadialGauge Unit="Things"
                        NeedleWidth="2"
                        TickWidth="2"
                        ScaleWidth="2" />

It’s interactive.

The previous versions of the Radial Gauge were rather passive: the control could only be used to display a Value. The new version comes with a property called ‘Isinteractive’ which make the control … interactive. Just like with a Slider control you can use touch, mouse, or pen to update the Value. And I admit: I didn’t try the XBox controller.

In interactive mode, you can place the Needle by tapping or dragging, and the control will adjust its Value. Here’s how this works: based on the value of the IsInteractive property the control registers or unregisters handlers for the Tapped and the ManipulationDelta events and it sets the corresponding ManipulationMode:

private static void OnInteractivityChanged(
	DependencyObject d, 
	DependencyPropertyChangedEventArgs e)
{
    RadialGauge radialGauge = (RadialGauge)d;

    if (radialGauge.IsInteractive)
    {
        radialGauge.Tapped += radialGauge.RadialGauge_Tapped;
        radialGauge.ManipulationDelta += 
		radialGauge.RadialGauge_ManipulationDelta;
        radialGauge.ManipulationMode = 
		ManipulationModes.TranslateX | 
		ManipulationModes.TranslateY;
    }
    else
    {
        radialGauge.Tapped -= radialGauge.RadialGauge_Tapped;
        radialGauge.ManipulationDelta -= 
		radialGauge.RadialGauge_ManipulationDelta;
        radialGauge.ManipulationMode = ManipulationModes.None;
    }
}

Both event handlers apply the same logic: the current position is translated to the corresponding needle angle, which is then translated to the gauge Value. You can not place the needle outside the scale – the triangle from MaxAngle to MinAngle) is happily ignored:

    private void RadialGauge_ManipulationDelta(
	object sender, 
	ManipulationDeltaRoutedEventArgs e)
        {
            SetGaugeValueFromPoint(e.Position);
        }

        private void RadialGauge_Tapped(
	object sender, 
	TappedRoutedEventArgs e)
        {
            SetGaugeValueFromPoint(e.GetPosition(this));
        }

        private void SetGaugeValueFromPoint(Point p)
        {
            var pt = new Point(
	p.X - (ActualWidth / 2), 
	-p.Y + (ActualHeight / 2));

            var angle = Math.Atan2(pt.X, pt.Y) * 180 / Math.PI;
            var value = Minimum + 
	((Maximum - Minimum) * (angle - MinAngle) / (MaxAngle - MinAngle));
            if (value < Minimum || value > Maximum)
            {
                // Ignore positions outside the scale angle.
                return;
            }

            Value = value;
        }

To prevent rounding issues when setting the Value through interaction, the control was extended with a StepSize dependency property. It determines the rounding interval for the Value. At the default StepSize value of zero, the control does no rounding at all. Set it to one if you only want integer values.

Each time the Value changes, the StepSize adjustment is applied:

if (radialGauge.StepSize != 0)
{
    radialGauge.Value = radialGauge.RoundToMultiple(
	radialGauge.Value, 
	radialGauge.StepSize);
}

I borrowed the rounding algorithm from the Rating Control. It’s unfortunate that there’s no such function in the System.Math class:

private double RoundToMultiple(double number, double multiple)
{
    double modulo = number % multiple;
    if ((multiple - modulo) <= modulo)
    {
        modulo = multiple - modulo;
    }
    else
    {
        modulo *= -1;
    }

    return number + modulo;
}

The begin and end angles of the scale are configurable.

In the previous iterations of the Radial Gauge, the arc of the scale was hardcoded and went clockwise from down left (-150°) to down right (150°). This gave the gauge a nice circular look – which is excellent in a responsive design context. It was also easy to find a place to display the value and unit information: centered at the bottom of the control. That 300° symmetrical look is still the default look, and here’s the corresponding style:

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

                        <!-- 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>
                    </Grid>
                </Viewbox>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

It’s a simple style: you just have to tell the control where to draw the arcs and where to draw the value and unit texts. That’s the only XAML in the gauge, the rest (needle and ticks) is drawn by the Composition API.

The scale’s minimum and maximum angles are now Dependency Properties, so you can configure them. The proposed value ranges are -180° to 0° for MinAngle and 0° to 180° for MaxAngle.

Gauge_Angles

Feel free to deviate from the proposed values for MinAngle and MaxAngle. These were just the ranges that I had in mind when implementing all the internal calculations. Here are two examples of fully functional gauges that operate outside of the proposed ranges:

Gauge_SmallArcs

When you deviate from the default minimum or maximum angles, you probably need to retemplate the control to position the value and unit measure text. So I created a sample app that allows you to build and test your own templates. Here’s its main page with some sample templates:

Gauge1.1

My favorite template is the 270° gauge, based on RPM and speed indicators in cars. It’s three quarters of a circle (from -180° to +90°), the unit measure is neatly placed at the end of the scale, and the value appears in big in the bottom right quadrant.

Here’s how 270° gauges look like in a SquareOfsquares test container:
Template270

This is the corresponding template – it’s called it ‘Audi’ because it looks like the gauges I stare at when commuting:

    <Style x:Key="Audi"
           TargetType="controls:RadialGauge">
        <Setter Property="MinAngle"
                Value="-180" />
        <Setter Property="MaxAngle"
                Value="90" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:RadialGauge">
                    <Viewbox>
                        <Grid x:Name="PART_Container"
                              Height="200"
                              Width="200"
                              Background="Transparent">
                            <Grid.RowDefinitions>
                                <RowDefinition />
                                <RowDefinition />
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>

                            <!-- Scale -->
                            <Path Name="PART_Scale"
                                  Stroke="{TemplateBinding ScaleBrush}"
                                  StrokeThickness="{TemplateBinding ScaleWidth}"
                                  Grid.ColumnSpan="2"
                                  Grid.RowSpan="2" />

                            <!-- Trail -->
                            <Path Name="PART_Trail"
                                  Stroke="{TemplateBinding TrailBrush}"
                                  StrokeThickness="{TemplateBinding ScaleWidth}"
                                  Grid.ColumnSpan="2"
                                  Grid.RowSpan="2" />

                            <!-- Value and Unit -->
                            <TextBlock Name="PART_ValueText"
                                       Foreground="{TemplateBinding ValueBrush}"
                                       FontSize="40"
                                       FontWeight="SemiBold"
                                       Text="{TemplateBinding Value}"
                                       TextAlignment="Center"
                                       VerticalAlignment="Center"
                                       Grid.Column="1"
                                       Grid.Row="1" />
                            <TextBlock Foreground="{TemplateBinding UnitBrush}"
                                       FontSize="16"
                                       FontWeight="Light"
                                       TextAlignment="Right"
                                       Text="{TemplateBinding Unit}"
                                       VerticalAlignment="Top"
                                       Margin="0 2 0 0"
                                       Grid.Column="1"
                                       Grid.Row="1" />
                        </Grid>
                    </Viewbox>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Here are some examples of other templates:

Template180

Template360

It has a NuGet package

The Radial Gauge is distributed through the Microsoft.Uwp.Toolkit.UI.Controls NuGet package:

NuGet

Make sure to install v1.1.0 or later.

As you see, the UWP Community Toolkit is a powerful and flexible control. If you want a playground to test some styles and templates, then checkout this sample app.

Enjoy!