Displaying XAML Controls in QuestPDF with WinUI

In this article we demonstrate how to render XAML controls from a WinUI 3 Desktop Application in a PDF document. This is the third part in a series on using QuestPDF for generating PDF documents. In the first article we introduced the API (text, images, tables, page numbers, …) and the patterns, in the second article we demonstrated how to embed OxyPlot charts or other SkiaSharp-based visuals in a PDF document. In this article we focus on XAML visuals. We will cover

  • basic controls such as sliders and radio buttons,
  • Windows Community Toolkit controls such as radial gauge and orbit view,
  • off-page rendering,
  • theming, and
  • adding a full app screenshot.

We added a new sample page to our QuestPDF sample app. The page has its own PDF document. Here’s how both look like:

Displaying XAML Visuals

Our sample page hosts some regular XAML controls:

<CheckBox x:Name="CheckBox"
            Content="5 golden rings"
            IsChecked="True" />
<RatingControl x:Name="RatingControl"
                Value="4"
                Caption="4 calling birds"
                HorizontalAlignment="Left" />
<RadioButtons x:Name="RadioButton"
                MaxColumns="2"
                SelectedIndex="1">
    <x:String>3 French hens</x:String>
    <x:String>2 turtle doves</x:String>
</RadioButtons>
<Button x:Name="Button">
    <StackPanel Orientation="Horizontal">
        <Image Source="/Assets/Partridge.png"
                Height="32"
                Margin="6" />
        <TextBlock Text="A partridge in a pear tree"
                    Margin="6"
                    VerticalAlignment="Center" />
    </StackPanel>
</Button>

<!-- More XAML Visuals ... -->

We also added some Windows Community Toolkit controls with a bit more complexity. The RadialGauge renders through the Composition Layer, and the OrbitView mixes XAML elements and images. Here’s how they are defined in the sample page:

<ContentControl VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Grid.Row="1"
                MaxHeight="200">
    <controls:RadialGauge x:Name="RadialGauge"
                            Value="7"
                            Maximum="12"
                            Unit="Swans a-swimming"
                            ...
                            Grid.Row="1" />
</ContentControl>

<controls:OrbitView x:Name="OrbitView"
                    Background="Transparent"
                    OrbitsEnabled="True"
                    AnchorsEnabled="False"
                    MinItemSize="30"
                    MaxItemSize="60"
                    AnchorColor="Gainsboro"
                    OrbitColor="Gainsboro"
                    Grid.Column="1"
                    VerticalAlignment="Top"
                    HorizontalAlignment="Left"
                    Margin="-60">
    <controls:OrbitView.ItemTemplate>
        <DataTemplate x:DataType="controls:OrbitViewDataItem">
            <!-- Template -->
    <controls:OrbitView.ItemsSource>
        <controls:OrbitViewDataItemCollection>
            <controls:OrbitViewDataItem Image="ms-appx:///Assets/Moons/callisto.png"
                                        Distance="0.2"
                                        Diameter="0.3" />
            <!-- More Moons -->
        </controls:OrbitViewDataItemCollection>
    </controls:OrbitView.ItemsSource>
    <controls:OrbitView.CenterContent>
        <Image Source="/Assets/Moons/sun.png"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Height="100" />
    </controls:OrbitView.CenterContent>
</controls:OrbitView>

Observe that the RadialGauge is wrapped in a ContentControl. This is to keep its ActualSize a square – otherwise the control would be stretched into a rectangle in the PDF document.

Each control that we want to render in the QuestPDF document, is first transformed to a byte array holding a PNG image;

var images = new Dictionary<string, byte[]>
{
    { "Slider", await Slider.AsPng() },
    { "Button", await Button.AsPng() },
    { "NumberBox", await NumberBox.AsPng() },
    { "RatingControl", await RatingControl.AsPng() },
    { "CheckBox", await CheckBox.AsPng() },
    { "RadioButton", await RadioButton.AsPng() },
    { "RadialGauge", await RadialGauge.AsPng() },
    { "OrbitView", await OrbitView.AsPng() }
};

The document itself is an IDocument with the dictionary of byte arrays as model:

internal class XamlControlDocument : IDocument
{
    public Dictionary<string, byte[]> Model { get; }

    public XamlControlDocument(Dictionary<string, byte[]> model)
    {
        Model = model;
    }

    // ...

}

It’s up to the document to decide where to place the XAML Visual and with which size:

column.Item().Text("Slider:");
column.Item()
    .Height(50)
    .Image(Model["Slider"], ImageScaling.FitArea);

column.Item().Text("Radial Gauge:");
column.Item()
    .Height(150)
    .Image(Model["RadialGauge"], ImageScaling.FitArea);

column.Item().Text("NumberBox:");
column.Item()
    .Height(30)
    .Image(Model["NumberBox"], ImageScaling.FitArea);

Off-page Rendering

When your PDF document needs to display a XAML Control that’s not shown in the app, then keep it outside the page by giving it a big enough negative margin. Keep the control invisible, otherwise it might influence the ActualSize of other ones. As already mentioned, we need all controls to have a representative ActualSize:

<CalendarView x:Name="CalendarView"
                Margin="-1000, 0, 0, 0"
                Visibility="Collapsed" />

Toggle the Visibility of off-page controls when generating the PNG:

CalendarView.Visibility = Visibility.Visible;
images.Add("CalendarView", await CalendarView.AsPng());
CalendarView.Visibility = Visibility.Collapsed;

Theming

When the app is in light theme, you can render the controls as-is – they will look on paper as they look on a white application background:

When you app is in the dark theme, you can temporary switch the controls to light:

var switchTheme = false;

if (ActualTheme == ElementTheme.Dark)
{
    switchTheme = true;
    RequestedTheme = ElementTheme.Light;
}

// Generate PNG images ...

if (switchTheme)
{
    RequestedTheme = ElementTheme.Default;
}

It’s true that this theme switch will be noticeable in the UI. If that bothers you then you can keep a version of these controls off-page and bind these to the same (view)model.

Here’s how the page looks like:

And here’s the corresponding PDF document:

Adding a screenshot

Our algorithm works for all XAML controls, that includes the root control of the app window (in many cases a NavigationView). We made it available to the app’s pages via a property in App.Xaml:

internal UIElement AppRoot => shell.AppRoot;

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

Here’s how a screenshot is added to our document’s images model. We didn’t switch it to light theme:

images.Add("Root", await (Application.Current as App).AppRoot.AsPng());

And here’s how the result looks like:

Generating a PNG

Here’s the code for generating a PNG from a XAML UIElement, nicely packaged in an asynchronous extension method. It is based on an archived Windows 8 sample.

A RenderTargetBitmap creates a bitmap in BGRA8 format (little-endian RGB, how your processor stores an image) with the actual size of the control. Its pixels are encoded as PNG by a BitMapEncoder through an InMemoryRandomAccessStream (which both only seem to exist in UWP and WPF documentation – not in WindowsAppSdk). We transform this stream to a byte array to eventually feed it to our PDF document:

public static async Task<byte[]> AsPng(this UIElement control)
{
    // Get XAML Visual in BGRA8 format
    var rtb = new RenderTargetBitmap();
    await rtb.RenderAsync(control, (int)control.ActualSize.X, (int)control.ActualSize.Y);

    // Encode as PNG
    var pixelBuffer = (await rtb.GetPixelsAsync()).ToArray();
    IRandomAccessStream mraStream = new InMemoryRandomAccessStream();
    var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, mraStream);
    encoder.SetPixelData(
        BitmapPixelFormat.Bgra8,
        BitmapAlphaMode.Premultiplied,
        (uint)rtb.PixelWidth,
        (uint)rtb.PixelHeight,
        184,
        184,
        pixelBuffer);
    await encoder.FlushAsync();

    // Transform to byte array
    var bytes = new byte[mraStream.Size];
    await mraStream.ReadAsync(bytes.AsBuffer(), (uint)mraStream.Size, InputStreamOptions.None);

    return bytes;
}

Why this is important

In our article series on QuestPDF we went much further than rendering text, images, tables and page numbers in a PDF document. We focused on generating charts and diagrams, rendering XAML controls, and embedding screenshots. Our aim was to investigate whether PDF generation could be a valid substitute for direct print support. Application print support in UWP is XAML based and always was -this is an understatement- overly difficult and unstable. Since the situation for WinUI 3 does not look any better, we were looking for an alternative.

We’re happy to conclude that there are free products on the market that can cover this need to generate a print-out of a WinUI application page in a decent and stable way. We are already busy with replacing the ‘Print’ button with a ‘PDF’ button in most of our apps.

Our sample app lives here on GitHub.

Enjoy!

Advertisement

Displaying OxyPlot charts in QuestPDF with WinUI

In this article we demonstrate how to render OxyPlot charts and diagrams in a QuestPDF document, from a WinUI Desktop application. In our previous blog post on QuestPDF we showed Microcharts charts and diagrams in XAML and PDF. We love Microcharts diagrams for their modern style and their startup animations, but the number of chart types is limited. OxyPlot on the other hand comes with more chart types that you will ever need to display in any of your apps. Check our first OxyPlot blog post for more details.

For our OxyPlot experiments we added a new XAML page and PDF document to our QuestPDF sample app:

The page hosts a handful of PlotModel instances, each for a different chart style:

public sealed partial class OxyPlotPage : Page
{
    private PlotModel areaSeriesModel;
    private PlotModel functionSeriesModel;
    private PlotModel lineSeriesModel;
    private PlotModel pieSeriesModel;

    public PlotModel AreaSeriesModel => areaSeriesModel;

    public PlotModel FunctionSeriesModel => functionSeriesModel;

    public PlotModel LineSeriesModel => lineSeriesModel;

    public PlotModel PieSeriesModel => pieSeriesModel;

    public OxyPlotPage()
    {
        InitializeComponent();

        InitializeAreaSeriesModel();
        InitializeFunctionSeriesModel();
        InitializeLineSeriesModel();
        InitializePieSeriesModel();
    }

    // ...

}

Here’s how the pie chart is defined, the model comes straight out of the OxyPlot samples:

private void InitializePieSeriesModel()
{
    pieSeriesModel = new PlotModel(); // { Title = "Pie Sample1" };

    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);
}

The XAML page hosts a PlotView control for each of the models:

<oxyplot:PlotView Model="{x:Bind LineSeriesModel}"
                  Background="Transparent" />
<oxyplot:PlotView Model="{x:Bind AreaSeriesModel}"
                  Background="Transparent"
                  Grid.Column="1" />
<oxyplot:PlotView Model="{x:Bind PieSeriesModel}"
                  Background="Transparent"
                  Grid.Row="1" />
<oxyplot:PlotView Model="{x:Bind FunctionSeriesModel}"
                  Background="Transparent"
                  Grid.Row="1"
                  Grid.Column="1" />

This is how the page looks like at runtime:

For the PDF document, we followed the QuestPDF Patterns and Practices and made it an IDocument. It takes a list of OxyPlot PlotModels as Model:

