Stretching the WinUI 3 Expander control

In this article we take the new WinUI 3 Expander control through a couple of scenarios that you don’t find in the documentation. We have built a Windows Desktop app that demonstrates

  • an attempt to build a horizontal Expander,
  • grouping Expanders into an Accordion control, and
  • grouping Expanders into a Wizard control, using data templates and binding.

Here’s how that app looks like:

From the -excellent- design document we learn that the Expander control lets you show or hide less important content that’s related to a piece of primary content that’s always visible. The Header is always visible and behaves like a ToggleButton. It allows the user to expand and collapse the Content area with the secondary content. The Expander UI is commonly used when display space is limited and when information or options can be grouped together.

Most UI stacks and component libraries have an Expander control – in Bootstrap it’s called Collapsible Panel. In the WinUI ecosystem the Expander is relatively new. Its proposal came only at the end of 2020. The control was built to these specs, and eventually appeared in the latest releases of WinUI 3. Before, an expander control was available via Community Toolkit.

We decided to take this new control for a test drive through some scenarios.

Getting started with the Expander Control

Before starting to experiment with the Expander control, take your time to read its documentation and open the WinUI 3 Gallery app. Here’s how the Expander demo looks like – pretty basic:

Our own sample app starts with an Intro page. Here’s the XAML definition of the very first Expander – again pretty basic:

<Expander>
    <Expander.Header>
        <TextBlock Foreground="Red">Red</TextBlock>
    </Expander.Header>
    <Grid Background="Red"
            Height="100"
            Width="200"
            CornerRadius="4" />
</Expander>

As you see in the next composite screenshot, the Header width is not aligned to the width of the Content. This looks more like a Button with a Flyout than like a ‘classic’ Expander:

If you don’t like this default, you can give the control or the Header a Width, or place it in a parent Panel that has its HorizontalContentAlignment set to Stretch. Here’s the XAML of the second expander on the Intro page – the one with the orange. The header has a fixed width that corresponds to the width of the Content. We also added an icon in that Header:

<Expander Width="234">
    <Expander.Header>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <BitmapIcon UriSource="/Assets/Orange.png"
                        ShowAsMonochrome="False"
                        Height="24" />
            <TextBlock Foreground="Orange"
                        HorizontalAlignment="Right"
                        VerticalAlignment="Center"
                        Grid.Column="1">Orange</TextBlock>
        </Grid>
    </Expander.Header>
    <Grid Background="Orange"
            Height="100"
            Width="200"
            CornerRadius="4" />
</Expander>

In the Intro page we stacked several expanders on top of each other – a common use of expanders that we elaborate on later in this article.

Building a Horizontal Expander

Unlike the ‘old’ WPF Expander, the WinUI 3 Expander only expands vertically: its ExpandDirection is limited to up and down. We made an attempt to implement a horizontal Expander – without altering the original XAML template. We hosted the control in a ContentControl that we submitted to a RenderTransform with a 90° Rotation to the left, and then rotated its Content back 90° to the right:

<ContentControl x:Name="HorizontalExpander"
                VerticalContentAlignment="Top">
    <ContentControl.RenderTransform>
        <RotateTransform Angle="270"
                            CenterX="117"
                            CenterY="117" />
    </ContentControl.RenderTransform>
    <Expander Width="234">
        <Grid Background="LightPink"
                Height="100"
                Width="200"
                CornerRadius="4">
            <BitmapIcon UriSource="/Assets/Heart.png"
                        ShowAsMonochrome="False"
                        VerticalAlignment="Center"
                        HorizontalAlignment="Center"
                        Height="80">
                <BitmapIcon.RenderTransform>
                    <RotateTransform Angle="90"
                                        CenterX="40"
                                        CenterY="40" />
                </BitmapIcon.RenderTransform>
            </BitmapIcon>
        </Grid>
    </Expander>
</ContentControl>

This is how the result looks like – it’s the pink one:

This horizontal expander looks functional, but it does not behave the same as the original. Under the hood, it has become a square control. We had to give the text box on its right a negative margin:

<TextBlock Text="Hello there"
            RelativePanel.RightOf="HorizontalExpander"
            Margin="-166 0 0 0" />

Also, it does not push its neighbors aside when expanding.

If you want to build a better WinUI 3 horizontal expander, you may get some inspiration in the source code of the Windows Community Toolkit UWP Expander. Just like its WPF and Silverlight ancestor, this one also expands to left and right.

Building an Accordion Control

A traditional usage of Expander controls is stacking some instances on top of each other to form an Accordion. Silverlight and WPF had one in their Toolkit, Syncfusion has an Accordion for UWP, and ComponentOne has one for WinUI.

Let’s see how far we can get with building an Expander-based WinUI 3 Accordion from scratch. [Spoiler alert: pretty far.]

Here are our requirements:

  • there should always be one accordion item open,
  • all other items should be closed, and
  • the accordion should always fill its vertical space.

Here’s how the AccordionPage in our sample app looks like:

The core XAML structure of the control consists of Expanders inside an ItemsControl inside some Panel to horizontally stretch them all, and decorated with a ScrollViewer:

<RelativePanel x:Name="Host"
                VerticalAlignment="Stretch"
                HorizontalAlignment="Stretch">
    <ScrollViewer x:Name="ScrollViewer">
        <ItemsControl x:Name="Accordion"
                        Width="480">
            <Expander HorizontalAlignment="Stretch"
                        HorizontalContentAlignment="Stretch">
                <!-- Header and Content here ... -->
            </Expander>
            <!-- More Expanders here ... -->
        </ItemsControl>
    </ScrollViewer>
</RelativePanel>

When the page opens, we add all expanders to a list, register an event handler, and open the first one by setting IsExpanded:

readonly List<Expander> _expanders = new();

private void AccordionPage_Loaded(object sender, RoutedEventArgs e)
{
    foreach (Expander expander in Accordion.Items)
    {
        _expanders.Add(expander);
        expander.Expanding += Expander_Expanding;
    }

    // Open the first one.
    ApplyScrollBar();
    _expanders[0].IsExpanded = true;
}

In the Expanding event handler, we loop through the list to close all expanders except the selected one. The open expander cannot be manually closed by clicking the header or the toggle button. It is forced to remain open by ‘locking’ its header:

private void Expander_Expanding(
     Expander sender, 
     ExpanderExpandingEventArgs args)
{
    foreach (var expander in _expanders)
    {
        // Close the others.
        expander.IsExpanded = sender == expander;

        // Force current to remain open by disabling the header.
        expander.IsLocked(sender != expander);
    }

    FillHeight(sender);
}

The IsLocked() in that code snippet is an extension method that we wrote:

public static void IsLocked(this Expander expander, bool locked)
{
    ((expander.Header as FrameworkElement).Parent as Control).IsEnabled = locked;
}

[Note: In a full-fledged Accordion control we would implement IsLocked as a dependency property.]

To ensure that the Accordion always fills its vertical space, we manipulate the height of the Content of the one open Expander. We couldn’t find a way to do this with stretching and binding, so we ended up calculating and explicitly setting the Height. When you look at the default XAML template there’s padding on top, padding on the bottom, and a border around the core content to consider in the calculation. So, when the app starts, we measure the height of the Accordion with all its expanders closed, and we store the total vertical padding around the content:

double _closedAccordionHeight;
readonly double _minimumContentHeight = 48;
double _contentPadding;

private void AccordionPage_Loaded(object sender, RoutedEventArgs e)
{
    _closedAccordionHeight = Accordion.ActualHeight;

    // ...

    _contentPadding = _expanders[0].Padding.Top + _expanders[0].Padding.Bottom + 2; // Border?

    Host.SizeChanged += Host_SizeChanged;
}

When the hosting panel is resized, and when a new expander opens, we calculate and set the height of the current item, respecting a minimum height for the content:

private void FillHeight(Expander expander)
{
    expander.SetContentHeight(Math.Max(
        _minimumContentHeight, 
        Host.ActualHeight - _closedAccordionHeight - _contentPadding));
}

Here’s the implementation of our SetContentHeight() extension method:

public static void SetContentHeight(this Expander expander, double contentHeight)
{
    (expander.Content as FrameworkElement).Height = contentHeight;
}

Here’s the event handler that’s fired when the page is resized;

private void Host_SizeChanged(object sender, SizeChangedEventArgs e)
{
    ApplyScrollBar();
    FillHeight(Current);
}

The ApplyScrollBar() call is there to ensure that the scroll bar does not pop up unnecessarily during the Expanding event where two of the expanders may be open simultaneously. Here’s the code:

private void ApplyScrollBar()
{
    if (Host.ActualHeight >= _closedAccordionHeight + _minimumContentHeight + _contentPadding)
    {
        ScrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
    }
    else
    {
        ScrollViewer.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
    }
}

Here’s how the height adjustment looks like at runtime:

We did not refactor the code into a stand-alone custom control, but we believe that this would be a relatively easy task. A future ‘WinUI 3 Community Toolkit’ could have an Accordion control…

In meantime we’re taking our Expander test a step further, to build the core code for a Wizard control.

Building a Wizard Control

In this last Expander experiment we tried to figure out how Expanders deals with data templates and data binding. We reenacted the old ASP.NET Wizard control, designed to guide the user through a sequence of steps. It looks like an Accordion with extra controls in each expander’s Header (to display the status/result of the step) and a set of fixed controls in each expander’s Content (description of the step, buttons to move back and forth). Here’s how it looks like:

Here are some or our requirements. Most of these were at least partly implemented:

  • multiple steps may be open,
  • the control occupies all available horizontal space,
  • the result of a step appears in its header,
  • a description of the step appears in its content,
  • the content shows the navigation buttons,
  • the navigation button text depends on the position (e.g., no ‘go back’ in the first step),
  • steps may be defined as ‘execute only once’ (e.g., payment)

[Disclaimer: our code base was written to evaluate the Expander. The Wizard code is not complete and it contains mockups.]

We started with designing a set of lightweight bindable viewmodels for wizard and steps. Here’s a class diagram:

[Note: here’s how to add the Class Diagram feature to your Visual Studio 2022]

We used Microsoft MVVM Toolkit to implement change notification and ‘bindability’. Here’s part of the code for the WizardViewModel. It hosts a list of viewmodels for the Steps and the logic to determine the Next and Previous of a particular step:

internal partial class WizardViewModel : ObservableObject
{
    [ObservableProperty]
    private List<WizardStepViewModel> _steps = new();

    internal WizardStepViewModel NextStep(WizardStepViewModel step)
    {
        var stepIndex = _steps.IndexOf(step);
        if (stepIndex < _steps.Count - 1)
        {
            return _steps[stepIndex + 1];
        }

        return null;
    }

    internal WizardStepViewModel PreviousStep(WizardStepViewModel step)
    {
        var stepIndex = _steps.IndexOf(step);
        if (stepIndex > 0)
        {
            return _steps[stepIndex - 1];
        }

        return null;
    }
}

The WizardStepViewModel contains the properties and button labels to be displayed, as well as the commands behind the navigation buttons and whether these buttons should be enabled. When the ‘continue’ button is clicked, we validate and commit the current step and then navigate forward:

private void Next_Executed()
{
    if (!Commit())
    {
        return;
    }

    var next = _wizard.NextStep(this);

    if (next is null)
    {
        return;
    }

    IsActive = false;
    next.IsActive = true;
}

public bool Commit()
{
    // Validate and persist Model
    // ...

    // Update Status - Mockup
    Status = "Succes";

    // Return result
    return true;
}

The basic XAML structure of the Wizard is the same as in the Accordion: Panel, ScrollViewer, ItemsControl with Expanders. It just comes with more data binding and data templates:

<RelativePanel x:Name="Host"
                VerticalAlignment="Stretch"
                HorizontalAlignment="Stretch">
    <ScrollViewer x:Name="ScrollViewer"
                    HorizontalAlignment="Stretch"
                    HorizontalContentAlignment="Stretch">
        <ItemsControl x:Name="Wizard"
                        HorizontalAlignment="Stretch"
                        HorizontalContentAlignment="Stretch">
            <Expander HeaderTemplate="{StaticResource WizardHeaderTemplate}"
                        ContentTemplate="{StaticResource WizardContentTemplate}"
                        HorizontalAlignment="Stretch"
                        HorizontalContentAlignment="Stretch"
                        DataContext="{x:Bind ViewModel.Steps[0], Mode=OneWay}"
                        IsExpanded="{x:Bind ViewModel.Steps[0].IsActive, Mode=TwoWay}">
                <!-- Custom Content here ... -->
            </Expander>
            <!-- More Expanders here ... -->
        </ItemsControl>
    </ScrollViewer>
</RelativePanel>

Via a DataTemplate the Header of each Expander has an extra text block displaying the Status (e.g. execution status, result, validation error) of the corresponding step:

<DataTemplate x:Name="WizardHeaderTemplate"
                x:DataType="viewmodels:WizardStepViewModel">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="auto" />
        </Grid.ColumnDefinitions>
        <TextBlock Text="{x:Bind Name, Mode=OneWay}" />
        <TextBlock Text="{x:Bind Status, Mode=OneWay}"
                    FontStyle="Italic"
                    Grid.Column="1" />
    </Grid>
</DataTemplate>

The content is also displayed through a data template. It has a description for the current step, buttons to move through the scenario, and a ContentPresenter for the core content:

<DataTemplate x:Name="WizardContentTemplate"
                x:DataType="viewmodels:WizardStepViewModel">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="auto" />
        </Grid.ColumnDefinitions>

        <!-- Custom Content -->
        <ContentPresenter Content="{Binding Content, RelativeSource={RelativeSource TemplatedParent}}" />

        <!-- Default Content -->
        <TextBlock Text="{x:Bind Description, Mode=OneWay}"
                    Grid.Column="1" />
        <StackPanel Orientation="Horizontal"
                    HorizontalAlignment="Right"
                    VerticalAlignment="Bottom"
                    Grid.Row="1"
                    Grid.Column="1">
            <Button Command="{x:Bind PreviousCommand, Mode=OneWay}"
                    Content="{x:Bind PreviousLabel, Mode=OneWay}" />
            <Button Command="{x:Bind NextCommand, Mode=OneWay}"
                    Content="{x:Bind NextLabel, Mode=OneWay}"
                    Margin="10 0 0 0" />
        </StackPanel>
    </Grid>
</DataTemplate>

For custom content we went for simple images. Here’s an example:

<Expander HeaderTemplate="{StaticResource WizardHeaderTemplate}"
            ContentTemplate="{StaticResource WizardContentTemplate}"
            HorizontalAlignment="Stretch"
            HorizontalContentAlignment="Stretch"
            DataContext="{x:Bind ViewModel.Steps[1], Mode=OneWay}"
            IsExpanded="{x:Bind ViewModel.Steps[1].IsActive, Mode=TwoWay}"
            Margin="0 4 0 0">
    <BitmapIcon UriSource="/Assets/Seating.png"
                ShowAsMonochrome="False"
                VerticalAlignment="Stretch"
                HorizontalAlignment="Center"
                Height="240" />
</Expander>

When the page opens, we populate the main ViewModel:

var viewModel = new WizardViewModel();
viewModel.Steps = new List<WizardStepViewModel>()
    {
    new WizardStepViewModel(viewModel)
    {
        IsActive = true,
        AllowReturn = true,
        Name = "Your movie",
        Description = "Select a movie and a time"
    },
    new WizardStepViewModel(viewModel)
    {
        AllowReturn = true,
        Name = "Your seat",
        Description = "Select a row and a seat"
    },
    new WizardStepViewModel(viewModel)
    {
        AllowReturn = false,
        Name = "Yourself",
        Description = "Provide contact information"
    },
    new WizardStepViewModel(viewModel)
    {
        AllowReturn = false,
        Name = "Your money",
        Description = "Select a payment method and ... pay"
    }
};
DataContext = viewModel;

