Category Archives: Windows App SDK

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!

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!