internal class OxyPlotDocument : IDocument
{
    public List<PlotModel> Model { get; }

    public OxyPlotDocument(List<PlotModel> model)
    {
        Model = model;
    }

    public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

    public void Compose(IDocumentContainer container)
    {
        container.Page(page =>
        {
            page.Margin(40);

            page.Header()
                .PaddingBottom(20)
                .Text("XAML Brewer QuestPDF & OxyPlot Sample")
                .FontSize(16);

            page.Content().Element(ComposeBody);

            page.Footer().Text(text =>
            {
                text.Span("page ");
                text.CurrentPageNumber();
            });
        });
    }

    // ...
}

The QuestPDF object model provides a Canvas element to draw on. It renders via the SkiaSharp engine. The OxyPlot PlotView also embeds a Canvas, and OxyPlot supports SkiaSharp. Our initial plan was to find a way to let the two Canvas types share the same model (like we did for Microcharts). Long story short: we learned a lot about SkiaSharp, but did not find that way.

The next plan was trying to serialize the PlotModel into a format that could be read by the QuestPDF SkiaSharp Canvas – think bitmap or vector graphics. We found out that OxyPlot comes with an SvgExporter, and discovered the Svg.Skia package that knows how to transform an SVG into a SkiaSharp Picture. All we needed was a MemoryStream to connect the dots.

Here’s the code in the PDF document’s that transforms OxyPlot models into pictures:

private void ComposeBody(IContainer body)
{
    body.Column(column =>
    {
        foreach (var plotModel in Model)
        {
            column.Item()
                .Height(300)
                .Canvas((canvas, size) =>
                {
                    using var stream = new MemoryStream();
                    var exporter = new SvgExporter
                    {
                        Width = 400,
                        Height = 300
                    };

                    exporter.Export(plotModel, stream);
                    stream.Position = 0;
                    var svg = new SKSvg();
                    svg.Load(stream);

                    canvas.DrawPicture(svg.Picture);
                });
        }
    });
}

In any app bigger than our sample app we would wrap it into an extension method.

Here’s the document:

The PlotModel that is showed on the page is reused as-is in the PDF document. That looks great, until your app is in Dark Theme while your document is white paper oriented. We applied a simple hack by switching the PlotModels to Light Theme before sending them to the document generator:

if (ActualTheme == ElementTheme.Dark)
{
    ApplyTheme(ElementTheme.Light);
}

Of course you need to switch that back afterwards. Here’s how this looks like:

For the sake of completeness, here’s the generation and display process. Saving to a file is not mandatory, you can keep it all in memory if you want:

private void ChartButton_Click(object sender, RoutedEventArgs e)
{
    var filePath = "C:\\Temp\\oxyplot.pdf";

    var document = new OxyPlotDocument(
        new List<PlotModel> {
            LineSeriesModel,
            AreaSeriesModel,
            PieSeriesModel,
            FunctionSeriesModel
        });

    document.GeneratePdf(filePath);

    var process = new Process
    {
        StartInfo = new ProcessStartInfo(filePath)
        {
            UseShellExecute = true
        }
    };

    process.Start();
}

Our sample app lives here on GitHub. It demonstrates some nice use cases around QuestPDF, and there’s probably more to come…

Enjoy!

Generating PDF Documents in WinUI

In this article we demonstrate how to generate PDF documents in a WinUI 3 Desktop application  with the free(!) QuestPDF library. Our intention is not to deeply dive into the API, but to walk through some representative use cases.

We will

  • test drive some of the samples that are provided by QuestPDF itself,
  • create a brochure type document from scratch, and
  • show how to reuse the source code for Microcharts diagrams in XAML and PDF.

As usual we created a sample app. It looks like this:

Getting Started

Getting started with QuestPDF is dead easy: there is excellent documentation right here. To get our first experience, we grabbed some sample documents from their repo and grouped these in a sample page of our own:

Since our sample app generates all documents in the C:\Temp folder, each document starts with defining the target path:

var filePath = "C:\\Temp\\hello.pdf";

Here’s the declaration for the ‘getting started’ document from the QuestPDF documentation:

var document = Document.Create(container =>
{
    container.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(2, Unit.Centimetre);
        page.PageColor(Colors.White);
        page.DefaultTextStyle(x => x.FontSize(20));

        page.Header()
            .Text("Hello PDF!")
            .SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);

        page.Content()
            .PaddingVertical(1, Unit.Centimetre)
            .Column(x =>
            {
                x.Spacing(20);
                x.Item().Text(Placeholders.LoremIpsum());
                x.Item().Image(Placeholders.Image(200, 100));
            });

        page.Footer()
            .AlignCenter()
            .Text(x =>
            {
                x.Span("Page ");
                x.CurrentPageNumber();
            });
    });
});

Notice the Fluent API with all methods returning an IContainer. The object model has nice placeholders for text and images that you can use during document design. Here’s how the PDF looks like:

GeneratePdf() … generates the PDF, in memory or in a file:

// var bytes = document.GeneratePdf();
document.GeneratePdf(filePath);

// document.ShowInPreviewer();

QuestPDF comes with a Previewer component – an Avalonia XAML app. It can be hooked to Visual Studio when you’re designing a document (hot reload is supported!). You can also programmatically send your document to it with a ShowInPreviewer() call. We couldn’t make it to work from our WinUI app (and we’re not the only one). A call from a Console app works fine:

Our sample app generates the PDF in a file, and then opens it. To open a PDF in the browser you could use this:

Process.Start("explorer.exe", filePath);

But it’s better to respect the user’s default PDF app, like this:

var process = new Process
{
    StartInfo = new ProcessStartInfo(filePath)
    {
        UseShellExecute = true
    }
};

process.Start();

An Invoice Document

We used the same approach for the second sample: QuestPDF’s sample Invoice Document – a multipage document with tables. Here’s the code behind our Invoice Document button:

var document = new InvoiceDocument(model);
document.GeneratePdf(filePath);

QuestPDF comes with some handy patterns and helper interfaces. It’s not mandatory to apply these, but they’re convenient. IDocument, for example, describes a template for a document with its settings, its data model, and its rendering code. The Invoice Document sample demonstrates a typical table-centric document. Here’s the structure of the corresponding InvoiceDocument:

public class InvoiceDocument : IDocument
{
    public InvoiceModel Model { get; }

    public InvoiceDocument(InvoiceModel model)
    {
        Model = model;
    }

    public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

    public void Compose(IDocumentContainer container)
    {
        // Here comes the real stuff ...
    }

}

Here’s how the Compose() is implemented. The Footer is straightforward, the generation of Content and Header are factored out into a separate method:

public void Compose(IDocumentContainer container)
{
    container
        .Page(page =>
        {
            page.Margin(50);

            page.Header().Element(ComposeHeader);

            page.Content().Element(ComposeContent);

            page.Footer().AlignCenter().Text(text =>
            {
                text.CurrentPageNumber();
                text.Span(" / ");
                text.TotalPages();
            });
        });
}

The Header of the document has some rows and columns, and an image (or at least a placeholder for it) to the right:

void ComposeHeader(IContainer container)
{
    container.Row(row =>
    {
        row.RelativeItem().Column(Column =>
        {
            Column
                .Item().Text($"Invoice #{Model.InvoiceNumber}")
                .FontSize(20).SemiBold().FontColor(Colors.Blue.Medium);

            Column.Item().Text(text =>
            {
                text.Span("Issue date: ").SemiBold();
                text.Span($"{Model.IssueDate:d}");
            });

            Column.Item().Text(text =>
            {
                text.Span("Due date: ").SemiBold();
                text.Span($"{Model.DueDate:d}");
            });
        });

        row.ConstantItem(100).Height(50).Placeholder();
    });
}

In the Content of the document, another pattern is used: IComponent provides a way to specify a reusable layout. The InvoiceDocument uses one to render both seller and buyer addresses with the same code:

void ComposeContent(IContainer container)
{
    container.PaddingVertical(40).Column(column =>
    {
        column.Spacing(20);

        column.Item().Row(row =>
        {
            row.RelativeItem().Component(new AddressComponent("From", Model.SellerAddress));
            row.ConstantItem(50);
            row.RelativeItem().Component(new AddressComponent("For", Model.CustomerAddress));
        });

        column.Item().Element(ComposeTable);

        // There's more ...
    });
}

public class AddressComponent : IComponent
{
    private string Title { get; }
    private Address Address { get; }

    public AddressComponent(string title, Address address)
    {
        Title = title;
        Address = address;
    }

    public void Compose(IContainer container)
    {
        container.ShowEntire().Column(column =>
        {
            // ...
            column.Item().Text(Address.CompanyName);
            column.Item().Text(Address.Street);
            // ...
        });
    }
}

Here’s how the document looks like:

If you need to render tables in your PDF document, then walk through the entire InvoiceDocument sample.

Drawings and Charts

QuestPDF goes further that just rendering text and images. You can use the Canvas element from SkiaSharp to draw custom shapes and charts. You can draw shapes directly, or use a SkiaSharp compatible library such as Microcharts.

Here’s how to draw a simple line diagram:

column
    .Item()
    .Border(1)
    .ExtendHorizontal()
    .Height(300)
    .Canvas((canvas, size) =>
    {
        var chart = new LineChart
        {
            Entries = entries,

            LabelOrientation = Microcharts.Orientation.Horizontal,
            ValueLabelOrientation = Microcharts.Orientation.Horizontal,

            IsAnimated = false,
        };

        chart.DrawContent(canvas, (int)size.Width, (int)size.Height);
    });

Here’s how the document looks like:

Bar Codes

Sometimes you need to closely respect a house style or provide a bar code in your document. In that case, it may be required to upload extra fonts. Here’s how to do this with QuestPDF:

var fontFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Fonts", "LibreBarcode39-Regular.ttf");
FontManager.RegisterFont(File.OpenRead(fontFile));

Here’s how to use the custom font in the document –  for the regular fonts QuestPDF provides enumerations:

page.Content().Element(container =>
{
    container
        .Background(Colors.White)
        .AlignCenter()
        .AlignMiddle()
        .Text("*QuestPDF*")
        .FontFamily("Libre Barcode 39")
        .FontSize(64);
});

This is the unsurprising result:

A Brochure Document

After running some more of the samples, we decided to get our hands dirty to make a document from scratch. We created a sample XAML page showing a list of moons, and a print button to generate a brochure-style document from it:

The BrochureDocument is an IDocument with a list of moons as model. It renders header, content and footer in separate methods according the QuestPDF patterns and practices:

internal class BrochureDocument : IDocument
{
    public List<Moon> Model { get; }

    public BrochureDocument(List<Moon> model)
    {
        Model = model;
    }

    public DocumentMetadata GetMetadata() => DocumentMetadata.Default;

    public void Compose(IDocumentContainer container)
    {
        container.Page(page =>
        {
            page.Margin(40);

            page.Header().Element(ComposeHeader);
            page.Content().Element(ComposeBody);
            page.Footer().Element(ComposeFooter);
        });
    }