There’s no further code behind, except to stretch the Wizard horizontally:

private void Host_SizeChanged(object sender, SizeChangedEventArgs e)
{
    ScrollViewer.Width = Host.ActualWidth;
}

By leveraging the new Expander control we were able to build the foundations of a Wizard control with remarkably few lines of code. Here’s how our Wizard looks like in action:

Not too bad, right?

The Verdict

The Expander control is a great new member of the WinUI 3 ecosystem. It definitely passed our stress test. We identified some room for improvement however. Future versions of the control could benefit from

  • horizontal expansion out-of-the-box,
  • a more complete set of events (Expanded, Collapsing), and
  • an easy way to lock the Header in closed or open position.

Our sample app lives here on GitHub.

Enjoy!

Multi-Windowing in WinUI 3

In this article we take a look at multi-windowing in a WinUI 3 Desktop application. The ability for an app to control multiple windows -possibly over multiple monitors- is a key differentiator to Web development. We’ll demonstrate

  • opening multiple windows in a WinUI 3 app,
  • messaging between components using MVVM Toolkit and Dependency Injection, and
  • controlling window properties using WinUIEx.

We built a sample WinUI 3 Desktop app. This is how it looks like on Windows 10:

Here’s the list of the Nuget packages that we installed in the project:

The app comes with some Pages displayed in two Window classes:

  • the default Main Window (it’s called Shell in our app) is opened when the app starts, and
  • a DetailWindow is opened with every click on the launch button on our home page.

Most WinUI apps only have a main window. It’s often populated with a NavigationView menu and a Frame to host the different page types. Our sample app has a similar main window, but it will allow opening extra detail windows. Here’s the XAML structure of that detail window: unsurprisingly a Window with a Page.

<Window x:Class="XamlBrewer.WinUI3.MultiWindow.Sample.Views.DetailWindow">
    <Page x:Name="Page">

        <Page.DataContext>
            <viewmodels:DetailPageViewModel />
        </Page.DataContext>

        <Grid x:Name="Root">
            <!-- ... -->
        </Grid>
    </Page>
</Window>

Opening extra windows

To open a new window, just instantiate one and call Activate(). Here’s the core code behind our launch button:

private void LaunchButton_Click(object sender, RoutedEventArgs e)
{
    DetailWindow window = new();
    // ..
    window.Activate();

    Unloaded += (sender, e) => { window.Close(); };
}

All Window instances run on their own foreground thread. When you close the main window, the app does not stop. The detail windows remain open and keep the app running. That might not always be the desired behavior. In our sample app, we ensure that the satellite detail windows are closed together with the home page. We scheduled their Close() in an Unloaded event handler on the home page. This is convenient, but don’t consider this a best practice. Using an event handler implies that the home page is holding references to the detail windows – which will NOT be garbage collected when you close them. There are better ways of messaging available; we’ll come to that a bit later.

Controlling window properties

If an app requires multi-window support, some of the windows will host tools, extra controls, or dashboards. You may want to control the size and behavior of these. In the current state of WinUI 3 this involves low level interop with framework dll’s and dealing with window handles. Fortunately, Morten Nielsen already did the heavy lifting and shared it in the WinUIEx project -a set of WinUI 3 extension methods on windowing, authentication, and testing.

Here’s how we control the size, position, and the command bar behavior of our detail windows. All of these are WinUIEx extension method calls:

DetailWindow window = new();
window.SetIsMaximizable(false);
window.SetIsResizable(false);
window.SetWindowSize(360, 240);
window.CenterOnScreen();

Messaging

With your models, view, and services possibly scattered around multiple windows, you may want to consider a decent messaging infrastructure to allow communication between these components. In our sample app we didn’t want to keep a central list of detail windows. However, we do need to send a set of messages between them, and also to and from the home page and main window.

MVVM Toolkit is a great choice for this task. Let’s use it in a couple of scenarios.

Window to Window communication

Our main window has a button to switch the Theme. We broadcast the theme change to all other windows in the app. MVVM Toolkit Messenger is the right component for this job.

First, we defined a message class for transporting the theme after a change – a ValueChangedMessage:

public class ThemeChangedMessage : ValueChangedMessage<ElementTheme>
{
    public ThemeChangedMessage(ElementTheme value) : base(value)
    {
    }
}

Via MVVM Toolkit base classes and interfaces, Models and ViewModels have multiple ways of getting access to the Messager service. Since they rely on inheritance, most of these ways are not available to Window instances. We called a little help from our friends Microsoft.Extensions.DependencyInjection and the MVVM Toolkit Ioc service provider helper.

When our app starts, a call to ConfigureServices() ensures that there’s an inversion-of-control container with a Messenger instance available to all components of the app:

Ioc.Default.ConfigureServices
    (new ServiceCollection()
        .AddSingleton<IMessenger>(WeakReferenceMessenger.Default)
        .BuildServiceProvider()
    );

When the Theme changes, the main window fetches the Messenger instance with GetService() and broadcasts the message with Send(). It does not need to know if there are other windows or how much there are:

Ioc.Default.GetService<IMessenger>().Send(
    new ThemeChangedMessage(Root.ActualTheme)
);

Let’s take a look at the receiver. When a detail window instance starts, it approaches the Messenger to Register() a delegate to be called on an incoming ThemeChangedMessage:

Root.ActualThemeChanged += Root_ActualThemeChanged;

var messenger = Ioc.Default.GetService<IMessenger>();

messenger.Register<ThemeChangedMessage>(this, (r, m) =>
{
    Root.RequestedTheme = m.Value;
});

// Don't forget!
Closed += (sender, e) => { messenger.UnregisterAll(this); };

When a detail window is closed we call UnregisterAll() to ensure all further messages are ignored by it. It may take a while before it is garbage collected -especially with our Unloaded event handler in the home page- and we don’t want closed windows to crash our app.

Here’s the result of switching the theme from dark to light:

Here’s a second scenario for window-to-window communication. To visually keep track of the detail windows, our home page has a button that makes all of them move to the foreground. Here’s the message definition – a plain C# class without the need for a particular base class or interface:

public class RaiseMessage { }

Here’s the call behind the button on the home page:

private void RaiseButton_Click(object sender, RoutedEventArgs e)
{
    Ioc.Default.GetService<IMessenger>().Send(new RaiseMessage());
}

Here’s the registration and response of the detail windows – and an opportunity to demonstrate yet another useful WinUIEx extension:

messenger.Register<RaiseMessage>(this, (r, m) =>
{
    this.SetForegroundWindow();
});

This is how it looks like at runtime. Here’s a screenshot before the call:

And here’s the ‘after’ shot:

ViewModel to ViewModel communication

Our next use case is on ViewModel-to-ViewModel communication. We have more help from MVVM Toolkit, since this is right in its core business. Here’s the scenario: our detail windows are actually mining bitcoin diamonds. When diamonds are found, a NumberBox is filled for the quantity and a button pressed. The information will then be broadcasted to the ecosystem.

Here’s the core XAML structure of the page in the detail window:

<Page>
    <Page.DataContext>
        <viewmodels:DetailPageViewModel />
    </Page.DataContext>
    <Grid x:Name="Root">
        <!-- Some content omitted ... -->
        <RelativePanel VerticalAlignment="Top"
                        HorizontalAlignment="Right">
            <NumberBox x:Name="DiamondsBox"
                        Header="Diamonds"
                        Value="{x:Bind ViewModel.Diamonds, Mode=TwoWay}" />
            <Button RelativePanel.Below="DiamondsBox"
                    Command="{x:Bind ViewModel.DiamondsFound}">Collect</Button>
        </RelativePanel>
    </Grid>
</Page>

Here’s the message definition – a ValueChangedMessage that transports an integer:

public class AssetsChangedMessage : ValueChangedMessage<int>
{
    public AssetsChangedMessage(int value) : base(value)
    { }
}

Here’s how (part of) the detail page ViewModel looks like:

public partial class DetailPageViewModel : 
    ObservableRecipient
{
    [ObservableProperty]
    private int diamonds;

    public DetailPageViewModel()
    {
        DiamondsFound = new RelayCommand(DiamondsFound_Executed);

        Messenger.Register(this);
    }

    // This code will be generated by the field attribute.
    // public int Diamonds
    // {
    //     get => diamonds
    //     set => SetProperty(ref diamonds, value);
    // }

    public ICommand DiamondsFound { get; }

    private void DiamondsFound_Executed()
    {
        Messenger.Send(new AssetsChangedMessage(Diamonds));
    }
}

Via inheritance from ObservableRecipient, the ViewModel gets access to change notification helpers (from ObservableObject) and to the messaging infrastructure – a Messenger property. Notice that the diamonds field is decorated with ObservableProperty. This conveniently generates the corresponding getter and setter. Via an ICommand property that is backed up by a RelayCommand, the method that sends the message is bound to the button in the View – all according to the MVVM pattern.

Here’s the message registering and receiving part in the ViewModel of the home page:

public partial class HomePageViewModel : 
    ObservableRecipient, 
    IRecipient<AssetsChangedMessage>
{
    [ObservableProperty]
    private int wealth;

    public HomePageViewModel()
    {
        Messenger.Register(this);
    }

    // This method is auto-registered by implementing the interface.
    public void Receive(AssetsChangedMessage message)
    {
        Wealth += message.Value;
    }
}

This ViewModel also inherits from ObservableRecipient. On top of that it implements IRecipient<T>. This conveniently auto-registers the Receive<TMessage>() method to the corresponding incoming message type. The home page updates its stock – the Wealth property. This property is also generated through the ObservableProperty attribute on the field variable.

For the sake of completeness (and definitely also for fun) we added another ViewModel-to-ViewModel messaging scenario. When a detail ViewModel finds a diamond, all of its colleagues get excited for a few seconds. Here’s the corresponding code:

public partial class DetailPageViewModel : 
    ObservableRecipient, 
    IRecipient<AssetsChangedMessage>
{
    [ObservableProperty]
    private bool isExcited;

    public DetailPageViewModel()
    {
        DiamondsFound = new RelayCommand(DiamondsFound_Executed);

        Messenger.Register(this);
    }

    public void Receive(AssetsChangedMessage message)
    {
        IsExcited = true;
        CoolDown();
    }

    private void DiamondsFound_Executed()
    {
        Messenger.Unregister<AssetsChangedMessage>(this); // Don't react to own message.
        Messenger.Send(new AssetsChangedMessage(Diamonds));
        Messenger.Register(this);
    }
}

There’s no need to go into details here: it’s all stuff we just covered before. Here’s how the result looks like.

For the record: the animation in the home page is not Lottie based. Lottie is not yet available for WinUI 3. In mean time we have to help ourselves with animated GIFs.

Multi-windowing is an important feature in Windows development. WinUI 3 desktop apps that use multi-windowing will definitely benefit from the MVVM Toolkit Messenger and from the WinUIEx extension methods.

Our sample app lives here on GitHub.

Enjoy!

A Dialog Service for WinUI 3

I this article we build an MVVM DialogService in a WinUI 3 Desktop application. It comes with the following features:

  • Message Dialog, Confirmation Dialog, Input Dialog
  • Works with {Binding} and {x:Bind}
  • Bindable to Command and Event Handler
  • Callable from View, ViewModel, Service and more
  • Theme-aware

Here’s a screenshot from the corresponding demo app:

The core class of the dialog service is based on the ModalView static class that we built long ago in a similar solution for UWP. It programmatically creates ContentDialog instances for which you can provide the title, and the content of all buttons. Here’s an extract of the original code:

var dialog = new ContentDialog
{
    Title = title,
    PrimaryButtonText = yesButtonText,
    SecondaryButtonText = noButtonText,
    CloseButtonText = cancelButtonText
};

When you run this code in a WinUI 3 Desktop app, nothing really happens. You get a “This element is already associated with a XamlRoot, it cannot be associated with a different one until it is removed from the previous XamlRoot.” exception:

The screenshot above comes from a click event handler. The app crashes and tell you what’s wrong, and that’s OK. However, when using a Command to open the dialog, the binding engine swallows the exception, and the app continues to run without the dialog opening. It’s not a bug -it’s what the binding engine does- but it smells like a bug, so people logged issues in WinUI and people logged issues in Prism.

When programmatically instantiating a ContentDialog in WinUI 3 -even in a View or Page- you need to provide a XamlRoot. We decided to transform our static methods into extension methods for FrameworkElement. Not only does this class come with a XamlRoot property, it also has a RequestedTheme that we can pass to ensure that the dialog follows the app’s theme. Here’s the set of methods to open a message box – a content dialog with a title, a piece of text (the message), and one single button (the classic OK button). It returns no result.

public static async Task MessageDialogAsync(
     this FrameworkElement element, 
     string title, 
     string message)
{
    await MessageDialogAsync(element, title, message, "OK");
}

public static async Task MessageDialogAsync(this FrameworkElement element, string title, string message, string buttonText)
{
    var dialog = new ContentDialog
    {
        Title = title,
        Content = message,
        CloseButtonText = buttonText,
        XamlRoot = element.XamlRoot,
        RequestedTheme = element.ActualTheme
    };

    await dialog.ShowAsync();
}

For each of the dialog types, the call to the extension method in the ModalView class is more or less the same. Only the return type is different: void, bool, nullable bool, string, …

For the first test, we create a button in the View, and call a classic event handler:

<Button Content="Message Dialog"
        Click="MessageBox_Click" />

In the event handler, we use the page itself (this) as the Framework element to pass. Here’s the call:

private async void MessageBox_Click(object sender, RoutedEventArgs e)
{
    await this.MessageDialogAsync("All we are saying:", "Give peace a chance.", "Got it");
}

Here’s how the result looks like:

Our second dialog type is the ConfirmationDialog, with a title and two (Yes/No) or three (Yes/No/Cancel) buttons. Here’s the main extension method for this one:

public static async Task<bool?> ConfirmationDialogAsync(
     this FrameworkElement element, 
     string title, 
     string yesButtonText, 
     string noButtonText, 
     string cancelButtonText)
{
    var dialog = new ContentDialog
    {
        Title = title,
        PrimaryButtonText = yesButtonText,
        SecondaryButtonText = noButtonText,
        CloseButtonText = cancelButtonText,
        XamlRoot = element.XamlRoot,
        RequestedTheme = element.ActualTheme
    };
    var result = await dialog.ShowAsync();

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

    return (result == ContentDialogResult.Primary);
}

To test it, we added a ViewModel, set it as DataContext to the View, and added a button:

<Button Content="2-Button Confirmation Dialog"
        Command="{Binding ConfirmationCommandYesNo}" />

Our next step was finding an appropriate Framework element to pass – not easy in a ViewModel that is unaware of the View it’s bound to. In UWP we could use Window.Current (and its Content). In WinUI 3 Window.Current is still in the API but always returns null. As an alternative we declared a MainRoot property in our application class. It refers to the Content element of the main window of our app, that we traditionally named ‘Shell’. Here’s the declaration and initialization:

public partial class App : Application
{
    private Shell shell;

    public static FrameworkElement MainRoot { get; private set; }

    protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
    {
        shell = new Shell();
        shell.Activate();
        MainRoot = shell.Content as FrameworkElement;
    }
}

When you’re building a multi-window app, you will probably need to change that logic. Anyway, all ViewModels and Services now have access to a Framework element to pass to the dialog service. Here’s how the ViewModel from our sample app opens a 2-Button Confirmation Dialog via an AsyncRelayCommand:

public ICommand ConfirmationCommandYesNo => new AsyncRelayCommand(ConfirmationYesNo_Executed);

private async Task ConfirmationYesNo_Executed()
{
    var confirmed = await App.MainRoot.ConfirmationDialogAsync(
            "What Pantone color do you prefer?",
            "Freedom Blue",
            "Energizing Yellow"
        );
}

This is how the dialog looks like:

The overload (extension) method with three buttons, opens the dialog that you saw in the first screenshot. There’s no need to paste the code here, it’s similar to the previous one.

Let’s jump to another dialog type: an Input Dialog to request a string from the user. In this scenario, we programmatically set a TextBox as Content of the Dialog, and return its text when the Dialog closes:

public static async Task<string> InputStringDialogAsync(
     this FrameworkElement element, 
     string title, 
     string defaultText, 
     string okButtonText, 
     string cancelButtonText)
{
    var inputTextBox = new TextBox
    {
        AcceptsReturn = false,
        Height = 32,
        Text = defaultText,
        SelectionStart = defaultText.Length
    };
    var dialog = new ContentDialog
    {
        Content = inputTextBox,
        Title = title,
        IsSecondaryButtonEnabled = true,
        PrimaryButtonText = okButtonText,
        SecondaryButtonText = cancelButtonText,
        XamlRoot = element.XamlRoot,
        RequestedTheme = element.ActualTheme
    };

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

This time we use {x:Bind} to a command in the View:

<Button Content="String Input Dialog"
        Command="{x:Bind ViewModel.InputStringCommand}" />

Here’s the code in the ViewModel:

public ICommand InputStringCommand => new AsyncRelayCommand(InputString_Executed);

private async Task InputString_Executed()
{
    var inputString = await App.MainRoot.InputStringDialogAsync(
            "How can we help you?",
            "I need ammunition, not a ride.",
            "OK",
            "Forget it"
        );
}

And the result:

The last type of input dialog in this article, is a multi-line text input dialog – typically one that you would use to collect comments or remarks:

public static async Task<string> InputTextDialogAsync(
     this FrameworkElement element, 
     string title, 
     string defaultText)
{
    var inputTextBox = new TextBox
    {
        AcceptsReturn = true,
        Height = 32 * 6,
        Text = defaultText,
        TextWrapping = TextWrapping.Wrap,
        SelectionStart = defaultText.Length
    };
    var dialog = new ContentDialog
    {
        Content = inputTextBox,
        Title = title,
        IsSecondaryButtonEnabled = true,
        PrimaryButtonText = "Ok",
        SecondaryButtonText = "Cancel",
        XamlRoot = element.XamlRoot,
        RequestedTheme = element.ActualTheme
    };

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

For the sake of completeness, we’ll bind it to an event handler in the ViewModel:

<Button Content="Text Input Dialog"
        Click="{x:Bind ViewModel.InputText_Click}" />

Here’s the code in the ViewModel:

public async void InputText_Click(object sender, RoutedEventArgs e)
{
    var inputText = await App.MainRoot.InputTextDialogAsync(
            "What would Faramir say?",
            "“War must be, while we defend our lives against a destroyer who would devour all; but I do not love the bright sword for its sharpness, nor the arrow for its swiftness, nor the warrior for his glory. I love only that which they defend.”\n\nJ.R.R. Tolkien"
        );
}

And this is what it looks like at runtime:

With just a handful lines of code, we built a dialog service for WinUI 3 Desktop applications. Our sample solution lives here in GitHub. Feel free to add your own dialogs, like for numeric or date input.

Enjoy!

Drawing charts and diagrams with OxyPlot in WinUI 3

In this article we demonstrate how to use H.OxyPlot to draw diagrams and charts in a WinUI 3 .NET 6 desktop application. We created a sample app with different pages that cover:

  • the ‘Hello World’ diagram,
  • an overview of many of the supported charts,
  • working with an interactive model, and
  • instant theming support.

Here’s how the app looks like:

It’s our first Visual Studio 2022 .NET 6 app. In some of these pages we test drive the new version of the Microsoft MVVM Toolkit, but our focus remains on OxyPlot.

OxyPlot

OxyPlot is an Open Source .NET library for drawing diagrams and charts on multiple platforms. It comes with a core object model for models, series, axes, annotations etcetera. Further there’s a RenderContext / PlotView pair for each UI technology: WPF, WinForms, SkiaSharp, UWP, Xamarin, … Windows Phone and Silverlight. The different PlotView implementations are relatively simple – a Canvas and a crosshair Tracker. The complexity of the UI platform specific items lies in the RenderContext implementations that physically draw the PlotModel’s colors, brushes, fonts, lines, ellipses, and geometries. Take a look at the renderers for WinUI 3, Windows Forms, Skia, and WPF.

The OxyPlot team is currently very busy refactoring its core code. They’re not focusing on creating PlotViews or RenderContexts for the more recent .NET UI platforms like WinUI 3. This is where H.OxyPlot comes in. H.OxyPlot is a project that provides UWP, WinUI 3, and Uno support for the latest OxyPlot core library. It combines the (abandoned) OxyPlot UWP code with the current version of the WPF code. It tries to match the latter as closely as possible in behavior, while adding some extra features – like Dark Theme support. It’s this version that we use in our sample app. Here’s an overview of that app’s dependencies:

Hello World

The HomePage of our sample app shows OxyPlot usage in its simplest form. The page exposes a PlotModel property: a model consisting of two axes and one line series with just a handful of points:

public PlotModel Model { get; private set; } = new PlotModel
{
    Title = "Hello WinUI 3",
    PlotAreaBorderColor = OxyColors.Transparent,
    Axes =
    {
        new LinearAxis { Position = AxisPosition.Bottom },
        new LinearAxis { Position = AxisPosition.Left },
    },
    Series =
    {
        new LineSeries
        {
            Title = "LineSeries",
            MarkerType = MarkerType.Circle,
            Points =
            {
                new DataPoint(0, 0),
                new DataPoint(10, 18),
                new DataPoint(20, 12),
                new DataPoint(30, 8),
                new DataPoint(40, 15),
            }
        }
    }
};

The PlotModel is bound to a PlotView in the page declaration:

<oxy:PlotView Model="{x:Bind Model}" />

That’s all there is! Here’s how the Hello World page looks like:

Model Gallery

We mentioned that the OxyPlot core components are being refactored, and that the WinUI 3 controls are brand new. That sounds like thin ice to walk on, no?  Well, to get an idea of the state of both projects and their usability in production quality apps, we decided to create a page with as much different models as possible. All of these diagrams were straight copied from the official OxyPlot samples repository. We just removed titles and legends.

Welcome to the Model Gallery page. Here’s how it looks like:

The page has its own ViewModel that exposes different PlotModel properties. The ViewModel is declared as DataContext of the page – classic MVVM style:

<Page.DataContext>
    <viewmodels:MultiPageViewModel x:Name="ViewModel" />
</Page.DataContext>

All PlotModels are declaratively bound to PlotView instances:

<oxyplot:PlotView Model="{x:Bind ViewModel.BarSeriesModel}" />

Here’s the ViewModel code for the PieSeries model property:

pieSeriesModel = new PlotModel(); 

pieSeriesModel.PlotAreaBorderColor = OxyColors.Transparent;

    dynamic seriesP1 = new PieSeries 
    { 
        StrokeThickness = 2.0, 
        InsideLabelPosition = 0.8, 
        AngleSpan = 360, 
        StartAngle = 0 
    };

seriesP1.Slices.Add(new PieSlice("Africa", 1030) 
    { 
        IsExploded = false, 
        Fill = OxyColors.PaleVioletRed 
    });
seriesP1.Slices.Add(new PieSlice("Americas", 929) 
    { 
        IsExploded = true 
    });
seriesP1.Slices.Add(new PieSlice("Asia", 4157) 
    { 
        IsExploded = true 
    });
seriesP1.Slices.Add(new PieSlice("Europe", 739) 
    { 
        IsExploded = true 
    });
seriesP1.Slices.Add(new PieSlice("Oceania", 35) 
    { 
        IsExploded = true 
    });

pieSeriesModel.Series.Add(seriesP1);

All diagrams are defined and created in exactly the same way, and the corresponding PlotViews are hooked in a VariableSizedWrapGrid. When the window is resized, we let all items move smoothly to their new position with a RepositionThemeTransition:

<GridViewItem.Transitions>
    <TransitionCollection>
        <RepositionThemeTransition />
    </TransitionCollection>
</GridViewItem.Transitions>

We did not observe any annoying performance or rendering issue that would make us decide not to use OxyPlot or H.OxyPlot in a production app. So, on to another test.

Interactive Model

Sometimes we need to display a chart with ever changing, dynamic data. Our sample app has a page to simulate this. The Interactive Model page has a PlotView populated with three StemSeries picturing differently parameterized normal distributions. With a Slider below the diagram, you can change the mean and the variance of the distributions. It will update all series in more or less real time. Here’s how it looks like in action:

Again, we use a ViewModel. Here are its main properties:

public PlotModel Model => model;

public double Variance
{
    get => variance;
    set
    {
        SetProperty(ref variance, value);
        updateSeries();
    }
}

The page controls are bound to these properties:

<oxyplot:PlotView Model="{x:Bind ViewModel.Model}"
                    Background="Transparent" />
<Slider Value="{x:Bind ViewModel.Variance, Mode=TwoWay}"
        Minimum="-5"
        Maximum="5"
        StepFrequency=".25"
        Margin="20 0 10 0"
        Grid.Row="1" />

Observe that the PlotView uses {x:Bind} with its default mode: OneTime. There’s no need to use classic data binding here. The PlotModel knows its PlotView and can broadcast its updates itself. It does not do this automatically – you’ll need an InvalidatePlot() call, like the one at the end of the following method. The call has a true parameter to indicate that the data was changed (and things like axes minima and maxima need to be recalculated):

private void updateSeries()
{
    model.Series.Clear();
    model.Series.Add(CreateNormalDistributionSeries(-5, 5, Variance, 0.2));
    model.Series.Add(CreateNormalDistributionSeries(-5, 5, -5, 5 + Variance));
    model.Series.Add(CreateNormalDistributionSeries(-5, 5, 5, 5 - Variance));

    model.InvalidatePlot(true);
}

In scenarios with very high update frequency of very large data series, you may not get the performance that you would like. In that case we advise you to look for (or develop) an OxyPlot PlotView/RenderContext pair that’s based on SkiaSharp. The SkiaSharp WPF rendering pretends to be 100 times faster than the WPF Canvas. We did not find a WinUI 3 compatible version of such a view…

Instant Theming

OxyPlot was developed way before Dark Theme or instant theming were a thing: models natively come a white background and black titles, axis lines and labels. If you want to change that, you need to recolor everything individually. H.OxyPlot (the WinUI 3 / Uno version) on the other hand adds a nice feature: it has with built-in support for a Dark Theme, and it uses a great trick for this. When the RequestedTheme is dark, the RenderContext reverses black and white by swapping the cached brushes for these colors:

if (color == OxyColors.Black)
{
    color = OxyColors.White;
}
else if (color == OxyColors.White)
{
    color = OxyColor.FromArgb(255, 32, 32, 32);
}

This looks too simply to be true, but it works surprisingly well for most of the diagram types.

In our sample app we wanted to take it a step further and support instant theme switching. We created a page with a copy of the Hello World model, and some other ones.

OxyPlot comes with its own color classes. This should not come as a surprise, since every .NET UI stack also has its own. But for XAML developers, that means that you cannot (at least directly) rely on Theme resource dictionaries. And even if the brushes and colors were compatible, XAML theming, styling, and templating would still not work. The PlotView is a Canvas on the outside, but apart from the crosshair Tracker there’s no XAML in it. Here’s a screenshot of the Live Visual Tree:

We couldn’t resist, and actually did some XAML theming. We replaced the black crosshair Tracker lines by the system accent color:

<oxy:PlotView.DefaultTrackerTemplate>
    <ControlTemplate>
        <oxy1:TrackerControl Position="{Binding Position}"
                                LineStroke="{ThemeResource SystemColorControlAccentBrush}"
                                Background="GhostWhite">
            <oxy1:TrackerControl.Content>
                <TextBlock Text="{Binding}"
                            Margin="5" />
            </oxy1:TrackerControl.Content>
        </oxy1:TrackerControl>
    </ControlTemplate>
</oxy:PlotView.DefaultTrackerTemplate>

And we like wat we see – in Dark as well as Light themes:

Let’s go back to instant theming. When the Theme is changed, we change the color of all text elements in the Hello World model, as well as the color of the axes and their ticks:

var foreground = theme == ElementTheme.Light 
    ? OxyColor.FromRgb(32, 32, 32) 
    : OxyColors.WhiteSmoke;

helloWorldmodel.TextColor = foreground;
foreach (var axis in helloWorldmodel.Axes)
{
    axis.TicklineColor = foreground;
    axis.AxislineColor = foreground;
}

We refactored this code into an extension method that toggles the color of all non-transparent items of these types:

public static void ApplyTheme(this PlotModel plotModel, ElementTheme theme)
{
    var foreground = theme == ElementTheme.Light 
        ? OxyColor.FromRgb(32, 32, 32)
        : OxyColors.WhiteSmoke;

    if (plotModel.TextColor != OxyColors.Transparent)
    {
        plotModel.TextColor = foreground;
    }

    foreach (var axis in plotModel.Axes)
    {
        if (axis.TicklineColor != OxyColors.Transparent)
        {
            axis.TicklineColor = foreground;
        }
        if (axis.AxislineColor != OxyColors.Transparent)
        {
            axis.AxislineColor = foreground;
        }
    }

    plotModel.InvalidatePlot(false);
}

We made this extension method run against all the models in the Model Gallery page. It may need some fine tuning but so far, we are happy with the result – check the screenshots above.

There’s more in a diagram than texts and axes: some of the series might also need some theming support. Most of the bars, lines, columns, dots, and areas look very good if you apply a decent color scheme. If the series’ shapes have a fill color and borders or decorations (e.g., an area series has a fill, upper border and lower border), then you may need some extra code for theming support.

A first example is the boxplot chart in the lower left corner. We assigned it two matching colors (a light and a dark) and swap these when the theme changes:

var series = boxPlotModel.Series[0] as BoxPlotSeries;
if (theme == ElementTheme.Light)
{
    series.Fill = OxyColors.LightSteelBlue;
    series.Stroke = OxyColors.LightSlateGray;
}
else
{
    series.Fill = OxyColors.LightSlateGray;
    series.Stroke = OxyColors.LightSteelBlue;
}

Another way of blending the shapes of a series better into the theme, is make them semi-transparent. As a bonus there’s no extra code to execute when the theme changes. That’s what we did to the pie chart in the lower right corner of the test page:

foreach (var slice in (pieChartModel.Series.First() as PieSeries).Slices)
{
    var color = slice.ActualFillColor;
    slice.Fill = OxyColor.FromArgb(90, color.R, color.G, color.B);
}

Here’s how all of this looks like:

For the sake of completion, here’s how the instant theming support is implemented. When the page is loaded and all controls are accessible, we apply the current theme:

public InteractivePage()
{
    InitializeComponent();
    Loaded += Page_Loaded;
}

private void Page_Loaded(object sender, RoutedEventArgs e)
{
    ApplyTheme(ActualTheme);
}

private void ApplyTheme(ElementTheme theme)
{
    ViewModel.Model.ApplyTheme(theme);
}

As long as the page is active, we listen to ActualThemeChanged and react appropriately:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    ActualThemeChanged += Page_ActualThemeChanged;
    base.OnNavigatedTo(e);
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    ActualThemeChanged -= Page_ActualThemeChanged;
    base.OnNavigatedFrom(e);
}

private void Page_ActualThemeChanged(FrameworkElement sender, object args)
{
    ApplyTheme(sender.ActualTheme);
}

The exact same logic is applied in the Model Gallery page and the Interactive Model page. And of course, we also tested everything in an app that’s more representative than our small sample. Here’s the result:

It’s one of the many pages to which we also applied our Master-Detail pattern. We added an area chart that supports instant theming. Since this type of chart does not allow to select an item (like a bar, a column, or a pie slice) we decorated it with a RectangleAnnotation to highlight the selected zone.

The Verdict

In our sample app we demonstrated how to use H.OxyPlot to draw diagrams and charts in a WinUI 3 .NET 6 desktop application. The project definitely meets enterprise development standards, and it sets the bar high for third party paid components.

Our sample app lives here on GitHub.

Enjoy!

Building a Master-Detail page with WinUI 3 and MVVM

In this article we show how to build a Master-Detail page in a WinUI 3 Desktop application. The Master-Detail (a.k.a. List-Details) pattern that we’ll build is inspired by the popular mail client UI. It comes with

  • a (filterable) list pane on the left,
  • a details pane on the right,
  • a command bar on top,
  • a command bar on mouse-over within in the list item, and
  • a dialog for editing and adding items.

We will make maximum use of the more recent WinUI 3 controls for the desktop, compiled bindings (x:Bind), and Microsoft MVVM Toolkit. If you’re already into Visual Studio 2022, then we propose go for the brand-new .NET Community Version of this MVVM Toolkit– it’s the same code but in a different namespace.

We built a small sample Win32 Desktop app, here’s how that looks like:

It uses Windows App SDK and MVVM Toolkit:

Here’s a class diagram of our pattern. We did not make any assumptions on the Model, also the generic ViewModel and the XAML structure of the View were designed with reusability in mind:

Model

The Model in our sample app implements INotifyPropertyChanged to facilitate data binding. It represents a character in a small screen series, it is called Character and it inherits from ObservableObject. We defined its fields and decorated them with ObservableProperty attributes. These attributes trigger the new MVVM source code generator for the properties – which also explains why the class is defined as partial:

public partial class Character : ObservableObject
{
    [ObservableProperty]
    private string name;

    [ObservableProperty]
    private string kind;

    [ObservableProperty]
    private string description;

    [ObservableProperty]
    private string imagePath;

    // ...
}

Our sample app uses the Name of the Character as identifier (primary key).

ViewModel

The pièce-de-résistance of our solution is a generic 100% reusable ViewModel class that pragmatically deals with most of the logic in a master-detail pattern. It hosts an ObservableCollection of Model instances and knows

  • which one is selected,
  • how to filter the list,
  • how to add, remove, and update items, and
  • it notifies all of this to its environment.

Here’s the part of its code that deals with the collection, maintaining the current selection, and filtering. We decided to expose only one Items collection and apply the filtering on that one – the Views can then remain bound to the Items property. The underlying field variable always refers to the unfiltered collection. Depending on your own use cases (e.g. if your View needs access to items that are filtered away), you may want to make the field protected instead of private.

The only thing that the generic base class does not know, is how to lookup items. That’s why ApplyFilter() is abstract:

public abstract partial class MasterDetailViewModel<T> : ObservableObject
{
    private readonly ObservableCollection<T> items = new();

    public ObservableCollection<T> Items =>
        filter is null
            ? items
            : new ObservableCollection<T>(items.Where(i => ApplyFilter(i, filter)));

    public T Current
    {
        get => current;
        set
        {
            SetProperty(ref current, value);
            OnPropertyChanged(nameof(HasCurrent));
        }
    }

    public string Filter
    {
        get => filter;
        set
        {
            var current = Current;

            SetProperty(ref filter, value);
            OnPropertyChanged(nameof(Items));

            if (current is not null && Items.Contains(current))
            {
                Current = current;
            }
        }
    }

    public bool HasCurrent => current is not null;

    public abstract bool ApplyFilter(T item, string filter);

    // ...

}

The base ViewModel hosts a set of virtual methods for adding, deleting, and replacing items. They work whether or not the collection is filtered, and all raise the CollectionChanged event so that the Views can update through databinding:

public virtual T AddItem(T item)
{
    items.Add(item);
    if (filter is not null)
    {
        OnPropertyChanged(nameof(Items));
    }

    return item;
}

public virtual T UpdateItem(T item, T original)
{
    var hasCurrent = HasCurrent;

    var i = items.IndexOf(original);
    items[i] = item; // Raises CollectionChanged.
            
    if (filter is not null)
    {
        OnPropertyChanged(nameof(Items));
    }

    if (hasCurrent && !HasCurrent)
    {
        // Restore Current.
        Current = item;
    }

    return item;
}

public virtual void DeleteItem(T item)
{
    items.Remove(item);

    if (filter is not null)
    {
        OnPropertyChanged(nameof(Items));
    }
}

The ViewModel in our sample app inherits from this MasterDetailViewModel<T> and overrides the ApplyFilter() method. When filtering Characters by a search string, we look it up in the Name, Kind, and Description properties. In most apps the search logic is independent from the View and its ViewModel, so it makes sense to implement it in the Model. We indeed pass the search logic to the Character:

public partial class HomePageViewModel : MasterDetailViewModel<Character>
{
    // ...

    public override bool ApplyFilter(Character item, string filter)
    {
        return item.ApplyFilter(filter);
    }

}

Here’s the ApplyFilter() method in the Model:

public bool ApplyFilter(string filter)
{
    return Name.Contains(filter, StringComparison.InvariantCultureIgnoreCase)
        || Kind.Contains(filter, StringComparison.InvariantCultureIgnoreCase)
        || Description.Contains(filter, StringComparison.InvariantCultureIgnoreCase);
}

The ViewModel is set as DataContext to the View:

<Page.DataContext>
    <viewmodels:HomePageViewModel x:Name="ViewModel" />
</Page.DataContext>

Let’s connect some more dots by taking a look at that View.

View

Our Master-Detail page consists of an AutoSuggestBox, a CommandBar, a ListView and a handful of Grid elements. If you’re on UWP, then you may consider bringing a ListDetailsView control in the equation.

On the left side lives a ListView which is bound to the Items and the Current of the ViewModel. For each item, the ‘master’ information (Name and Kind) is hosted in a RelativePanel. To allow compiled data binding within the data template, we specified the target class via x:DataType. The mouse-over status is monitored via PointerEntered and PointerExited event handlers:

<ListView x:Name="CharacterListView"
            ItemsSource="{x:Bind ViewModel.Items, Mode=OneWay}"
            SelectedItem="{x:Bind ViewModel.Current, Mode=TwoWay}">
    <ListView.ItemTemplate>
        <DataTemplate x:DataType="models:Character">
            <UserControl PointerEntered="ListViewItem_PointerEntered"
                         PointerExited="ListViewItem_PointerExited">
                <RelativePanel Background="Transparent">
                    <!-- more ... -->

Whenever the Current changes, we ensure it becomes visible:

private void ViewModel_PropertyChanged(
    object sender, 
    System.ComponentModel.PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Current" && ViewModel.HasCurrent)
    {
        CharacterListView.ScrollIntoView(ViewModel.Current);
    }
}

Use Cases

Filtering

We already covered the Model and ViewModel parts of filtering. The visual part is an AutoSuggestBox that we blended into the command bar on top:

<AutoSuggestBox x:Name="SearchBox"
                QuerySubmitted="SearchBox_QuerySubmitted"
                QueryIcon="Find" />

Its input is passed to the ViewModel:

private void SearchBox_QuerySubmitted(
    AutoSuggestBox sender, 
    AutoSuggestBoxQuerySubmittedEventArgs args)
{
    ViewModel.Filter = args.QueryText;
}

You already know the rest of the story: the search logic is further passed to the Model, and the ViewModel’s Items collection becomes filtered:

public ObservableCollection<T> Items =>
    filter is null
        ? items
        : new ObservableCollection<T>(items.Where(i => ApplyFilter(i, filter)));

Here’s the app with a filter applied:

Commands

Our Master-Detail solution has a CommandBar on top, hosting the usual suspects (Add, Edit, Delete) but there’s also room for custom commands (like Duplicate). Some of these are always actionable, some only when there’s a Current. The data template of the items list on the left has also room for commands: commands that act on the list element that has mouse focus – whether or not it is the Current. These are grouped into a panel called HoverButtons, of which the visibility is managed by a VisualStateManager:

<RelativePanel>
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="HoveringStates">
            <VisualState x:Name="HoverButtonsHidden" />
            <VisualState x:Name="HoverButtonsShown">
                <VisualState.Setters>
                    <Setter Target="HoverButtons.Visibility"
                            Value="Visible" />
                </VisualState.Setters>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <!-- more ... -->
private void ListViewItem_PointerEntered(object sender, PointerRoutedEventArgs e)
{
    if (e.Pointer.PointerDeviceType is PointerDeviceType.Mouse or PointerDeviceType.Pen)
    {
        VisualStateManager.GoToState(sender as Control, "HoverButtonsShown", true);
    }
}

private void ListViewItem_PointerExited(object sender, PointerRoutedEventArgs e)
{
    VisualStateManager.GoToState(sender as Control, "HoverButtonsHidden", true);
}

Duplicate & Delete

The Duplicate and Delete commands appear in multiple places in the View. Thanks to the XamlUICommand and StandardUICommand classes, we were able to define these only once:

<Page.Resources>
    <XamlUICommand x:Name="DuplicateCommand"
                   Command="{x:Bind ViewModel.DuplicateCommand}"
                   Description="Create a clone of this character"
                   Label="Clone">
        <XamlUICommand.IconSource>
            <SymbolIconSource Symbol="Copy" />
        </XamlUICommand.IconSource>
        <XamlUICommand.KeyboardAccelerators>
            <KeyboardAccelerator Key="D"
                                 Modifiers="Control" />
        </XamlUICommand.KeyboardAccelerators>
    </XamlUICommand>
    <StandardUICommand x:Name="DeleteCommand"
                       Kind="Delete"
                       Command="{x:Bind ViewModel.DeleteCommand}"
                       Description="Remove this character" />
</Page.Resources>

Here’s how these UI commands are used in buttons on the top command bar, with the Current as parameter:

<AppBarButton Command="{StaticResource DuplicateCommand}"
              CommandParameter="{x:Bind ViewModel.Current.Name, Mode=OneWay}"
              IsEnabled="{x:Bind ViewModel.HasCurrent, Mode=OneWay}" />
<AppBarButton Command="{x:Bind DeleteCommand}"
              CommandParameter="{x:Bind ViewModel.Current.Name, Mode=OneWay}"
              IsEnabled="{x:Bind ViewModel.HasCurrent, Mode=OneWay}" />

Here are the same commands in the HoverButtons panel. They’re bound to the Model under the mouse cursor:

<StackPanel x:Name="HoverButtons"
            Orientation="Horizontal"
            Visibility="Collapsed">
    <AppBarButton IsCompact="True"
                  Command="{StaticResource DuplicateCommand}"
                  CommandParameter="{x:Bind Name}" />
    <AppBarButton IsCompact="True"
                  Command="{StaticResource DeleteCommand}"
                  CommandParameter="{x:Bind Name}" />
</StackPanel>

Inside the ViewModel the Delete and Duplicate commands are RelayCommand instances:

public ICommand DuplicateCommand => new RelayCommand<string>(DuplicateCommand_Executed);

public ICommand DeleteCommand => new RelayCommand<string>(DeleteCommand_Executed);

When they’re executed, they update the ViewModel’s collection via the helper methods:

private void DeleteCommand_Executed(string parm)
{
    if (parm is not null)
    {
        var toBeDeleted = Items.FirstOrDefault(c => c.Name == parm);
        DeleteItem(toBeDeleted);
    }
}

private void DuplicateCommand_Executed(string parm)
{
    var toBeDuplicated = Items.FirstOrDefault(c => c.Name == parm);
    var clone = toBeDuplicated.Clone();
    AddItem(clone);
    if (Items.Contains(clone))
    {
        Current = clone;
    }
}

Here’s an animation that shows these commands in action. We duplicate an item that’s not the Current, then select the clone and scroll it into view:

Insert & Update

The Insert and Update scenarios require some extra UI: the View has a ContentDialog that’s opened for entering a new Character, or for editing an existing one. RelativePanel is a great host for this type of content:

<ContentDialog x:Name="EditDialog"
                PrimaryButtonText="Update"
                CloseButtonText="Cancel">
    <RelativePanel HorizontalAlignment="Stretch">
        <TextBox x:Name="Name"
                    Header="Name"
                    Text="{Binding Name, Mode=TwoWay}" />
        <TextBox x:Name="Kind"
                    Header="Kind"
                    Text="{Binding Kind, Mode=TwoWay}"
                    RelativePanel.RightOf="Name"
                    RelativePanel.AlignRightWithPanel="True" />
        <TextBox x:Name="ImagePath"
                    Header="Path to Image"
                    Text="{Binding ImagePath, Mode=TwoWay}"
                    RelativePanel.Below="Name"
                    RelativePanel.AlignLeftWith="Name"
                    RelativePanel.AlignRightWith="Kind" />
        <TextBox x:Name="Description"
                    Header="Description"
                    Text="{Binding Description, Mode=TwoWay}"
                    TextWrapping="Wrap"
                    RelativePanel.Below="ImagePath"
                    RelativePanel.AlignLeftWith="ImagePath"
                    RelativePanel.AlignRightWith="ImagePath" />
    </RelativePanel>
</ContentDialog>

Here’s how the dialog looks like. We know: there’s room for improvement in the color scheme:

It should not come as a surprise that we’re using RelayCommands again:

public ICommand NewCommand => new AsyncRelayCommand(OpenNewDialog);

public ICommand EditCommand => new AsyncRelayCommand(OpenEditDialog);

Both commands configure and open the context dialog, which is bound to a Character that’s not in the official Items collection – to prevent side effects and facilitate the Cancel operation:

private async Task OpenNewDialog()
{
    EditDialog.Title = "New Character";
    EditDialog.PrimaryButtonText = "Insert";
    EditDialog.PrimaryButtonCommand = InsertCommand;
    EditDialog.DataContext = new Character();
    await EditDialog.ShowAsync();
}

private async Task OpenEditDialog()
{
    EditDialog.Title = "Edit Character";
    EditDialog.PrimaryButtonText = "Update";
    EditDialog.PrimaryButtonCommand = UpdateCommand;
    var clone = ViewModel.Current.Clone();
    clone.Name = ViewModel.Current.Name;
    EditDialog.DataContext = clone;
    await EditDialog.ShowAsync();
}

Another set of RelayCommands is bound to the Close button of the dialog, and update ViewModel’s items collection:

private void Update()
{
    ViewModel.UpdateItem(EditDialog.DataContext as Character, ViewModel.Current);
}

private void Insert()
{
    var character = ViewModel.AddItem(EditDialog.DataContext as Character);
    if (ViewModel.Items.Contains(character))
    {
        ViewModel.Current = character;
    }
}

Reusability

Here are the steps to take if you want to apply the proposed Master-Detail implementation in your own MVVM app:

  1. Copy/paste the generic MasterDetailViewModel.
  2. Make your own subclass of it, with your own Model.
  3. Use the XAML structure and bindings of our View, but applied to your own Model.

That’s all.  We successfully did this exercise a couple of times in a relatively big app, with a few variations in the scenario, such as a read-only collection, no search, and insert only via selection from another list. Here’s how that app looks like:

It took a few iterations, but we’re pretty confident that this pattern and its component are reusable.

Our sample app lives here on GitHub.

Enjoy!

Upgrading Radial Gauge from UWP to WinUI 3

In this article we describe the process of migrating a user control from UWP to WinUI 3. The subject is the Radial Gauge that we wrote many years ago and is currently part of Windows Community Toolkit. For its UI the radial gauge uses XAML as well as the Composition API. To successfully migrate the control from UWP to WinUI3

  • we had to remove some UWP calls that don’t exist in WinUI 3,
  • we replaced some UWP calls by their WinUI 3 counterpart, and
  • we decided to give the control a more rounded Windows 11 look-and-feel.

Setting up the environment

When we started this journey, the most recent version of Windows App SDK was the experimental preview of v1.0, so the list of required NuGet packages looked like this:

We installed the new Visual Studio templates:

After the upgrade to the official Windows App SDK 1.0 release, the dependencies list conveniently shrunk to this:

We created a sample app wherein all relevant UWP Radial Gauge source files from Windows Community Toolkit were copy-pasted.

Here’s the taxonomy of the current Windows 8 -style Radial Gauge control:

There was no need to change these properties, just as there was no need to change the control’s XAML template:

<ControlTemplate TargetType="local:RadialGauge">
    <Viewbox>
        <Grid x:Name="PART_Container"
              Width="200"
              Height="200">
 
            <!--  Scale  -->
            <Path Name="PART_Scale" />

            <!--  Trail  -->
            <Path Name="PART_Trail" />

            <!--  Value and Unit  -->
            <StackPanel HorizontalAlignment="Center"
                        VerticalAlignment="Bottom">
                <TextBlock Name="PART_ValueText" />
            </StackPanel>
        </Grid>
    </Viewbox>
</ControlTemplate>

The scale and trail of the gauge are Path elements, and then there are text boxes for value and unit. The rest of the UI parts -needle, ticks, and scale ticks- are drawn by the Composition API.

Goodbye Windows.UI namespace

The first step in migrating a code base from UWP to WinUI would be changing the namespace from Windows.UI.* to Microsoft.UI.* in a zillion places. All compiled well after this change…

Goodbye UI Events

… but at runtime the Toolkit’s ThemeListener crashed the app. It’s a known issue and the reason is that UWP’s UISettings.ColorValuesChanged Event and AccessibilitySettings.HighContrastChanged Event are no longer supported in desktop apps. Unlike UWP apps, WinUI 3 desktop apps are not notified when the user changes the theme, the contrast color, or high contrast mode. This is not a showstopper – as long as you stick to ThemeResources in your XAML. Since theming support is a popular feature these days, we assume that the majority of recent UWP apps is already using theme resource dictionaries. Make sure you don’t forget one for HighContrast. You don’t really need a notification when the theme changes at runtime: all XAML and Composition elements that get their color from a ThemeResource will immediately and automatically change color.

Here’s how we configured the Radial Gauge instances on the Home page of the sample app:

<ResourceDictionary.ThemeDictionaries>
    <ResourceDictionary x:Key="HighContrast">
        <!-- This makes the background disappear in High Contrast mode -->
        <x:Double x:Key="BackgroundOpacity">0</x:Double>
        <SolidColorBrush x:Key="TenPercentContrastBrush"
             Color="{ThemeResource SystemColorWindowTextColor}" />
        <SolidColorBrush x:Key="SystemAccentColorBrush"
             Color="{StaticResource SystemAccentColor}" />
        <SolidColorBrush x:Key="AppNeedleBrush"
             Color="{ThemeResource SystemAccentColor}" />
    </ResourceDictionary>
    <ResourceDictionary x:Key="Dark">
        <x:Double x:Key="BackgroundOpacity">.075</x:Double>
        <SolidColorBrush x:Key="TenPercentContrastBrush"
             Color="White"
             Opacity=".1" />
        <Color x:Key="SystemAccentColor">CadetBlue</Color>
        <SolidColorBrush x:Key="SystemAccentColorBrush"
             Color="{StaticResource SystemAccentColor}"
             Opacity=".5" />
        <SolidColorBrush x:Key="AppNeedleBrush"
             Color="OrangeRed" />
    </ResourceDictionary>
    <ResourceDictionary x:Key="Light">
        <x:Double x:Key="BackgroundOpacity">.15</x:Double>
        <SolidColorBrush x:Key="TenPercentContrastBrush"
             Color="Black"
             Opacity=".1" />
        <Color x:Key="SystemAccentColor">CadetBlue</Color>
        <SolidColorBrush x:Key="SystemAccentColorBrush"
             Color="{StaticResource SystemAccentColor}"
             Opacity=".5" />
        <SolidColorBrush x:Key="AppNeedleBrush"
             Color="OrangeRed" />
    </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

Here’s that page in Light, Dark, and HighContrast theme:

As it turned out that all the Toolkit’s ThemeListener related code in the Radial Gauge is actually obsolete in a WinUI 3 desktop context, we removed that code.

Goodbye CoreWindow

Radial Gauge supports keyboard input. The Value property can be changed via the arrow keys, with the CTRL key to manage the change interval. To detect the virtual key press Radial Gauge was using CoreWindow.GetKeyState():

var ctrl = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control);
if (ctrl.HasFlag(CoreVirtualKeyStates.Down))
{
    step = LargeChange;
}

