|
| 1 | +# GitHub Copilot Instructions — WPF File-based App (FBA) Template |
| 2 | + |
| 3 | +> Pragmatic, production-ready guidance for building **.NET 10** WPF apps as a **single C# file** with **MVVM (CommunityToolkit.Mvvm)**. |
| 4 | +> **Apartment model:** Per field validation, WPF startup **must** perform the two-step STA initialization: **`Unknown → STA`**. |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +## 1) Project Overview |
| 9 | + |
| 10 | +Author a modern WPF desktop app using the **File-based App (FBA)** model: keep build/runtime directives at the very top of one `.cs` file, place top-level bootstrapping first, and define all types (App, Window, ViewModels, etc.) after it. |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## 2) FBA Directives (Windows-only GUI) |
| 15 | + |
| 16 | +```csharp |
| 17 | +// Shebang optional for Windows-only apps |
| 18 | +
|
| 19 | +#:sdk Microsoft.NET.Sdk |
| 20 | + |
| 21 | + |
| 22 | +#:property OutputType=WinExe |
| 23 | +#:property TargetFramework=net10.0-windows |
| 24 | +#:property UseWPF=True |
| 25 | +#:property UseWindowsForms=False |
| 26 | +#:property PublishAot=False // WPF is not AOT-compatible |
| 27 | +#:property Nullable=enable |
| 28 | +``` |
| 29 | + |
| 30 | +**Notes** |
| 31 | + |
| 32 | +* Use `net10.0-windows` to enable Windows APIs. |
| 33 | +* Keep every directive at the top of the file. |
| 34 | +* If you add more packages, **pin exact versions**. |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## 3) Quick Start (single file, DI + required STA init, XAML-free UI) |
| 39 | + |
| 40 | +```csharp |
| 41 | +// wpf.cs |
| 42 | +// FBA directives appear above (see section 2) |
| 43 | +
|
| 44 | +using System; |
| 45 | +using System.Threading; |
| 46 | +using System.Windows; |
| 47 | +using System.Windows.Controls; |
| 48 | +using System.Windows.Data; |
| 49 | +using Microsoft.Extensions.DependencyInjection; |
| 50 | +using Microsoft.Extensions.Hosting; |
| 51 | +using CommunityToolkit.Mvvm.ComponentModel; |
| 52 | +using CommunityToolkit.Mvvm.Input; |
| 53 | +using LinqUI.WPF; |
| 54 | +// --- Top-level bootstrap must precede type declarations --- |
| 55 | +
|
| 56 | +// **CRITICAL**: Two-step STA initialization (empirically required) |
| 57 | +Thread.CurrentThread.SetApartmentState(ApartmentState.Unknown); |
| 58 | +Thread.CurrentThread.SetApartmentState(ApartmentState.STA); |
| 59 | + |
| 60 | +// Host for DI/config/logging |
| 61 | +using var host = Host.CreateApplicationBuilder(args) |
| 62 | + .ConfigureServices(s => |
| 63 | + { |
| 64 | + s.AddSingleton<App>(); |
| 65 | + s.AddSingleton<MainWindow>(); |
| 66 | + s.AddSingleton<CounterViewModel>(); |
| 67 | + }) |
| 68 | + .Build(); |
| 69 | + |
| 70 | +// Create WPF Application and wire global behavior |
| 71 | +var app = host.Services.GetRequiredService<App>(); |
| 72 | +app.ShutdownMode(ShutdownMode.OnMainWindowClose) |
| 73 | + .OnDispatcherUnhandledException((_, e) => |
| 74 | + { |
| 75 | + MessageBox.Show(e.Exception.ToString(), "Unhandled", MessageBoxButton.OK, MessageBoxImage.Error); |
| 76 | + e.Handled = true; |
| 77 | + }); |
| 78 | + |
| 79 | +// Compose main window & VM |
| 80 | +var main = host.Services.GetRequiredService<MainWindow>(); |
| 81 | +main.DataContext = host.Services.GetRequiredService<CounterViewModel>(); |
| 82 | + |
| 83 | +// Run (blocks until closed). Call Application.Current.Shutdown() for scripted/agent flows. |
| 84 | +app.Run(main); |
| 85 | + |
| 86 | +// ---- Types follow here ---- |
| 87 | +
|
| 88 | +public sealed class App : Application { } |
| 89 | + |
| 90 | +public sealed class MainWindow : Window |
| 91 | +{ |
| 92 | + public MainWindow() |
| 93 | + { |
| 94 | + this.Title()"WPF FBA (.NET 10) — MVVM (Toolkit)") |
| 95 | + .Size(480, 280) |
| 96 | + .WindowStartupLocation(WindowStartupLocation.CenterScreen) |
| 97 | + .Content( |
| 98 | + new StackPanel() |
| 99 | + .VCenter() |
| 100 | + .Margin(16) |
| 101 | + .Children( |
| 102 | + new TextBlock() |
| 103 | + .FontSize(16) |
| 104 | + .Margin(bottom: 12) |
| 105 | + .Bind(TextBlock.TextProperty, new Binding(nameof(CounterViewModel.Greeting))), |
| 106 | + new TextBlock() |
| 107 | + .FontSize(32) |
| 108 | + .Margin(bottom: 12) |
| 109 | + .Bind(TextBlock.TextProperty, new Binding(nameof(CounterViewModel.Count))), |
| 110 | + new StackPanel() |
| 111 | + .Orientation(Orientation.Horizontal) |
| 112 | + .Children( |
| 113 | + new Button() |
| 114 | + .Content("Increment") |
| 115 | + .MinWidth(120) |
| 116 | + .Margin(right: 8) |
| 117 | + .Bind(Button.CommandProperty, new Binding(nameof(CounterViewModel.IncrementCountCommand))), |
| 118 | + new Button() |
| 119 | + .Content("Decrement") |
| 120 | + .MinWidth(120) |
| 121 | + .Bind(Button.CommandProperty, new Binding(nameof(CounterViewModel.DecrementCountCommand))) |
| 122 | + ) |
| 123 | + ) |
| 124 | + ) |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +// ViewModel with Toolkit generators |
| 129 | +public sealed partial class CounterViewModel : ObservableObject |
| 130 | +{ |
| 131 | + [ObservableProperty] private int _count = 0; |
| 132 | + public string Greeting => $"Hello, today is {DateTime.Now:yyyy-MM-dd}"; |
| 133 | + |
| 134 | + [RelayCommand] private void Increment() => Count++; |
| 135 | + [RelayCommand] private void Decrement() => Count--; |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +**Run** |
| 140 | + |
| 141 | +```powershell |
| 142 | +dotnet run wpf.cs |
| 143 | +``` |
| 144 | + |
| 145 | +--- |
| 146 | + |
| 147 | +## 4) Application Initialization |
| 148 | + |
| 149 | +* **STA Apartment (CRITICAL):** |
| 150 | + Execute **both**: |
| 151 | + |
| 152 | + ```csharp |
| 153 | + Thread.CurrentThread.SetApartmentState(ApartmentState.Unknown); |
| 154 | + Thread.CurrentThread.SetApartmentState(ApartmentState.STA); |
| 155 | + ``` |
| 156 | + |
| 157 | + This two-step init prevents intermittent COM initialization issues and ensures stable WPF UI thread behavior. |
| 158 | +* **App lifecycle:** single `Application` instance; set `ShutdownMode` as needed. |
| 159 | +* **Unhandled exceptions:** handle `DispatcherUnhandledException` to fail gracefully. |
| 160 | +* **Termination for agents:** call `Application.Current.Shutdown()` when objectives are achieved. |
| 161 | + |
| 162 | +--- |
| 163 | + |
| 164 | +## 5) Declarative UI in Code |
| 165 | + |
| 166 | +* Use **object/collection initializers** to compose the visual tree. |
| 167 | +* Bind with `SetBinding` and `Binding`; prefer `nameof(ViewModel.Property)` to avoid string typos. |
| 168 | +* Keep layout simple (`Grid`, `StackPanel`, `DockPanel`); avoid absolute coordinates. |
| 169 | + |
| 170 | +--- |
| 171 | + |
| 172 | +## 6) MVVM with CommunityToolkit.Mvvm |
| 173 | + |
| 174 | +* Base view models on `ObservableObject`. |
| 175 | +* Use `[ObservableProperty]` for bindable state and `[RelayCommand]`/`AsyncRelayCommand` for actions. |
| 176 | +* Inject dependencies via DI; keep VMs UI-agnostic and testable. |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## 7) Data Binding & Validation |
| 181 | + |
| 182 | +* Use OneWay bindings by default; switch to TwoWay only where input is required. |
| 183 | +* Validation: `INotifyDataErrorInfo` or data annotations; present errors clearly. |
| 184 | +* Collections: `ObservableCollection<T>`; use `CollectionViewSource` for filter/sort. |
| 185 | + |
| 186 | +--- |
| 187 | + |
| 188 | +## 8) Threading & Async |
| 189 | + |
| 190 | +* UI access must occur on the **Dispatcher** (`Dispatcher.Invoke/BeginInvoke`). |
| 191 | +* Offload long work: `await Task.Run(...)` with `CancellationToken`. |
| 192 | +* Avoid blanket `ConfigureAwait(false)` in UI code (you typically need the captured context). |
| 193 | + |
| 194 | +--- |
| 195 | + |
| 196 | +## 9) Styling, Theming, Resources |
| 197 | + |
| 198 | +* Centralize styles/brushes in `Application.Resources` (build programmatically in FBA as needed). |
| 199 | +* Respect system/high-contrast themes; avoid hard-coded colors. |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +## 10) Performance |
| 204 | + |
| 205 | +* Reduce binding churn; compute heavy values lazily. |
| 206 | +* Virtualize lists (`VirtualizingStackPanel`) and avoid retaining event handlers unintentionally. |
| 207 | +* Monitor allocations/leaks in long-running sessions. |
| 208 | + |
| 209 | +--- |
| 210 | + |
| 211 | +## 11) Testing |
| 212 | + |
| 213 | +* **Unit:** ViewModels, commands, and validation (no WPF references). |
| 214 | +* **Integration/UI:** automate critical flows (e.g., WinAppDriver/Playwright for Windows). |
| 215 | +* **Perf/Leak checks:** open/close windows and watch for survivors/GC pressure. |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +## 12) Deployment |
| 220 | + |
| 221 | +* Package with MSIX/installer; document Windows/.NET requirements. |
| 222 | +* Store user settings under user scope; protect secrets (DPAPI/Credential Locker). |
| 223 | +* Provide safe upgrade paths and migrations. |
| 224 | + |
| 225 | +--- |
| 226 | + |
| 227 | +## 13) File-based Dev Standards |
| 228 | + |
| 229 | +* Default to **single-file FBA**; only convert on explicit request. |
| 230 | +* Keep directives (`#:sdk`, `#:property`, `#:package`) at the top. |
| 231 | +* If the file grows, add `Directory.Build.props` for shared MSBuild—but the FBA file remains authoritative. |
| 232 | + |
| 233 | +--- |
| 234 | + |
| 235 | +## 14) Project Conversion (on request) |
| 236 | + |
| 237 | +* Convert when asked: |
| 238 | + |
| 239 | + ```bash |
| 240 | + dotnet project convert wpf.cs -o WpfProject |
| 241 | + ``` |
| 242 | +* Preserve the original FBA; mirror properties and pinned packages. |
| 243 | + |
| 244 | +--- |
| 245 | + |
| 246 | +## 15) Agent Execution Compatibility |
| 247 | + |
| 248 | +* Don’t block with `Console.ReadLine()`/`ReadKey()`. |
| 249 | +* Terminate deterministically (`Application.Current.Shutdown()` or close MainWindow) so agents can capture outputs/exit codes. |
| 250 | + |
| 251 | +--- |
| 252 | + |
| 253 | +## 16) Review Checklist |
| 254 | + |
| 255 | +* [ ] `TargetFramework=net10.0-windows`, `UseWPF=True`, `PublishAot=False`, `Nullable=enable` |
| 256 | +* [ ] **Apartment state set: `Unknown` → `STA` (two-step)** |
| 257 | +* [ ] Top-level bootstrap before types; DI host configured |
| 258 | +* [ ] Bindings use `nameof`; TwoWay only where necessary; validation strategy chosen |
| 259 | +* [ ] Dispatcher marshaling respected; no blanket `ConfigureAwait(false)` in UI paths |
| 260 | +* [ ] Resources/styles centralized; DPI/contrast respected |
| 261 | +* [ ] Single-file FBA; exact package versions pinned for any dependencies |
| 262 | + |
| 263 | +--- |
| 264 | + |
| 265 | +## 17) MCP Integration (NuGet & Docs) |
| 266 | + |
| 267 | +* **Never guess versions. ** Resolve with your **`nuget`** MCP server and pin with `#:package [email protected]`. |
| 268 | +* Use **`microsoft_learn`** MCP to confirm current WPF/MVVM guidance, DPI, accessibility, and deployment patterns. |
0 commit comments