    // ...
}

Here’s how the document looks like:

The banner and the subtitle only appear in the header on the title page, that’s what ShowOnce() does – and yes there’s a SkipOnce() too:

column.Item().ShowOnce()
    .Background(Colors.BlueGrey.Darken4)
    .PaddingVertical(20)
    .Image(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "Moons", "Banner.png"));

Also notice the optional fluent notation for color tones: there are 10 shades of grey from Colors.Grey.Lighten5 to Colors.Grey to Colors.Grey.Darken4. [If you want 50 shades, use the RGB flavor.]

In the document’s body, we iterate through the model’s moons to draw an IComponent for each, and a page break (except at the end):

private void ComposeBody(IContainer body)
{
    body.Column(column =>
        {
            foreach (var moon in Model)
            {
                column.Item().Component(new MoonComponent(moon));
                if (moon != Model.Last())
                {
                    column.Item().PageBreak();
                }
            }
        });
}

Here’s the structure of the MoonComponent – no Apollo rocket science here:

internal class MoonComponent : IComponent
{
    private readonly Moon moon;

    public MoonComponent(Moon model)
    {
        moon = model;
    }

    public void Compose(IContainer container)
    {
        container.Column(column =>
        {
            // ...
        }
    }
}

The footer of our brochure has alternating left-or-right page numbers. This required rocket science:

private void ComposeFooter(IContainer footer)
{
    footer.Dynamic(new AlternatingFooter());
}

The AlternatingFooter is a so-called dynamic component – a component that is evaluated during rendering for each individual page. It has access to the page context (‘which page am I on and how much space is left on it?’) and optional component-specific internal state.

A powerful example of a dynamic component is this one displaying a running total or sum for a table that spans multiple pages. Our brochure is less demanding and uses no internal state. We only want to know on which page we’re rendering the footer, so we can adapt its alignment. We cannot write a condition against the CurrentPageNumber() that you saw in one of the previous snippets. That expression is merely a marker saying ‘please inject page number here during rendering’ – it doesn’t hold the page number itself.

Here’s the code for alternating alignment:

internal class AlternatingFooter : IDynamicComponent<int>
{
    public int State { get; set; }

    public DynamicComponentComposeResult Compose(DynamicContext context)
    {
        var content = context.CreateElement(element =>
        {
            element
                .Element(x => context.PageNumber % 2 == 0 ? x.AlignLeft() : x.AlignRight())
                .Text(text =>
                {
                    text.CurrentPageNumber();
                });
        });

        return new DynamicComponentComposeResult()
        {
            Content = content,
            HasMoreContent = false
        };
    }
}

We’re happy with our brochure document, and we learned a lot about QuestPDF while developing it.

Bonus section: reusing chart code

For texts and images, there’s no way to share layout elements between a XAML view and a PDF layout. For drawings and charts -like the Microcharts line chart above- however, it would be nice to be able to reuse their source code. Microcharts does not yet support WinUI officially, although it’s coming as a side effect of two ongoing MAUI and UNO pull requests. In mean time, we found a way to display their charts in a XAML view.

Here’s one of the chart definitions in the ViewModel of our ChartsPage:

public Chart BarChart
{
    get
    {
        var chart = new BarChart
        {
            Entries = Entries,

            LabelOrientation = Microcharts.Orientation.Horizontal,
            ValueLabelOrientation = Microcharts.Orientation.Horizontal,

            IsAnimated = false
        };

        return chart;
    }
}

Here’s how we refer to it in our sample ChartsDocument:

column.Item()
    .Height(300)
    .Canvas((canvas, size) =>
    {
        chart.DrawContent(canvas, (int)size.Width, (int)size.Height);
    });

We embedded some different charts in our sample document:

Then we started building the XAML page. It turned out looking like this:

Both XAML and PDF environments use the same chart source code. Here’s one of the XAML elements hosting a chart:

<controls:ChartCanvas Chart="{x:Bind BarChart}" />

The ChartCanvas is a small custom control that we built on top of the SKXamlCanvas from SkiaSharp.Views.WinUI. It’s a Canvas that takes a Microchart chart as (Dependency) Property. It renders on load and rerenders when the Canvas resizes and when the Chart changes:

/// <summary>
/// A SkiaSharp WinUI Canvas that draws a Microcharts Chart.
/// </summary>
public class ChartCanvas : SKXamlCanvas
{
    public static readonly DependencyProperty ChartProperty =
        DependencyProperty.Register(nameof(Chart), typeof(Chart), typeof(ChartCanvas), new PropertyMetadata(null));

    public Chart Chart
    {
        get { return (Chart)GetValue(ChartProperty); }
        set
        {
            if (Chart != null)
            {
                Chart.PropertyChanged -= (o, e) => { Invalidate(); };
            }

            SetValue(ChartProperty, value);
            Invalidate();

            if (Chart != null)
            {
                Chart.PropertyChanged += (o, e) => { Invalidate(); };
            }
        }
    }

    protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
    {
        e.Surface.Canvas.Clear();

        Chart?.DrawContent(e.Surface.Canvas, e.Info.Width, e.Info.Height);

        base.OnPaintSurface(e);
    }
}

Pending official WinUI3 support from Microcharts, we’ll stick to this solution. It even respects the chart’s beautiful startup animations:

Conclusion

We had a lot of fun test driving QuestPDF in WinUI. The package comes with all you need for generating PDS documents from your apps … all for free. We sure have more plans with it: we’ll be replacing the ‘Print’ button by a ‘PDF’ button in some of our apps.

Our sample app lives here on NuGet – don’t forget to create a c:\temp folder before running the samples or to change the target path in the source code.

Enjoy!

XAML Behaviors and WinUI 3

In this article we demonstrate how to use and write XAML Behaviors in a WinUI 3 Desktop app. We’ll cover some of the existing behaviors that are exposed via Community Toolkit and its sample app, and we also write a new one. XAML Behaviors have their name well chosen: they are a way to declaratively extend XAML elements with extra behavior. This avoids writing code-behind in the view that host the element, or in a child class. As an example, here’s how we decorate a TextBlock with the capability of automatically transforming the URLs in its text to real hyperlinks:

<TextBlock x:Name="url">
    <interactivity:Interaction.Behaviors>
        <sample:TextBlockHyperlinkBehavior />
    </interactivity:Interaction.Behaviors>
</TextBlock>

All the code in this article comes from our WinUI 3 desktop sample app that looks like this:

Some history

XAML Behaviors are almost as old as XAML itself: they were introduced in Blend for Silverlight 3. Most of the classic behaviors (like Event-to-Command) were meant to ease the use of XAML controls in application frameworks and architectures such as MVVM. Very soon after Silverlight, WPF had its own XAML Behaviors repo – which is still actively maintained. In the Windows 8 era, the Microsoft.Xaml.Interactivity namespace with the core behavior classes disappeared from the ecosystem. A team of heroic Windows Developer MVPs resurrected these into the WinRtBehaviors repo. This repo later evolved into the XamlBehaviors for UWP that we still have today. The base classes are actively maintained there, and were made compatible with WinUI. Other XAML technologies also have Behavior repos in the open. All of these started with a port from the WPF or the UWP version: here’s one for .NET MAUI, one for Uno Platform, and one for Avalonia.

There are requests here and there to move the classic XamlBehaviors repo to Microsoft.UI.Xaml namespace, but there’s not much action around these. While it would make sense to move the Behavior base classes into Windows App SDK, there’s a lot in the repo that is less relevant. There’s for example a collection of reusable Actions to assemble behaviors in XAML. Most of these classic WPF/UWP behavior helpers make no sense in a WinUI context. In WinUI,  controls expose Commands next to their Events and we’re capable of binding to methods. We can easily live without InvokeCommand and CallMethod. The repo also contains a bunch of sample behaviors that are collecting dust – the last modification in that area was 7 years ago.

For WinUI, here’s the good news: it’s Community Toolkit Behaviors that hosts the new XAML Behaviors. The package uses the ‘old’ XamlBehaviors sources via XamlBehaviors.WinUI.Managed which does not contain the legacy stuff we just mentioned. Community Toolkit adds a new base class and a handful of useful modern XAML behaviors, but the old base classes still do the heavy lifting.

How it works

XAML Behaviors are classes that are attached to a control, and then start listening to changes inside that control (typically a raised event or a property change) to trigger an action. A helper class –Interaction– puts Behaviors into a collection and assigns that collection to a control as a custom attached property (a dependency property that stores its value externally). You don’t need much infrastructure to make this happen:

  • a description for a behavior -the IBehavior interface-,
  • base classes to get you up to speed immediately, think Behavior and Behavior<T>, and
  • a class to hook behaviors to a XAML control which then becomes the behavior’s AssociatedObject: Interaction.

All of these live in the Microsoft.Xaml.Interactivity.Shared project of the ‘classic’ XamlBehaviors package and are still relevant for WinUI:

The canonical example

The AutoFocus behavior is the ‘Hello World’ of XAML Behaviors. It sets the focus to the AssociatedObject when it’s loaded. Here’s how to use it:

<TextBox Text="My content is selected when loaded">
    <interactivity:Interaction.Behaviors>
        <behaviors:AutoFocusBehavior />
    </interactivity:Interaction.Behaviors>
</TextBox>

As mentioned, the behavior is attached to the TextBox through an Interaction instance. That instance can carry multiple behaviors. In the next XAML snippet we not only focus the TextBox but also preselect its entire content:

<TextBox Text="My content is selected when loaded">
    <interactivity:Interaction.Behaviors>
        <behaviors:AutoFocusBehavior />
        <behaviors:AutoSelectBehavior />
    </interactivity:Interaction.Behaviors>
</TextBox>

Here’s the reason why AutoFocus is the canonical XAML Behavior example. Its custom class definition is literally a one-liner:

public sealed class AutoFocusBehavior : BehaviorBase<Control>
{
    protected override void OnAssociatedObjectLoaded() => AssociatedObject.Focus(FocusState.Programmatic);
}

Yes: in code-behind this would also be an easy job. Apart from creating a Loaded event handler and giving TextBox an x:Name you would need to weave the behavior logic through the rest of the code. When there are multiple controls in a view and they require more complex behavior logic, then spaghetti code is guaranteed.

If you’re lucky, there’s no need to create a custom class. For the sake of completeness, here’s how to use the helper Actions that we mentioned before. Here’s how to assemble an obsolete(!) XAML Behavior that turns a Click event into a Command call:

<Button x:Name="button1">
    <Interactivity:Interaction.Behaviors>
        <Interactions:EventTriggerBehavior EventName="Click" SourceObject="{Binding ElementName=button1}">
            <Interactions:InvokeCommandAction Command="{Binding UpdateCountCommand}"/>
        </Interactions:EventTriggerBehavior>
    </Interactivity:Interaction.Behaviors>
</Button>

Windows Community Toolkit Behaviors

All XAML Behavior goodies for WinUI are exposed via Windows Community Toolkit. Its WinUI Behaviors NuGet package hosts some useful behaviors that are more than just one-liners. Let’s take a look at some of these.

ViewportBehavior

ViewPortBehavior is a class that monitors its associated FrameworkElement entering or exiting a ScrollViewer viewport and raises appropriate events. The behavior is useful in an autoplay scenario.

It holds a field to store the ScrollViewer and defines four events:

public class ViewportBehavior : BehaviorBase<FrameworkElement>
{

