csharp-winui
Skill17 starsChanged 11 days agoMITView on GitHub
C# 14 and WinUI 3 code rules for Windows apps, including threading, x:Bind, observable models, SQLite, Win32 interop, and fixes for common build and launch errors. Use when writing, reviewing, or debugging C# or XAML in a WinUI app, or on XamlParseException, 0xC000027B crashes, binding, or threading errors.
In this repository23 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- agent-cliSkill
- improve-architectureSkill
- os-integrationsSkill
- performanceSkill
- app-iconSkill
- design-from-appSkill
- design-kitSkill
- design-systemSkill
- layout-gridSkill
- ux-designSkill
- windows-designSkill
- cloneSkill
- researchSkill
- user-storiesSkill
- build-and-verifySkill
- distributeSkill
- improve-loopSkill
- test-windows-appSkill
- app-briefSkill
- bawaSkill
- new-windows-appSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 0 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 28
Keep the DispatcherQueue of the window (window.DispatcherQueue) for code that runs outside a page.
- Very specific: line 49
After changing the object a page binds through (Model.Selection), call Bindings.Update() if the binding path starts above the changed …
- Very specific: line 67
Every row has a Guid id. Never use the row number or list index as an identity.
- Very specific: line 79
Every command is a button or menu item with a KeyboardAccelerator.
- Very specific: line 125
Keep unsafe to the block that takes the address: a class marked unsafe cannot await anywhere (CS4004).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
C# and WinUI
The code is right when it builds with zero warnings, launches without a
CRASH line, and the app does what the story says. For how it should
look, call the Skill tool with windows-design. For copyable XAML, read
../../design/windows-design/patterns.md.
Threads
WinUI has one UI thread per window. Every control and every object bound to a control lives on it.
| Work | Put it in |
Pages, view models, ObservableCollections bound to lists | The UI thread (the default in event handlers and OnLaunched) |
| CPU-heavy pure work (parsing, image work, diffing) | await Task.Run(() => ...), then set results back on the UI thread |
| File and database work | async methods; small SQLite calls on the UI thread are fine, anything over a few ms goes to Task.Run |
| A Win32 callback (hotkey, hook, timer from another thread) | Hop back with DispatcherQueue.TryEnqueue(() => ...) |
awaitin an event handler returns to the UI thread on its own. InsideTask.Runyou are on a pool thread: never touch a control there.async voidonly for event handlers. Everything else returnsTask.- Keep the
DispatcherQueueof the window (window.DispatcherQueue) for code that runs outside a page. - A crash on a background thread kills the app with no
CRASHline fromUnhandledException. Catch and log in everyTask.Runbody you start and forget, and run every background path once after changing it.
State
- Models are
partial class X : ObservableObjectwith[ObservableProperty] public partial string Title { get; set; }(CommunityToolkit.Mvvm 8.4 and C# 14). Initialize partial properties in the constructor: they cannot have initializers. [NotifyPropertyChangedFor(nameof(DisplayTitle))]for computed properties.- Lists bind to an
ObservableCollection<T>. Rebuild it in place (Clear,Add), never assign a new collection to a bound property. x:Bindis compiled and checked, so prefer it over{Binding}. Its default mode is OneTime: writeMode=OneWayfor anything that changes andMode=TwoWayfor inputs, andUpdateSourceTrigger=PropertyChangedon aTextBoxthat saves as you type.- After changing the object a page binds through (
Model.Selection), callBindings.Update()if the binding path starts above the changed object. - JSON goes through source generation: add each saved type to
AppJson([JsonSerializable(typeof(T))]) and passAppJson.Default.T. Reflection JSON breaks under NativeAOT and trimming (IL2026,IL3050). - Small preferences go in
Prefs(JSON next to the database). Unpackaged apps have noApplicationData.Current.LocalSettings: it throws. Secrets go in the Windows Credential Locker (PasswordVault) or DPAPI (ProtectedData), never in the JSON. - A property set to the value it already has fires no change. Fill lists
in the constructor too, not only in
OnXChanged.
SQLite
- One
SqliteConnectionper app, opened once, WAL mode, in<App>.Core. - Schema changes are new steps in
ItemStore.Migrations, keyed byPRAGMA user_version. Never edit a step that shipped. A new column has aDEFAULT, so old files keep loading. - Every row has a
Guidid. Never use the row number or list index as an identity. APP_STOREoverrides the path, and the app refuses test modes unless it points into%TEMP%. Tests never touch%LOCALAPPDATA%\<App>\<App>.db.- Undo: keep the deleted model in memory and save it back. SQLite has no undo of its own across commits.
- Several processes on one file (a CLI, a second instance): send commands
to the running app instead (
agent-cli), so the file keeps one writer.
Commands and keys
- Every command is a button or menu item with a
KeyboardAccelerator. The accelerator works wherever focus is in the window. - Keys that also mean something while typing (Delete, Backspace, arrows,
Enter, plain letters) are scoped:
ScopeOwner="{x:Bind List}", and setargs.Handled = trueinInvoked. - Flyout and context menu items are not in the visual tree: they get no
DataContext. Pass the row withTag="{x:Bind}".
Names that collide
Window, Page, Frame, Layout, Path, Rectangle, Grid, Color,
Colors, Thickness, Point, Size, and Application all exist in WinUI
namespaces. Do not name your own types after them. Path collides with
System.IO.Path as soon as a file imports Microsoft.UI.Xaml.Shapes:
write System.IO.Path. Task collides with System.Threading.Tasks.Task:
name a to-do Todo or TaskItem.
Namespaces collide too, and worse. C# looks for a name in the
enclosing namespaces before the using lines. A namespace
MyApp.Features.Windows makes Windows.Foundation mean
MyApp.Features.Windows.Foundation in every sibling namespace
(CS0234); MyApp.Features.Clipboard hides the WinRT Clipboard class;
Colors and Launcher do the same. Each feature compiles alone, and the
break shows only when two are merged. Never name a namespace segment
Windows, Microsoft, System, or after a type you call
(Clipboard, Colors, Launcher): use Snapping, ClipboardHistory,
Palette, AppLauncher.
Win32 interop
WinUI covers windows, pages, and controls. Win32 is still the answer for:
- The window handle:
WinRT.Interop.WindowNative.GetWindowHandle(window). - Pickers and dialogs from WinRT (
FileOpenPicker,FolderPicker):WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd), or they throw. - DPI:
GetDpiForWindow.AppWindowsizes are physical pixels; XAML is in epx. The template'sWindowBehaviorscales between them. - Global hotkeys, the tray icon, hooks, other apps' windows: call the Skill
tool with
os-integrations.
Declare calls with [LibraryImport] in one Win32.cs, with
AllowUnsafeBlocks on. Keep every P/Invoke in that file. Callbacks
(window procedures, hooks, EnumDisplayMonitors) are
[UnmanagedCallersOnly] static methods passed as &Method; catch every
exception inside them, because one that escapes ends the process.
Keep unsafe to the block that takes the address: a class marked
unsafe cannot await anywhere (CS4004).
UI built in code
Building controls in C# is fine, with three traps:
- Theme brushes.
Application.Current.Resources["TextFillColorSecondaryBrush"]returns the brush for the app's theme, not the theme of the element it lands in. A panel or page set to dark (RequestedTheme, Settings' theme choice, snapshot mode) then shows light text on dark or dark on dark. Give code-built elements aStylefrom XAML whose setters use{ThemeResource ...}, or build the part as a XAMLUserControl. - Measuring before the window shows.
Measureon code-built content before it is in a shown window gets heights wrong (templates are not applied yet). Size a panel to its content after the first layout: inSizeChanged, or oneDispatcherQueueturn after showing it. - Property order in initializers.
new TextBox { Text = s, AcceptsReturn = true }keeps only the first line ofs:Textis set whileAcceptsReturnis still false. SetAcceptsReturn(andTextWrapping) first.
Events fire when code sets a value, not only when a person does:
ToggleSwitch.Toggled runs when you set IsOn to show the current state,
and TextBox.TextChanged runs later, after Text is set. Guard handlers
with a _loading flag while a page fills itself, and in a self-test call
the search or filter function directly instead of waiting for
TextChanged.
Glyphs in source
Segoe Fluent Icons glyphs are private-use characters that show as nothing
in an editor or a diff. Write them as code points:
char.ConvertFromUtf32(0xE734) in C#,  in XAML. Never paste the
character itself.
Build and launch errors
| Error | Fix |
XamlParseException: Failed to assign to property 'ColumnDefinition.Width' ... 'Double' cannot be assigned to 'GridLength' | The resource is the wrong type: a GridLength resource for widths of rows and columns, x:Double for Width and Spacing |
0xC000027B exit and a CRASH ... XamlParseException | Read the property and type in the message; the line number is often 0 |
0xC000027B and no CRASH line at all | An exception in the App constructor, before the handler exists. Keep the constructor to InitializeComponent(); do setup in OnLaunched |
COMException 0x8000FFFF from Application.Current.Resources | Called in the App constructor. Move it to OnLaunched |
CS0104 'Path' is an ambiguous reference | System.IO.Path |
CS0104 'Layout' is an ambiguous reference | Your type shares a WinUI name: rename it (SnapLayout) |
CS4004 Cannot await in an unsafe context | Remove unsafe from the class; wrap only the pointer line in unsafe { } |
CS0234 'Foundation' does not exist in the namespace 'MyApp.Features.Windows' | A namespace segment shadows a framework name (see "Names that collide"): rename the namespace |
A crash at load with no build error after Padding="{StaticResource SpaceSm}" | Spacing tokens are x:Double; Padding, Margin, and BorderThickness need a Thickness. Use a Thickness token (CardPadding) |
0xC000027B with no CRASH line, from a DispatcherQueue.TryEnqueue callback | An exception in an enqueued callback ends the process before UnhandledException. Wrap the body in try and log with AgentLog.Line |
App dies at once after dotnet publish | Publish without EnableMsixTooling=true drops <App>.pri. Use tools/publish.ps1 |
A multi-line TextBox shows only the first line | Text was set before AcceptsReturn. Set AcceptsReturn first |
| Dark text on dark, or light cards, in one panel only | A brush looked up in code; see "UI built in code" |
ContentDialog.ShowAsync throws | Set XamlRoot = this.XamlRoot first |
The property 'X' was not found in type 'Y' in x:Bind | Add x:DataType to the DataTemplate, and make the property public |
| A binding that never updates | x:Bind is OneTime by default: add Mode=OneWay |
ApplicationData.Current throws InvalidOperationException | The app is unpackaged. Use Prefs or a file in AppPaths.Folder |
Warning CS8618 non-nullable field | Initialize it in the constructor, or make it nullable. Fix every warning |
Fix the first error first. Later errors often come from it.
Done when the app builds with zero warnings, the self-test passes, and the story's acceptance checks pass.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

