Skip to content

Commit b423a19

Browse files
johnml1135claude
andcommitted
Phase-1 base: Avalonia migration spine (UIMode defaults Legacy)
First of a four-PR stack landing Phase 1 of the FieldWorks WinForms-to-Avalonia migration. Every Avalonia surface is gated behind the UIMode setting, which DEFAULTS TO "Legacy" (Src/Common/FwUtils/Properties/Settings.Designer.cs), so default users see no behavioral change. Contents: - The Avalonia migration framework: region/composer (FullEntryRegionComposer), the typed view-definition IR compiled from XML layouts, owned dense controls, the seam contracts, and the region-editor plugin registry. - The base detail-editor surfaces active under UIMode=New: lexiconEdit, lexiconEditPopup, notebookEdit, posEdit. - The Avalonia browse table (LexicalBrowseView, LexicalBrowseHostControl, BulkEditBarView, ClerkBrowse*) and its RecordBrowseView product wiring, shipped DORMANT (fused to base wiring; activated by the table follow-up PR). - Migration skills/playbook (incl. the inert-surface activation recipe), the two-phase plan, and the openspec change specs. - Back-out of four cleanly-removable unwired Avalonia dialogs (SpecialCharacter, WritingSystemProperties, DeleteConfirmation, LexReferenceDetails); nine woven-in dialogs ride inert behind UIMode=New. The interlinear and rule-formula detail editors are carved out to their own stacked follow-up PRs; the inert tool lists are LexicalEditSurfaceRegistry.Phase1FollowUpSurfaceTools and LexicalEditSurfaceResolver.Phase1FollowUpBrowseTools. The per-screen migration docs (the JIRA-ticket basis) live on the separate never-merged phase1-docs branch. Phase 2 (avalonia-end-game: net multiplatform + shell conversion + WinForms removal) is planned only and gated on Phase 1 + tester burn-down. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 20eaf74 commit b423a19

657 files changed