    private ScrollViewer _hostScrollViewer;

    public event EventHandler EnteredViewport;
    public event EventHandler ExitedViewport;
    public event EventHandler EnteringViewport;
    public event EventHandler ExitingViewport;

    // ...

}

When it’s attached to the element, it looks up the ScrollViewer and registers an event handler for its ViewChanged:

_hostScrollViewer = AssociatedObject.FindAscendant<ScrollViewer>();
if (_hostScrollViewer == null)
{
    throw new InvalidOperationException("This behavior can only be attached to an element which has a ScrollViewer as a parent.");
}

_hostScrollViewer.ViewChanged += ParentScrollViewer_ViewChanged;

That handler has the logic to raise in turn one of the four behavior events, check the source code here.

When hooking the behavior to an element, you provide your own event handlers for them, like in this code snippet from our sample app:

<ScrollViewer>
    <Grid Height="3000">
        <Border Width="100"
                Height="100">
            <interactivity:Interaction.Behaviors>
                <behaviors:ViewportBehavior x:Name="ViewportBehavior"
                                            IsAlwaysOn="True"
                                            EnteringViewport="Element_EnteringViewport"
                                            EnteredViewport="Element_EnteredViewport"
                                            ExitingViewport="Element_ExitingViewport"
                                            ExitedViewport="Element_ExitedViewport" />
            </interactivity:Interaction.Behaviors>
            <FontIcon Glyph=""
                        FontSize="40" />
        </Border>
    </Grid>
</ScrollViewer>

Our sample app just updates the text of a TextBlock. Here’s how the demo looks like in practice:

FadeHeaderBehavior

FadeHeaderBehavior makes the header of a ListView or a GridView fade away when scrolling. It shares some code with ViewportBehavior, but instead of raising events it applies an animation:

// Use the ScrollViewer's Y offset and the header's height to calculate the opacity percentage. Clamp it between 0% and 100%
var headerHeight = (float)HeaderElement.RenderSize.Height;
var opacityExpression = ExpressionFunctions.Clamp(1 - (-scrollPropSet.Translation.Y / headerHeight), 0, 1);

// Begin animating
var targetElement = ElementCompositionPreview.GetElementVisual(HeaderElement);
targetElement.StartAnimation("Opacity", opacityExpression);

Optionally, you can provide the part of the header to which the animation should apply. Behaviors can have dependency properties:

public static readonly DependencyProperty HeaderElementProperty = DependencyProperty.Register(
    nameof(HeaderElement), 
    typeof(UIElement), 
    typeof(FadeHeaderBehavior), 
    new PropertyMetadata(null, PropertyChangedCallback));

public UIElement HeaderElement
{
    get { return (UIElement)GetValue(HeaderElementProperty); }
    set { SetValue(HeaderElementProperty, value); }
}

Here’s how our sample app configures the behavior:

<ListView>
    <interactivity:Interaction.Behaviors>
        <behaviors:FadeHeaderBehavior HeaderElement="{Binding ElementName=TheGrid}" />
    </interactivity:Interaction.Behaviors>
    <ListView.Header>
        <Grid x:Name="TheGrid"
                MinHeight="50"
                Background="{ThemeResource SystemAccentColor}">
            <!-- ... -->
        </Grid>
    </ListView.Header>
    <!-- ... -->
</ListView>

And this is how it looks like in practice:

Hidden Gems

All of the previous samples are exposed by Community Toolkit and you can test them with their sample app. Many open-sourced desktop applications use behaviors internally. WinUI Gallery for example has its own ImageScrollBehavior, a more powerful version of FadeHeaderBehavior. Another of these hidden gems is  TextBlockHyperlinkBehavior from the Community Toolkit Sample app itself. It conveniently automagically transforms the urls in a TextBlock to hyperlinks.

This is how the About page of our sample app does hyperlinks:

<TextBlock>
    <Run>This WinUI 3 Desktop app demonstrates</Run>
    <LineBreak />
    <Run Text="* some of the Community Toolkit XAML Behaviors found in " /><Hyperlink NavigateUri="https://aka.ms/windowstoolkitapp">Windows Community Toolkit Sample App</Hyperlink><Run Text=", and" />
    <LineBreak />
    <Run Text="* a WinUI 3 version of a XAML Behavior found in " /><Hyperlink NavigateUri="https://github.com/CommunityToolkit/Maui">.NET MAUI Community Toolkit</Hyperlink><Run Text="." />
</TextBlock>

A hardcoded TextBlock-Run-Hyperlink construction is OK when the text is fixed, but what if the content came from a database or a resource file? Enter TextBlockHyperlinkBehavior. Here’s how our sample app applies it:

<TextBlock x:Name="url">
    <interactivity:Interaction.Behaviors>
        <sample:TextBlockHyperlinkBehavior />
    </interactivity:Interaction.Behaviors>
</TextBlock>

At runtime we provide a text containing some URLs:

url.Text = "Please find the blog post at https://xamlbrewer.wordpress.com/ and the source code repo at https://github.com/XamlBrewer .";

This is what happens at runtime, the two URLs in the text are transformed to real hyperlinks:

Here’s the structure of the behavior class. It uses a regular expression to recognize URLs, and reacts on PropertyChanged of its associated element’s Text property. No surprises here:

public class TextBlockHyperlinkBehavior : BehaviorBase<TextBlock>
{
    private static readonly Regex UrlRegex = 
        new Regex(@"(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&%\$#_]*)?", 
        RegexOptions.Compiled);

    private long? _textChangedRegistration;

    protected override bool Initialize()
    {
        if (_textChangedRegistration == null)
        {
            _textChangedRegistration = AssociatedObject.RegisterPropertyChangedCallback(TextBlock.TextProperty, Text_PropertyChanged);
        }

        return base.Initialize();
    }

    protected override void OnDetaching()
    {
        if (AssociatedObject != null && _textChangedRegistration != null && _textChangedRegistration.HasValue)
        {
            AssociatedObject.UnregisterPropertyChangedCallback(TextBlock.TextProperty, _textChangedRegistration.Value);
        }

        base.OnDetaching();
    }

    // ...

}

You find the details on the transformation of the string into Run and Hyperlink instances in the full source code. We’re a huge fan of this behavior, but for the sake of completeness, allow us to mention the Community Toolkit’s MarkdownTextBlock as a decent alternative.

Rolling your own

After dissecting some existing complex XAML behaviors, we’re ready to roll one ourselves: an extension of AutoSuggestBox that notifies you when the user stopped typing in its text box and it’s time to run the potentially expensive query to lookup the suggestions. There’s a .NET MAUI version of this XAML Behavior right here. In our sample app, we first created a version in code-behind and then refactored it into a XAML behavior.

Here’s how to apply our UserStoppedTyping behavior:

<AutoSuggestBox PlaceholderText="Search"
                QueryIcon="Find"
                QuerySubmitted="AutoSuggestBox_QuerySubmitted">
    <interactivity:Interaction.Behaviors>
        <brewhaviors:UserStoppedTypingBehavior UserStoppedTyping="AutoSuggestBox_UserStoppedTyping"
                                                MinimumDelay="1500"
                                                MinimumCharacters="2" />
    </interactivity:Interaction.Behaviors>
</AutoSuggestBox>

We used one of the classic base classes as parent, added a field for a timer, properties for the minimum delay to detect if the user stopped typing and the minimum length of the input for raising the event, and then the event itself:

public class UserStoppedTypingBehavior : Behavior<AutoSuggestBox>
{
    private DispatcherTimer timer;

    public int MinimumDelay { get; set; } = 3000;

    public int MinimumCharacters { get; set; } = 3;

    public event EventHandler UserStoppedTyping;

    // ...

}

When the associated AutoSuggestBox is loaded, we register event handlers for its TextChanged and for our own timer’s Tick. We live to the “for every += there should be a -=” principle, so we nicely unregister everything when detaching:

protected override void OnAttached()
{
    base.OnAttached();

    AssociatedObject.TextChanged += AssociatedObject_TextChanged;
    timer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(MinimumDelay) };
    timer.Tick += Timer_Tick;
}

protected override void OnDetaching()
{
    base.OnDetaching();

    AssociatedObject.TextChanged -= AssociatedObject_TextChanged;
    timer.Tick -= Timer_Tick;
}

When the input text is long enough, and it was changed by the user typing (not by selecting a suggestion) then we start the timer:

private void AssociatedObject_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
    if ((args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) && (sender.Text.Length >= MinimumCharacters))
    {
        timer.Start();
    }
    else
    {
        timer.Stop();
    }
}

When the timer elapses, we raise our event:

private void Timer_Tick(object sender, object e)
{
    UserStoppedTyping?.Invoke(AssociatedObject, null);
    timer.Stop();
}

Side note: if you prefer to raise a strongly typed event (one with AutoSuggestBox instead of object as sender) then it suffices to bring in the following delegate:

public delegate void EventHandler<in TSender, in TArgs>(TSender sender, TArgs args);

Then you can define the event as follows:

public event EventHandler<AutoSuggestBox, EventArgs> UserStoppedTyping;

Our AutoSuggestBox’s reaction to the UserStoppedTyping event is the same as if the user pressed <ENTER> or clicked the search icon (QuerySubmitted). Here’s the code in the view – the list of cats and the lookup code was borrowed from WinUI 3 Gallery:

private void AutoSuggestBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
    DisplaySuggestions(sender);
}

private void AutoSuggestBox_UserStoppedTyping(object sender, EventArgs e)
{
    DisplaySuggestions(sender as AutoSuggestBox);
}

private void DisplaySuggestions(AutoSuggestBox sender)
{
    var suitableItems = new List<string>();
    var splitText = sender.Text.ToLower().Split(" ");
    foreach (var cat in Cats)
    {
        var found = splitText.All((key) =>
        {
            return cat.ToLower().Contains(key);
        });
        if (found)
        {
            suitableItems.Add(cat);
        }
    }

    if (suitableItems.Count == 0)
    {
        suitableItems.Add("No results found");
    }

    sender.ItemsSource = suitableItems;
}

Here’s how all of this looks like at runtime – with a minimum input length of 2 and a wait time of 1.5 seconds:

Programmatically adding a Behavior

XAML Behaviors were created to declaratively decorate an element. But what if the attachment depends on a runtime condition, or you want to attach a behavior to a control that is created programmatically? Well, here’s how to programmatically attach a behavior. We start with a named AutoSuggestBox:

