Skip to content

Wiring Up a WinUI 3 App with the LCTWorks WinUI Toolkit

Leisvan Cordero edited this page Jul 30, 2026 · 1 revision

Wiring Up a WinUI 3 App with the LCTWorks WinUI Toolkit

This guide shows how to structure a WinUI 3 desktop app around the LCTWorks.WinUI toolkit: a dependency-injection host, the toolkit's activation and navigation services, a shell page with a NavigationView and custom title bar, and a Settings page registered for navigation.

Replace MyApp with your own root namespace throughout.


1. NuGet package

The toolkit is a single package:

<PackageReference Include="LCTWorks.WinUI" Version="1.0.2607.1" />

LCTWorks.WinUI brings in what you need transitively — Microsoft.Extensions.Hosting, Microsoft.Extensions.DependencyInjection, and CommunityToolkit.Mvvm (the source of ObservableObject). Your project also needs the Windows App SDK (Microsoft.WindowsAppSDK), which is already present in any WinUI 3 project template, so nothing extra to add there.

The toolkit also has an optional companion, LCTWorks.Telemetry, used only if you enable the AddSentryAndSerilog(...) call shown later. Skip it unless you want telemetry.


2. App.xaml

Standard WinUI application resources — merge in XamlControlsResources:

<?xml version="1.0" encoding="utf-8" ?>
<Application x:Class="MyApp.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
                <!--  Other merged dictionaries here  -->
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

3. App.xaml.cs — the heart of the setup

This is where the toolkit is wired up. App implements the toolkit's IAppExtended interface, builds a generic host with a DI container, registers services/views/viewmodels, and configures the page map.

using LCTWorks.WinUI;
using LCTWorks.WinUI.Activation;
using LCTWorks.WinUI.Navigation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml;
using MyApp.ViewModels;
using MyApp.Views;
using System;

namespace MyApp;

public partial class App : Application, IAppExtended
{
    public App()
    {
        InitializeComponent();

        Host = Microsoft.Extensions.Hosting.Host.
        CreateDefaultBuilder().
        UseContentRoot(AppContext.BaseDirectory).
        ConfigureServices((context, services) =>
        {
            services

            // Default Activation Handler
            .AddTransient<ActivationHandler<LaunchActivatedEventArgs>, DefaultActivationHandler>()

            // Services
            .AddSingleton<ActivationService>()
            .AddSingleton<FrameNavigationService>()

            // Views and ViewModels
            .AddSingleton<ShellViewModel>()
            .AddTransient<ShellPage>()
            ;
        }).Build();

        InitializePageHelper();
    }

    public static Window MainWindow { get; } = new MainWindow();

    public IHost Host { get; }

    Window IAppExtended.MainWindow => MainWindow;

    public static T GetService<T>()
        where T : class
    {
        if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
        {
            throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs.");
        }
        return service;
    }

    protected override async void OnLaunched(LaunchActivatedEventArgs args)
    {
        base.OnLaunched(args);

        var shellPage = GetService<ShellPage>();
        await GetService<ActivationService>().ActivateAsync(args, shellPage);
    }

    private static void InitializePageHelper()
    {
        NavigationPageMap.Configure<SettingsViewModel, SettingsPage>();
    }
}

What each piece does

IAppExtended — the toolkit interface your App must implement. It exposes MainWindow so the toolkit's helpers (activation, title bar, theming) can reach the app's window.

The HostHost.CreateDefaultBuilder() builds a Microsoft.Extensions.Hosting generic host. UseContentRoot(AppContext.BaseDirectory) points configuration/file resolution at the app folder. Everything the app needs is registered in ConfigureServices and resolved from Host.Services.

Registrations, in order:

  • ActivationHandler<LaunchActivatedEventArgs>DefaultActivationHandler — the fallback handler that runs on a normal launch. Register additional ActivationHandler<T> implementations here to react to other activation kinds (file, protocol, toast, etc.).
  • ActivationService (singleton) — orchestrates startup: runs activation handlers, initializes services, and shows the shell.
  • FrameNavigationService (singleton) — wraps a Frame and drives page navigation by ViewModel type.
  • ShellViewModel (singleton) and ShellPage (transient) — the app's root chrome.

Register each new page and viewmodel here as your app grows. Use AddTransient for pages and page viewmodels; AddSingleton for services and long-lived state.

GetService<T>() — a static service-locator convenience used from places that can't get constructor injection (like OnLaunched). It throws a clear error if you forgot to register a type.