Lines changed: 124913 additions & 2489 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
---
2+
name: fieldworks-avalonia-ui
3+
description: "Build, review, or fix Avalonia UI code in FieldWorks: XAML, MVVM, view models, owned controls, headless tests, preview host, accessibility identity, and product-vs-preview wiring. Use for any change under Src/Common/FwAvalonia/, Src/Common/FwAvaloniaPreviewHost/, or Src/**/*.Avalonia/, and for net48/net8 Avalonia test changes — even if the request only mentions a control, a binding, a style, or a flaky UI test. For whole-surface migration planning use fieldworks-winforms-to-avalonia-migration first."
4+
---
5+
6+
# FieldWorks Avalonia UI
7+
8+
## Use This For
9+
10+
- Avalonia XAML, view models, commands, lifetimes, dispatching, and
11+
resource/style changes.
12+
- New or changed projects under `Src/**/**/*.Avalonia/`,
13+
`Src/Common/FwAvalonia/`, and `Src/Common/FwAvaloniaPreviewHost/`.
14+
- Preview Host module registration, sample data providers, and UI
15+
diagnostics (see `.github/instructions/avalonia.instructions.md` for
16+
build/preview commands and project layout rules).
17+
- UI host wiring that selects between Avalonia and legacy UI — apply
18+
`fieldworks-ui-wiring-review` alongside this skill.
19+
20+
## Start From the Established Patterns
21+
22+
Do not design controls or seams from scratch. The migration hub skill
23+
(`fieldworks-winforms-to-avalonia-migration`) documents the decided
24+
architecture; its `references/architecture-patterns.md` covers owned
25+
controls, writing-system text fields, dialogs/flyouts, validation, and
26+
lifetime. Canonical code to imitate:
27+
28+
- Owned field controls: `Src/Common/FwAvalonia/Region/FwFieldControls.cs`,
29+
`FwOptionPicker.cs`, `RegionMenuFlyout.cs`, `HoverReveal.cs`
30+
- Region view + focus memory: `LexicalEditRegionView.cs`,
31+
`RegionFocusMemory.cs`
32+
- Seams (scheduler, lifetime, clipboard, edit sessions):
33+
`Src/Common/FwAvalonia/Seams/ISeams.cs`
34+
- Headless test setup: `Src/Common/FwAvalonia/FwAvaloniaTests/TestAppBuilder.cs`;
35+
examples in `RegionEditingTests.cs`, `VisualParityAndDensityTests.cs`
36+
- Density constants: `Src/Common/FwAvalonia/Poc/PocDensity.cs`
37+
- **Dialog kit (XAML + CommunityToolkit.Mvvm + compiled bindings):**
38+
`Src/Common/FwAvaloniaDialogs/``OptionsDialogView.axaml`/`.axaml.cs` +
39+
`OptionsDialogViewModel.cs`; headless tests in `FwAvaloniaDialogsTests/`.
40+
This is the verified template for hand-authored dialogs — see
41+
"Converting a WinForms dialog (MVVM kit)" below.
42+
43+
**Re-implementing a Phase-1 deferred screen (JIRA tickets).** Many screens were documented
44+
and backed out to land the Phase-1 PR; each has a ticket + a `Docs/migration/<screen>.md`
45+
(what it is, legacy PNGs, parity checklist, gotchas, the git sha of the backed-out stub).
46+
Start there: recover the stub from git history as a skeleton, copy the **canonical screen**
47+
for that primitive (the hub skill's "Phase-1 Landing Strategy" names them — e.g. ChooserDialog
48+
for tree/multi-select, OptionsDialog for tabs, InsertEntryDialog for owned-control forms), then
49+
follow the per-region Workflow. Re-wire the call site behind `UIMode=New` (Legacy keeps the
50+
WinForms path).
51+
52+
## Converting a WinForms dialog (MVVM kit)
53+
54+
Hand-authored dialogs/wizards use **XAML + CommunityToolkit.Mvvm + compiled
55+
bindings** — NOT the region/IR pattern (that is only for XML-view-definition
56+
surfaces). Decided 2026-06-15; rationale in
57+
`avalonia-migration-roadmap/complete-migration-program.md` §11.3. Full
58+
step-by-step + the working template:
59+
`references/dialog-conversion.md`. The shape, per dialog:
60+
61+
1. **View** `XyzDialogView.axaml` (+ `.axaml.cs`): a `UserControl` (not a
62+
`Window` — see modality below), `x:DataType` set to the view-model,
63+
compiled `{Binding}`s, and a stable `AutomationProperties.AutomationId`
64+
on every interactive control. Reuse owned controls (`FwMultiWsTextField`,
65+
`FwOptionPicker`) for writing-system fields and list pickers.
66+
2. **View-model** `XyzDialogViewModel.cs`: `ObservableObject` with
67+
`[ObservableProperty]` state and `[RelayCommand]` actions; expose the
68+
result (e.g. `Accepted`). Keep it LCModel-free for the view; bind real
69+
settings/domain through the app-settings/edit-session seams.
70+
3. **Tests** `XyzDialogTests.cs` (headless `[AvaloniaTest]`): assert the
71+
compiled bindings propagate both directions and the commands fire, AND
72+
capture a PNG at **each interaction stage** (empty, populated, error/disabled,
73+
post-action) for visual review — this is the per-dialog definition of done.
74+
75+
### Style system (density + borders, per surface)
76+
77+
The font/density tokens and the field-border rule are a GLOBAL system, calibrated to WinForms density
78+
(~12px font, ~22px control height) — not the roomy Fluent defaults — and applied per-control-tree (the only
79+
mechanism that renders in BOTH the runtime host and the headless tests). Full detail, the calibrated numbers,
80+
and the per-surface intent: **`references/style-system.md`**. The short version:
81+
82+
- **Dialog inputs are BOXED** (`TextBox`/`ComboBox`, and the `PART_*Host` borders wrapping owned editors get a
83+
visible 1px gray border via `Border.fwFieldHost`); **detail/region values are FLAT** with subtle separators
84+
(the DataTree look — do not box them); **browse keeps its grid lines** — just denser font everywhere.
85+
- One source of truth per family: dialog density/borders in `DialogTheme.axaml` (applied by
86+
`DialogThemeBootstrap.Apply`); region/browse font in `FwSurfaceStyles` (applied in those view ctors).
87+
- **Borders/colors that must render headlessly use a CONCRETE value, never a Fluent `DynamicResource`** — the
88+
field-host border is `#FF7A7A7A`, because `ControlStrokeColorDefault` resolved to nothing headlessly.
89+
90+
### Dialog spacing
91+
92+
All dialog spacing/borders come from the shared tokens in
93+
`Src/Common/FwAvaloniaDialogs/DialogTheme.axaml` (applied to each dialog body by
94+
`DialogThemeBootstrap.Apply(this)`, called from every dialog view ctor). The current calibrated tokens:
95+
`DialogWindowPadding` 10, `DialogControlGap` 6, `DialogLabelFieldGap` 4, `DialogMinControlHeight` 22,
96+
`DialogFieldPadding` 3, `DialogFontSize` 12. Hard rules:
97+
98+
- **Every dialog root carries `DialogWindowPadding`** — put `Classes="fwDialogRoot"` on the root
99+
`UserControl` and drop any root `Margin` literal.
100+
- **No text-bearing or `PART_*Host` control with 0 padding** — host borders carry `Classes="fwFieldHost"`
101+
(a real border + `DialogFieldPadding`); never leave a host border without a `BorderThickness`.
102+
- **OK/Cancel use the standard button strip gap** (`DialogButtonStripGap` + `DialogControlGapAbove`).
103+
- **Never hardcode a margin/spacing literal — use a token** (`DialogControlGap`, `DialogLabelFieldGap`,
104+
…); add a new token to `DialogTheme.axaml` rather than inlining a number.
105+
- The headless `DialogLayoutAssert.AssertNoCrowding(view)` tripwire gates this in every dialog's
106+
realized-view test. Full detail: `references/dialog-conversion.md` §2a-bis.
107+
- **Capture a PNG at EACH stage and look at it (hard rule, part of the per-dialog definition of done).**
108+
Every dialog test captures `DialogSnapshot.Capture(view, "<Surface>-<NN>-<stage>")` (→ gitignored
109+
per-surface folder `Output/Snapshots/<Surface>/<NN>-<stage>.png`) at every meaningful UI state it drives — empty/initial, populated,
110+
validation-error / OK-disabled, post-action — *before* any `AssertNoCrowding`. After running the dialog
111+
tests, the agent MUST **Read each per-stage PNG** and explicitly answer the **six subjective-quality
112+
questions** per image — (1) alignment of words/columns/icons/labels, (2) appropriate spacing between
113+
words/fields/graphics, (3) borders/containment, (4) clipping/overflow, (5) overlap, (6) legibility &
114+
consistency — writing the answers down, not just glancing. An off-looking PNG is a real finding even when
115+
`AssertNoCrowding` passes. The loop is: capture → run → Read each PNG → answer the six questions → fix
116+
view/token → re-capture. Applies to region/browse surfaces too. Full checklist + workflow:
117+
`references/visual-snapshot-testing.md`.
118+
119+
Rules specific to dialogs:
120+
121+
- **It lives in `Src/Common/FwAvaloniaDialogs/`** (the dedicated XAML project),
122+
never in the pure-C# `FwAvalonia` foundation. Add new dialog projects to
123+
`FieldWorks.sln` (restore + VS) but build them in `build.ps1`'s Avalonia
124+
loop; keep the XAML compile off the main `FieldWorks.proj` traversal (the
125+
exclude pattern there). Exclude any nested test folder from the library's
126+
compile glob (`<Compile Remove="XxxTests/**/*.cs"/>`).
127+
- **Modality during coexistence:** no Avalonia `Window.ShowDialog`. Show the
128+
dialog `UserControl` via **`AvaloniaDialogHost.ShowModal(owner, view, vm, title)`**
129+
(in `Src/Common/FwAvalonia/`), which hosts it in a WinForms-owned modal `Form`
130+
(per the hub's `dialog-ownership.md`). The view-model implements
131+
**`IDialogViewModel`** and raises `CloseRequested(bool)` from OK/Cancel — no
132+
windowing in the VM. A dialog is "view + VM + `ShowModal`."
133+
- **Scope:** simple/confirmation/settings dialogs are good junior+AI work;
134+
Views-engine-coupled dialogs (Find/Replace, Styles host `IVwRootSite`)
135+
belong with the document engine (Stage 9), NOT this kit.
136+
137+
## Required Checks
138+
139+
- Use current Avalonia docs for uncertain APIs; do not guess dispatcher,
140+
headless, automation, or binding behavior.
141+
- Keep field labels on the StringTable lane. Product-facing Avalonia chrome
142+
should join the existing LocalizationManager/L10NSharp XLIFF catalog,
143+
preferably by reusing existing `Palaso`/`Chorus` ids when they actually
144+
match; otherwise add unique Avalonia-prefixed ids there. The current
145+
English-default source is the accessor code, not a parallel Avalonia
146+
`.resx` runtime lane. Prototype hardcoded strings must be called out as
147+
gaps.
148+
- Stamp stable, nonlocalized `AutomationProperties.AutomationId` (derived
149+
from IR `StableId` where applicable) and localized
150+
`AutomationProperties.Name` on user-facing controls.
151+
- UI logic stays in bindings/view models where practical; avoid
152+
logic-heavy code-behind.
153+
- For any Avalonia "select from a list" surface, prefer the shared
154+
`FwOptionPicker` pattern in `Src/Common/FwAvalonia/Region/FwOptionPicker.cs`
155+
(AutoCompleteBox-based, keyboard-safe, search-capable, compact density)
156+
over ad hoc `ListBox` popups or one-off editable selectors. Reach for a raw
157+
`ComboBox` only when the UX explicitly needs an always-visible inline combo
158+
rather than the shared flyout selector.
159+
- Do not fix Avalonia keyboard, focus, filtering, selection, popup, or
160+
rendering bugs by patching `System.Windows.Forms` hosts, WinForms
161+
interop message handling, or other legacy host-only routes unless the
162+
task explicitly targets interop behavior. Default to fixing the issue
163+
inside the Avalonia control tree or Avalonia-owned seams.
164+
- Marshal to the UI thread through `IUiScheduler` (or Avalonia dispatcher
165+
in non-region code); no hidden `Task.Run`, no sync-over-async.
166+
- Keep preview data lightweight unless the change explicitly opts into
167+
LCModel/project data; product-facing paths use real edit-session/domain
168+
contracts — detached DTO-only models remain preview-only.
169+
- Headless tests: simulate input on `Window`, flush with
170+
`Dispatcher.UIThread.RunJobs()`, and capture visual regression frames
171+
with Skia (`UseHeadlessDrawing=false` + `CaptureRenderedFrame()`).
172+
- If Avalonia chrome uses LocalizationManager, bootstrap it once per
173+
preview-host or headless-test process before localized strings are
174+
requested; do not pay that cost per test.
175+
- Evidence runs through `./build.ps1` and `./test.ps1` via the normal repo
176+
graph, not branch-only lanes.
177+
178+
## Review Red Flags
179+
180+
- A Common project directly references a feature module without an
181+
explicit architecture decision.
182+
- Preview-only code launched from product UI without a feature gate.
183+
- Tests manually call `OnPropertyChanged(...)`, `ShowRecord()`, or similar
184+
instead of proving the real broadcast/wiring path.
185+
- The active Avalonia path drives hidden legacy rendering/menu
186+
infrastructure (see the hub skill's hard rules).
187+
- Sleep-based or timing-sensitive UI tests.
188+
- Claims of accessibility, localization, IME, or keyboard parity without
189+
executable evidence (see the hub skill's
190+
`references/parity-evidence.md` §"Evidence language").
191+
192+
## Handoff
193+
194+
Report Avalonia docs consulted, tests run, remaining prototype gaps,
195+
whether the change is product-facing or preview-only, and how the live
196+
wiring path was validated for each affected host. For parity work, say
197+
whether visual evidence is control-level headless capture or live desktop
198+
capture, and which automation identities were assigned.
199+
200+
## Keep This Skill Current
201+
202+
When a control pattern, headless-test technique, or Avalonia API gotcha
203+
proves out (or a pointer above goes stale), update this skill in the same
204+
PR — and route durable architecture lessons through the protocol in
205+
`fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`.

0 commit comments

Comments
 (0)