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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-winui-bfa4ebd.zip · 10 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

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

  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:
    <!-- Unpackaged: add to .csproj -->
    <WindowsPackageType>None</WindowsPackageType>
    
  3. 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;
    }
    
  4. Use x:Bind for compiled bindings — better performance and compile-time checking than {Binding}:
    <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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related