OnLaunched — resolves the ShellPage and hands it to ActivationService.ActivateAsync(args, shellPage). The activation service takes over from there: it sets the shell as the window content and runs the activation pipeline.

InitializePageHelper / NavigationPageMap.Configure<TViewModel, TPage>() — the ViewModel-first navigation map. Each call tells the navigation system "when someone navigates to SettingsViewModel, show SettingsPage." Add one Configure<VM, Page>() line per page in your app.

Optional extension points

As your app grows, the same ConfigureServices block is where you plug in more. For example:

// Telemetry (requires the LCTWorks.Telemetry package)
.AddSentryAndSerilog(telemetryKey, sentryProjectName, environment, isDebug, contextData)

// Persisted settings binding
.Configure<LocalSettingsOptions>(context.Configuration.GetSection(nameof(LocalSettingsOptions)))

// Your own services, repositories, viewmodels, and pages
.AddSingleton<IMyService, MyService>()

4. MainWindow

Keep the window thin — it exists mostly as a host surface with a Mica backdrop. The real UI lives in ShellPage, which the activation service injects as the window content.

MainWindow.xaml:

<Window x:Class="MyApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="using:MyApp"
        Title="MyApp">
    <Window.SystemBackdrop>
        <MicaBackdrop />
    </Window.SystemBackdrop>
</Window>

MainWindow.xaml.cs:

using Microsoft.UI.Xaml;

namespace MyApp;

public sealed partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

For the custom title bar to work, make sure your app.manifest declares Windows 10+ compatibility and PerMonitorV2 DPI awareness — the standard WinUI project template already does this.


IMPORTANT: MainWindow.xaml should not have content, even an empty <Grid/> would make the ActivationService skip the UI population.

5. ShellPage — navigation chrome & title bar

The shell hosts three things: a custom TitleBar, a NavigationView, and the Frame that pages navigate into.

ShellPage.xaml

<Page x:Class="MyApp.Views.ShellPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:lctworks="using:LCTWorks.WinUI.Controls"
      xmlns:local="using:MyApp.Views"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <TitleBar x:Name="AppTitleBar"
                  IsBackButtonVisible="False"
                  IsPaneToggleButtonVisible="False">
            <TitleBar.Content>
                <AutoSuggestBox Width="360"
                                VerticalAlignment="Center"
                                PlaceholderText="Search.."
                                QueryIcon="Find" />
            </TitleBar.Content>
        </TitleBar>

        <NavigationView x:Name="NavigationViewControl"
                        Grid.Row="1"
                        ExpandedModeThresholdWidth="1280"
                        IsBackButtonVisible="Collapsed">
            <Grid>
                <Frame x:Name="NavigationFrame" Background="Transparent" />
            </Grid>
        </NavigationView>
    </Grid>
</Page>

The named elements — AppTitleBar, NavigationViewControl, and the NavigationFrame inside it — are the anchors the code-behind wires up. The lctworks XML namespace is declared so you can drop in toolkit controls (e.g. AdaptiveView, AdaptiveImage) as the app grows. The TitleBar.Content (search box) is illustrative — put whatever you like there, or nothing.

ShellPage.xaml.cs

using LCTWorks.WinUI.Extensions;
using LCTWorks.WinUI.Helpers;
using LCTWorks.WinUI.Navigation;
using Microsoft.UI.Xaml.Controls;
using MyApp.ViewModels;

namespace MyApp.Views;

public sealed partial class ShellPage : Page
{
    public ShellPage(ShellViewModel viewModel,
        FrameNavigationService navigationService)
    {
        ViewModel = viewModel;
        InitializeComponent();

        navigationService.Frame = NavigationFrame;
        NavigationViewHelper.Configure(navigationService, NavigationViewControl, typeof(SettingsViewModel).FullName!);
        TitleBarHelper.Extend(AppTitleBar, NavigationViewControl, navigationService);
    }

    public ShellViewModel ViewModel { get; private set; }
}

The three wiring lines are the core of the shell:

  1. navigationService.Frame = NavigationFrame; — hands the shell's Frame to the navigation service so all navigation flows through it.
  2. NavigationViewHelper.Configure(navigationService, NavigationViewControl, settingsKey) — connects the NavigationView to the navigation service and tells it which navigation key represents the built-in Settings item (here, the full type name of SettingsViewModel). This makes the gear/Settings item navigate to your settings page automatically.
  3. TitleBarHelper.Extend(AppTitleBar, NavigationViewControl, navigationService) — extends content into the custom title bar region, keeps it in sync with the nav view, and wires the back button to navigation.

