Category Archives: UWP Community Toolkit

An elementary Dialog Service for UWP

In this article we present a Service that provides the elementary Modal Views in a UWP application. The dialogs and flyouts guidance learns us that dialogs are UI overlays that provide contextual app information and block interaction with the app window until they are explicitly dismissed. That makes them different from Flyouts, Tooltips or Toast Notifications, which are easily dismissed or even close automatically. Dialogs are typically used in the following scenarios, and these are exactly the views that are exposed by the ModalView service:

  • to display an important message to the user,
  • to request for confirmation from the user,
  • or to request long and short text input from the user.

Dialogs should be callable from anywhere inside your app’s code base: from a View, from a UserControl, from a ViewModel, from Business Logic, etcetera. In an MVVM(ish) architecture functionality like this typically ends up in a so-called Service. Here’s where I located the service in my Visual Studio solution, and the class diagram that shows its API:

ServiceAPI

The ModalView service comes with a small sample app that calls most of the methods in its API. Here’s how that sample app looks like:

MainPage

Basically we need a control to display some text(s), one, two, or three buttons, an optional input field, and an overlay to cover the app window to make it inaccessible. Well, that’s exactly what ContentDialog does. By the way: try to get rid of the old Windows 8 MessageDialog.

To open a message box, al you need to do is provide values for the Title, the Content (subTitle) and the CloseButtonText of the ContentDialog, call ShowAsyc(), and … sit and wait:

public static async Task MessageDialogAsync(
	string title, 
	string message, 
	string buttonText)
{
    var dialog = new ContentDialog
    {
        Title = title,
        Content = message,
        CloseButtonText = buttonText
    };

    await dialog.ShowAsync();
}

That’s exactly what the ModalView service does, and it provides an overload with a default value (“OK”) for the close button text. Here’s the service call from the sample app:

await ModalView.MessageDialogAsync(
	"Ready to go?", 
	"Place a cat, a flask of poison, and a radioactive source in a sealed box.", 
	"Got it");

And this is how it looks like:

MessageBox

The call to open a confirmation box is almost identical. There are only two differences:

  • instead of a single button, there will be two (yes, no) or three (yes, no, cancel), and
  • the caller would want to know which of these buttons was pressed.

All we need to do is provide values for PrimaryButtonText and SecondaryButtonText. You don’t need to specify IsSecondaryButtonEnabled (and the corresponding setting for the other buttons). If you don’t specify a text, the button will not appear. Here’s the service’s implementation of ConfirmationDialogAsync, which returns a nullable boolean to represent the yes/no/cancel response from the user, translated from ContentDialogResult:

public static async Task<bool?> ConfirmationDialogAsync(
	string title, 
	string yesButtonText, 
	string noButtonText, 
	string cancelButtonText)
{
    var dialog = new ContentDialog
    {
        Title = title,
        //IsPrimaryButtonEnabled = true,
        PrimaryButtonText = yesButtonText,
        SecondaryButtonText = noButtonText,
        CloseButtonText = cancelButtonText
    };
    var result = await dialog.ShowAsync();

    if (result == ContentDialogResult.None)
    {
        return null;
    }

    return (result == ContentDialogResult.Primary);
}

Again, there are overloads that use default values for the buttons: the not highly original “Yes”, “No”, and “Cancel”.

Here’s how the sample app opens a two-button confirmation dialog:

private bool? confirmed;

confirmed = await ModalView.ConfirmationDialogAsync(
        "Are you planning to open the box?",
        "Sure",
        "No, thanks"
   );

Here’s the result:

2ButtonConfirmation

When the app window becomes too small in height or width, the ContentDialog automagically snaps to the sides, like this:

Stretched

Here’s a call for a three-button confirmation dialog. It also shows how to create a multi-line title:

confirmed = await ModalView.ConfirmationDialogAsync(
        "So, what's the status the cat?\nHint: use Quantum Mechanics.",
        "It's alive",
        "It's dead",
        "It's both"
    );

And this is how it looks like at runtime:

3ButtonConfirmation

To transform a content dialog into an input dialog, it suffices to replace the default text Content by an input control, such as a TextBox. The ModalView Service provides two input dialogs: one for a string value, and one for a larger text value. Feel free to add your own versions to request for a number, a date, or a selection from a list.

Here’s the implementation of InputStringDialogAsync, it requests for a string without line breaks (hence value for AcceptsReturn). You can provide a default value for the response, and we will place the caret at the end through SelectionStart:

public static async Task<string> InputStringDialogAsync(
	string title, 
	string defaultText, 
	string okButtonText, 
	string cancelButtonText)
{
    var inputTextBox = new TextBox
    {
        AcceptsReturn = false,
        Height = 32,
        Text = defaultText,
        SelectionStart = defaultText.Length,
        BorderThickness = new Thickness(1),
        BorderBrush = new SolidColorBrush((Color)Application.Current.Resources["CustomDialogBorderColor"])
    };
    var dialog = new ContentDialog
    {
        Content = inputTextBox,
        Title = title,
        IsSecondaryButtonEnabled = true,
        PrimaryButtonText = okButtonText,
        SecondaryButtonText = cancelButtonText
    };

    if (await dialog.ShowAsync() == ContentDialogResult.Primary)
    {
        return inputTextBox.Text;
    }
    else
    {
        return string.Empty;
    }
}

Here’s the call from the sample app:

private string inputString;

