-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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 theAddSentryAndSerilog(...)call shown later. Skip it unless you want telemetry.
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>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>();
}
}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 Host — Host.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 additionalActivationHandler<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 aFrameand drives page navigation by ViewModel type. -
ShellViewModel(singleton) andShellPage(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.
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>()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.manifestdeclares Windows 10+ compatibility andPerMonitorV2DPI awareness — the standard WinUI project template already does this.
IMPORTANT: MainWindow.xaml should not have content, even an empty
<Grid/>would make theActivationServiceskip the UI population.
The shell hosts three things: a custom TitleBar, a NavigationView, and the Frame that pages navigate into.
<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.
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:
-
navigationService.Frame = NavigationFrame;— hands the shell'sFrameto the navigation service so all navigation flows through it. -
NavigationViewHelper.Configure(navigationService, NavigationViewControl, settingsKey)— connects theNavigationViewto the navigation service and tells it which navigation key represents the built-in Settings item (here, the full type name ofSettingsViewModel). This makes the gear/Settings item navigate to your settings page automatically. -
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>().
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
{
}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 SettingsViewModel → SettingsPage 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
SettingsViewModelinjected into the page, register both in DI (.AddTransient<SettingsViewModel>()and.AddTransient<SettingsPage>()) and giveSettingsPagea constructor that takesSettingsViewModel— exactly likeShellPagedoes.
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.
The repeatable pattern for every page after the shell:
- Create
XViewModel(anObservableObject) andXPage. - Add
NavigationPageMap.Configure<XViewModel, XPage>()inInitializePageHelper. - Register in DI if the page needs constructor injection (
.AddTransient<XPage>(), plus its viewmodel). - Add a
NavigationViewItemto the shell whose navigation key is the viewmodel type — the toolkit routes the click throughFrameNavigationServiceto the mapped page.
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.WinUIassembly. Check the package's own docs/IntelliSense for exact method signatures, as they may evolve between versions.