Since UWP’s CoreWindow is not available in WinUI 3 desktop apps, we had to look for another way to detect key presses. We found it in the appropriately named KeyboardInput class. Here’s the new code:

if (KeyboardInput.GetKeyStateForCurrentThread(VirtualKey.Control) == CoreVirtualKeyStates.Down)
{
    step = LargeChange;
};

Goodbye KeyboardInput

Just a few days after that last modification, we upgraded the project to Windows App SDK v1.0 only to observe that the KeyboardInput class was not there anymore. We abandoned the keyboard events and went for Keyboard Accelerators. In hindsight that would always have been the proper approach. We created an extension method to facilitate the declaration:

public static void AddKeyboardAccelerator(this UIElement element,
  VirtualKeyModifiers keyModifiers,
  VirtualKey key,
  TypedEventHandler<KeyboardAccelerator, KeyboardAcceleratorInvokedEventArgs> handler)
{
    var accelerator =
      new KeyboardAccelerator()
      {
          Modifiers = keyModifiers,
          Key = key
      };
    accelerator.Invoked += handler;
    element.KeyboardAccelerators.Add(accelerator);
}

Then we replaced the keypress event handler by eight keyboard accelerators (one for each arrow key, with and without CTRL). Here are two of those:

this.AddKeyboardAccelerator(
    VirtualKeyModifiers.Control,
    VirtualKey.Left,
    (ka, kaea) =>
    {
        Value = Math.Max(Minimum, Value - LargeChange);
        kaea.Handled = true;
    });