inputString = await ModalView.InputStringDialogAsync(
        "How do you want to call this phenomenon?",
        "Verschränkung",
        "Claim",
        "Forget it"
    );

And here’s the corresponding UI:

StringInput

The InputTextDialogAsync method listens for a possibly longer text input, and looks very similar to the previous one. The input TextBox is higher, accepts line feeds, and does TextWrapping:

public static async Task<string> InputTextDialogAsync(
	string title, 
	string defaultText, 
	string okButtonText, 
	string cancelButtonText)
{
    var inputTextBox = new TextBox
    {
        AcceptsReturn = true,
        Height = 32 * 6,
        Text = defaultText,
        TextWrapping = TextWrapping.Wrap,
        SelectionStart = defaultText.Length,
        Opacity = 1,
        BorderThickness = new Thickness(1),
        BorderBrush = new SolidColorBrush((Color)Application.Current.Resources["CustomDialogBorderColor"])
    };
    var dialog = new ContentDialog
    {
        Content = inputTextBox,
        Title = title,
        IsSecondaryButtonEnabled = true,
        PrimaryButtonText = okButtonText,
        SecondaryButtonText = cancelButtonText
    };

    if (await dialog.ShowAsync() == ContentDialogResult.Primary)
    {
        return inputTextBox.Text;
    }
    else
    {
        return string.Empty;
    }
}

By the way: if you try to use WrapWholeWords as a value forTextBox.TextWrapping an exception is thrown. You can guess what my first assignment was…

Here’s the call from the sample app:

var inputText = await ModalView.InputTextDialogAsync(
        "What whas your point actually?",
        "Some large string containing line break (\n) characters."
    );

And the corresponding UI:

TextInput

Did you observe that I slightly pimped the default style of the ContentDialog? To add some contrast –and to get rid of the default AccentColor– I applied a custom BorderBrush to the dialog itself and to the input TextBoxes. I also added a DropShadowPanel from UWP Community Toolkit around it. The new Style is available in a ResourceDictionary (don’t forget to register it in your app.xaml). Here are the upgraded parts:


<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:toolkit="using:Microsoft.Toolkit.Uwp.UI.Controls">

    <!-- Custom style for Windows.UI.Xaml.Controls.ContentDialog -->
    <Color x:Key="CustomDialogBorderColor">#FFF05F66</Color>
    <SolidColorBrush x:Key="CustomDialogBorderBrush"
                     Color="#FFF05F66" />
    <Color x:Key="CustomDialogBackgroundColor">#FF4F2316</Color>
    <SolidColorBrush x:Key="CustomDialogBackgroundBrush"
                     Color="#FF4F2316" />

    <!-- From  \(Program Files)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\10.0.10586.0\Generic  -->
    <Style TargetType="ContentDialog">
        <!-- .. -->
            <Setter.Value>
                <ControlTemplate TargetType="ContentDialog">
                    <Border x:Name="Container">
                        <!-- ... -->
                        <Grid x:Name="LayoutRoot">
                            <!-- Added -->
                            <toolkit:DropShadowPanel Color="{StaticResource CustomDialogBorderColor}"
                                                     BlurRadius="20">
                                <!-- Modified Background and BorderBrush -->
                                <Border x:Name="BackgroundElement"
                                        Background="{StaticResource CustomDialogBackgroundBrush}"
                                        FlowDirection="{TemplateBinding FlowDirection}"
                                        BorderThickness="{ThemeResource ContentDialogBorderWidth}"
                                        BorderBrush="{StaticResource CustomDialogBorderBrush}"
                                        MaxWidth="{TemplateBinding MaxWidth}"
                                        MaxHeight="{TemplateBinding MaxHeight}"
                                        MinWidth="{ThemeResource ContentDialogMinWidth}"
                                        MinHeight="{ThemeResource ContentDialogMinHeight}">
                                    <!-- .. -->
                                </Border>
                            </toolkit:DropShadowPanel>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

This is how the InputStringDialog looks like, without the customization:

StringInputDefaultStyle

Feel free to add your company’s or app’s logo to the template, and/or a background image. Here’s an example of ModalView in a real Store app:

RealAppSample

Be aware: if you retemplate built-in controls you have to closely follow up and verify your styles against all the new SDK-releases. Future native ContentDialogs may have a different structure or visual states, or will make use of new features like the AcrylicBrush (for the overlay) or RevealBrush on the button(s). I’m pretty sure you wouldn’t want to miss these.

For the sake of completeness, here’s the XAML code behind the sample app’s buttons. It uses different types of commanding and binding, just to illustrate its architectural neutrality:

<!-- Click Event Handler in the View -->
<Button Content="Message Dialog"
        Click="MessageBox_Click" />
<!-- Binding to Command in ViewModel -->
<Button Content="2-Button Confirmation Dialog"
        Command="{Binding ConfirmationCommandYesNo}" />
<!-- Binding to Command in ViewModel -->
<Button Content="3-Button Confirmation Dialog"
        Command="{Binding ConfirmationCommandYesNoCancel}" />
<!-- x:Bind to Command in ViewModel -->
<Button Content="String Input Dialog"
        Command="{x:Bind ViewModel.InputStringCommand}" />
<!-- x:Bind to Method in ViewModel -->
<Button Content="Text Input Dialog"
        Click="{x:Bind ViewModel.InputText_Click}" />

The ModalView Service and its sample app live in this GitHub repo.

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!