<AutoSuggestBox x:Name="TheBox"
                PlaceholderText="Search"
                QueryIcon="Find"
                QuerySubmitted="AutoSuggestBox_QuerySubmitted">
</AutoSuggestBox>

Then we do in C#-behind exactly the same as in the previous XAML snippets. We create a collection of behaviors and hook it to the element, instantiate and configure our behavior, and put it in the collection:

var behaviors = Interaction.GetBehaviors(TheBox);
var userStoppedTyping = new UserStoppedTypingBehavior { MinimumDelay = 1500, MinimumCharacters = 2 };
userStoppedTyping.UserStoppedTyping += AutoSuggestBox_UserStoppedTyping;
behaviors.Add(userStoppedTyping);

That’s all there is to attach a XAML behavior at runtime.

Sprinkling some Windows 11 dust

Since this is our first sample app that was developed on Windows 11, we took the opportunity to add some Windows 11 – only WinUI features.

Mica

The app uses Mica material as its background. When you look at the overly complex way of applying it to your WinUI app, you may be tempted to give up. Fortunately Morten Nielsen’s WinUIEx NuGet package provides a nice and much easier way. Using WindowEx you can not only declaratively assign Title and Icon, but also assign a Backdrop such as mica:

<winex:WindowEx x:Class="XamlBrewer.WinUI3.XamlBehaviors.Sample.Shell"
                Title="XAML Brewer WinUI 3 XAML Behaviors Sample"
                TaskBarIcon="Assets/Beer.ico"
                xmlns:winex="using:WinUIEx">
    <winex:WindowEx.Backdrop>
        <winex:MicaSystemBackdrop />
    </winex:WindowEx.Backdrop>

    <!-- ... -->

</winex:WindowEx>

If Window were a DependencyObject, we would have refactored mica Backdrop as a XAML Behavior…

InfoBadge

The blue dot in the sample app’s UserStoppedTyping header is an instance of the new InfoBadge control. We followed this guide to create a custom accent color scheme around beer colors, and use the complementary color for the badge:

InfoBadge doesn’t seem to display icons correctly yet (numerical values show OK, however):

<InfoBadge Background="{ThemeResource SystemAccentColorComplementary}"
            HorizontalAlignment="Right"
            VerticalAlignment="Top">
    <!-- No show (neither does SymbolIcon) -->
    <!--<InfoBadge.IconSource>
        <FontIconSource Glyph="" />
    </InfoBadge.IconSource>-->
</InfoBadge>

To bring us back to our original topic: decorating a XAML element with an InfoBadge can be written as a XAML Behavior.

After all these years, the XamlBehaviors base classes remain solid and relevant. So keep on writing new XAML Behaviors in your WinUI apps.

Our WinUI app lives here on NuGet. It has room for extra samples ….

Enjoy!

Displaying DataGrid Row Details in a TabView in WinUI 3

In this article we demonstrate how to display multiple DataGrid row details as tabs in TabView. We’ll also implement tear-off of a TabViewItem into a separate window. All of this happens in a WinUI 3 / WinAppSdk 1.2 application. Here’s how our sample app looks like:

It covers:

  • programmatical TabViewItem creation,
  • creating a reusable ‘TabsWindow’ control,
  • drag-and-drop of TabViewItem instances,
  • title bar customization, and
  • application-wide dynamic Theming.

We copied the ‘old’ Entity Framework page into a new page of our existing Enterprise DataGrid sample. This is how the Home page of that application displays the selected record in its RowDetailsTemplate:

The scenario

In a previous blog post we already expressed our preference to see these row details in a separate view, and that’s exactly wat a TabView control allows us to do. Here’s the new scenario.

In the new ‘Tabbed Detail‘ page in our sample app the Community Toolkit DataGrid is hosted in a TabViewItem:

When a row is selected the ‘Details’ button in the top toolbar is enabled. When it is clicked, a new TabViewItem is created on the (View)Model of the selected row, and added to the TabView:

This scenario allows to open multiple detail views whilst keeping the original DataGrid intact and accessible. This is convenient since the View may host the result of an expensive query, or it may have active filters and groupings that you want to keep alive while looking at the row details.

We also implemented ‘Tab Tear-off’ – inspired by this WinUI Gallery sample. When a TabViewItem is dragged off the page, it opens in a separate window:

The item can be dragged back into the original page, or into yet another new window. Our ‘TabsWindow’ can host details from different sources, as this screenshot from an app in progress -not the sample app- shows:

Getting started with the TabView

Here’s the core structure of our ‘Tabbed Detail’ page. The DataGrid is hosted in a ‘fixed’ TabViewItem:  with IsClosable and CanDrag set to false (we’ll work around this CanDrag bug later). The surrounding TabView itself has IsAddTabButtonVisible to false, since we’ll only be adding tabs programmatically. It also has all the necessary drag-and-drop events covered:

<TabView x:Name="tvMountains"
            IsAddTabButtonVisible="False"
            TabCloseRequested="Tab_TabCloseRequested"
            CanDragTabs="True"
            AllowDrop="True"
            TabDragStarting="TabView_TabDragStarting"
            DragOver="TabView_DragOver"
            Drop="TabView_Drop"
            TabDroppedOutside="TabView_TabDroppedOutside">
    <TabViewItem Header="All"
                    IsClosable="False"
                    CanDrag="False">
        <TabViewItem.IconSource>
            <FontIconSource Glyph="&#xE8FD;" />
        </TabViewItem.IconSource>
        <Grid>
            <!-- ... -->
            <ctWinUI:DataGrid x:Name="DataGrid">
                  <!-- ... -->
            </ctWinUI:DataGrid>
        </Grid>
    </TabViewItem>
</TabView>

To display the details of the selected row -a mountain in the Himalayas- we went for a page with a RelativePanel:

<RelativePanel>
    <TextBox x:Name="Name"
                Header="Name"
                IsReadOnly="True"
                Text="{x:Bind ViewModel.Name}" />
    <TextBox x:Name="ParentMountain"
                Header="Parent Mountain"
                IsReadOnly="True"
                Text="{x:Bind ViewModel.ParentMountain}"
                RelativePanel.RightOf="Name" />
    <!-- More of these ... -->
<RelativePanel>

The mountain top’s coordinates are displayed as numbers, because we didn’t find a free Map control for WinUI 3.

When a single row is selected in the DataGrid, the ‘Details’ button in the top toolbar is enabled (check this blog post on how to achieve this). When that button is clicked, we create a new Details page, set the selected grid item as its DataContext, create a new TabViewItem around it (tweaking its style by overriding some Theme Resources), add it to the TabView, and select it:

private void DetailsButton_Click(object sender, RoutedEventArgs e)
{
    var item = DataGrid.SelectedItem as Mountain;
    DetailsPage page = new()
    {
        DataContext = item
    };

    TabViewItem tabViewItem = new()
    {
        Header = item.Name,
        Content = page,
        CanDrag = true,
        IconSource = new BitmapIconSource() { UriSource = new System.Uri("ms-appx:///Assets/mountain.png") }
    };

    // <StaticResource x:Key="TabViewItemHeaderBackgroundSelected" ResourceKey = "SystemAccentColorBrush" />
    tabViewItem.Resources.Add("TabViewItemHeaderBackgroundSelected", Application.Current.Resources["SystemAccentColorBrush"]);

    tvMountains.TabItems.Add(tabViewItem);
    tvMountains.SelectedItem = tabViewItem;
}

Of course, you can add multiple details tabs:

Creating the TabsWindow

Our sample app contains a reusable window that comes with an empty TabView. It overlaps the title bar, like most browser windows. It serves a host window where TabViewItems can be dragged into an out of. Here’s its core structure:

<Window x:Class="XamlBrewer.WinUI.Controls.TabsWindow">
    <Grid x:Name="Root"
          Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TabView x:Name="tabView"
                 AllowDrop="True"
                 AllowDropTabs="True"
                 CanDragTabs="True"
                 TabCloseRequested="Tab_TabCloseRequested"
                 TabDragStarting="TabView_TabDragStarting"
                 DragOver="TabView_DragOver"
                 Drop="TabView_Drop"
                 TabStripDrop="TabView_TabStripDrop"
                 TabDroppedOutside="TabView_TabDroppedOutside"
                 TabItemsChanged="TabView_TabItemsChanged"
                 TabDragCompleted="TabView_TabDragCompleted" />
    </Grid>
</Window>

This is how it looks like:

The TabView in this window will share its tear-off drag-and-drop code with the one in the Tabbed Details page. It also shares the close tab code:

private void Tab_TabCloseRequested(TabView sender, TabViewTabCloseRequestedEventArgs args)
{
    tabView.TabItems.Remove(args.Tab);
}

Contrary to the Details Page, the TabsWindow closes together with the last remaining tab:

private void TabView_TabItemsChanged(TabView sender, IVectorChangedEventArgs args)
{
    if (sender.TabItems.Count == 0)
    {
        Close();
    }
}

Implementing tear-off and drag-and-drop

Now that we have potential sources and destinations for TabViewItems, let’s implement drag-and-drop between these. There’s a general introduction to drag-and-drop right here, and the TabView comes with all the necessary events to put this to practice.

Drag-and-drop boils down to transporting a ‘data package’ from one place to another. The package is accessible through the event handlers argument. Both source and destination need to agree on the type of data package to exchange – not every item can be dropped anywhere. Since our drag-and-drop scenario stays within the application, a constant string will suffice to identify a travelling tab item:

private const string DataIdentifier = "TabItem";

When we start dragging a TabViewItem, the TabDragStarting event takes place. We store a reference to the TabViewItem in the travelling package using the shared constant as key, and indicate that we’re after a move:

private void TabView_TabDragStarting(TabView sender, TabViewTabDragStartingEventArgs args)
{
    args.Data.Properties.Add(DataIdentifier, args.Tab);
    args.Data.RequestedOperation = DataPackageOperation.Move;
}

When the dragging stops in the void (over a non-destination) then we assume the tear-off scenario. In the TabDroppedOutside event handler, we create a new TabsWindow and move the Tab to it:

private void TabView_TabDroppedOutside(TabView sender, TabViewTabDroppedOutsideEventArgs args)
{
    var tab = args.Tab;
    tabView.TabItems.Remove(tab);

    TabsWindow window = new(Root.ActualTheme) { Title = (Application.Current as App).Title };
    window.AddTab(tab);
    window.Activate();
}

When we’re hovering over a TabView that might be interested in the package, the code in the DragOver event handler confirmes this:

private void TabView_DragOver(object sender, DragEventArgs e)
{
    if (e.DataView.Properties[DataIdentifier] is TabViewItem)
    {
        e.AcceptedOperation = DataPackageOperation.Move;
    }
}

Windows will then update its UI:

When a TabViewItem is dropped, the Drop handler checks if we’re in an expected destination, and moves the tab under its new host:

private void TabView_Drop(object sender, DragEventArgs e)
{
    if (e.DataView.Properties[DataIdentifier] is TabViewItem)
    {
        var tvi = e.DataView.Properties[DataIdentifier] as TabViewItem;
        var tvlv = tvi?.Parent as TabViewListView;
        if (tvlv is not null)
        {
            tvlv.Items.Remove(tvi);
            tabView.TabItems.Add(tvi);
        }
    }
}

As already mentioned, there’s a bug that ignores the CanDrag property of TabViewItems. When your TabView contains fixed items (like the one with the main DataGrid on our sample page), then you have to prevent drag operations yourself. Here’s what we did. We set “CanNotDrag” as Tag for the fixed items, then added this code in the beginning of the relevant drag event handlers to abandon the operation:

if (args.Tab.Tag?.ToString() == "CanNotDrag")
{
    return;
}

When the last item in the TabsWindow was dragged out of its main TabView, the window closes itself via TabDragCompleted:

private void TabView_TabDragCompleted(TabView sender, TabViewTabDragCompletedEventArgs args)
{
    if (sender.TabItems.Count == 0)
    {
        Close();
    }
}

Customizing the title bars

Both the app’s main page and the TabsWindow have a customized title bar – a feature from UWP/WinUI2 which in WinUI3 was reserved to Windows 11 only. Windows App SDK 1.2 finally makes it available to Windows 10. In the main page, we added a custom icon in the top left corner. First, we created a reusable extension method to get the AppWindow for a Window:

public static AppWindow GetAppWindow(this Window window)
{
    IntPtr hWnd = WindowNative.GetWindowHandle(window);
    WindowId wndId = Win32Interop.GetWindowIdFromWindow(hWnd);
    return AppWindow.GetFromWindowId(wndId);
}

Here’s how to set the icon:

var appWindow = this.GetAppWindow();
appWindow.SetIcon("Assets/Beer.ico");

This is the result. We are (finally!) able to use our own icon in Windows 10:

We wanted the TabsWindow to look like your Browser: without a title bar. When you extend your page content into your window’s title bar, the operating system only takes care of the top right buttons. You need to reserve a XAML space on top yourself to enable drag-and-drop of the window, and to react on clicks and double clicks (e.g. for resizing). Fortunately, a TabView control has the ideal candidate for this: its TabStripFooter to the right of the tabs. We gave it a name and a minimum width:

<TabView.TabStripHeader>
    <Image Stretch="UniformToFill"
            Source="/Assets/Beer.png"
            Height="20"
            VerticalAlignment="Center"
            Margin="8 0" />
</TabView.TabStripHeader>
<TabView.TabStripFooter>
    <Grid x:Name="CustomDragRegion"
            MinWidth="188"
            Background="Transparent" />
</TabView.TabStripFooter>

Here’s how customization code looks like:

SetTitleBar(CustomDragRegion); 
ExtendsContentIntoTitleBar = true;

Via the app’s resources we made the title bar background transparent, and applied our accent color to the window’s action buttons:

<SolidColorBrush x:Key="WindowCaptionBackground">Transparent</SolidColorBrush>
<SolidColorBrush x:Key="WindowCaptionBackgroundDisabled">Transparent</SolidColorBrush>
<SolidColorBrush x:Key="WindowCaptionForeground"
                    Color="{StaticResource SystemAccentColor}" />
<SolidColorBrush x:Key="WindowCaptionForegroundDisabled"
                    Color="{StaticResource SystemAccentColor}" />

Here’s the result:

Implementing dynamic theming

Adding TabsWindow turned our sample project into a multi-window app. We used a technique described in a previous blog post to implement dynamic theming: the button in the top left corner of the main page switches the theme of all open application windows. It’s just a few lines of code that make use of the Microsoft.Extensions.DependencyInjection NuGet package.

We defined a message to broadcast the new ElementTheme:

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

When the application starts, it initializes an IOC-container:

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

The code behind the button in the main page sends the theme message:

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

The TabsWindow registers a reception handler for that message:

var messenger = Ioc.Default.GetService<IMessenger>();
messenger.Register<ThemeChangedMessage>(this, (r, m) =>
{
    Root.RequestedTheme = m.Value;
});

Here’s how a theme switch looks like at runtime:

Our enterprise DataGrid sample app lives here on GitHub. It already demonstrated populating the Community Toolkit DataGrid from Entity Framework and Sqlite, with column customization, pagination, searching, grouping, and filtering. Now it has tabbed row details with tear-off.

You may have 99 reasons to go for 3rd party toolkits for WinUI 3 development, but a data grid ain’t one.

Enjoy!

Building a SQL Connection Settings Panel in WinUI3

In this article we demonstrate how to build a WinUI3 settings panel control to configure a SQL Server connection string. The panel allows you to:

  • enter the server name,
  • select the authentication protocol,
  • enter your credentials – if required,
  • select the database from a list or type it directly,
  • view and edit the connection string, and
  • test the connection.

The brand new SettingsCard experiment from the upcoming Windows Community Toolkit Labs gives the panel the common look-and-feel of a Windows 11 settings panel:

This UI is so much nicer than the current outmoded dialogs that we have e.g., in SQL Management Studio:

Windows Community Toolkit Labs

A long time ago in a galaxy not far away, it started as a relatively simple library of modern XAML components. Over time, Windows Community Toolkit gained more and more features. On the inside, it evolved into a very complex system to which it became harder and harder to contribute (been there, done that). Challenged by breaking changes between UWP and Windows App SDK/WinUI3, and by the ambition to bring the XAML cross-platform via Uno, the team decided for a new approach.

Windows Community Toolkit Labs is the new environment to discuss, develop, and test new features and controls for a future Community Toolkit (v8.0) that targets UWP, WinUI3 as well as the Uno Platform with a single source code base. Labs allow contributors to focus on the development of code, documentation, samples, and tests of a single candidate toolkit component (a so-called ‘experiment’) via a Visual Studio template. These experiments are then individually exposed through their own NuGet package, currently via a preview feed.

SettingsCard

One of the current experiments -and probably the most mature- is the SettingsCard, a control to provide consistent settings user experience for apps.

Before building our SQL Connections Settings Panel, we wanted to test drive this SettingsCard control. We cloned the experiments repo to run it locally. The repo comes with the source code for the experimental control, but also documentation, unit tests, and a sample page. Since our current apps have a lot of sliders in their settings panel, we added a card with a slider to the sample page. Here’s how the modified sample page looks like in WinUI 3:

The exact same code also runs in UWP and in the browser (thanks to the Uno platform):

Here’s the XAML for that UI. It shows different configurations for the SettingsCard:

<labs:SettingsCard Description="Windows and apps might use your country or region to give you local content"
                    Header="Country or region"
                    Icon=""
                    IsEnabled="{x:Bind IsCardEnabled, Mode=OneWay}">
    <ComboBox SelectedIndex="0">
        <ComboBoxItem>United States</ComboBoxItem>
        <ComboBoxItem>The Netherlands</ComboBoxItem>
        <ComboBoxItem>Germany</ComboBoxItem>
    </ComboBox>
</labs:SettingsCard>

<labs:SettingsCard Header="Preferred languages"
                    IsEnabled="{x:Bind IsCardEnabled, Mode=OneWay}">
    <labs:SettingsCard.Description>
        <HyperlinkButton Content="Learn more about Phone Link" />
    </labs:SettingsCard.Description>
    <Button Content="Open Phone Link" />
</labs:SettingsCard>

<labs:SettingsCard Click="OnCardClicked"
                    Description="When IsClickEnabled the Click event can be used to trigger an action"
                    Header="Clickable card"
                    Icon=""
                    IsClickEnabled="True"
                    IsEnabled="{x:Bind IsCardEnabled, Mode=OneWay}">
    <FontIcon FontSize="14"
                Glyph="" />
</labs:SettingsCard>

<labs:SettingsCard Click="OnCardClicked"
                    Description="You can also have other custom content when the card is clickable"
                    Header="Clickable + content"
                    IsClickEnabled="True"
                    IsEnabled="{x:Bind IsCardEnabled, Mode=OneWay}">
    <StackPanel Orientation="Horizontal"
                Spacing="12">
        <Button Content="Add device"
                Style="{StaticResource AccentButtonStyle}" />
        <FontIcon FontSize="14"
                    Glyph="" />
    </StackPanel>
</labs:SettingsCard>

<labs:SettingsCard Header="Intensity"
                    Description="How serious are you?"
                    Icon=""
                    IsEnabled="{x:Bind IsCardEnabled, Mode=OneWay}">
    <StackPanel Orientation="Horizontal">
        <Slider x:Name="sldSeriousness"
                MinWidth="100" />
        <TextBlock>
                    <Run Text=" " />
                    <Run Text="{x:Bind sldSeriousness.Value, Mode=OneWay}" />
                    <Run Text="%" />
        </TextBlock>
    </StackPanel>
</labs:SettingsCard>

We installed the SettingsCard’s NuGet package in an empty Windows App, and copied that same XAML snippet. Here’s the result – not bad for ‘experimental’ technology:

Here’s a class diagram for the control:

Building the Panel

When looking for a more ‘enterprise’ scenario for testing this new control, we decided to group some SettingCard instances together into a configuration panel for a SQL Server connection string. Our SqlConnectionSettingsPanel is straightforward: it’s a UserControl that implements INotifyPropertyChanged by directly hosting a PropertyChanged event handler (no need to bring in an MVVM framework). It exposes the most common SQL connection string attributes via regular properties that are internally linked to a SqlConnectionStringBuilder instance.

Here’s the core code for the class, with the properties for DataSource and UserID:

public sealed partial class SqlConnectionSettingsPanel : 
	UserControl, 
	INotifyPropertyChanged
{
    private readonly SqlConnectionStringBuilder builder = new();

    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Gets or sets the current server name.
    /// </summary>
    public string Server
    {
        get { return builder.DataSource; }
        set
        {
            builder.DataSource = value;
            OnPropertyChanged();
        }
    }

    /// <summary>
    /// Gets or sets the user identifier.
    /// </summary>
    public string UserId
    {
        get { return builder.UserID; }
        set
        {
            builder.UserID = value;
            OnPropertyChanged();
        }
    }

    // More of these ...
}

Because of the many dependencies between property values, any change broadcasts change notifications for all of the properties:

private void OnPropertyChanged()
{
    // Broadcast all properties.
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
}

UI-wise the SqlConnectionSettingsPanel is nothing but a vertical stack of SettingsCard instances:

<UserControl x:Class="XamlBrewer.WinUI3.Controls.SqlConnectionSettingsPanel">
    <StackPanel Spacing="6">
        <labs:SettingsCard Description="Name or network address of the SQL Server instance."
                           Header="Instance Name"
                           Icon="">
            <TextBox Text="{x:Bind Server, Mode=TwoWay}"
                     MinWidth="200" />
        </labs:SettingsCard>

        <labs:SettingsCard Header="Authentication"
                           Description="Authentication method."
                           Icon="">
            <ComboBox x:Name="cboAuthenticationProtocols"
                      SelectedIndex="0"
                      ItemsSource="{x:Bind AuthenticationProtocols}"
                      SelectionChanged="Authentication_SelectionChanged"
                      MinWidth="200" />
        </labs:SettingsCard>

        <!-- More Setting Cards ... -->

    </StackPanel>