this.AddKeyboardAccelerator(
    VirtualKeyModifiers.None,
    VirtualKey.Left,
    (ka, kaea) =>
    {
        Value = Math.Max(Minimum, Value - SmallChange);
        kaea.Handled = true;
    });

Goodbye DesignMode

One of the major differences between UWP and Win32 Desktop is the application model. Unsurprisingly the Windows.ApplicationModel namespace did not survive into Windows App SDK v1.0. As a result, the DesignMode to detect whether the control is hosted by a Visual Designer is not available anymore. It is used in the Toolkit’s DesignTimeHelper class. There seem to be no plans for a WinUI 3 Desktop XAML Designer, and with features like XAML Live Preview and XAML Hot Reload there’s no urgent need for one. Again, we happily removed another part of the Radial Gauge code.

Goodbye Windows 8 style

The original looks of the Radial Gauge were a product of the Windows 8 design style: sharp (“rounded corners, shadows, and gradients are bad for battery life”) and plump (“touch first: designed for fingers”). We decided it was time for a make-over to bring the UI closer to Windows 11 design. We made another copy of the control (“RadialGauge2”) where we

  • decreased the default scale width,
  • rounded the Scale and Trail shapes by changing their PenLineCaps in the XAML template,
  • rounded the Tick, ScaleTick and Needle elements, and
  • provided support for Opacity in these elements.

Rounding the shapes was not a trivial job, since we were relying on SpriteVisual instances – unrounded rectangles filled with a non-transparent CompositionColorBrush. Here’s the old code for the ticks:

SpriteVisual tick;
for (double i = radialGauge.Minimum; i <= radialGauge.Maximum; i += radialGauge.TickSpacing)
{
    tick = radialGauge._compositor.CreateSpriteVisual();
    tick.Size = new Vector2((float)radialGauge.TickWidth, (float)radialGauge.TickLength);
    tick.Brush = radialGauge._compositor.CreateColorBrush(radialGauge.TickBrush.Color);
    tick.Offset = new Vector3(100 - ((float)radialGauge.TickWidth / 2), 0.0f, 0);
    tick.CenterPoint = new Vector3((float)radialGauge.TickWidth / 2, 100.0f, 0);
    tick.RotationAngleInDegrees = (float)radialGauge.ValueToAngle(i);
    radialGauge._root.Children.InsertAtTop(tick);
}

The new Radial Gauge uses a CompositionRoundedRectangleGeometry to fill a CompositionSpriteShape for each tick, and then groups all the ticks into a single ShapeVisual. Transparent brushes are not possible in this part of the API (well, at least not in a simple way). As a work-around we applied the opacity of the source brush to the Opacity of the Visual. Here’s the -much longer- new code:

var ticks = radialGauge._compositor.CreateShapeVisual();
ticks.Size = new Vector2((float)radialGauge.Height, (float)radialGauge.Width);
ticks.BorderMode = CompositionBorderMode.Soft;
ticks.Opacity = (float)radialGauge.TickBrush.Opacity;
 
var roundedTickRectangle = radialGauge._compositor.CreateRoundedRectangleGeometry();
roundedTickRectangle.Size = new Vector2((float)radialGauge.TickWidth, (float)radialGauge.TickLength);
roundedTickRectangle.CornerRadius = new Vector2((float)radialGauge.TickWidth / 2, (float)radialGauge.TickWidth / 2);
 
var tssFillBrush = radialGauge._compositor.CreateColorBrush(radialGauge.TickBrush.Color);
var tssOffset = new Vector2(100 - ((float)radialGauge.TickWidth / 2), 0.0f);
var tssCenterPoint = new Vector2((float)radialGauge.TickWidth / 2, 100.0f);
 
for (double i = radialGauge.Minimum; i <= radialGauge.Maximum; i += radialGauge.TickSpacing)
{
    var tickSpriteShape = radialGauge._compositor.CreateSpriteShape(roundedTickRectangle);
    tickSpriteShape.FillBrush = tssFillBrush;
    tickSpriteShape.Offset = tssOffset;
    tickSpriteShape.CenterPoint = tssCenterPoint;
    tickSpriteShape.RotationAngleInDegrees = (float)radialGauge.ValueToAngle(i);
 
    ticks.Shapes.Add(tickSpriteShape);
}
 
radialGauge._root.Children.InsertAtTop(ticks);

The Home page of our sample app compares the new look to the original one:

The Gallery page displays two sets of controls: the top row has some gauge configurations using the old style, the second row has the same gauges but is using the new style:

We believe that the second row looks better in any configuration.

Here’s how it looks like with the official v1.0.0. of Windows App SDK, bringing the Windows 11 style into the equation:

Here’s how the upgraded Radial Gauge looks like in a candidate production app that is in the middle of its migration to WinUI 3:

For the sake of completion: that other control in the screenshot is a Radial Range Indicator.

The Verdict

Migrating controls and apps from UWP to WinUI 3 involves more than a namespace change, and we’re OK with that. Some API’s have disappeared, but none of these was crucial – not to this user control and not to the apps we’re currently migrating.

We are happy with the result: Radial Gauge now has better looks and less source code to maintain.

The sample app lives here on GitHub.

Enjoy!

Pagination with Entity Framework Core and Microsoft MVVM in WinUI 3

In this article we present a LINQ-based pagination solution for list controls as well as a pager user interface, all in a WinUI 3 application. We use SQLite as data provider and a Windows Community Toolkit DataGrid as host control, but the solution applies to any Entity Framework Core (EF Core) store and any WinUI 3 ItemsControl. For the implementation of the MVVM pattern, we chose the usual suspect: Microsoft MVVM Toolkit.

Here’s how the sample page looks like:

This is a new XAML page that we added to the sample app for our previous article on using the DataGrid in WinUI 3.

The heart of our pagination infrastructure is PaginatedList<T> – a subclass of List<T>.

It holds a specific page from a query result as a list of items can be bound to any ItemsControl as ItemsSource. A PaginatedList<T> instance not only embeds the (partial) result of the query but it also exposes the current page number, and the total number of pages for the whole query (quite convenient for a pager UI). The page size (number of records wanted) is not a property but is provided to the constructor.

Here’s how the class definition looks like:

public class PaginatedList<T> : List<T>
{
    public int PageIndex { get; private set; }

    public int PageCount { get; private set; }

    private PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
    {
        PageIndex = pageIndex;
        PageCount = (int)Math.Ceiling(count / (double)pageSize);
        AddRange(items);
    }

    public static async Task<PaginatedList<T>> CreateAsync(
	IQueryable<T> source, 
	int pageIndex, 
	int pageSize)
    {
        int count = await source.CountAsync();
        List<T> items = await source
	.Skip((pageIndex - 1) * pageSize)
	.Take(pageSize)
	.ToListAsync();

        return new PaginatedList<T>(items, count, pageIndex, pageSize);
    }
}

We’re using some of the asynchronous EF Core IQueryable Extensions here, but feel free to add a synchronous version if that better fits your use case.

The PaginatedList is used inside the XAML page’s ViewModel (a Microsoft MVVM Toolkit’s ObservableObject) to produce a List<Mountain> to which the DataGrid is bound. The ViewModel also exposes the current page number and the total number of pages. These are used by the pager on top. Here’s the ViewModel’s code:

public class PaginationPageViewModel : ObservableObject
{
    private int _pageSize = 10;
    private int _pageNumber;
    private int _pageCount;
    private List<Mountain> _mountains;

    public int PageNumber
    {
        get => _pageNumber;
        private set => SetProperty(ref _pageNumber, value);
    }

    public int PageCount
    {
        get => _pageCount;
        private set => SetProperty(ref _pageCount, value);
    }

    public List<Mountain> Mountains
    {
        get => _mountains;
        private set => SetProperty(ref _mountains, value);
    }

    private async Task GetMountains(int pageIndex, int pageSize)
    {
        using MountainDbContext dbContext = new();
        PaginatedList<Mountain> pagedMountains = await PaginatedList<Mountain>.CreateAsync(
            dbContext.Mountains
                .OrderBy(m => m.Rank),
            pageIndex,
            pageSize);
        PageNumber = pagedMountains.PageIndex;
        PageCount = pagedMountains.PageCount;
        Mountains = pagedMountains;
    }
}

Here’s how the ‘current page of Mountains’ is bound to the DataGrid:

<ctWinUI:DataGrid x:Name="DataGrid"
                    ItemsSource="{x:Bind ViewModel.Mountains, Mode=OneWay}"
                    AutoGenerateColumns="False"
                    CanUserSortColumns="False"
                    SelectionMode="Single"
                    IsReadOnly="True"
                    RowDetailsVisibilityMode="Collapsed">
    <ctWinUI:DataGrid.Columns>
        <ctWinUI:DataGridTextColumn Header="Rank"
                                    Binding="{Binding Rank}" />
        <ctWinUI:DataGridComboBoxColumn Header="Mountain"
                                        Binding="{Binding Name}" />
        <!-- More columns -->
    </ctWinUI:DataGrid.Columns>
</ctWinUI:DataGrid>

The ViewModel also exposes 4 commands -implementors of IAsyncRelayCommand– to allow refreshing the DataGrid with a new logical page:

public IAsyncRelayCommand FirstAsyncCommand { get; }

public IAsyncRelayCommand PreviousAsyncCommand { get; }

public IAsyncRelayCommand NextAsyncCommand { get; }

public IAsyncRelayCommand LastAsyncCommand { get; }

We would have loved to use one of the new WinUI Pager controls for user interface, but tody these are available in WinUI 2 only. Therefor we brewed our own CommandBar-based pagination control with the canonical navigation buttons to the first, previous, next, and last pages:

<CommandBar DefaultLabelPosition="Right">
    <AppBarButton ToolTipService.ToolTip="First"
                    Icon="Previous"
                    Command="{x:Bind ViewModel.FirstAsyncCommand, Mode=OneWay}" />
    <AppBarButton ToolTipService.ToolTip="Previous"
                    Icon="Back"
                    Command="{x:Bind ViewModel.PreviousAsyncCommand, Mode=OneWay}" />
    <AppBarElementContainer>
        <TextBlock Text="Page" />
    </AppBarElementContainer>
    <AppBarElementContainer>
        <TextBlock Text="{x:Bind ViewModel.PageNumber, Mode=OneWay}" />
    </AppBarElementContainer>
    <! -- And so on ... -->

Here’s how the AsyncRelayCommands are initialized in the ViewModel with their respective Execute and CanExecute logic:

FirstAsyncCommand = new AsyncRelayCommand(
    async () => await GetMountains(1, _pageSize),
    () => _pageNumber != 1
);
PreviousAsyncCommand = new AsyncRelayCommand(
    async () => await GetMountains(_pageNumber - 1, _pageSize),
    () => _pageNumber > 1
);
NextAsyncCommand = new AsyncRelayCommand(
    async () => await GetMountains(_pageNumber + 1, _pageSize),
    () => _pageNumber < _pageCount
);
LastAsyncCommand = new AsyncRelayCommand(
    async () => await GetMountains(_pageCount, _pageSize),
    () => _pageNumber != _pageCount
);

Each time a new page is fetched and displayed, we need to tell the commands that their CanExecute property was updated so that the corresponding buttons in the pager will enable or disable themselves:

FirstAsyncCommand.NotifyCanExecuteChanged();
PreviousAsyncCommand.NotifyCanExecuteChanged();
// And so on ...

To improve the user experience, we enhanced PaginatedList<T> to implement some edge cases. When the result of the full query is empty (page count is zero), we set the current page number to zero:

int count = await source.CountAsync();
if (count == 0)
{
   // No results -> return page 0.
   return new PaginatedList<T>(new List<T>(), 0, 0, pageSize);
}

This is how the pager then looks like:

When the requested page is out of range (has no records) then PaginatedList<T> returns the last page:

List<T> items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
if (items.Count == 0)
{
    // Requested page is out of range -> return last page.
    pageIndex = (int)Math.Ceiling(count / (double)pageSize);
    items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
}

return new PaginatedList<T>(items, count, pageIndex, pageSize);

Here’s how the pager looks like when we try to navigate to page 1000:

Last but not least, we allow the user to change the page size. We first added the property and a list of possible values to the ViewModel:

public List<int> PageSizes => new() { 5, 10, 20, 50, 100 };

public int PageSize
{
    get => _pageSize;
    set
    {
        SetProperty(ref _pageSize, value);
        Refresh();
    }
}

Then we added a ComboBox and a TextBlock to the AppBar, via AppBarElementContainer instances:

<AppBarSeparator />
<AppBarElementContainer VerticalContentAlignment="Center">
    <ComboBox ItemsSource="{x:Bind ViewModel.PageSizes}"
                SelectedItem="{x:Bind ViewModel.PageSize, Mode=TwoWay}" />
</AppBarElementContainer>
<AppBarElementContainer VerticalContentAlignment="Center">
    <TextBlock Text="rows per page"
                Margin="8 0" />
</AppBarElementContainer>

Here’s how that part of the UI looks like:

paginationpagesize

Finally we implemented the Refresh() method – triggered by a change of the PageSize property. We simply navigate to the first page. Before that we make sure that the command can execute by setting the page number to zero:

private void Refresh()
{
    _pageNumber = 0;
    FirstAsyncCommand.Execute(null);
}

With a handful lines of code and a little help from our friends Microsoft MVVM Toolkit and EF Core we just implemented a WinUI 3 pagination use case. Our sample app lives here on GitHub.

Enjoy!

Using the Windows Community Toolkit DataGrid with WinUI 3 and Entity Framework Core

In this article we demonstrate the Windows Community Toolkit DataGrid in a desktop application powered by the Windows App SDK and a Sqlite relational database. We’ll cover

  • populating the DataGrid,
  • sorting rows via a click on a column header,
  • filtering rows on predefined criteria,
  • grouping rows,
  • searching for rows,
  • instant theme switching,

Here’s how our sample app looks like on Windows 10:

 DatabaseSearch

Recently we started migrating some UWP apps to WinUI 3. Some of these apps have 3rd party DataGrid controls, and we wanted to know whether Community Toolkit DataGrid is a decent candidate to replace these – after all: it’s free. We had no idea how this DataGrid control would behave in a WinUI 3 desktop app, since there are no official samples yet: 

WinUIControlsGallery

On top of that the Community Toolkit DataGrid control is still in its first version which was a port from Silverlight to UWP. When you look at its source you’ll notice that a lot of the code is already 3 years old. Would it run on the brand new Windows App SDK?

For all these reasons we were a bit reluctant to immediately start using this DataGrid in a production WinUI 3 desktop application. So we decided to test drive it first in a more representative setting. This test drive became the sample app that we’re describing in this article.

Migrating the Community Toolkit Sample app

We started our adventure by migrating the UWP DataGrid sample page from the Community Toolkit Sample app to WinUI 3. It has all the functionality we need (filtering, grouping, sorting, theming) and more (editing), and it’s Open Sourced. Here’s how the original looks like:

CommunityToolkitSampleApp

The migration was easier than expected and basically boiled down to updating the “windows.ui.” namespace references to “microsoft.ui.” all over the place. We then applied some cosmetic changes, like adding transparency to the column header background and adding elegance to the CommandBar on top. We also couldn’t resist modernizing the C# syntax here and there.

The Home page of our sample app is the result of the migration from an UWP/WinRT XAML page to WinUI 3/.NET 5:

HomeSort

All features of the original Toolkit demo were successfully ported to our own sample app. You can for example apply a filter to the displayed records:

HomeFilter

You can group records:

HomeGroup

You can sort the records by clicking on a column header:

HomeSearch

The icon in the top right corner op the page allows you to immediately switch between light and dark theme. As long as you stick to ‘lightweight styling’ (i.e. just overriding theme color resources) the following one-liner will do the trick:

Root.RequestedTheme = Root.RequestedTheme == ElementTheme.Light 
	? ElementTheme.Dark 
	: ElementTheme.Light;

From the moment you start to programmatically modify resources, or retemplate (parts of) a control, this instant theme-switching becomes problematic. Not only the control’s XAML but also the defined animations refer to expected colors, opacities, and more – and it’s super hard to override all of these. But let’s focus on DataGrid’s core features.

It takes some development and design effort to implement sorting, filtering, and especially grouping with the Community Toolkit DataGrid. In third party control toolkits such as the ones from DevExpress, Syncfusion, or Telerik [no ranking, just alphabetic order] such features can be configured declaratively and they come with a built-in UI. Nevertheless we were happy with the result of this first page, and decided to build a more modern/representative version of it by bringing some new components to the equation, like

  • Microsoft MVVM Toolkit to replace the custom change propagation code in Models and ViewModels,
  • a Sqlite relational database to replace the CSV file, and
  • Entity Framework Core as an Object-Relational Mapper to run LINQ queries against the data.

Here’s an overview of the NuGet packages – ignore the version numbers, they were all upped multiple times since we made the screenshot:

NuGetPackages

We skipped editing and validation in our sample app, but still all Models and ViewModels were renamed and became children of MVVM Toolkit’s ObservableObject class:

public class Mountain : ObservableObject
{
    private uint _rank;
    private string _name;
    // more fields ...

    // Key

    [Key]
    public int Id { get; set; }

    // Fields

    public uint Rank
    {
        get => _rank;
        set => SetProperty(ref _rank, value);
    }

    // more properties ...

}

For more details on Microsoft MVVM Toolkit, check this introduction. In the rest of this article we’ll focus on Entity Framework (EF) Core, since that is most probably new to UWP developers who are migrating to Windows App SDK.

Configuring Entity Framework Core

For the enterprise sample we replaced the .csv file with a relational Sqlite database. An Entity Framework DbContext was defined to host a table with Mountain entities – a DbSet<Mountain>. Everything is persisted in the app’s local folder:

public class MountainDbContext : DbContext
{
    public DbSet<Mountain> Mountains { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        string path = Path.Combine(
	ApplicationData.Current.LocalFolder.Path, 
	"mountains.db");

        string connectionStringBuilder = new
            SqliteConnectionStringBuilder()
        {
            DataSource = path
        }
            .ToString();

        SqliteConnection connection = new SqliteConnection(connectionStringBuilder);
        optionsBuilder.UseSqlite(connection);
    }
}

Before accessing it, the ViewModel ensures that the database is created:

dbContext.Database.EnsureCreated();

This checks whether the database exists -it may of may not have been deployed with your app- and if not it creates an empty one. Our sample app then reads the original .CSV file and populates the Mountains table with it. Now we can write LINQ queries against the data, such as this one to get all of the Mountains:

public async Task<IEnumerable<Mountain>> AllMountainsAsync()
{
    using (MountainDbContext dbContext = new())
    {
        return await dbContext.Mountains
	.OrderBy(m => m.Rank)
	.AsNoTracking()
	.ToListAsync();
    }
}

Observe that EF Core comes with an asynchronous version of ToList(). When you’re not modifying the content, then it’s a good idea to disable EF change tracking, that’s what the AsNoTracking() does.

Making a read-only DataGrid

The DataGrid definition is similar to the one in the Home page, except that we made the grid read-only since we’re not huge fans of editable DataGrids:

<ctWinUI:DataGrid x:Name="DataGrid"
                    AutoGenerateColumns="False"
                    CanUserSortColumns="True"
                    Sorting="DataGrid_Sorting"
                    LoadingRowGroup="DataGrid_LoadingRowGroup"
                    SelectionMode="Single"
                    IsReadOnly="True">
    <ctWinUI:DataGrid.Columns>
        <ctWinUI:DataGridTextColumn Header="Rank"
                                    Binding="{Binding Rank}"
                                    Tag="Rank" />
        <ctWinUI:DataGridComboBoxColumn Header="Mountain"
                                        Binding="{Binding Name}"
                                        Tag="Name" />
        <!-- More columns ... -->
    </ctWinUI:DataGrid.Columns>
</ctWinUI:DataGrid>

It does not make sense to allow selecting individual cells, so we implemented “row-selection mode” by turning the focus brushes for individually selected cells transparent:

<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush">Transparent</SolidColorBrush>
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush">Transparent</SolidColorBrush>

Be aware that Community Toolkit DataGrid does not support the {x:Bind} syntax for column property bindings, even when you provide bindings for DataContext and ItemsSource:

xBind

Drawing a Template Column

The apps that we’re migrating use templated columns, so we looked for an excuse to define a DataGridTemplateColumn in this sample app. We decided to display the height of the Mountain as a Slider. Here’s the template, observe that the use of {x:Bind} is supported, at least when you declare the DataType :

<ctWinUI:DataGridTemplateColumn Header="Height"
                                Tag="Height">
    <ctWinUI:DataGridTemplateColumn.CellTemplate>
        <DataTemplate x:DataType="models:Mountain">
            <Grid Background="Transparent"
                    ToolTipService.ToolTip="{x:Bind HeightDescription}">
                <Slider Minimum="7200"
                        Maximum="8848"
                        Value="{x:Bind Height}"
                        IsHitTestVisible="False"
                        IsTabStop="False" />
            </Grid>
        </DataTemplate>
    </ctWinUI:DataGridTemplateColumn.CellTemplate>
</ctWinUI:DataGridTemplateColumn>

Here’s how the page now looks like:

DatabaseTemplate

We kept IsEnabled to true to allow the Slider to keep its accent color instead of being grayed out, and set both IsHitTestVisible and IsTabStop to false to prevent user interaction.

Applying a Filter

Here are the LINQ queries that correspond to the filter options:

case FilterOptions.Rank_High:
    return await dbContext.Mountains
	.Where(m => m.Rank > 50)
	.OrderBy(m => m.Rank)
	.AsNoTracking()
	.ToListAsync();

case FilterOptions.Height_High:
    return await dbContext.Mountains
	.Where(m => m.Height > 8000)
	.OrderBy(m => m.Rank)
	.AsNoTracking()
	.ToListAsync();

As already mentioned, third party libraries have built-in UI to allow your end user to define and apply his own filters. Check this article for an example. Here’s how a filtered dataset looks like in our sample app – with less rows, and an extra indicator on the command button to notify the user that a filter is applied:

DatabaseFilter

Sorting by clicking a column header

For the column sort feature, we wanted to avoid a huge switch statement with a different LINQ expression for each column – as in the original sample. We went for a solution that takes the name of the column and the sort direction as parameter:

public async Task<IEnumerable<Mountain>> SortedMountainsAsync(
	string sortBy, 
	bool ascending)
{
    using (MountainDbContext dbContext = new())
    {
        return await dbContext.Mountains
	.OrderBy(sortBy, !ascending)
	.AsNoTracking()
	.ToListAsync();
    }
}

The OrderBy() in the previous code is an -admittedly cryptic- extension method that builds the appropriate LINQ expression:

public static IOrderedQueryable<TEntity> OrderBy<TEntity>(
    this IQueryable<TEntity> source,
    string orderByProperty,
    bool desc)
{
    string command = desc ? "OrderByDescending" : "OrderBy";
    Type type = typeof(TEntity);
    PropertyInfo property = type.GetProperty(orderByProperty);
    ParameterExpression parameter = Expression.Parameter(
            type,
            "p");
    MemberExpression propertyAccess = Expression.MakeMemberAccess(
            parameter,
            property);
    LambdaExpression orderByExpression = Expression.Lambda(
            propertyAccess,
            parameter);
    MethodCallExpression resultExpression = Expression.Call(
            typeof(Queryable),
            command,
            new Type[] { type, property.PropertyType },
            source.Expression, Expression.Quote(orderByExpression));
    return (IOrderedQueryable<TEntity>)source
            .Provider
            .CreateQuery<TEntity>(resultExpression);
}

When working with EF or EF Core, it’s always good to have IQueryable extensions like this hanging around.

Implementing the Mode

Clicking on a column header (or on one of the command bar buttons) changes the ItemsSource of the DataGrid but also changes its UI (arrow indicators in the column header, and command bar decorators above the grid). You have to make sure that all ‘old’ arrow indicators are removed when you apply a filter -something that was overlooked in the official Toolkit Sample App.

We centralized the UI logic behind a “mode switch” in a PropertyChangedCallback for ItemsSourceProperty:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    _token = DataGrid.RegisterPropertyChangedCallback(
	ctWinUI.DataGrid.ItemsSourceProperty,
	DataGridItemsSourceChangedCallback);
    base.OnNavigatedTo(e);
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    DataGrid.UnregisterPropertyChangedCallback(
	ctWinUI.DataGrid.ItemsSourceProperty, 
	_token);
    base.OnNavigatedFrom(e);
}

private void DataGridItemsSourceChangedCallback(DependencyObject sender, DependencyProperty dp)
{
    // Remove Sort Indicators.
    if (dp == ctWinUI.DataGrid.ItemsSourceProperty)
    {
        foreach (var column in (sender as ctWinUI.DataGrid).Columns)
        {
            column.SortDirection = null;
        }
    }

    // Other display mode dependent UI logic
    // ...
}

Here’s how we implemented the button decorators in the command bar to indicate the active mode – as an Icon in an AppBarElementContainer that has a negative margin:

<CommandBar DefaultLabelPosition="Right"
            Background="Transparent"
            VerticalAlignment="Center">
    <AppBarButton Icon="Filter"
                    Label="Filter"
                    Width="80">
        <AppBarButton.Flyout>
            <MenuFlyout>
                <!-- Menu items -->
            </MenuFlyout>
        </AppBarButton.Flyout>
    </AppBarButton>
    <AppBarElementContainer x:Name="FilterIndicator"
                            Visibility="Collapsed"
                            Margin="-16 0 0 0">
        <FontIcon Glyph=""
                    FontSize="12"
                    Foreground="Coral"
                    VerticalAlignment="Top" />
    </AppBarElementContainer>

    <!-- More buttons and indicators -->

