dotnet-winui · diff
v1.0.0 to v1.0.1
56 added, 522 removed. Audit A to A.
---
name: dotnet-winui
- version: "1.0.0"
- category: "Desktop"
- description: "Build or review WinUI 3 applications with the Windows App SDK, modern Windows desktop patterns, packaging decisions, and interop boundaries with other .NET stacks."
+ version: "1.0.1"
+ category: "Desktop and Mobile"
+ description: "Build or review WinUI 3 applications with the Windows App SDK, including MVVM patterns, packaging decisions, navigation, theming, windowing, and interop boundaries with other .NET stacks. Use when building modern Windows-native desktop UI."
compatibility: "Requires a WinUI 3, Windows App SDK, or MAUI-on-Windows integration scenario."
---
# WinUI 3 and Windows App SDK
## Trigger On
- building native modern Windows desktop UI on WinUI 3
- integrating Windows App SDK features into a .NET app
- deciding between WinUI, WPF, WinForms, and MAUI for Windows work
- implementing MVVM patterns in Windows App SDK applications
- ## Documentation
-
- - [WinUI 3 Overview](https://learn.microsoft.com/en-us/windows/apps/winui/winui3/)
- - [Create Your First WinUI 3 App](https://learn.microsoft.com/en-us/windows/apps/winui/winui3/create-your-first-winui3-app)
- - [Windows App SDK Overview](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/)
- - [MVVM Toolkit with WinUI](https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro)
- - [Controls Reference](https://learn.microsoft.com/en-us/windows/apps/design/controls/)
-
- ### References
-
- - [patterns.md](references/patterns.md) - WinUI 3 patterns including MVVM, navigation, services, and Windows App SDK integration
- - [anti-patterns.md](references/anti-patterns.md) - Common WinUI mistakes and how to avoid them
-
## Workflow
- 1. **Confirm WinUI is the right choice** — use when modern Windows-native UI and Windows App SDK capabilities are needed
- 2. **Choose packaging model** — packaged (MSIX) vs unpackaged differ materially
- 3. **Apply MVVM pattern** — keep views dumb, logic in ViewModels
- 4. **Use Fluent Design** — leverage modern Windows 11 styling
- 5. **Handle Windows App SDK features** — windowing, app lifecycle, notifications
- 6. **Validate on Windows targets** — behavior depends on runtime environment
-
- ## Project Structure
-
- ```
- MyWinUIApp/
- ├── MyWinUIApp/
- │ ├── App.xaml # Application entry
- │ ├── MainWindow.xaml # Main window
- │ ├── Views/ # XAML pages
- │ ├── ViewModels/ # MVVM ViewModels
- │ ├── Models/ # Domain models
- │ ├── Services/ # Business logic
- │ ├── Helpers/ # Utility classes
- │ └── Assets/ # Images, fonts
- ├── MyWinUIApp (Package)/ # MSIX packaging project (if packaged)
- └── MyWinUIApp.Tests/
- ```
-
- ## MVVM Pattern
-
- ### ViewModel with MVVM Toolkit
- ```csharp
- public partial class ProductsViewModel : ObservableObject
- {
- private readonly IProductService _productService;
- private readonly INavigationService _navigationService;
-
- [ObservableProperty]
- private ObservableCollection<Product> _products = [];
-
- [ObservableProperty]
- [NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
- private Product? _selectedProduct;
-
- [ObservableProperty]
- [NotifyCanExecuteChangedFor(nameof(LoadProductsCommand))]
- private bool _isLoading;
-
- public ProductsViewModel(IProductService productService, INavigationService navigationService)
- {
- _productService = productService;
- _navigationService = navigationService;
- }
-
- [RelayCommand(CanExecute = nameof(CanLoadProducts))]
- private async Task LoadProductsAsync()
- {
- IsLoading = true;
- try
- {
- var items = await _productService.GetAllAsync();
- Products = new ObservableCollection<Product>(items);
- }
- finally
- {
- IsLoading = false;
- }
- }
-
- private bool CanLoadProducts() => !IsLoading;
-
- [RelayCommand(CanExecute = nameof(CanDelete))]
- private async Task DeleteAsync()
- {
- if (SelectedProduct is null) return;
- await _productService.DeleteAsync(SelectedProduct.Id);
- Products.Remove(SelectedProduct);
- SelectedProduct = null;
- }
-
- private bool CanDelete() => SelectedProduct is not null;
-
- [RelayCommand]
- private void NavigateToDetail(Product product)
- {
- _navigationService.NavigateTo<ProductDetailViewModel>(product);
- }
- }
- ```
-
- ### View Binding
- ```xml
- <Page x:Class="MyWinUIApp.Views.ProductsPage"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:vm="using:MyWinUIApp.ViewModels"
- xmlns:models="using:MyWinUIApp.Models">
-
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto"/>
- <RowDefinition Height="*"/>
- </Grid.RowDefinitions>
-
- <CommandBar Grid.Row="0" DefaultLabelPosition="Right">
- <AppBarButton Icon="Refresh" Label="Refresh"
- Command="{x:Bind ViewModel.LoadProductsCommand}"/>
- <AppBarButton Icon="Delete" Label="Delete"
- Command="{x:Bind ViewModel.DeleteCommand}"/>
- </CommandBar>
-
- <ListView Grid.Row="1"
- ItemsSource="{x:Bind ViewModel.Products, Mode=OneWay}"
- SelectedItem="{x:Bind ViewModel.SelectedProduct, Mode=TwoWay}"
- SelectionMode="Single">
- <ListView.ItemTemplate>
- <DataTemplate x:DataType="models:Product">
- <Grid Padding="12" ColumnSpacing="12">
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="*"/>
- <ColumnDefinition Width="Auto"/>
- </Grid.ColumnDefinitions>
- <TextBlock Text="{x:Bind Name}" Style="{StaticResource SubtitleTextBlockStyle}"/>
- <TextBlock Grid.Column="1" Text="{x:Bind Price}"
- Style="{StaticResource BodyTextBlockStyle}"/>
- </Grid>
- </DataTemplate>
- </ListView.ItemTemplate>
- </ListView>
-
- <ProgressRing Grid.Row="1"
- IsActive="{x:Bind ViewModel.IsLoading, Mode=OneWay}"
- Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay}"/>
- </Grid>
- </Page>
- ```
-
- ### Code-Behind with x:Bind
- ```csharp
- public sealed partial class ProductsPage : Page
- {
- public ProductsViewModel ViewModel { get; }
-
- public ProductsPage()
- {
- ViewModel = App.GetService<ProductsViewModel>();
- InitializeComponent();
- }
-
- protected override async void OnNavigatedTo(NavigationEventArgs e)
- {
- base.OnNavigatedTo(e);
- await ViewModel.LoadProductsCommand.ExecuteAsync(null);
- }
- }
- ```
-
- ## Dependency Injection
-
- ```csharp
- public partial class App : Application
- {
- private static IHost? _host;
-
- public App()
- {
- InitializeComponent();
-
- _host = Host.CreateDefaultBuilder()
- .ConfigureServices((context, services) =>
- {
- // Services
- services.AddSingleton<IProductService, ProductService>();
- services.AddSingleton<INavigationService, NavigationService>();
- services.AddSingleton<IDialogService, DialogService>();
-
- // ViewModels
- services.AddTransient<ProductsViewModel>();
- services.AddTransient<ProductDetailViewModel>();
- services.AddTransient<SettingsViewModel>();
-
- // Views
- services.AddTransient<MainWindow>();
- services.AddTransient<ProductsPage>();
- services.AddTransient<ProductDetailPage>();
- })
- .Build();
- }
-
- public static T GetService<T>() where T : class
- => _host!.Services.GetRequiredService<T>();
-
- protected override void OnLaunched(LaunchActivatedEventArgs args)
- {
- m_window = GetService<MainWindow>();
- m_window.Activate();
- }
-
- private Window? m_window;
- }
- ```
-
- ## Navigation Service
-
- ```csharp
- public interface INavigationService
- {
- bool CanGoBack { get; }
- void NavigateTo<TViewModel>(object? parameter = null) where TViewModel : class;
- void GoBack();
- }
-
- public class NavigationService : INavigationService
- {
- private readonly IServiceProvider _serviceProvider;
- private Frame? _frame;
-
- public NavigationService(IServiceProvider serviceProvider)
- {
- _serviceProvider = serviceProvider;
- }
-
- public void Initialize(Frame frame) => _frame = frame;
-
- public bool CanGoBack => _frame?.CanGoBack ?? false;
-
- public void NavigateTo<TViewModel>(object? parameter = null) where TViewModel : class
- {
- var pageType = GetPageType<TViewModel>();
- _frame?.Navigate(pageType, parameter);
- }
-
- public void GoBack()
- {
- if (_frame?.CanGoBack == true)
- {
- _frame.GoBack();
- }
- }
-
- private static Type GetPageType<TViewModel>()
- {
- var viewModelName = typeof(TViewModel).Name;
- var pageName = viewModelName.Replace("ViewModel", "Page");
- var pageType = Type.GetType($"MyWinUIApp.Views.{pageName}");
- return pageType ?? throw new ArgumentException($"Page not found for {viewModelName}");
- }
- }
- ```
-
- ## Windowing
-
- ```csharp
- public sealed partial class MainWindow : Window
- {
- private AppWindow _appWindow;
-
- public MainWindow()
- {
- InitializeComponent();
-
- // Get AppWindow for advanced windowing
- var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
- var windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
- _appWindow = AppWindow.GetFromWindowId(windowId);
-
- // Customize title bar
- if (AppWindowTitleBar.IsCustomizationSupported())
- {
- var titleBar = _appWindow.TitleBar;
- titleBar.ExtendsContentIntoTitleBar = true;
- titleBar.ButtonBackgroundColor = Colors.Transparent;
- titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
- }
-
- // Set window size and position
- _appWindow.Resize(new SizeInt32(1200, 800));
- _appWindow.Move(new PointInt32(100, 100));
- }
-
- // Center window on screen
- private void CenterOnScreen()
- {
- var displayArea = DisplayArea.GetFromWindowId(_appWindow.Id, DisplayAreaFallback.Primary);
- var centerX = (displayArea.WorkArea.Width - _appWindow.Size.Width) / 2;
- var centerY = (displayArea.WorkArea.Height - _appWindow.Size.Height) / 2;
- _appWindow.Move(new PointInt32(centerX, centerY));
- }
- }
- ```
-
- ## Theming
-
- ```csharp
- public class ThemeService
- {
- public void SetTheme(ElementTheme theme)
- {
- if (App.MainWindow.Content is FrameworkElement rootElement)
- {
- rootElement.RequestedTheme = theme;
- }
- }
-
- public ElementTheme GetCurrentTheme()
- {
- if (App.MainWindow.Content is FrameworkElement rootElement)
- {
- return rootElement.RequestedTheme;
- }
- return ElementTheme.Default;
- }
- }
- ```
-
- ```xml
- <!-- App.xaml - Custom theme colors -->
- <Application.Resources>
- <ResourceDictionary>
- <ResourceDictionary.MergedDictionaries>
- <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls"/>
- </ResourceDictionary.MergedDictionaries>
-
- <!-- Custom accent colors -->
- <SolidColorBrush x:Key="SystemAccentColor" Color="#0078D4"/>
-
- <!-- Custom styles -->
- <Style x:Key="PrimaryButtonStyle" TargetType="Button">
- <Setter Property="Background" Value="{ThemeResource SystemAccentColor}"/>
- <Setter Property="Foreground" Value="White"/>
- <Setter Property="CornerRadius" Value="4"/>
- </Style>
- </ResourceDictionary>
- </Application.Resources>
- ```
-
- ## Dialogs
-
- ```csharp
- public class DialogService : IDialogService
- {
- public async Task<bool> ShowConfirmationAsync(string title, string message)
- {
- var dialog = new ContentDialog
- {
- Title = title,
- Content = message,
- PrimaryButtonText = "Yes",
- CloseButtonText = "No",
- DefaultButton = ContentDialogButton.Close,
- XamlRoot = App.MainWindow.Content.XamlRoot
- };
-
- var result = await dialog.ShowAsync();
- return result == ContentDialogResult.Primary;
- }
-
- public async Task ShowErrorAsync(string title, string message)
- {
- var dialog = new ContentDialog
- {
- Title = title,
- Content = message,
- CloseButtonText = "OK",
- XamlRoot = App.MainWindow.Content.XamlRoot
- };
-
- await dialog.ShowAsync();
- }
- }
- ```
-
- ## Packaging Options
-
- ### Packaged (MSIX)
- ```xml
- <!-- Package.appxmanifest -->
- <Package>
- <Identity Name="MyCompany.MyWinUIApp" Publisher="CN=MyCompany" Version="1.0.0.0"/>
- <Properties>
- <DisplayName>My WinUI App</DisplayName>
- <PublisherDisplayName>My Company</PublisherDisplayName>
- </Properties>
- <Dependencies>
- <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.22621.0"/>
- </Dependencies>
- <Applications>
- <Application Id="App" Executable="MyWinUIApp.exe" EntryPoint="MyWinUIApp.App">
- <uap:VisualElements DisplayName="My WinUI App" BackgroundColor="transparent">
- <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/>
- </uap:VisualElements>
- </Application>
- </Applications>
- <Capabilities>
- <rescap:Capability Name="runFullTrust"/>
- </Capabilities>
- </Package>
- ```
-
- ### Unpackaged
- ```xml
- <!-- .csproj for unpackaged -->
- <Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <OutputType>WinExe</OutputType>
- <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
- <UseWinUI>true</UseWinUI>
- <WindowsPackageType>None</WindowsPackageType>
- </PropertyGroup>
- </Project>
- ```
-
- ## Anti-Patterns to Avoid
-
- | Anti-Pattern | Why It's Bad | Better Approach |
- |--------------|--------------|-----------------|
- | Logic in code-behind | Hard to test | Use MVVM with ViewModels |
- | Ignoring x:Bind | Poor performance | Use compiled bindings |
- | Blocking UI thread | Frozen UI | Use async/await |
- | Hardcoded styles | Inconsistent theming | Use resource dictionaries |
- | Ignoring packaging choice | Deployment issues | Choose packaged vs unpackaged early |
- | Direct service access in views | Tight coupling | Use dependency injection |
- | Ignoring XamlRoot | Dialog failures | Always set XamlRoot for dialogs |
- | Manual property notifications | Boilerplate, errors | Use MVVM Toolkit attributes |
-
- ## Best Practices
-
- 1. **Use x:Bind for compiled bindings:**
+ 1. **Confirm WinUI is the right choice** — use when modern Windows-native UI, Fluent Design, and Windows App SDK capabilities are needed. For cross-platform, consider MAUI instead.
+ 2. **Choose packaging model early** — packaged (MSIX) vs unpackaged differ materially for deployment, identity, and API access:
```xml
- <TextBlock Text="{x:Bind ViewModel.Title, Mode=OneWay}"/>
+ <!-- Unpackaged: add to .csproj -->
+ <WindowsPackageType>None</WindowsPackageType>
```
-
- 2. **Implement proper navigation:**
+ 3. **Apply MVVM pattern** with the MVVM Toolkit — keep views dumb, logic in ViewModels:
```csharp
- protected override void OnNavigatedTo(NavigationEventArgs e)
+ public partial class ProductsViewModel : ObservableObject
{
- base.OnNavigatedTo(e);
- if (e.Parameter is Product product)
- {
- ViewModel.Initialize(product);
- }
- }
- ```
-
- 3. **Use InfoBar for notifications:**
- ```xml
- <InfoBar x:Name="SuccessInfoBar"
- Title="Success"
- Message="Changes saved"
- Severity="Success"
- IsOpen="{x:Bind ViewModel.ShowSuccess, Mode=OneWay}"/>
- ```
+ [ObservableProperty]
+ private ObservableCollection<Product> _products = [];
- 4. **Handle app lifecycle:**
- ```csharp
- public App()
- {
- InitializeComponent();
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
+ private Product? _selectedProduct;
- // Handle suspension
- Suspending += (s, e) =>
+ [RelayCommand(CanExecute = nameof(CanDelete))]
+ private async Task DeleteAsync()
{
- var deferral = e.SuspendingOperation.GetDeferral();
- // Save state
- deferral.Complete();
- };
+ if (SelectedProduct is null) return;
+ await _productService.DeleteAsync(SelectedProduct.Id);
+ Products.Remove(SelectedProduct);
+ }
+ private bool CanDelete() => SelectedProduct is not null;
}
```
-
- 5. **Virtualize large lists:**
- ```xml
- <ListView ItemsSource="{x:Bind ViewModel.Items}"
- VirtualizingStackPanel.VirtualizationMode="Recycling">
- ```
-
- 6. **Use semantic zoom for large datasets:**
+ 4. **Use x:Bind for compiled bindings** — better performance and compile-time checking than `{Binding}`:
```xml
- <SemanticZoom>
- <SemanticZoom.ZoomedInView>
- <ListView ItemsSource="{x:Bind ViewModel.GroupedItems}"/>
- </SemanticZoom.ZoomedInView>
- <SemanticZoom.ZoomedOutView>
- <GridView ItemsSource="{x:Bind ViewModel.GroupHeaders}"/>
- </SemanticZoom.ZoomedOutView>
- </SemanticZoom>
+ <TextBlock Text="{x:Bind ViewModel.Title, Mode=OneWay}"/>
```
-
- ## Testing
-
- ```csharp
- [Fact]
- public async Task LoadProducts_UpdatesCollection()
- {
- var mockService = new Mock<IProductService>();
- var mockNavigation = new Mock<INavigationService>();
- mockService.Setup(s => s.GetAllAsync())
- .ReturnsAsync(new[] { new Product { Name = "Test" } });
-
- var viewModel = new ProductsViewModel(mockService.Object, mockNavigation.Object);
-
- await viewModel.LoadProductsCommand.ExecuteAsync(null);
-
- Assert.Single(viewModel.Products);
- Assert.Equal("Test", viewModel.Products[0].Name);
- }
+ 5. **Wire DI through `Host.CreateDefaultBuilder`** — register services, ViewModels, and views. Resolve via `App.GetService<T>()`.
+ 6. **Implement navigation service** — map ViewModels to Pages by convention. See references/patterns.md for the full pattern.
+ 7. **Handle Windows App SDK features** — windowing (AppWindow), custom title bar, app lifecycle, notifications.
+ 8. **Always set `XamlRoot`** when showing ContentDialog — omitting this causes silent failures.
+ 9. **Validate on Windows targets** — behavior depends on runtime, packaging model, and Windows version.
- [Fact]
- public void DeleteCommand_CannotExecute_WhenNoSelection()
- {
- var mockService = new Mock<IProductService>();
- var mockNavigation = new Mock<INavigationService>();
- var viewModel = new ProductsViewModel(mockService.Object, mockNavigation.Object);
+ ```mermaid
+ flowchart LR
+ A["Choose WinUI"] --> B["Select packaging model"]
+ B --> C["MVVM + DI setup"]
+ C --> D["Navigation and views"]
+ D --> E["Windows App SDK features"]
+ E --> F["Validate on target runtime"]
+ ```
- viewModel.SelectedProduct = null;
+ ## Key Decisions
- Assert.False(viewModel.DeleteCommand.CanExecute(null));
- }
- ```
+ | Decision | Guidance |
+ |----------|----------|
+ | Packaged vs unpackaged | Packaged (MSIX) for Store, auto-update, and full API access; unpackaged for simpler deployment |
+ | x:Bind vs Binding | Always prefer x:Bind — compiled, faster, type-safe |
+ | MVVM Toolkit attributes | Use `[ObservableProperty]`, `[RelayCommand]` to eliminate boilerplate |
+ | Navigation | Convention-based ViewModel→Page mapping via navigation service |
+ | Theming | Use `RequestedTheme` on root element; respect system theme by default |
## Deliver
- modern Windows UI code with clear platform boundaries
- explicit deployment and packaging assumptions
- - cleaner interop between shared and Windows-specific layers
- MVVM pattern with testable ViewModels
+ - cleaner interop between shared and Windows-specific layers
## Validate
- - WinUI is chosen for a real product reason
- - Windows App SDK dependencies are explicit
- - packaging and runtime assumptions are tested
- - x:Bind is used for compiled bindings
- - navigation and dialogs work correctly
+ - WinUI is chosen for a real product reason, not defaulted to
+ - Windows App SDK dependencies are explicit in the project file
+ - packaging and runtime assumptions are tested on target
+ - x:Bind is used for compiled bindings throughout
+ - navigation and ContentDialog both work with correct XamlRoot
+ - custom title bar renders correctly on Windows 10 and 11
+
+ ## References
+
+ - references/patterns.md - WinUI 3 patterns including MVVM, navigation services, DI setup, windowing, theming, dialogs, and lifecycle handling
+ - references/anti-patterns.md - common WinUI mistakes with explanations and corrections