</UserControl>

The required authentication protocols can be selected from a ComboBox. It’s populated with the values of the SqlAuthenticationMethod enumeration:

private IEnumerable<string> AuthenticationProtocols
{
    get
    {
        foreach (string str in Enum.GetNames(typeof(SqlAuthenticationMethod)))
        {
            yield return str.SplitCamelCase();
        }
    }
}

We applied a fancy CamelCase-to-TitleCase extension method to transform the Enum’s values into text:

public static string SplitCamelCase(this string str)
{
    return Regex.Replace(
        Regex.Replace(
            str,
            @"(\P{Ll})(\P{Ll}\p{Ll})",
            "$1 $2"
        ),
        @"(\p{Ll})(\P{Ll})",
        "$1 $2"
    );
}

Here’s how that ComboBox looks like:

When the selected authentication method is changed, we appropriately enable or disable the settings cards for UserId and Password:

private void Authentication_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    builder.Authentication = (SqlAuthenticationMethod)(sender as ComboBox).SelectedIndex;

    switch (builder.Authentication)
    {
        case SqlAuthenticationMethod.ActiveDirectoryIntegrated:
            ActivateUserId(); // User Id is optional.
            DeactivatePassword();
            break;
        case SqlAuthenticationMethod.ActiveDirectoryInteractive:
            ActivateUserId(); // User Id is optional.
            DeactivatePassword(); // Password provided via authentication prompt.
            break;
        case SqlAuthenticationMethod.ActiveDirectoryPassword:
            ActivateUserId();
            ActivatePassword();
            break;
        // more cases ...
    }
}

When an attribute is not expected, we clear and disable the field, and remove the key from the connection string’s dictionary:

private void ActivateUserId()
{
    crdUserId.IsEnabled = true;
}

private void DeactivateUserId()
{
    UserId = string.Empty;
    builder.Remove(nameof(UserId));
    crdUserId.IsEnabled = false;
}

When the user opens the Database ComboBox, we start an indeterminate ProgressBar underneath it and kick off the database lookup. There’s probably room for improvement in the user experience here (our initial idea was to use an AutoSuggestionBox here, but we couldn’t get it right). We fetch the list of databases on the server via a classic SqlCommand on a SqlConnection that we read with a SqlDataReader – there’s no need to bring in Entity Framework just for this:

private async void DatabaseComboBox_DropDownOpened(object sender, object e)
{
    if (string.IsNullOrWhiteSpace(Server))
    {
        return;
    }

    IsFetchingDatabases = true;

    try
    {
        // Remove initial catalog
        var connector = new SqlConnectionStringBuilder(builder.ConnectionString);
        connector.InitialCatalog = String.Empty;

        var databases = new List<string>();

        using (var connection = new SqlConnection(connector.ConnectionString))
        {
            await connection.OpenAsync();

            using var command = connection.CreateCommand();
            command.CommandText = "SELECT [name] FROM sys.databases ORDER BY [name]";

            using SqlDataReader reader = await command.ExecuteReaderAsync();
            while (await reader.ReadAsync())
            {
                databases.Add(reader.GetString(0));
            }
        }

        if (!string.IsNullOrEmpty(Database) || !databases.Contains(Database))
        {
            databases.Insert(0, Database);
        }

        Databases = databases;
    }
    catch (Exception ex)
    {
        ClearDatabaseList();
    }
    finally
    {
        IsFetchingDatabases = false;
    }
}

The control’s Database property is set from the selected item in the ComboBox:

private void Database_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.AddedItems.Any())
    {
        Database = e.AddedItems.First().ToString();
    }
}

The list of databases is not always available, and the database name can also be set by typing it in the editable combo box, or via modifying the connection string itself, or via data binding. Therefor we created a binding to the Text property, not the SelectedItem.

Here’s the structure of the Database SettingsCard:

<labs:SettingsCard Description="Name of the database."
                    Header="Database"
                    Icon="">
    <Grid>
        <ComboBox DropDownOpened="DatabaseComboBox_DropDownOpened"
                    ItemsSource="{x:Bind Databases, Mode=OneWay}"
                    Text="{x:Bind Database, Mode=TwoWay}"
                    SelectionChanged="Database_SelectionChanged"
                    IsEditable="True"
                    MinWidth="200" />
        <ProgressBar IsIndeterminate="{x:Bind IsFetchingDatabases, Mode=OneWay}"
                        VerticalAlignment="Bottom" />
    </Grid>
</labs:SettingsCard>

Here’s how it looks like in action:

The ConnectionString SettingsCard is the most advanced. It has button with a Flyout that allows to directly access and test the current connection string. It also comes with a ProgressBar, and green and red icons to visualize the result of the test. The red icon has the connection error message in its ToolTip:

<labs:SettingsCard Header="Connection String"
                    Description="See, modify, and test the connection string."
                    Icon="">
    <Button Content="Open"
            MinWidth="200"
            Style="{StaticResource AccentButtonStyle}">
        <Button.Flyout>
            <Flyout Opened="ConnectionFlyout_Opened">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*" />
                        <RowDefinition Height="auto" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <TextBox Text="{x:Bind ConnectionString, Mode=TwoWay}"
                                TextWrapping="Wrap"
                                MinWidth="200"
                                MaxWidth="200"
                                Grid.ColumnSpan="2" />
                    <Button Content="Test"
                            Click="ConnectionButton_Click"
                            Margin="0 6 6 0"
                            Grid.Row="1"
                            Style="{StaticResource AccentButtonStyle}" />
                    <FontIcon x:Name="RedIcon"
                                Visibility="Collapsed"
                                Glyph=""
                                Grid.Row="1"
                                Grid.Column="1"
                                HorizontalAlignment="Right"
                                Foreground="Red">
                        <ToolTipService.ToolTip>
                            <TextBlock x:Name="ErrorText"
                                        TextWrapping="Wrap" />
                        </ToolTipService.ToolTip>
                    </FontIcon>
                    <FontIcon x:Name="GreenIcon"
                                Visibility="Collapsed"
                                Glyph=""
                                Grid.Row="1"
                                Grid.Column="1"
                                HorizontalAlignment="Right"
                                Foreground="Green">
                        <ToolTipService.ToolTip>OK</ToolTipService.ToolTip>
                    </FontIcon>
                    <ProgressBar IsIndeterminate="{x:Bind IsConnecting, Mode=OneWay}"
                                    VerticalAlignment="Bottom"
                                    Grid.Row="0"
                                    Grid.ColumnSpan="2" />
                </Grid>
            </Flyout>
        </Button.Flyout>
    </Button>
</labs:SettingsCard>

Here’s the source code to manipulate this settings card:

private async void ConnectionButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
    IsConnecting = true;
    try
    {
        using (var connection = new SqlConnection(builder.ConnectionString))
        {
            await connection.OpenAsync();
        }

        RedIcon.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
        GreenIcon.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
    }
    catch (Exception ex)
    {
        ErrorText.Text = ex.Message;
        GreenIcon.Visibility = Microsoft.UI.Xaml.Visibility.Collapsed;
        RedIcon.Visibility = Microsoft.UI.Xaml.Visibility.Visible;
    }
    finally
    {
        IsConnecting = false;
    }
}

When the connection string’s value is updated (from the flyout, or via one of the settings cards, or through data binding), we update all fields and combo boxes – through PropertyChanged. Since the Authentication Protocol is not directly bound, we update its combo box programmatically:

public string ConnectionString
{
    get { return builder.ConnectionString; }
    set
    {
        builder.ConnectionString = value;
        cboAuthenticationProtocols.SelectedIndex = (int)builder.Authentication;
        OnPropertyChanged();
    }
}

Hosting the Panel

Our sample app comes with a TabView that demonstrates different ways to host a SqlConnectionSettingsPanel. It can be displayed as direct content in other panels. The host then determines the size:

<StackPanel>
    <TextBlock Foreground="{StaticResource SystemAccentColor}">SQL Server Connection Settings</TextBlock>
    <controls:SqlConnectionSettingsPanel />
</StackPanel>

Here’s how that looks like:

The panel can also appear in a button flyout; in this scenario you need to provide a minimum width for it:

<Button.Flyout>
    <Flyout>
        <Flyout.FlyoutPresenterStyle>
            <Style TargetType="FlyoutPresenter">
                <Setter Property="MinWidth"
                        Value="640" />
            </Style>
        </Flyout.FlyoutPresenterStyle>
        <controls:SqlConnectionSettingsPanel />
    </Flyout>
</Button.Flyout>

This is how it looks like (closed and open):

We couldn’t resist a final hosting exercise. Here’s our SqlConnectionSettingsPanel opened from … a SettingsCard. This is probably how we’re going to use it in our apps:

There’s no need to show the code, since there’s nothing new here: it’s a SqlConnectionSettingsPanel in a flyout of a button in a SettingsCard.

Conclusion

In this article we pulled the experimental SettingsCard out of its laboratory and placed it in a more representative enterprise scenario. It passed the test. SettingsCard is a useful and mature new control, we’re sure you’ll encounter it in many applications. We hope to see more of such controls and features coming out of Windows Community Labs in the future.

Feel free to play with our SqlConnectionSettingPanel in your own apps: add some connection attributes, improve the user experience. We were not able to test the panel against more exotic connection string types, so there might be some tweaking to do …

Our sample application lives here on GitHub.

Enjoy!

Building a gRPC client app with WinUI 3

In this article we describe how to build a gRPC client in a desktop application. The sample app demonstrates

  • simple RPC calls,
  • server-side streaming,
  • client-side streaming,
  • bidirectional streaming,
  • client-side load balancing,
  • client-side logging, and
  • health monitoring.

The app is built with WinUI 3 and WinAppSdk. Unlike UWP these two libraries run on .NET 6.0. The app has access to the full Windows API, and can integrate any .NET Core NuGet package. Therefor we believe that the future is bright for XAML and WinUI 3 in building rich integrated tools and utilities – including clients for gRPC servers.

Our sample Visual Studio solution contains an ASP.NET gRPC Host (requiring Visual Studio 2022 with the ASP.NET and Web Development workload installed) and a WinUI 3 Desktop Client. In this article we’ll focus only on the client. Here’s a great intro into the architecture of gRPC. If you’re more into source code, then check this code sample. If you have experience with WCF and want to learn about gRPC, then this is an awesome PDF for you.

The service we implemented hosts simple calls, server-side streaming, client-side streaming, and bidirectional streaming methods. It represents Star Trek’s transporter room (“Beam me up, Scotty!”) and comes with methods to move lifeforms or groups of lifeforms from and to different places. For a deeper introduction to the use case, check our UWP blog post from which we boldly reused the gRPC server side. The client app represents a console panel for the server app.