</CommandBar>

In the future InfoBadge might be a nice alternative but that control is not yet pushed to WinUI 3.

Searching for records

Here’s our initial LINQ query behind the search feature:

return await dbContext.Mountains
            .Where(m => m.Name.Contains(queryText))
            .OrderBy(m => m.Rank)
            .AsNoTracking()
            .ToListAsync();

At first sight no surprises here, except that … it’s case sensitive:

DatabaseSearchWithContains

The LINQ Provider for Sqlite (in contrast to the one for SQL Server) translates Contains() to a case sensitive comparison. The following modification to the query only makes things worse:

return await dbContext.Mountains
            .Where(m => m.Name.Contains(queryText, StringComparison.InvariantCulture)) // Crashes at runtime
            .OrderBy(m => m.Rank)
            .AsNoTracking()
            .ToListAsync();

It will crash at runtime since the LINQ Provider does not know how to translate this C# to its SQL syntax. Again you’ll find enough extension methods to solve this … but in this case EF Core itself has one: EF.Functions.Like(). Here’s how it’s used to generate a SQL LIKE:

return await dbContext.Mountains
            .Where(m => EF.Functions.Like(m.Name, $"%{queryText}%"))
            .OrderBy(m => m.Rank)
            .AsNoTracking()
            .ToListAsync();

Here’s how the result of the non-case-sensitive search looks like in the sample app:

DatabaseSearch

Grouping rows

This is the first version of our LINQ query for the grouping feature:

IEnumerable<GroupInfoCollection<string, Mountain>> query = dbContext.Mountains
                .OrderBy(m => m.Range)
                .ThenBy(m => m.Rank)
                .AsNoTracking()
                .GroupBy(m => m.Range, (key, list) => new GroupInfoCollection<string, Mountain>(key, list));

Bad luck: the LINQ Provider for Sqlite does not know how to translate this C# into SQL syntax, and crashes at runtime:

MissingGroupByLinq

We opted to bail out of the Sqlite Provider with a ToList() in the middle of the expression. The GroupBy() is then not applied to the query (an IQueryable) but to the result of the query (an IEnumerable). The responsibility was passed to the native .NET 5 LINQ provider, who knows how to handle this:

IEnumerable<GroupInfoCollection<string, Mountain>> query = dbContext.Mountains
                .OrderBy(m => m.Range)
                .ThenBy(m => m.Rank)
                .AsNoTracking()
                .ToList()
                .GroupBy(m => m.Range, (key, list) => new GroupInfoCollection<string, Mountain>(key, list));

Here’s how grouping looks like in our sample app:

DatabaseGroup

Showing Row Details

The command bar has a button to reveal (or hide) the details of the selected row:

DatabaseDetails

We prefer this pattern over the ‘every-click-opens-details-row’ that’s built into the DataGrid. Firstly because you cannot unselect a row – clicking on the selected row does not unselect it. The only way to close the details row is selecting another row – which then opens a new details row. Secondly we believe that in most cases it’s better to display the details not inside the DataGrid but in a separate place (navigate to another page, open a dialog, …).

Stability

Running V1 of a ported Silverlight control in an application ecosystem that’s only in v0.8: what could possibly go wrong? Smile 

When browsing the open DataGrid related issues on GitHub, you encounter some stability issues. When we started building the sample app, we ran into some of these. When rapidly scrolling, switching mode, and clicking column headers, the DataGrid control gave up rather quickly. For the sake of completeness: with the same behavior we also managed to crash all of the 3rd party sample apps.

We were relieved to observe that most if not all of the performance and stability issues were solved with the release of Windows App SDK 0.8.2! From that release on the DataGrid is behaving as expected.

Conclusion

As far as we are concerned, Community Toolkit DataGrid on WinUI 3 has passed the tests, and is ready for prime time. Some of the features -like filtering and grouping- require more design and development effort than you would have with third party components. This is a challenge, but it’s also an opportunity for you to come up with a friendly non-technical user experience for these scenarios. Here are a couple of examples (NOT included in the sample app) of custom filtering and grouping that would even be hard with third party grids.

  • Removing the Lagers from a list of beer styles. This is a filter on a substring in a hidden column:

Sample

  • Grouping a list of hops by origin. This is a grouping on an n-to-n relation:

Sample2

It’s great for the end user to see the most relevant filtering and groupings of the data and just select one from a menu.

Our sample app lives here on GitHub.

Enjoy!

Navigating in a WinUI 3 Desktop application

In this article we describe a minimal framework for a navigation service in a WinUI 3 Desktop application on top of a NavigationView control. We will cover

  • navigating from a menu item to an application page,
  • navigating to a menu item from code behind,
  • retrieving the current menu item,
  • hiding and showing menu items, and
  • dynamically adding menu items.

Our purpose is to describe the interaction between some of the core classes and come up with a pattern that you can reuse in your own WinUI 3 apps. For that reason we deliberately stay away from MVVM and Dependency Injection libraries. We created a small sample app, here’s how it looks like:

Werchter

The app is a WinUI 3 Desktop app built on top of the new Windows App SDK v0.8 (previously known as Project Reunion) with the regular Visual Studio 2019 (no preview stuff needed). From their own documentation, we learn that 

the Windows App SDK is a set of new developer components and tools that represent the next evolution in the Windows app development platform. The Windows App SDK provides a unified set of APIs and tools that can be used in a consistent way by any desktop app on Windows 11 and downlevel to Windows 10, version 1809,

and

Windows UI Library (WinUI) 3 is a native user experience (UX) framework for building modern Windows apps. It ships independently from the Windows operating system as a part of Project Reunion (now called the Windows App SDK). The 0.8 Preview release provides Visual Studio project templates to help you start building apps with a WinUI 3-based user interface.

Check this link on how to prepare your development environment for this. When all prerequisites are met, you should be able to create new WinUI 3 projects:

ReunionTemplates

UWP developers will feel at home in a WinUI 3 Desktop application: it looks like UWP and it feels like UWP, except that

  • the solution currently (and temporarily) comes with a separate MSIX installation project, and
  • the main page (our Shell) is not a Page but a Window – one that supports both UWP and Win32 app models.

The main beef of our Shell Page Window is the WinUI 3 version of the NavigationView control. In the first releases of UWP we developers needed to create a navigation UI from scratch. In a couple of years modern XAML navigation UI evolved from DIY SplitView-based implementations (been there, done that) to simply putting a full fledged NavigationView control on the main page. NavigationView comes with different modes (left menu/top menu), built-in adaptive behavior, two-level hierarchical menu structure, footer menu items, back button support, animations, different icon types, … Apart from the menu, the control also comes with a Header, and a Frame to host the application pages.

Here’s the main structure of the Shell page in our sample app:

<NavigationView x:Name="NavigationView"
                Loaded="NavigationView_Loaded"
                SelectionChanged="NavigationView_SelectionChanged" 
                Header="WinUI 3 Navigation Sample"
                IsBackButtonVisible="Collapsed"
                IsSettingsVisible="False">
    <NavigationView.MenuItems>
        <NavigationViewItem Content="Home"
                            Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.HomePage"
                            ToolTipService.ToolTip="Home">
            <NavigationViewItem.Icon>
                <BitmapIcon UriSource="/Assets/Home.png"
                            ShowAsMonochrome="False" />
            </NavigationViewItem.Icon>
        </NavigationViewItem>
        <!-- More items -->
    </NavigationView.MenuItems>
    <NavigationView.FooterMenuItems>
        <NavigationViewItem Content="About"
                            Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.AboutPage">
            <NavigationViewItem.Icon>
                <BitmapIcon UriSource="/Assets/About.png"
                            ShowAsMonochrome="False" />
            </NavigationViewItem.Icon>
        </NavigationViewItem>
    </NavigationView.FooterMenuItems>
    <Frame x:Name="ContentFrame" />
</NavigationView>

The Festivals item demonstrates hierarchical navigation using nested menu items:

<NavigationViewItem Content="Festivals"
                    Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.FestivalPage"
                    ToolTipService.ToolTip="Festivals">
    <NavigationViewItem.MenuItems>
        <NavigationViewItem Content="Tomorrowland"
                            Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.FestivalDetailsPage"
                            ToolTipService.ToolTip="Tomorrowland" />
        <NavigationViewItem Content="Rock Werchter"
                            Tag="XamlBrewer.WinUI3.Navigation.Sample.Views.FestivalDetailsPage"
                            ToolTipService.ToolTip="Rock Werchter" />
    </NavigationViewItem.MenuItems>
</NavigationViewItem>

There’s much more on NavigationView than we cover in this article. For more details check these guidelines and play around with the WinUI 3 Controls Gallery app:

WinUI3ControlsGallery

Basic Navigation

Our navigation pattern assumes/enforces that all navigation in the app is initiated by the NavigationView instance in the Shell window. We believe that this is applicable to a huge number of apps – at least to the ones that we are currently migrating from UWP. All navigation requests must refer to a NavigationViewItem instance that corresponds with an entry in the menu. The menu items define the target page in their Tag and Content fields, as you saw in the XAML snippets above. It’s the SelectionChanged event that triggers the navigation:

private void NavigationView_SelectionChanged(
	NavigationView sender, 
	NavigationViewSelectionChangedEventArgs args)
{
    SetCurrentNavigationViewItem(args.SelectedItemContainer as NavigationViewItem);
}

This first call into our micro-framework looked up the selected menu item and updated the content frame. Here’s how the code is structured:

  1. All navigation-related code is implemented in a partial class of the Shell window,
  2. encapsulated in an interface that is
  3. exposed via the App instance to
  4. the different XAML pages.

ProjectStructure

When a menu item is selected, we look up the target page information from that menu item, and pass it to Frame.Navigate(). We set the appropriate page header and update the menu’s SelectedItem. That last line is needed in case the navigation was triggered from code behind.

public void SetCurrentNavigationViewItem(
	NavigationViewItem item)
{
    if (item == null)
    {
        return;
    }

    if (item.Tag == null)
    {
        return;
    }

    ContentFrame.Navigate(
	Type.GetType(item.Tag.ToString()), 
	item.Content);
    NavigationView.Header = item.Content;
    NavigationView.SelectedItem = item;
}

Feel free add a test to prevent navigating to an invisible menu item and some exception handling, if you want.

To avoid showing an empty content frame, the app auto-navigates to the Home page when the app starts. The navigation logic is the same throughout all use cases in the app:

  1. look up the menu item that corresponds to the target page, and
  2. use it in the SetCurrentNavigationViewItem() call.
private void NavigationView_Loaded(
	object sender, 
	RoutedEventArgs e)
{
    // Navigates, but does not update the Menu.
    // ContentFrame.Navigate(typeof(HomePage));

    SetCurrentNavigationViewItem(GetNavigationViewItems(typeof(HomePage)).First());
}

Finding menu items

GetNavigationViewItems() retrieves a flattened list of all menu items of the NavigationView: the MenuItems, the FooterMenuItems, and their children. We added two overloads – one to filter on page type (e.g. to find all detail pages in a list) and another to filter on page type and title (to find a specific detail page):

public List<NavigationViewItem> GetNavigationViewItems()
{
    var result = new List<NavigationViewItem>();
    var items = NavigationView.MenuItems.Select(i => (NavigationViewItem)i).ToList();
    items.AddRange(NavigationView.FooterMenuItems.Select(i => (NavigationViewItem)i));
    result.AddRange(items);

    foreach (NavigationViewItem mainItem in items)
    {
        result.AddRange(mainItem.MenuItems.Select(i => (NavigationViewItem)i));
    }

    return result;
}

public List<NavigationViewItem> GetNavigationViewItems(
	Type type)
{
    return GetNavigationViewItems().Where(i => i.Tag.ToString() == type.FullName).ToList();
}

public List<NavigationViewItem> GetNavigationViewItems(
	Type type, 
	string title)
{
    return GetNavigationViewItems(type).Where(ni => ni.Content.ToString() == title).ToList();
}

Feel free to filter away NavigationViewItemHeader and NavigationViewItemSeparator instances from the flat list, if they’re in your way.

We also disclose the currently selected menu item:

public NavigationViewItem GetCurrentNavigationViewItem()
{
    return NavigationView.SelectedItem as NavigationViewItem;
}

Exposing the menu items

The previous methods were all implemented in the Shell Window. To make them available all over the app, we first defined them in an interface:

public interface INavigation
{
    NavigationViewItem GetCurrentNavigationViewItem();

    List<NavigationViewItem> GetNavigationViewItems();

    List<NavigationViewItem> GetNavigationViewItems(Type type);

    List<NavigationViewItem> GetNavigationViewItems(Type type, string title);

    void SetCurrentNavigationViewItem(NavigationViewItem item);
}

Then we exposed the implementation via the App instance – it knows the Shell because it creates it on start-up:

private Shell shell;

public INavigation Navigation => shell;
        
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
    shell = new Shell();
    shell.Activate();
}

All parts of the code base can now easily access the lightweight Navigation Service:

(Application.Current as App).Navigation

Using the Navigation Service

Showing and hiding existing menu items

Our sample app has a ‘Beer’ page that only becomes visible when the user confirms she’s old enough to handle its content. The page is defined in the NavigationView menu, but is initially invisible. The Home page has a checkbox in the lower right corner:

HomePage

When the box is checked, the hidden menu item appears:

HomePageCheck

Here’s the code in the Homepage. It looks up the BeerPage NavigationViewItem, and manipulates its Visibility:

private static NavigationViewItem BeerItem => 
	(Application.Current as App)
		.Navigation
		.GetNavigationViewItems(typeof(BeerPage))
		.First();

private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
    BeerItem.Visibility = Visibility.Visible;
}

private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
{
    BeerItem.Visibility = Visibility.Collapsed;
}

Programmatically navigating to an existing menu item

The FormulaOnePage in our sample app has a hyperlink to the FestivalPage:

Formula1Page

The code behind that hyperlink looks up the target menu item using the GetNavigationViewItems overload with the page type, and then navigates to it – very straightforward:

private void Hyperlink_Click(
	Hyperlink sender, 
	HyperlinkClickEventArgs args)
{
    var navigation = (Application.Current as App).Navigation;
    var festivalItem = navigation.GetNavigationViewItems(typeof(FestivalPage)).First();
    navigation.SetCurrentNavigationViewItem(festivalItem);
}

There’s a similar hyperlink in the HomePage, to test whether we can reach footer menu items in the same way:

FestivalPage

Under the Festival menu item, there is a list of FestivalDetails pages – all of the same type, but with a different topic of course. The hyperlinks on that Festival page use the GetNavigationViewItems overload with page type and content, and also ensure that the parent (Festival) menu item gets expanded:

private void Hyperlink_Click(
	Hyperlink sender, 
	HyperlinkClickEventArgs args)
{
    var navigation = (Application.Current as App).Navigation;
    navigation.GetCurrentNavigationViewItem().IsExpanded = true;
    var festivalItem = navigation.GetNavigationViewItems(
	typeof(FestivalDetailsPage), 
	"Rock Werchter").First();
    navigation.SetCurrentNavigationViewItem(festivalItem);
}

Here’s one of the detail pages:

FestivalDetailPage

Dynamically adding menu items

The BeerPage has a button to programmatically add BeerDetailPage items:

BeerPage

It looks up the parent, adds a menu item of the appropriate type and with its specific title, and makes sure that the parent is expanded:

private void Button_Click(
	object sender, 
	RoutedEventArgs e)
{
    var beerItem = (Application.Current as App)
	.Navigation
	.GetNavigationViewItems(this.GetType())
	.First();
    beerItem.MenuItems.Add(new NavigationViewItem
    {
        Content = $"Round {beerItem.MenuItems.Count + 1}",
        Tag = typeof(BeerDetailsPage).FullName
    });
    beerItem.IsExpanded = true;
}

