diff --git a/content/wpf-linqui-fba/.cursor/mcp.json b/content/wpf-linqui-fba/.cursor/mcp.json new file mode 100644 index 0000000..ede919a --- /dev/null +++ b/content/wpf-linqui-fba/.cursor/mcp.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "nuget": { + "command": "dnx", + "args": [ + "NuGet.Mcp.Server", + "--prerelease", + "--yes" + ] + }, + "microsoft_learn": { + "url": "https://learn.microsoft.com/api/mcp" + } + } +} \ No newline at end of file diff --git a/content/wpf-linqui-fba/.template.config/template.json b/content/wpf-linqui-fba/.template.config/template.json new file mode 100644 index 0000000..03ed7cb --- /dev/null +++ b/content/wpf-linqui-fba/.template.config/template.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "lukwire", + "classifications": [ "Common", "WPF", "FBA" ], + "identity": "wpf-linqui-fba", + "name": "WPF Application (File-based App)", + "shortName": "wpf-linqui-fba", + "sourceName": "Program", + "preferNameDirectory": true, + "tags": { + "language": "C#", + "type": "project" + }, + "symbols": { + "Framework": { + "type": "parameter", + "description": "The target framework for the project (e.g., net10.0-windows).", + "datatype": "string", + "defaultValue": "net10.0-windows", + "replaces": "NET_TFM_WINDOWS" + } + } +} \ No newline at end of file diff --git a/content/wpf-linqui-fba/.vscode/mcp.json b/content/wpf-linqui-fba/.vscode/mcp.json new file mode 100644 index 0000000..31ad477 --- /dev/null +++ b/content/wpf-linqui-fba/.vscode/mcp.json @@ -0,0 +1,17 @@ +{ + "servers": { + "nuget": { + "type": "stdio", + "command": "dnx", + "args": [ + "NuGet.Mcp.Server", + "--prerelease", + "--yes" + ] + }, + "microsoft_learn": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + } + } +} \ No newline at end of file diff --git a/content/wpf-linqui-fba/AGENTS.md b/content/wpf-linqui-fba/AGENTS.md new file mode 100644 index 0000000..b5c8524 --- /dev/null +++ b/content/wpf-linqui-fba/AGENTS.md @@ -0,0 +1,268 @@ +# GitHub Copilot Instructions — WPF File-based App (FBA) Template + +> Pragmatic, production-ready guidance for building **.NET 10** WPF apps as a **single C# file** with **MVVM (CommunityToolkit.Mvvm)**. +> **Apartment model:** Per field validation, WPF startup **must** perform the two-step STA initialization: **`Unknown → STA`**. + +--- + +## 1) Project Overview + +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. + +--- + +## 2) FBA Directives (Windows-only GUI) + +```csharp +// Shebang optional for Windows-only apps + +#:sdk Microsoft.NET.Sdk +#:package CommunityToolkit.Mvvm@8.4.0 +#:package LinqUI.WPF@1.0.6 +#:property OutputType=WinExe +#:property TargetFramework=net10.0-windows +#:property UseWPF=True +#:property UseWindowsForms=False +#:property PublishAot=False // WPF is not AOT-compatible +#:property Nullable=enable +``` + +**Notes** + +* Use `net10.0-windows` to enable Windows APIs. +* Keep every directive at the top of the file. +* If you add more packages, **pin exact versions**. + +--- + +## 3) Quick Start (single file, DI + required STA init, XAML-free UI) + +```csharp +// wpf.cs +// FBA directives appear above (see section 2) + +using System; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LinqUI.WPF; +// --- Top-level bootstrap must precede type declarations --- + +// **CRITICAL**: Two-step STA initialization (empirically required) +Thread.CurrentThread.SetApartmentState(ApartmentState.Unknown); +Thread.CurrentThread.SetApartmentState(ApartmentState.STA); + +// Host for DI/config/logging +using var host = Host.CreateApplicationBuilder(args) + .ConfigureServices(s => + { + s.AddSingleton(); + s.AddSingleton(); + s.AddSingleton(); + }) + .Build(); + +// Create WPF Application and wire global behavior +var app = host.Services.GetRequiredService(); +app.ShutdownMode(ShutdownMode.OnMainWindowClose) + .OnDispatcherUnhandledException((_, e) => + { + MessageBox.Show(e.Exception.ToString(), "Unhandled", MessageBoxButton.OK, MessageBoxImage.Error); + e.Handled = true; + }); + +// Compose main window & VM +var main = host.Services.GetRequiredService(); +main.DataContext = host.Services.GetRequiredService(); + +// Run (blocks until closed). Call Application.Current.Shutdown() for scripted/agent flows. +app.Run(main); + +// ---- Types follow here ---- + +public sealed class App : Application { } + +public sealed class MainWindow : Window +{ + public MainWindow() + { + this.Title()"WPF FBA (.NET 10) — MVVM (Toolkit)") + .Size(480, 280) + .WindowStartupLocation(WindowStartupLocation.CenterScreen) + .Content( + new StackPanel() + .VCenter() + .Margin(16) + .Children( + new TextBlock() + .FontSize(16) + .Margin(bottom: 12) + .Bind(TextBlock.TextProperty, new Binding(nameof(CounterViewModel.Greeting))), + new TextBlock() + .FontSize(32) + .Margin(bottom: 12) + .Bind(TextBlock.TextProperty, new Binding(nameof(CounterViewModel.Count))), + new StackPanel() + .Orientation(Orientation.Horizontal) + .Children( + new Button() + .Content("Increment") + .MinWidth(120) + .Margin(right: 8) + .Bind(Button.CommandProperty, new Binding(nameof(CounterViewModel.IncrementCountCommand))), + new Button() + .Content("Decrement") + .MinWidth(120) + .Bind(Button.CommandProperty, new Binding(nameof(CounterViewModel.DecrementCountCommand))) + ) + ) + ) + } +} + +// ViewModel with Toolkit generators +public sealed partial class CounterViewModel : ObservableObject +{ + [ObservableProperty] private int _count = 0; + public string Greeting => $"Hello, today is {DateTime.Now:yyyy-MM-dd}"; + + [RelayCommand] private void Increment() => Count++; + [RelayCommand] private void Decrement() => Count--; +} +``` + +**Run** + +```powershell +dotnet run wpf.cs +``` + +--- + +## 4) Application Initialization + +* **STA Apartment (CRITICAL):** + Execute **both**: + + ```csharp + Thread.CurrentThread.SetApartmentState(ApartmentState.Unknown); + Thread.CurrentThread.SetApartmentState(ApartmentState.STA); + ``` + + This two-step init prevents intermittent COM initialization issues and ensures stable WPF UI thread behavior. +* **App lifecycle:** single `Application` instance; set `ShutdownMode` as needed. +* **Unhandled exceptions:** handle `DispatcherUnhandledException` to fail gracefully. +* **Termination for agents:** call `Application.Current.Shutdown()` when objectives are achieved. + +--- + +## 5) Declarative UI in Code + +* Use **object/collection initializers** to compose the visual tree. +* Bind with `SetBinding` and `Binding`; prefer `nameof(ViewModel.Property)` to avoid string typos. +* Keep layout simple (`Grid`, `StackPanel`, `DockPanel`); avoid absolute coordinates. + +--- + +## 6) MVVM with CommunityToolkit.Mvvm + +* Base view models on `ObservableObject`. +* Use `[ObservableProperty]` for bindable state and `[RelayCommand]`/`AsyncRelayCommand` for actions. +* Inject dependencies via DI; keep VMs UI-agnostic and testable. + +--- + +## 7) Data Binding & Validation + +* Use OneWay bindings by default; switch to TwoWay only where input is required. +* Validation: `INotifyDataErrorInfo` or data annotations; present errors clearly. +* Collections: `ObservableCollection`; use `CollectionViewSource` for filter/sort. + +--- + +## 8) Threading & Async + +* UI access must occur on the **Dispatcher** (`Dispatcher.Invoke/BeginInvoke`). +* Offload long work: `await Task.Run(...)` with `CancellationToken`. +* Avoid blanket `ConfigureAwait(false)` in UI code (you typically need the captured context). + +--- + +## 9) Styling, Theming, Resources + +* Centralize styles/brushes in `Application.Resources` (build programmatically in FBA as needed). +* Respect system/high-contrast themes; avoid hard-coded colors. + +--- + +## 10) Performance + +* Reduce binding churn; compute heavy values lazily. +* Virtualize lists (`VirtualizingStackPanel`) and avoid retaining event handlers unintentionally. +* Monitor allocations/leaks in long-running sessions. + +--- + +## 11) Testing + +* **Unit:** ViewModels, commands, and validation (no WPF references). +* **Integration/UI:** automate critical flows (e.g., WinAppDriver/Playwright for Windows). +* **Perf/Leak checks:** open/close windows and watch for survivors/GC pressure. + +--- + +## 12) Deployment + +* Package with MSIX/installer; document Windows/.NET requirements. +* Store user settings under user scope; protect secrets (DPAPI/Credential Locker). +* Provide safe upgrade paths and migrations. + +--- + +## 13) File-based Dev Standards + +* Default to **single-file FBA**; only convert on explicit request. +* Keep directives (`#:sdk`, `#:property`, `#:package`) at the top. +* If the file grows, add `Directory.Build.props` for shared MSBuild—but the FBA file remains authoritative. + +--- + +## 14) Project Conversion (on request) + +* Convert when asked: + + ```bash + dotnet project convert wpf.cs -o WpfProject + ``` +* Preserve the original FBA; mirror properties and pinned packages. + +--- + +## 15) Agent Execution Compatibility + +* Don’t block with `Console.ReadLine()`/`ReadKey()`. +* Terminate deterministically (`Application.Current.Shutdown()` or close MainWindow) so agents can capture outputs/exit codes. + +--- + +## 16) Review Checklist + +* [ ] `TargetFramework=net10.0-windows`, `UseWPF=True`, `PublishAot=False`, `Nullable=enable` +* [ ] **Apartment state set: `Unknown` → `STA` (two-step)** +* [ ] Top-level bootstrap before types; DI host configured +* [ ] Bindings use `nameof`; TwoWay only where necessary; validation strategy chosen +* [ ] Dispatcher marshaling respected; no blanket `ConfigureAwait(false)` in UI paths +* [ ] Resources/styles centralized; DPI/contrast respected +* [ ] Single-file FBA; exact package versions pinned for any dependencies + +--- + +## 17) MCP Integration (NuGet & Docs) + +* **Never guess versions.** Resolve with your **`nuget`** MCP server and pin with `#:package PackageId@X.Y.Z`. +* Use **`microsoft_learn`** MCP to confirm current WPF/MVVM guidance, DPI, accessibility, and deployment patterns. diff --git a/content/wpf-linqui-fba/Program.cs b/content/wpf-linqui-fba/Program.cs new file mode 100644 index 0000000..719ee29 --- /dev/null +++ b/content/wpf-linqui-fba/Program.cs @@ -0,0 +1,69 @@ +#:sdk Microsoft.NET.Sdk + +#:package CommunityToolkit.Mvvm@8.4.0 +#:package LinqUI.WPF@1.0.6 + +#:property OutputType=WinExe +#:property TargetFramework=NET_TFM_WINDOWS +#:property UseWPF=True +#:property UseWindowsForms=False + +// WPF cannot use AOT compilation. +#:property PublishAot=False + +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LinqUI.WPF; + +// https://github.com/dotnet/winforms/issues/5071#issuecomment-908789632 +Thread.CurrentThread.SetApartmentState(ApartmentState.Unknown); +Thread.CurrentThread.SetApartmentState(ApartmentState.STA); + +new Application() + .ShutdownMode(ShutdownMode.OnMainWindowClose) + .OnDispatcherUnhandledException((_, e) => + { + MessageBox.Show(e.Exception.ToString(), "Unhandled", MessageBoxButton.OK, MessageBoxImage.Error); + e.Handled = true; + }) + .Run(new Window() + .WindowStartupLocation(WindowStartupLocation.CenterScreen) + .Title($"Hello, World! - {Thread.CurrentThread.GetApartmentState()}") + .Size(640, 240) + .DataContext(new CounterViewModel()) + .Content( + new StackPanel() + .Children( + new TextBlock() + .FontSize(24) + .Margin(8) + .HCenter() + .Bind(TextBlock.TextProperty, new Binding(nameof(CounterViewModel.Count))), + new Button() + .Content("Increment (+1)") + .Margin(8) + .Bind(Button.CommandProperty, new Binding(nameof(CounterViewModel.IncrementCountCommand))), + new Button() + .Content("Decrement (-1)") + .Margin(8) + .Bind(Button.CommandProperty, new Binding(nameof(CounterViewModel.DecrementCountCommand))) + ) + ) + ); + +public sealed partial class CounterViewModel : ObservableObject +{ + [ObservableProperty] + private int _count = 0; + + [RelayCommand] + private void IncrementCount() + => Count++; + + [RelayCommand] + private void DecrementCount() + => Count--; +}