Claude
Skill
dotnet-winui
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.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-winui-bfa4ebd.zip · 10 KB
Install
skills CLI
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-winui
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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
Workflow
- 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.
- Choose packaging model early — packaged (MSIX) vs unpackaged differ materially for deployment, identity, and API access:
<!-- Unpackaged: add to .csproj --> <WindowsPackageType>None</WindowsPackageType> - Apply MVVM pattern with the MVVM Toolkit — keep views dumb, logic in ViewModels:
public partial class ProductsViewModel : ObservableObject { [ObservableProperty] private ObservableCollection<Product> _products = []; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DeleteCommand))] private Product? _selectedProduct; [RelayCommand(CanExecute = nameof(CanDelete))] private async Task DeleteAsync() { if (SelectedProduct is null) return; await _productService.DeleteAsync(SelectedProduct.Id); Products.Remove(SelectedProduct); } private bool CanDelete() => SelectedProduct is not null; } - Use x:Bind for compiled bindings — better performance and compile-time checking than
{Binding}:<TextBlock Text="{x:Bind ViewModel.Title, Mode=OneWay}"/> - Wire DI through
Host.CreateDefaultBuilder— register services, ViewModels, and views. Resolve viaApp.GetService<T>(). - Implement navigation service — map ViewModels to Pages by convention. See references/patterns.md for the full pattern.
- Handle Windows App SDK features — windowing (AppWindow), custom title bar, app lifecycle, notifications.
- Always set
XamlRootwhen showing ContentDialog — omitting this causes silent failures. - Validate on Windows targets — behavior depends on runtime, packaging model, and Windows version.
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"]
Key Decisions
| 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
- MVVM pattern with testable ViewModels
- cleaner interop between shared and Windows-specific layers
Validate
- 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
Files (dotnet-skills)
-
references
-
anti-patterns.md 13 KB
# WinUI 3 Anti-Patterns Common mistakes to avoid when building WinUI 3 applications. ## MVVM Violations ### Logic in Code-Behind **Problem:** Business logic placed directly in XAML code-behind. ```csharp // Bad: Logic in code-behind public sealed partial class OrderPage : Page { private async void SubmitButton_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(CustomerNameTextBox.Text)) { await ShowError("Customer name is required"); return; } var order = new Order { CustomerName = CustomerNameTextBox.Text, Total = decimal.Parse(TotalTextBox.Text) }; using var client = new HttpClient(); await client.PostAsJsonAsync("https://api.example.com/orders", order); } } ``` **Solution:** Move logic to ViewModel with proper commands. ```csharp // Good: Logic in ViewModel public partial class OrderViewModel : ObservableObject { private readonly IOrderService _orderService; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SubmitCommand))] private string _customerName = string.Empty; [RelayCommand(CanExecute = nameof(CanSubmit))] private async Task SubmitAsync() { await _orderService.CreateOrderAsync(new Order { CustomerName = CustomerName }); } private bool CanSubmit() => !string.IsNullOrEmpty(CustomerName); } ``` ### Manual Property Change Notifications **Problem:** Writing boilerplate INotifyPropertyChanged code. ```csharp // Bad: Manual implementation public class ProductViewModel : INotifyPropertyChanged { private string _name; public string Name { get => _name; set { if (_name != value) { _name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DisplayName))); } } } public event PropertyChangedEventHandler? PropertyChanged; } ``` **Solution:** Use MVVM Toolkit source generators. ```csharp // Good: MVVM Toolkit public partial class ProductViewModel : ObservableObject { [ObservableProperty] [NotifyPropertyChangedFor(nameof(DisplayName))] private string _name = string.Empty; public string DisplayName => $"Product: {Name}"; } ``` ## Binding Issues ### Using Binding Instead of x:Bind **Problem:** Using traditional `{Binding}` instead of compiled `{x:Bind}`. ```xml <!-- Bad: Classic binding (runtime, slower) --> <TextBlock Text="{Binding Path=Title}"/> <Button Command="{Binding SaveCommand}"/> ``` **Solution:** Use x:Bind for compile-time binding. ```xml <!-- Good: Compiled binding (faster, type-safe) --> <TextBlock Text="{x:Bind ViewModel.Title, Mode=OneWay}"/> <Button Command="{x:Bind ViewModel.SaveCommand}"/> ``` ### Missing Mode in x:Bind **Problem:** Not specifying binding mode when needed. ```xml <!-- Bad: Default is OneTime, won't update --> <TextBlock Text="{x:Bind ViewModel.Status}"/> ``` **Solution:** Specify appropriate mode. ```xml <!-- Good: Updates when property changes --> <TextBlock Text="{x:Bind ViewModel.Status, Mode=OneWay}"/> <!-- Good: Two-way for input controls --> <TextBox Text="{x:Bind ViewModel.Name, Mode=TwoWay}"/> ``` ## Threading Issues ### Blocking the UI Thread **Problem:** Performing synchronous I/O on the UI thread. ```csharp // Bad: Blocks UI private void LoadData() { var client = new HttpClient(); var response = client.GetAsync("https://api.example.com/data").Result; var data = response.Content.ReadAsStringAsync().Result; ProcessData(data); } ``` **Solution:** Use async/await properly. ```csharp // Good: Non-blocking private async Task LoadDataAsync() { using var client = new HttpClient(); var response = await client.GetAsync("https://api.example.com/data"); var data = await response.Content.ReadAsStringAsync(); ProcessData(data); } ``` ### Updating UI from Background Thread **Problem:** Modifying UI elements from non-UI thread. ```csharp // Bad: Direct UI update from background Task.Run(() => { var data = LoadExpensiveData(); StatusTextBlock.Text = "Loaded"; // Crashes or undefined behavior }); ``` **Solution:** Use DispatcherQueue to marshal to UI thread. ```csharp // Good: Marshal to UI thread Task.Run(() => { var data = LoadExpensiveData(); DispatcherQueue.TryEnqueue(() => { StatusTextBlock.Text = "Loaded"; }); }); ``` ## Dialog and Picker Issues ### Missing XamlRoot **Problem:** Not setting XamlRoot on dialogs and pickers. ```csharp // Bad: Missing XamlRoot var dialog = new ContentDialog { Title = "Confirm", Content = "Are you sure?" }; await dialog.ShowAsync(); // Throws exception ``` **Solution:** Always set XamlRoot. ```csharp // Good: XamlRoot set var dialog = new ContentDialog { Title = "Confirm", Content = "Are you sure?", XamlRoot = Content.XamlRoot // Or rootElement.XamlRoot }; await dialog.ShowAsync(); ``` ### Pickers Without Window Handle **Problem:** Using file pickers without initializing with window handle. ```csharp // Bad: Missing initialization var picker = new FileOpenPicker(); picker.FileTypeFilter.Add(".txt"); var file = await picker.PickSingleFileAsync(); // Fails ``` **Solution:** Initialize picker with window handle. ```csharp // Good: Properly initialized var picker = new FileOpenPicker(); var hWnd = WindowNative.GetWindowHandle(App.MainWindow); InitializeWithWindow.Initialize(picker, hWnd); picker.FileTypeFilter.Add(".txt"); var file = await picker.PickSingleFileAsync(); ``` ## Resource and Styling Issues ### Hardcoded Colors and Sizes **Problem:** Using hardcoded values instead of resources. ```xml <!-- Bad: Hardcoded values --> <TextBlock Foreground="#333333" FontSize="14"/> <Border Background="#0078D4"/> ``` **Solution:** Use theme resources. ```xml <!-- Good: Theme-aware resources --> <TextBlock Foreground="{ThemeResource TextFillColorPrimary}" Style="{StaticResource BodyTextBlockStyle}"/> <Border Background="{ThemeResource AccentFillColorDefaultBrush}"/> ``` ### Not Supporting Theme Changes **Problem:** App doesn't respond to system theme changes. ```csharp // Bad: Fixed theme rootElement.RequestedTheme = ElementTheme.Light; ``` **Solution:** Support theme switching and system theme. ```csharp // Good: Respect user/system preference public void ApplyTheme(ElementTheme theme) { if (Content is FrameworkElement root) { root.RequestedTheme = theme; // Default follows system } } ``` ## List and Collection Issues ### Not Virtualizing Large Lists **Problem:** Loading all items without virtualization. ```xml <!-- Bad: No virtualization, loads all items --> <StackPanel> <ItemsControl ItemsSource="{x:Bind ViewModel.LargeCollection}"> <!-- All items created immediately --> </ItemsControl> </StackPanel> ``` **Solution:** Use virtualizing panels. ```xml <!-- Good: Virtualized list --> <ListView ItemsSource="{x:Bind ViewModel.LargeCollection, Mode=OneWay}" VirtualizingStackPanel.VirtualizationMode="Recycling"/> ``` ### Replacing Entire Collection **Problem:** Replacing collection instead of updating items. ```csharp // Bad: Causes full UI refresh Items = new ObservableCollection<Item>(await _service.GetItemsAsync()); ``` **Solution:** Update items incrementally when possible. ```csharp // Good: Incremental update for better UX var newItems = await _service.GetItemsAsync(); foreach (var item in newItems.Except(Items)) { Items.Add(item); } foreach (var item in Items.Except(newItems).ToList()) { Items.Remove(item); } ``` ## Navigation Issues ### Tightly Coupled Navigation **Problem:** Direct Frame access scattered throughout code. ```csharp // Bad: Direct coupling to Frame public sealed partial class ProductPage : Page { private void GoToDetails(Product product) { Frame.Navigate(typeof(ProductDetailPage), product.Id); } } ``` **Solution:** Use a navigation service. ```csharp // Good: Decoupled via service public partial class ProductViewModel : ObservableObject { private readonly INavigationService _navigation; [RelayCommand] private void GoToDetails(Product product) { _navigation.NavigateTo<ProductDetailViewModel>(product.Id); } } ``` ### Not Handling Back Navigation **Problem:** Ignoring back navigation and history. ```csharp // Bad: No back navigation support ``` **Solution:** Handle system back button and navigation history. ```csharp // Good: Handle back navigation public MainWindow() { InitializeComponent(); var navigationView = FindName("NavigationViewControl") as NavigationView; navigationView.BackRequested += (s, e) => { if (_navigationService.CanGoBack) { _navigationService.GoBack(); } }; } ``` ## Packaging and Deployment Issues ### Ignoring Packaging Choice Impact **Problem:** Assuming packaged and unpackaged apps work identically. ```csharp // Bad: Using packaged-only API in unpackaged app var localFolder = ApplicationData.Current.LocalFolder; // Throws in unpackaged ``` **Solution:** Check packaging state and use appropriate APIs. ```csharp // Good: Handle both scenarios public string GetStorageFolder() { if (IsPackaged()) { return ApplicationData.Current.LocalFolder.Path; } return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyApp"); } private static bool IsPackaged() { try { return Package.Current.Id != null; } catch { return false; } } ``` ### Wrong Target Framework **Problem:** Using incompatible target framework for Windows App SDK. ```xml <!-- Bad: Missing Windows version --> <TargetFramework>net8.0</TargetFramework> ``` **Solution:** Use correct Windows target framework. ```xml <!-- Good: Correct TFM for WinUI 3 --> <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework> <UseWinUI>true</UseWinUI> ``` ## Service and Dependency Issues ### Creating Services in Views **Problem:** Instantiating services directly in views. ```csharp // Bad: Tight coupling, hard to test public sealed partial class OrderPage : Page { private readonly HttpClient _client = new(); private readonly JsonSerializerOptions _options = new(); private async void LoadOrders() { var response = await _client.GetAsync("..."); // ... } } ``` **Solution:** Inject services via constructor. ```csharp // Good: Dependency injection public sealed partial class OrderPage : Page { public OrderViewModel ViewModel { get; } public OrderPage() { ViewModel = App.GetService<OrderViewModel>(); InitializeComponent(); } } ``` ### Not Disposing Resources **Problem:** Not disposing IDisposable resources. ```csharp // Bad: Resource leak public async Task DownloadFileAsync(string url) { var client = new HttpClient(); var stream = await client.GetStreamAsync(url); // client never disposed } ``` **Solution:** Use using statements or patterns. ```csharp // Good: Proper disposal public async Task DownloadFileAsync(string url) { using var client = new HttpClient(); await using var stream = await client.GetStreamAsync(url); // ... } ``` ## Windowing Issues ### Not Handling DPI Changes **Problem:** Fixed pixel sizes that don't scale. ```csharp // Bad: Fixed pixel size _appWindow.Resize(new SizeInt32(800, 600)); ``` **Solution:** Consider DPI-aware sizing when appropriate. ```csharp // Good: Consider display scale factor var displayArea = DisplayArea.GetFromWindowId(_appWindow.Id, DisplayAreaFallback.Primary); var scaleFactor = GetScaleFactor(); // Get from DisplayInformation var width = (int)(800 * scaleFactor); var height = (int)(600 * scaleFactor); _appWindow.Resize(new SizeInt32(width, height)); ``` ### Multiple Window Confusion **Problem:** Not tracking window instances properly. ```csharp // Bad: Lost reference to additional windows private void OpenNewWindow() { var window = new SecondaryWindow(); window.Activate(); // Window reference lost, may be GC'd } ``` **Solution:** Track window instances. ```csharp // Good: Track windows private readonly List<Window> _windows = []; private void OpenNewWindow() { var window = new SecondaryWindow(); _windows.Add(window); window.Closed += (s, e) => _windows.Remove((Window)s); window.Activate(); } ``` ## Summary Table | Category | Anti-Pattern | Impact | Solution | |----------|-------------|--------|----------| | MVVM | Code-behind logic | Untestable | Use ViewModels | | Binding | Using {Binding} | Slower, no type safety | Use {x:Bind} | | Threading | Blocking UI | Frozen app | Use async/await | | Dialogs | Missing XamlRoot | Runtime crash | Set XamlRoot | | Styling | Hardcoded values | Poor theming | Use resources | | Lists | No virtualization | Poor performance | Use ListView | | Navigation | Tight coupling | Hard to test | Use service | | Packaging | Wrong TFM | Build failures | Use correct TFM | | Services | Direct instantiation | Tight coupling | Use DI | | Windows | Lost references | GC issues | Track instances | -
patterns.md 12 KB
# WinUI 3 Patterns Reference patterns for building WinUI 3 applications with Windows App SDK. ## MVVM Pattern ### Core Principles 1. **View** - XAML UI, minimal code-behind, binds to ViewModel 2. **ViewModel** - Exposes data and commands, contains presentation logic 3. **Model** - Domain data and business rules ### MVVM Toolkit Integration Use `CommunityToolkit.Mvvm` for source-generated MVVM: ```csharp public partial class OrderViewModel : ObservableObject { [ObservableProperty] [NotifyPropertyChangedFor(nameof(TotalDisplay))] private decimal _total; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SubmitCommand))] private bool _isValid; public string TotalDisplay => $"Total: {Total:C}"; [RelayCommand(CanExecute = nameof(IsValid))] private async Task SubmitAsync() { // Submit order } } ``` ### ViewModel Initialization Initialize ViewModels through navigation or page lifecycle: ```csharp public sealed partial class OrderPage : Page { public OrderViewModel ViewModel { get; } public OrderPage() { ViewModel = App.GetService<OrderViewModel>(); InitializeComponent(); } protected override async void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); if (e.Parameter is int orderId) { await ViewModel.LoadAsync(orderId); } } } ``` ## Service Pattern ### Service Registration Register services at application startup: ```csharp services.AddSingleton<ISettingsService, SettingsService>(); services.AddSingleton<INavigationService, NavigationService>(); services.AddTransient<IFileService, FileService>(); services.AddHttpClient<IApiService, ApiService>(); ``` ### Service Abstraction Abstract Windows-specific APIs behind interfaces for testability: ```csharp public interface IFilePickerService { Task<StorageFile?> PickFileAsync(IEnumerable<string> extensions); Task<StorageFolder?> PickFolderAsync(); } public class FilePickerService : IFilePickerService { private readonly Window _window; public FilePickerService(Window window) { _window = window; } public async Task<StorageFile?> PickFileAsync(IEnumerable<string> extensions) { var picker = new FileOpenPicker(); InitializeWithWindow.Initialize(picker, WindowNative.GetWindowHandle(_window)); foreach (var ext in extensions) { picker.FileTypeFilter.Add(ext); } return await picker.PickSingleFileAsync(); } } ``` ## Navigation Pattern ### Frame-Based Navigation Use a navigation service that wraps Frame navigation: ```csharp public class NavigationService : INavigationService { private readonly Dictionary<Type, Type> _viewModelToPageMap = new(); private Frame? _frame; public void RegisterPage<TViewModel, TPage>() where TViewModel : class where TPage : Page { _viewModelToPageMap[typeof(TViewModel)] = typeof(TPage); } public bool NavigateTo<TViewModel>(object? parameter = null) { if (_viewModelToPageMap.TryGetValue(typeof(TViewModel), out var pageType)) { return _frame?.Navigate(pageType, parameter) ?? false; } return false; } } ``` ### NavigationView Integration Integrate with NavigationView for shell navigation: ```csharp private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args) { if (args.IsSettingsSelected) { _navigationService.NavigateTo<SettingsViewModel>(); return; } if (args.SelectedItemContainer?.Tag is string tag) { var viewModelType = Type.GetType($"MyApp.ViewModels.{tag}ViewModel"); if (viewModelType != null) { _navigationService.NavigateTo(viewModelType); } } } ``` ## Window Management Pattern ### AppWindow Abstraction Wrap AppWindow operations for cleaner code: ```csharp public class WindowHelper { private readonly AppWindow _appWindow; public WindowHelper(Window window) { var hWnd = WindowNative.GetWindowHandle(window); var windowId = Win32Interop.GetWindowIdFromWindow(hWnd); _appWindow = AppWindow.GetFromWindowId(windowId); } public void SetSize(int width, int height) { _appWindow.Resize(new SizeInt32(width, height)); } public void CenterOnScreen() { var display = DisplayArea.GetFromWindowId(_appWindow.Id, DisplayAreaFallback.Primary); var x = (display.WorkArea.Width - _appWindow.Size.Width) / 2; var y = (display.WorkArea.Height - _appWindow.Size.Height) / 2; _appWindow.Move(new PointInt32(x, y)); } public void SetTitle(string title) { _appWindow.Title = title; } public void CustomizeTitleBar(Color backgroundColor) { if (AppWindowTitleBar.IsCustomizationSupported()) { var titleBar = _appWindow.TitleBar; titleBar.ExtendsContentIntoTitleBar = true; titleBar.ButtonBackgroundColor = backgroundColor; } } } ``` ## Messaging Pattern ### WeakReferenceMessenger Use the messaging system for loosely coupled communication: ```csharp // Define message public record UserLoggedInMessage(User User); // Send message WeakReferenceMessenger.Default.Send(new UserLoggedInMessage(user)); // Receive message in ViewModel public partial class DashboardViewModel : ObservableRecipient { protected override void OnActivated() { Messenger.Register<DashboardViewModel, UserLoggedInMessage>(this, (r, m) => { r.CurrentUser = m.User; }); } } ``` ### Request Messages Use request messages for data retrieval across ViewModels: ```csharp public class CurrentThemeRequestMessage : RequestMessage<ElementTheme> { } // Handler Messenger.Register<SettingsViewModel, CurrentThemeRequestMessage>(this, (r, m) => { m.Reply(r.CurrentTheme); }); // Requester var theme = WeakReferenceMessenger.Default.Send<CurrentThemeRequestMessage>(); ``` ## Settings Pattern ### Settings Service Persist settings using local storage: ```csharp public class SettingsService : ISettingsService { private readonly ApplicationDataContainer _localSettings; public SettingsService() { _localSettings = ApplicationData.Current.LocalSettings; } public T? Get<T>(string key, T? defaultValue = default) { if (_localSettings.Values.TryGetValue(key, out var value)) { return (T)value; } return defaultValue; } public void Set<T>(string key, T value) { _localSettings.Values[key] = value; } } ``` ### Unpackaged Settings Alternative For unpackaged apps, use file-based settings: ```csharp public class FileSettingsService : ISettingsService { private readonly string _settingsPath; private Dictionary<string, object?> _settings = new(); public FileSettingsService() { _settingsPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyApp", "settings.json"); Load(); } private void Load() { if (File.Exists(_settingsPath)) { var json = File.ReadAllText(_settingsPath); _settings = JsonSerializer.Deserialize<Dictionary<string, object?>>(json) ?? new(); } } private void Save() { Directory.CreateDirectory(Path.GetDirectoryName(_settingsPath)!); var json = JsonSerializer.Serialize(_settings); File.WriteAllText(_settingsPath, json); } } ``` ## Data Template Selector Pattern Select templates based on data type: ```csharp public class NotificationTemplateSelector : DataTemplateSelector { public DataTemplate? InfoTemplate { get; set; } public DataTemplate? WarningTemplate { get; set; } public DataTemplate? ErrorTemplate { get; set; } protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container) { return item switch { InfoNotification => InfoTemplate, WarningNotification => WarningTemplate, ErrorNotification => ErrorTemplate, _ => base.SelectTemplateCore(item, container) }; } } ``` ```xml <Page.Resources> <local:NotificationTemplateSelector x:Key="NotificationSelector"> <local:NotificationTemplateSelector.InfoTemplate> <DataTemplate x:DataType="models:InfoNotification"> <InfoBar Severity="Informational" Title="{x:Bind Title}"/> </DataTemplate> </local:NotificationTemplateSelector.InfoTemplate> <!-- Other templates --> </local:NotificationTemplateSelector> </Page.Resources> <ListView ItemsSource="{x:Bind ViewModel.Notifications}" ItemTemplateSelector="{StaticResource NotificationSelector}"/> ``` ## Async Loading Pattern Handle async data loading with loading states: ```csharp public partial class DataViewModel : ObservableObject { [ObservableProperty] private bool _isLoading; [ObservableProperty] private string? _errorMessage; [ObservableProperty] private ObservableCollection<Item> _items = []; [RelayCommand] private async Task LoadAsync() { IsLoading = true; ErrorMessage = null; try { var data = await _dataService.GetItemsAsync(); Items = new ObservableCollection<Item>(data); } catch (Exception ex) { ErrorMessage = ex.Message; } finally { IsLoading = false; } } } ``` ```xml <Grid> <ListView ItemsSource="{x:Bind ViewModel.Items}" Visibility="{x:Bind ViewModel.IsLoading, Converter={StaticResource InverseBoolToVisibility}}"/> <ProgressRing IsActive="{x:Bind ViewModel.IsLoading, Mode=OneWay}" Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay}"/> <InfoBar IsOpen="{x:Bind ViewModel.ErrorMessage, Converter={StaticResource NullToBool}}" Severity="Error" Title="Error" Message="{x:Bind ViewModel.ErrorMessage, Mode=OneWay}"/> </Grid> ``` ## Activation Pattern Handle different activation scenarios: ```csharp protected override void OnLaunched(LaunchActivatedEventArgs args) { m_window = new MainWindow(); var activatedArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); switch (activatedArgs.Kind) { case ExtendedActivationKind.File: HandleFileActivation(activatedArgs); break; case ExtendedActivationKind.Protocol: HandleProtocolActivation(activatedArgs); break; case ExtendedActivationKind.ToastNotification: HandleToastActivation(activatedArgs); break; default: HandleDefaultActivation(); break; } m_window.Activate(); } private void HandleFileActivation(AppActivationArguments args) { if (args.Data is IFileActivatedEventArgs fileArgs) { var file = fileArgs.Files.FirstOrDefault() as StorageFile; if (file != null) { _navigationService.NavigateTo<FileViewerViewModel>(file.Path); } } } ``` ## Background Task Pattern Register and handle background tasks: ```csharp public static class BackgroundTaskHelper { public static async Task RegisterTimerTaskAsync(string taskName, uint intervalMinutes) { var access = await BackgroundExecutionManager.RequestAccessAsync(); if (access is BackgroundAccessStatus.DeniedBySystemPolicy or BackgroundAccessStatus.DeniedByUser) { return; } foreach (var task in BackgroundTaskRegistration.AllTasks) { if (task.Value.Name == taskName) { return; // Already registered } } var builder = new BackgroundTaskBuilder { Name = taskName }; builder.SetTrigger(new TimeTrigger(intervalMinutes, false)); builder.Register(); } } ```
-
-
SKILL.md 4.1 KB
--- name: dotnet-winui 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 ## Workflow 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 <!-- Unpackaged: add to .csproj --> <WindowsPackageType>None</WindowsPackageType> ``` 3. **Apply MVVM pattern** with the MVVM Toolkit — keep views dumb, logic in ViewModels: ```csharp public partial class ProductsViewModel : ObservableObject { [ObservableProperty] private ObservableCollection<Product> _products = []; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(DeleteCommand))] private Product? _selectedProduct; [RelayCommand(CanExecute = nameof(CanDelete))] private async Task DeleteAsync() { if (SelectedProduct is null) return; await _productService.DeleteAsync(SelectedProduct.Id); Products.Remove(SelectedProduct); } private bool CanDelete() => SelectedProduct is not null; } ``` 4. **Use x:Bind for compiled bindings** — better performance and compile-time checking than `{Binding}`: ```xml <TextBlock Text="{x:Bind ViewModel.Title, Mode=OneWay}"/> ``` 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. ```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"] ``` ## Key Decisions | 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 - MVVM pattern with testable ViewModels - cleaner interop between shared and Windows-specific layers ## Validate - 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
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.