Here’s such a detail page:

BeerDetailsPage

It has to buttons to iterate back and forth through its list of siblings. The ‘previous’ button navigates backwards through the list of all BeerDetailPage items in the menu. These may be spread over multiple parent items. The ‘next’ button shows how to limit the navigation to the parent of the detail page. This algorithm is a bit more cumbersome since child menu items don’t have a reference to their parent:

private void Button_Click(
	object sender, 
	RoutedEventArgs e)
{
    // Navigation through colleagues
    var navigation = (Application.Current as App).Navigation;
    var item = navigation.GetCurrentNavigationViewItem();
    var siblings = navigation.GetNavigationViewItems(this.GetType());
    var index = siblings.IndexOf(item);
    if (index > 0)
    {
        navigation.SetCurrentNavigationViewItem(siblings[index - 1]);
    }
}

private void Button_Click_1(
	object sender, 
	RoutedEventArgs e)
{
    // Navigation within parent
    var navigation = (Application.Current as App).Navigation;
    var item = navigation.GetCurrentNavigationViewItem();
    var mainItems = navigation.GetNavigationViewItems();
    foreach (var mainItem in mainItems)
    {
        // Find the parent
        if (mainItem.MenuItems.Contains(item))
        {
            var siblings = mainItem.MenuItems;
            var index = siblings.IndexOf(item);
            if (index < siblings.Count - 1)
            {
                navigation.SetCurrentNavigationViewItem((NavigationViewItem)siblings[index + 1]);
            }
        }
    }
}

It’s a wrap

There is definitely room for extra helper methods and a higher abstraction level, but in just a handful lines of C# we created the core of a service that covers most of the navigation requirements for an WinUI 3 app that uses a NavigationView control.

Our sample app lives here on GitHub.

Enjoy!

Data Validation with the Microsoft MVVM Toolkit

In this article we will walk through a handful of scenarios and techniques for input validation with the ObservableValidator class of Microsoft MVVM Toolkit. We will cover

  • canonical property validation,
  • comparing two (or more) properties,
  • comparing the new value of a property to its previous one,
  • delaying the validation, and
  • preventing to assign an invalid value to a property.

We created a sample app in UWP, it looks like this:

ComparingProperties

Input Validation in UWP

The System.Component.DataAnnotations is almost as old as the .NET Framework itself. It contains attribute classes to decorate ViewModels and Models with metadata for validation purposes – among other. These attributes describe property validation rules. When the rules are broken, the instance exposes its validation errors via its INotifyDataError members. This validation technique is intensively used in several ASP.NET frameworks and also made its way to Silverlight and WPF. Until very recently, data validation in UWP did not get much attention from Microsoft. Most development teams embedded third-party solutions such as Prism, Template10, or Calcium into their UWP apps, or rolled their own custom solution.

With Microsoft MVVM toolkit there now is a Microsoft provided alternative, and it’s even ready for Reunion. Now that we’re talking about the future: WinUI 3 controls will come with templates that react upon the INotifyDataErrorInfo status of the (View)Model that they are bound to. There is currently a bug that inhibits a deeper dive into this. Nevertheless, the WinUI3 Controls Gallery app already contains a sample page. It’s currently broken, but it reveals a glimpse of the near future of input validation in WinUI:

WinuiControlsGallery

That’s not a spectacular screenshot, so for reference here’s an example of similar control templates in WPF:

customerrortemplateWPF

While the UI parts of UWP Data Validation are not yet ready for prime time, the supporting Microsoft MVVM Toolkit is fully operational. Welcome to ObservableValidator.

ObservableValidator

The great official documentation teaches us that

‘The ObservableValidator is a base class implementing the INotifyDataErrorInfo interface, providing support for validating properties exposed to other application modules. It also inherits from ObservableObject, so it implements INotifyPropertyChanged and INotifyPropertyChanging as well. It can be used as a starting point for all kinds of objects that need to support both property change notifications and property validation.’.

Microsoft MVVM Toolkit is fully developed in the open, the source code for ObservableValidator is right here. Models and ViewModels that require validation just need to inherit from it, like this:

public class Suspect : ObservableValidator
{
    private string _name;
    private string _socialSecurityNumber;

    // ... there's more
}

The instance will expose its error status through its INotifyDataErrorInfo members. The Views in our sample app have bindings to ErrorsChanged and HasErrors. Check our previous blog post to see how this was done. Were’ reusing its ‘error popup’ approach. Here’s its XAML definition:

<SymbolIcon Symbol="ReportHacked"
            Foreground="Red"
            Visibility="{x:Bind ViewModel.Suspect.HasErrors, Mode=OneWay}">
    <ToolTipService.ToolTip>
        <TextBlock Text="{x:Bind ViewModel.Suspect.Errors, Mode=OneWay}"
                    Foreground="Red" />
    </ToolTipService.ToolTip>
</SymbolIcon>

Using existing Validation Attributes

The System.Component.DataAnnotations namespace hosts a huge list of attributes available for the validation of individual properties: required, minimum and maximum length, range, Enum, Regex … These cover most of the usual suspects. There are not too much XAML examples on the market, so when you search for sample code, you’ll probably end up in ASP.NET MVC projects. Don’t worry about that: MVC Models are very similar to MVVM ViewModels.

To implement validation in a class, it suffices to decorate its properties with one or more of these validation attributes and call one of its SetProperty() overloads with true as the third parameter in the property setter. Here’s how our sample app evaluates whether Keyser Söze has a required Name of minimum length and has his SocialSecurityNumber checked against a regular expression:

[Required(
	ErrorMessage = "Name is Required")]
[MinLength(
	2, 
	ErrorMessage = "Name should be longer than one character")]
public string Name
{
    get => _name;
    set => SetProperty(ref _name, value, true);
}

[RegularExpression(
	@"^(?!000)(?!666)(?!9)\d{3}([- ]?)(?!00)\d{2}\1(?!0000)\d{4}$", 
	ErrorMessage = "Invalid Social Security Number.")]
public string SocialSecurityNumber
{
    get => _socialSecurityNumber;
    set => SetProperty(ref _socialSecurityNumber, value, true);
}

The validation is triggered whenever the property gets a new value. All we need in the View is a two-way binding to the property:

<TextBox Text="{x:Bind ViewModel.Suspect.Name, Mode=TwoWay}"
            PlaceholderText="Name" />
<TextBox Text="{x:Bind ViewModel.Suspect.SocialSecurityNumber, Mode=TwoWay}"
            PlaceholderText="Social Security Number" />

Here’s how our sample app reacts to an evil social security number:

ValidationAttributes

The regular expression correctly refuses a number that starts with 666.

Rolling your own Validation Attributes

It’s easy to roll your own reusable validation attribute: inherit from ValidationAttribute and provide your own implementation of the IsValid() method. The method gets the ValidationContext injected as a parameter, exposing the whole instance that is being validated, not only the decorated property. This allows you to write validation rules over more than one property, like:

  • property a should be less than property b, or
  • if property a has a value, then property b becomes required.

Allow us to mention that there already *is* a validation attribute that compares two properties -the CompareAttribute– but it only checks for equality. It’s used in the confirmation fields for email addresses and passwords (and probably nowhere else)

Our sample app contains a validation rule to compare two dates. It is used in the common ‘if the EndDate is filled out it then should come after StartDate’ scenario.

Let’s first show how the GreaterThan attribute is applied in the ViewModel class:

[GreaterThan (
	nameof(StartDate), 
	"End date should come after start date.")]
public DateTime? EndDate
{
    get => _endDate;
    set { 
        SetProperty(ref _endDate, value, true);
    }
}

Here’s the implementation of the validation attribute itself. It can be reused across applications:

public sealed class GreaterThanAttribute : ValidationAttribute
{
    private string _errorMessage;

    public GreaterThanAttribute(string propertyName, string errorMessage)
    {
        PropertyName = propertyName;
        _errorMessage = errorMessage;
    }

    public string PropertyName { get; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value == null)
        {
            return ValidationResult.Success;
        }

        var instance = validationContext.ObjectInstance;
        var otherValue = instance.GetType().GetProperty(PropertyName).GetValue(instance);

        if (((IComparable)value).CompareTo(otherValue) > 0)
        {
            return ValidationResult.Success;
        }

        return new ValidationResult(_errorMessage);
    }
}

Since we’ve created a dependency between the values of Start- and EndDate in the ViewModel, we need to make sure that the validation is triggered by both properties’ changes. That’s why we make a call to ValidateProperty() in the setter of StartDate:

public DateTime? StartDate
{
    get => _startDate;
    set { SetProperty(ref _startDate, value, true);
        ValidateProperty(EndDate, nameof(EndDate));
    }
}

In the View, we bound both properties to a Date in a CalendarDatePicker:

<CalendarDatePicker Date="{x:Bind ViewModel.NoDelorean.GetStartDate(), BindBack=ViewModel.NoDelorean.SetStartDate, Mode=TwoWay}"
                    PlaceholderText="Start Date" />
<CalendarDatePicker Date="{x:Bind ViewModel.NoDelorean.GetEndDate(), BindBack=ViewModel.NoDelorean.SetEndDate, Mode=TwoWay}"
                    PlaceholderText="End Date" />

[We used {x:Bind} with function bindings to plug in a transformation between the DateTime of the properties and the DateTimeOffset values of the controls.]

Here’s how an invalid instance looks like in the sample app:

ComparingProperties

Also, please note that this rule does not apply to all ViewModels. Sometimes it should be possible to go back in time:

delorean

Using a CustomValidation method

Not every validation rule should be cast in a universally reusable validation attribute class. For a local rule, you can get away with writing a static validation method and applying it to a property via a CustomValidation attribute. Our sample app uses this technique to compare a new value of a property to its old value. The ViewModel represents a CountDown class – the validation rule ensures that we don’t ‘count up’.

The values to compare must have their own fields:

private int _value = 10;
private int _previousValue;

We created a static method that takes the property’s type (int in our Counter case) and a ValidationContext, and returns the result of the validation as a ValidationResult:

public static ValidationResult ValidateValue(
	int value, 
	ValidationContext context)
{
    var instance = (Countdown)context.ObjectInstance;
    var isValid = value < instance._previousValue;

    if (isValid)
    {
        return ValidationResult.Success;
    }

    return new ValidationResult("We're not supposed to count up.");
}

Again the validation context gives us access to the instance being validated, so it allows us to compare the new counter value to the old. Here’s how the rule is applied to the Value property:

[CustomValidation(typeof(Countdown), nameof(ValidateValue))]
public int Value
{
    get => _value;
    set
    {
        _previousValue = _value;
        SetProperty(ref _value, value, true);
    }
}

And this is how violating the counter rule looks like in the sample app:

ComparingNewToOld

Delaying Validation

Up until now, we always triggered the validation on the assignment of the property. This is not needed or wanted in every scenario or for every property. The third parameter in the SetProperty() call was always set to true in the previous examples. In our next sample, the validation will be triggered by a button click, so we start with a false in the property setter:

[Required(
	ErrorMessage = "Name is Required")]
[MinLength(
	2, 
	ErrorMessage = "Name should be longer than one character")]
public string Name
{
    get => _name;
    set => SetProperty(ref _name, value, false);
}

[RegularExpression(
	@"^(?!000)(?!666)(?!9)\d{3}([- ]?)(?!00)\d{2}\1(?!0000)\d{4}$", 
	ErrorMessage = "Invalid Social Security Number.")]
public string SocialSecurityNumber
{
    get => _socialSecurityNumber;
    set => SetProperty(ref _socialSecurityNumber, value, false);
}

For the sake of simplicity we validate all properties together – there’s a call for this: ValidateAllProperties(). The call is hooked to the button with an instance of MVVM Toolkit’s RelayCommand:

public ICommand ValidateCommand => 
	new RelayCommand(() => ValidateAllProperties());

Here’s the binding:

<Button Content="Validate"
        Command="{x:Bind ViewModel.SuspectWithDelayedValidation.ValidateCommand, Mode=OneWay}" />

And this is how the result looks like in the app:

DelayedValidation

There is a Try

In the previous examples we started the validation after the assignment of a property. We were continuously breaking an ancient object oriented programming principle:

it should not be possible to bring a properly encapsulated object into an invalid state via the public interface.

In some scenarios or parts of your app (e.g. in the Models) you should indeed prevent the assignment of invalid values to properties. Fortunately Microsoft MVVM Toolkit comes with TrySetProperty() … there *is* a Try.

notry

TrySetProperty inspects the new value that you (try to) assign, and only succeeds when that value is a valid one. On the upside you’ll never have instances in an invalid state, on the downside you need to provide you own Errors store – instances will never have ‘official’ errors in their INotifyDataErrorInfo members.

In our sample app we decorated the ViewModel with alternative members – a list of ValidationResult instances to store the errors per property and a Boolean that returns whether that list has members:

public class NotYoda : ObservableValidator
{
    private List<ValidationResult> _errors = new List<ValidationResult>();

    public string Errors => string.Join(Environment.NewLine, from ValidationResult e in _errors select e.ErrorMessage);

    // Since HasErrors is not virtual:
    public bool ErrorsHaveI => Errors.Length > 0;

    // More, there is...
}

In the property setters we first clean up the previous messages for the property, then call the TrySetProperty, use its output parameter to update our custom error list, and then notify the custom error property changes:

[Required(
	ErrorMessage = "Name is Required")]
[MinLength(
	2, 
	ErrorMessage = "Name should be longer than one character")]
public string Name
{
    get => _name;
    set
    {
        _errors.RemoveAll(v => v.MemberNames.Contains(nameof(Name)));

        TrySetProperty(ref _name, value, out IReadOnlyCollection<ValidationResult> errors);

        _errors.AddRange(errors);
        OnPropertyChanged(nameof(Errors));
        OnPropertyChanged(nameof(ErrorsHaveI));
    }
}

[RegularExpression(
	@"^(?!000)(?!666)(?!9)\d{3}([- ]?)(?!00)\d{2}\1(?!0000)\d{4}$", 
	ErrorMessage = "Invalid Social Security Number.")]
public string SocialSecurityNumber
{
    get => _socialSecurityNumber;
    set
    {
        _errors.RemoveAll(v => v.MemberNames.Contains(nameof(SocialSecurityNumber)));

        TrySetProperty(ref _socialSecurityNumber, value, out IReadOnlyCollection<ValidationResult> errors);

        _errors.AddRange(errors);
        OnPropertyChanged(nameof(Errors));
        OnPropertyChanged(nameof(ErrorsHaveI));
    }
}

In the View, the bindings for the properties are not different from the other samples:

<TextBox Text="{x:Bind ViewModel.NotYoda.SocialSecurityNumber, Mode=TwoWay}"
            PlaceholderText="Social Security Number" />
<TextBlock Text="{x:Bind ViewModel.NotYoda.SocialSecurityNumber, Mode=TwoWay}" />

The error icon is of course bound to the custom error properties:

<SymbolIcon Symbol="ReportHacked"
            Foreground="Red"
            Visibility="{x:Bind ViewModel.NotYoda.ErrorsHaveI, Mode=OneWay}"
            HorizontalAlignment="Right">
    <ToolTipService.ToolTip>
        <TextBlock Text="{x:Bind ViewModel.NotYoda.Errors, Mode=OneWay}"
                    Foreground="Red" />
    </ToolTipService.ToolTip>
</SymbolIcon>

Our sample app displays the current (valid) value next to the input boxes:

TrySetProperty

The shape of things to come

If you compare the different ViewModels in our sample app, you’ll definitely observe the copy/paste patterns around most of the calls to SetProperty() and OnPropertyChanged(). In a future version, MVVM Toolkit will be enhanced with property attributes that will generate the source code for these – as illustrated in this tweet from the author:

WhatsNext

We’ll keep you informed on this. In mean time, our sample app lives here on GitHub.

Enjoy!