ShellViewModel and FrameNavigationService arrive via constructor injection because they're registered in App.xaml.cs — which is why the shell is registered in DI and resolved with GetService<ShellPage>().

ShellViewModel

Empty to start — an ObservableObject you grow as the shell needs bindable state:

using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp.ViewModels;

public partial class ShellViewModel : ObservableObject
{
}

6. Settings page

Settings is a first-class navigation target. Three things make it work:

1. The page map entry (in App.xaml.cs):

NavigationPageMap.Configure<SettingsViewModel, SettingsPage>();

2. The Settings key passed to the nav helper (in ShellPage.xaml.cs):

NavigationViewHelper.Configure(navigationService, NavigationViewControl,
    typeof(SettingsViewModel).FullName!);

Passing SettingsViewModel's full name as the settings key connects NavigationView's built-in Settings button to your SettingsViewModelSettingsPage pair, so clicking the gear navigates there without a manual menu item.

3. The page and viewmodel themselves.

SettingsViewModel.cs:

using CommunityToolkit.Mvvm.ComponentModel;

namespace MyApp.ViewModels;

public partial class SettingsViewModel : ObservableObject
{
}

SettingsPage.xaml:

<Page x:Class="MyApp.Views.SettingsPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:MyApp.Views"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">
    <Grid />
</Page>

SettingsPage.xaml.cs:

using Microsoft.UI.Xaml.Controls;

namespace MyApp.Views;

public sealed partial class SettingsPage : Page
{
    public SettingsPage()
    {
        InitializeComponent();
    }
}

The page map alone is enough for navigation to construct the page via its parameterless constructor. If you want SettingsViewModel injected into the page, register both in DI (.AddTransient<SettingsViewModel>() and .AddTransient<SettingsPage>()) and give SettingsPage a constructor that takes SettingsViewModel — exactly like ShellPage does.

Persisted settings (local settings)

The toolkit ships a LocalSettingsHelper and a LocalSettingsOptions type for reading/writing persisted app settings, plus ThemeSelectorHelper for theme persistence (LoadThemeFromSettings / SaveThemeInSettings). To enable options binding, add this line in ConfigureServices:

.Configure<LocalSettingsOptions>(context.Configuration.GetSection(nameof(LocalSettingsOptions)));

Then inject IOptions<LocalSettingsOptions> where you need it, or use LocalSettingsHelper directly for ad-hoc reads/writes.


7. Adding a new screen

The repeatable pattern for every page after the shell:

  1. Create XViewModel (an ObservableObject) and XPage.
  2. Add NavigationPageMap.Configure<XViewModel, XPage>() in InitializePageHelper.
  3. Register in DI if the page needs constructor injection (.AddTransient<XPage>(), plus its viewmodel).
  4. Add a NavigationViewItem to the shell whose navigation key is the viewmodel type — the toolkit routes the click through FrameNavigationService to the mapped page.

8. Toolkit reference (LCTWorks.WinUI)

Types you'll reach for most often, by namespace:

Namespace Type Role
LCTWorks.WinUI IAppExtended Interface your App implements; exposes MainWindow.
LCTWorks.WinUI.Activation ActivationService Runs the startup/activation pipeline.
LCTWorks.WinUI.Activation ActivationHandler<T> Base for activation handlers.
LCTWorks.WinUI.Activation DefaultActivationHandler Fallback launch handler.
LCTWorks.WinUI.Navigation FrameNavigationService ViewModel-first frame navigation.
LCTWorks.WinUI.Navigation NavigationPageMap Configure<VM, Page>() map.
LCTWorks.WinUI.Navigation NavigationViewHelper Binds a NavigationView to the nav service.
LCTWorks.WinUI.Helpers TitleBarHelper Extends content into the custom title bar.
LCTWorks.WinUI.Helpers ThemeSelectorHelper Load/save/apply app theme.
LCTWorks.WinUI.Helpers LocalSettingsHelper Read/write persisted local settings.
LCTWorks.WinUI.Helpers AppStorageHelper Local-folder file read/write helpers.
LCTWorks.WinUI.Dialogs DialogService Register and show content dialogs.
LCTWorks.WinUI.Controls AdaptiveView, AdaptiveImage Adaptive layout/image controls.
LCTWorks.WinUI.Extensions UIExtensions, UriExtensions Rendering and URI/text helpers.

These types are from the shipped LCTWorks.WinUI assembly. Check the package's own docs/IntelliSense for exact method signatures, as they may evolve between versions.