Here’s how the UI of our sample client looks like. It comes with

  • a console to display messages,
  • an on/off button,
  • a button to switch the beam direction,
  • a button to switch between single target and group target,
  • a button to activate the beam (energize), and
  • a panic button to switch two parties – just an excuse to have a bidirectional streaming call.

gRPC makes the interaction between client and server look like local procedure calls. That’s not a surprise: it’s basically the definition of RPC. The API looks like methods that accept and return data structures. The contract is expressed in the ProtoBuf format.

Here’s the definition of the data in our sample app – a location and a lifeform:

// It's life, Jim, but not as we know it.
message LifeForm {
  string species = 1;
  string name = 2;
  string rank = 3;
}

// A place in space.
message Location {
  string description = 1;
}

And these are the transporter room’s methods:

// Transporter Room API
service Transporter {
  // Beam up a single life form from a location.
  rpc BeamUp(Location) returns (LifeForm) {}

  // Beam up a party of life forms to a location.
  rpc BeamUpParty(Location) returns (stream LifeForm) {}

  // Beam down a single life form, and return the location.
  rpc BeamDown(LifeForm) returns (Location) {}

  // Beam up a party of life forms, and return the location.
  rpc BeamDownParty(stream LifeForm) returns (Location) {} 

  // Replace a beamed down party of life forms by another.
  rpc ReplaceParty(stream LifeForm) returns (stream LifeForm) {}
}

All the C# classes that are needed can be generated from these protobuf files in the server project, including the ones for the client app. We generated them in the default location. You can override all of this to e.g., generate the code into a separate project:

We made a copy of these generated classes in the WinUI 3 client:

Here’s an overview of the classes – a LifeForm, a Location, and the TransporterClient:

Just add the Google.Protobuf and Grpc.Core NuGet packages and you’re ready to go. As you see in the following screenshot, more packages will be added later on:

Setting up the connection

The TransporterClient is a lightweight ClientBase<T> proxy to the gRPC service. It sends and receives messages and data through a Channel. Our client app keeps these in some private fields:

private ChannelBase _channel;
private TransporterClient _client;

Creating a Channel is relatively slow and expensive, but you can keep it open as long as you want. A channel can be shared between different clients – we’ll do that later on. Here’s how to open a channel:

private void OpenChannel()
{
    _channel = new Channel("localhost:5175", ChannelCredentials.Insecure);

    // Optional: deadline.
    // Uncomment the delay in Server Program.cs to test this.
    // await _channel.ConnectAsync(deadline: DateTime.UtcNow.AddSeconds(2));

    _client = new TransporterClient(_channel);
}

And this is how you close one:

private async void CloseChannel()
{
    await _channel.ShutdownAsync();
}

We were using the Grpc.Core.Channel class here. We’ll introduce you to a more powerful alternative later on.

Sending messages

Here’s an example of a simple call –BeamUpOne()– that takes a Location and returns a LifeForm:

private void BeamUpOne()
{
    var location = new Location
    {
        Description = Data.Locations.WhereEver()
    };

    var lifeForm = _client.BeamUp(location);

    WriteLog($"Beamed up {lifeForm.Rank} {lifeForm.Name} ({lifeForm.Species}) from {location.Description}.");
}

As you see: no rocket science involved here. All methods have an asynchronous variant that supports a timeout scenario. Here’s how our BeamDownOne() method looks like:

private async Task BeamDownOne()
{
    var lifeForm = Data.LifeForms.WhoEver();

    try
    {
        // Uncomment the delay in the Service method to test the deadline.
        var location = await _client.BeamDownAsync(lifeForm, deadline: DateTime.UtcNow.AddSeconds(5));

        WriteLog($"Beamed down {lifeForm.Rank} {lifeForm.Name} ({lifeForm.Species}) to {location.Description}.");
    }
    catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
    {
        WriteLog("!!! Beam down timeout.");
    }
}

Our sample app goes into server-side streaming mode when the target is set to ‘Party’, and the beam direction to ‘Up’: a group of lifeforms will be sent to the transporter room. The call returns a ResponseStream through which our client can iterate with MoveNext() and get the individual lifeforms via Current:

private async Task BeamUpParty()
{
    var location = new Location
    {
        Description = Data.Locations.WhereEver()
    };

    using (var lifeForms = _client.BeamUpParty(location))
    {
        while (await lifeForms.ResponseStream.MoveNext())
        {
            var lifeForm = lifeForms.ResponseStream.Current;
            WriteLog($"- Beamed up {lifeForm.Rank} {lifeForm.Name} ({lifeForm.Species}).");
        }
    }
}

We enter client-side streaming mode when a ‘Party’ is beamed ‘Down’ to a location. The client initiates the call, writes all lifeforms to a RequestStream, and calls Complete() when he’s done:

private async void BeamDownParty()
{
    var rnd = _rnd.Next(2, 5);
    var lifeForms = new List<LifeForm>();
    for (int i = 0; i < rnd; i++)
    {
        var lifeForm = Data.LifeForms.WhoEver();
        lifeForms.Add(lifeForm);
    }

    using (var call = _client.BeamDownParty())
    {
        foreach (var lifeForm in lifeForms)
        {
            await call.RequestStream.WriteAsync(lifeForm);
            WriteLog($"- Beamed down {lifeForm.Rank} {lifeForm.Name} ({lifeForm.Species}).");
        }

        await call.RequestStream.CompleteAsync();

        var location = await call.ResponseAsync;

        WriteLog($"- Party beamed down to {location.Description}.");
    }
}

Unsurprisingly bidirectional streaming is a combination of server-side and client-side streaming: we have a RequestStream as well as a ResponseStream to walk through. In our ReplaceParty method (the one behind the ‘Red Alert’ button) we used a DispatcherQueue to enable reading and writing simultaneously:

private async void ReplaceParty()
{
    var rnd = _rnd.Next(2, 5);
    var lifeForms = new List<LifeForm>();
    for (int i = 0; i < rnd; i++)
    {
        var lifeForm = Data.LifeForms.WhoEver();
        lifeForms.Add(lifeForm);
    }

    DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();
    using (var call = _client.ReplaceParty())
    {
        var responseReaderTask = Task.Run(async () =>
        {
            while (await call.ResponseStream.MoveNext())
            {
                var beamedDown = call.ResponseStream.Current;
                dispatcherQueue.TryEnqueue(() =>
                {
                    WriteLog($"- Beamed down {beamedDown.Rank} {beamedDown.Name} ({beamedDown.Species}).");
                });
            }
        });

        foreach (var request in lifeForms)
        {
            await call.RequestStream.WriteAsync(request);
            WriteLog($"- Beamed up {request.Rank} {request.Name} ({request.Species}).");
        };

        await call.RequestStream.CompleteAsync();

        await responseReaderTask;
    }
}

Here’s a screenshot of the action. Observe that the up- and downloads happen simultaneously:

Client-side load balancing

The gRPC service in our sample app listens to two ports, as configured in its launchSettings.json file:

"applicationUrl": "http://localhost:5175;http://localhost:7175"

To balance the load over these two addresses on the client side, we switched to a more powerful type of channel: Grpc.NET.Client.GrpcChannel. Our client creates a resolver for the two addresses, provides it to a channel with a local address via a ServiceCollection, and configures it for round-robin:

private void OpenLoadBalancingChannel()
{
    var factory = new StaticResolverFactory(addr => new[]
    {
        new BalancerAddress("localhost", 7175),
        new BalancerAddress("localhost", 5175)
    });

    var services = new ServiceCollection();
    services.AddSingleton<ResolverFactory>(factory);

    _channel = GrpcChannel.ForAddress(
        "static:///transporter-host",
        new GrpcChannelOptions
        {
            Credentials = ChannelCredentials.Insecure,
            ServiceProvider = services.BuildServiceProvider(),
            ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() 
        });

    _client = new TransporterClient(_channel);
}

We’re using classes from the Grpc.Net.Client and Microsoft.Extensions.DependencyInjection NuGet packages here. In the log you see that the client (or actually the channel) now alternates between the two addresses:

Here’s the full documentation for client-side load balancing.

Logging

Here’s how we made the gRPC client logging.  We added the Microsoft.Extensions.Logging and Microsoft.Extensions.Debug NuGet packages. We created a LoggerFactory:

var loggerFactory = LoggerFactory.Create(logging =>
{
    logging.AddDebug();
    logging.SetMinimumLevel(LogLevel.Debug);
});

And added it to the channel options:

_channel = GrpcChannel.ForAddress(
    "static:///transporter-host",
    new GrpcChannelOptions
    {
        Credentials = ChannelCredentials.Insecure,
        ServiceProvider = services.BuildServiceProvider(),
        LoggerFactory = loggerFactory,
        ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() } }
    });

_client = new TransporterClient(_channel);

That’s OK for debug logging, for production logging we wanted to add a separate console window to the project. Here’s the LoggerFactory for this – it requires the Microsoft.Extensions.Logging NuGet package:

var loggerFactory = LoggerFactory.Create(logging =>
{
    logging.AddConsole();
});

This will log to the console window, but our client app does not have one yet. There are two ways to add a console window to a desktop application:

  • changing the Output type of the project from Windows Application to Console Application, or
  • explicitly calling AllocConsole().

The former looks and feels a bit like a hack. We chose the latter option, since it clearly expresses the intention. AllocConsole() is an unmanaged (non-.NET) Windows function, so you have to import it before you can call it. Here’s the import and the call:

[System.Runtime.InteropServices.DllImport("kernel32.dll")]
private static extern bool AllocConsole();

public MainWindow()
{
    InitializeComponent();
    AllocConsole();
}

Here we are now, with separate consoles for the server and the client side:

Health Monitoring

If you want to do heartbeat checks and health monitoring in your gRPC client, all you need is … yet another NuGet package: Grpc.HealthCheck. For the sake of completeness, we first added a canonical health check to our server:

builder.Services.AddGrpcHealthChecks()
                .AddCheck("Proforma", () => HealthCheckResult.Healthy());

// ...

app.MapGrpcHealthChecksService();

In the client, we added a timer to monitor the server state at a regular interval. We created a new HealthClient and shared the channel of our transporter client. As we already mentioned: multiple clients can share the same channel. Here’s the code:

private void HeartBeatTimer_Tick(object sender, object e)
{
    var client = new Health.HealthClient(_channel);

    try
    {
        var response = client.Check(new HealthCheckRequest());
        WriteLog($"*** Transporter service status: {response.Status}.");
    }
    catch (Exception ex)
    {
        WriteLog($"*** Transporter service error: {ex.Message}.");
    }
}

Here’s a view of our sample solution in action, with regular calls, health check calls, a client log, and a server log:

Here’s the full documentation on gRPC health checks.

In this article we went through a gRPC client in a WinAppSDK and WinUI 3 desktop app. We demonstrated simple and streaming calls, client-side load balancing, client-side logging, and health monitoring. That sample app lives here on GitHub.

Enjoy!

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!