diff --git a/.claude/skills/convert-dialog/SKILL.md b/.claude/skills/convert-dialog/SKILL.md new file mode 100644 index 0000000000..4a3c2ead90 --- /dev/null +++ b/.claude/skills/convert-dialog/SKILL.md @@ -0,0 +1,318 @@ +--- +name: convert-dialog +description: Drive the conversion of one WinForms dialog to Avalonia through analysis, developer alignment, integration-test planning, exemplar mapping, design, and scaffold/implement. Use when the user invokes /convert-dialog with a dialog class name, or asks to start converting a specific WinForms dialog. +--- + +# Convert Dialog + +Converts one WinForms dialog to Avalonia with the developer in the loop at +every gate. The developer decides and confirms; this skill analyzes, drafts, +builds, and never advances past a gate without their explicit go-ahead. + +## Two rules that override everything else + +**1. The developer's FieldWorks is untouchable.** They are exploring the +dialog while you work. NEVER close, kill, restart, or drive their running +FieldWorks, and never change the UI-mode registry setting under them. The +live-app capture route (fieldworks-winapp) owns the app lifecycle +(relaunch-per-tool, `CloseMainWindow`) -- it is FORBIDDEN while their +instance is open. Do not run `build.ps1`/`test.ps1` without asking either: a +build fails on binaries their running app holds locked, and killing their +process to unblock a build is never acceptable. If a build or test fails on +locked files, STOP and ask. + +**2. Every gate is a real stop.** A gate is an interactive question to the +developer (use the question tool, which returns only when they answer) -- +never a sentence in a message you then continue past. At a gate: end your +turn on the question and take no further action. Do NOT summarize or restate +a report's contents in chat -- the document is the artifact, and a chat +summary invites skipping the review the gate exists for. Point at the file +and stop. + +## How to talk about where you are + +The stages below have names. USE THEM with the developer, in plain language +about the work: "I've finished understanding the dialog", "we've agreed on +how it works, so next is deciding what to prove", "the replacement plan is +ready for your review". NEVER say "Phase 2", "phase 4 gate passed", or any +numbered-stage shorthand -- it means nothing to someone who has not read +this file. The stages, in order: **Understanding the dialog** -> **Agreeing +on how it works** -> **Deciding what to prove** -> **Planning the +replacement** -> **Building it** -> **Proving it works**. + +Input: the WinForms dialog class name (e.g. `MergeEntryDlg`). +Scope: PORT with low-cost improvements. If the developer's verdict is +"redesign, not port", stop after Understanding the dialog -- redesign is out of +scope; the dialog waits. + +## Working documents + +All working artifacts live in `Docs/migration/working//` +(gitignored; create on first use): + +- `-analysis.md` -- from Understanding the dialog +- `-integration-test-plan.md` -- from Deciding what to prove +- `-design.md` -- from Planning the replacement +- `-state.md` -- one-page state: current phase, plus any OPEN + GATE and what it waits on (e.g. `waiting-on: MSAGroupBox (control, + Docs/migration/working/MSAGroupBox/)`); kept current at every gate and + phase transition + +**Resume rule:** on invocation, read `-state.md` first +(falling back to artifact-presence inference when it is absent: analysis +present -> resume at Agreeing on how it works; test plan -> Planning the replacement; design -> +scaffold/implement choice). Then RE-EVALUATE every open gate against +reality, not the note: if the exemplar map now carries the awaited +control's row, or the awaited child dialog now has an Avalonia route, the +gate self-clears and the dialog resumes past it. If a gate is still +blocked, drop into the blocker's own sub-cycle where IT left off (its +working dir carries its sub-phase by the same rules -- resumable +recursively). State what was found and CONFIRM with the developer before +proceeding. Never silently redo a completed phase. + +The working documents remain in place after completion; what to keep, +delete, or attach anywhere is the developer's call. + +## Understanding the dialog (read-only; safe to run while they explore) + +The steps below are pure reading -- source, git history, Jira, layout XML. No +build, no test run, no app automation. This is what makes the phase safe to +run concurrently with the developer's own exploration of the live dialog. +The "before" evidence (the last step here) is NOT part of that concurrent work. + +Before source analysis, read `Docs/lessons/avalonia-migration/README.md` and +any card matching the dialog's capabilities. Carry applicable constraints and +rejected assumptions into the analysis only after verifying them against the +current tree and legacy behavior; a card never authorizes an old design. + +1. Read the dialog source and every related file (designer, resx, helpers). +2. Pull the file history and investigate related Jira issues (read-only + Atlassian tooling); document them chronologically as framing context for + why the dialog's features exist. +3. Find and document ALL calling sites and the conditions under which each + opens the dialog. Check every launch-site kind the conversion documents + call out: launcher edges (`Lcm*DialogLauncher`), xCore command handlers + and dialog listeners (the `EntryDlgListener`/`RecordDlgListener` + pattern), menu/toolbar command XML, DetailControls slice launchers (the + `ReferenceLauncher` "..." family), popup-tree manager items ("More..."/ + "Create..." -- `MSAPopupTreeManager` and kin), other dialogs that chain + into this one, context menus, and direct static `RunDialog`-style helpers + (e.g. `ValidCharactersDlg.RunDialog`). +4. Analyze the dialog's logic and its interaction with data, internal and + external: static calls, LCM objects, and any LCM Units of Work -- + including the undo/redo task labels it uses. +5. Build the interaction picture: every supported user flow through cancel / + OK / apply (and next / back / finish for wizards); whether the dialog is + modal or MODELESS; the help topic; every CHILD dialog or message box the + dialog itself opens (each is a conversion dependency -- catalog them + explicitly). +6. Catalog the enabled/disabled state of every control and everything that + affects it. +6b. Inventory the dialog's localizable strings (resx usage) and note the + project-data prerequisites needed to exercise each flow (these feed the + test plan and the manual checkpoint). +7. "Before" evidence -- NEVER while their FieldWorks is open. Ask which the + developer prefers, and wait for the answer: + - **They capture it** (default while they are exploring): they are + already looking at the dialog, so ask them to grab the screenshots and + say where they put them. Zero risk to their session. + - **The harness captures it** (unattended, repeatable): add a + `Cap`/`CapLoop` case to `ScreenshotHarnessTests` and run + `.\test.ps1 -SkipNative -TestProject LexTextControlsTests -TestFilter + "FullyQualifiedName~ScreenshotHarness"` -- but ONLY once they confirm + FieldWorks is closed (it is an in-process `DrawToBitmap` render that + never touches the desktop, yet the build it needs fails on binaries + their running app locks). + The live-app automation route is a last resort for behavior the harness + cannot fake, and only with their explicit go-ahead that the app is yours + to drive. + +Write `-analysis.md` with exactly these sections: + +1. **Dialog purpose** +2. **Data interaction** + - A. All model data displayed and modified, including WHEN modification + occurs (on OK/apply, on focus change, etc.) + - B. Any properties or settings displayed or modified, including when +3. **Control interactions** -- interactions between controls, specific about + enabled/disabled state and what drives it +4. **Layout strategy** -- current control layout, control sizes and minimum + sizes, resizing behavior, and any persisted bounds/splitter state + +## Agreeing on how it works (gate) + +When the analysis document is written, STOP. Ask -- via the question tool, +then end your turn -- "I've finished my analysis. Are you ready to align our +understanding?" Do not describe what you found. Do not summarize the +document. Wait. + +When they say yes, point them at the file (path only) and ask whether they +see errors, gaps, or have questions -- then stop again and wait. When they +paste a section back with a correction, fix the document and ask again. +Re-ask with varied phrasings ("anything else that looks off?", "any +interaction I've missed?") until you get a clear negative. Each round is its +own stop: one question, end of turn. + +The gate is the developer declaring the document an accurate description of +the current dialog. + +## Deciding what to prove (gate) + +Ask the developer for guidance on integration-test creation -- one question, +end of turn, wait -- then design the test set for the dialog's capabilities +WITH them using the `grill-with-docs` skill (one question at a time, each its own +stop, recommendations offered) until the set is defined. +Every test item must be expressible as: drive the scenario, assert the +behavioral outcome, capture a labeled snapshot. + +Save the result as `-integration-test-plan.md`. The +create-integration-test skill consumes this file verbatim. + +## Planning the replacement (gate) + +0. FIRST check kit membership: if the dialog belongs to a family an + existing kit already covers (the EntryGo/BaseGoDlg family, the + ChooserDialog family), the conversion is a new kit consumer + launcher, + not a new dialog -- present that finding and shrink the remaining phases + accordingly. +0b. DEPENDENCY GATE -- children convert first: every child dialog the + target opens must already have an Avalonia route (converted, covered by + a kit, or a plain message box via `FwMessageBox`). If any child lacks + one, present the dependency tree with a recommended conversion order, + record the open gate and its waiting-on list in + `-state.md`, and ASK the developer whether to start + converting the deepest unconverted child NOW, in this session. Yes: + launch convert-dialog for that child class (from context) and carry it + forward -- the parent resumes when the child completes. No: pause this + conversion; resume re-evaluates the gate. +1. Analyze every WinForms control and design pattern in the dialog. +2. Map each against `.claude/skills/fieldworks-winforms-to-avalonia-migration/references/control-exemplar-map.md` (the exemplar map + in the migration skill). +3. From the analysis document's layout section, propose an Avalonia layout + that AT MINIMUM replicates the WinForms capability; exceed it where the + effort is low. +4. For items with no exemplar: catalog the item's purpose and capabilities, + then look for an adequate replacement in Avalonia packages -- both those + already imported and others not yet used (an un-imported package is a + finding to surface, not a decision to make alone). + +**Custom-control sub-cycle.** A CUSTOM WinForms control with no exemplar +gets a capability-first treatment -- what it does, never how it is painted: + +1. From the dialog analysis and the control's own source, list every + capability the control provides. +2. Produce a capability-comparison table: capability -> the stock Avalonia / + composed / package answer -> classified **covered**, + **possible-but-costly**, or **not-possible**. +3. The DEVELOPER makes the convert-vs-replace verdict on that table. + Default bias is stock/composed (unnecessary owned controls are the + snowflakes the idiomatic audit removed). Not-possible earns an owned + conversion; possible-but-costly is the developer's judgment. +4. If the verdict is CONVERT: the control becomes its own conversion, and + **the control converts first**. Inform the developer and ASK whether to + start converting it NOW, in this session. Yes: open + `Docs/migration/working//` with its own analysis / design + / test-plan / state documents, record the gate in + `-state.md`, and run the control's cycle (align -> grilled + test plan -> design -> implement -> create-integration-test -> + exemplar-map row), then resume the dialog with the control as an + available exemplar. No: record the gate the same way and pause this + conversion; resume re-evaluates the gate. +5. Placement rule for a new owned control (the two-assembly layering IS the + legacy FwCoreDlgs/FwCoreDlgControls separation -- FwAvalonia is the + controls layer, FwAvaloniaDialogs the dialogs layer): + - used by dialogs only -> `Src/Common/FwAvaloniaDialogs/Controls/` (the + dialog-composite subfolder; dialogs themselves stay at the project + root); + - shared with the detail view or other non-dialog surfaces -> + `Src/Common/FwAvalonia/` (in `Detail/` when detail-specific, the root + when general); + - tests mirror the SUT's folder either way. + +Produce the design report with these sections: + +1. Each WinForms control type -> its Avalonia exemplar -> the proposed + replacement +2. Each WinForms code pattern identified -> the proposed Avalonia + replacement +3. Each control/pattern with NO exemplar -> a proposed replacement, or an + explicit "none found" +4. The proposed Avalonia layout, with every deliberate difference from the + WinForms version called out + +Point the developer at the report and ask them to review it for errors, +gaps, and suggestions -- then stop and wait, without summarizing its +contents. Capture the agreed result as `-design.md`. If a missing conversion or +capability surfaced, run `grill-with-docs` with the developer to produce the fill +plan (the exemplar-promotion path: the first implementation is built on the +existing idiom rules and, on the developer's approval, a +control-exemplar-map row lands in the same PR naming it THE exemplar -- +draft the row and show it before committing). + +## Building it (developer's choice) + +Offer the choice explicitly -- as a question, then stop and wait: +**generate scaffold** or **implement design**. + +**Merge policy: scaffold is branch-state only.** The scaffold/implement +choice is about how much the AI builds first, not what ships -- nothing +merges until Proving it works passes (implemented and verified), so preview users +never meet an empty dialog. + +**Scaffold** means: create the Avalonia files following +`.claude/skills/fieldworks-avalonia-ui/references/dialog-conversion.md`; the result +compiles and launches (empty); and EVERY launch site from the analysis +document's calling-sites section is adapted to open the Avalonia or WinForms +dialog based on the UIMode setting (fail-closed: Legacy stays the default). + +**Implement** means: scaffold first, then build out the dialog from +`-design.md` and `-integration-test-plan.md`. + +Build conventions the result must satisfy (confirm each in the diff): + +- Naming: legacy stem + role suffix (`FooDlgInput` / `FooDlgView.axaml` / + `FooDlgViewModel` / `FooDlgPayload`) in `Src/Common/FwAvaloniaDialogs/`; + kits keep general names. +- Boundaries: the Input carries everything LCModel-free the dialog needs; + the ViewModel never touches LCModel; the launcher owns all LCModel work inside + one undo task with the legacy undo text. +- Presentation, modal: `AvaloniaDialogHost.ShowModal` with the WinForms + owner -- never an Avalonia-owned window during coexistence; owner icon or + none; focus returns to the invoking control. +- Presentation, modeless: no exemplar exists yet -- the FIRST modeless + conversion designs the hosting pattern through the exemplar-gap path + (`grill-with-docs`), with the constraint that WinForms owns the window during + coexistence (an Avalonia surface hosted in a modeless WinForms Form via + the host control), and promotes it as the modeless exemplar. +- Strings in `.resx` (accessors appended in order); no L10NSharp; no + hardcoded UI text. Concrete brushes only. Validation through + `DialogViewModelBase.GetValidationErrors` with the inline-error exemplar; + help through `HelpRequested` -> launcher. +- Watch-outs: `Flyout` over free `Popup`; arrow/Enter keys claimed at the + host; WS-sensitive inputs are never plain TextBoxes (Go-family exemplar); + match the legacy commit semantics (OK-gated vs commit-on-select) and say + which in the ViewModel summary. +- Accessibility and keyboard parity: stable automation ids per the owned + control convention (pinned by `OwnedControlAutomationConventionTests`); + tab order and mnemonics match the legacy dialog. +- The repository comment standard applies to everything written. + +## Proving it works + +1. Run create-integration-test against the plan (TDD before implementation, + or verification after; if the developer hand-implemented inside the + scaffold, first evaluate their implementation against + `-design.md` and report deviations). +2. The Avalonia visual test emits the paired `-after.png` (same data + flavor as the `-before`). +3. Ask the developer to manually test -- then stop and wait for their + findings: walk the analysis document's the Data interaction and Control interactions sections line by line against + the live dialog in New UI mode, and compare against the `-before` + captures. They own the app for this; do not drive it or change its UI + mode for them. +4. Legacy-mode smoke: with the toggle OFF, every launch site from the + analysis document still opens the legacy dialog unchanged. +5. Add any new exemplars created during this conversion to the exemplar map + (the promotion row from Planning the replacement, if not already landed). +6. Land: comment audit against the repository standard, preflight the + branch, and PR per the repo's conventions. diff --git a/.claude/skills/convert-slice/SKILL.md b/.claude/skills/convert-slice/SKILL.md new file mode 100644 index 0000000000..663ce8b800 --- /dev/null +++ b/.claude/skills/convert-slice/SKILL.md @@ -0,0 +1,167 @@ +--- +name: convert-slice +description: Drive the conversion of one legacy slice type to the Avalonia detail view through analysis, developer alignment, integration-test planning, route/exemplar mapping, design, and scaffold/implement. Use when the user invokes /convert-slice with a slice class or layout identity, or asks to convert a slice type or an Unsupported detail row. +--- + +# Convert Slice + +The slice-type counterpart of convert-dialog: the same stages, gates, and +working-document conventions -- with the deltas a slice demands. The +developer decides and confirms; this skill analyzes, drafts, and builds. + +## Two rules that override everything else + +**1. The developer's FieldWorks is untouchable.** They are exploring the +slice while you work. NEVER close, kill, restart, or drive their running +FieldWorks, and never change the UI-mode setting under them. The live-app +capture route owns the app lifecycle (relaunch-per-tool, `CloseMainWindow`) +-- FORBIDDEN while their instance is open. Ask before any +`build.ps1`/`test.ps1` run: a build fails on binaries their app holds +locked, and killing their process to unblock it is never acceptable. On a +locked-file failure, STOP and ask. + +**2. Every gate is a real stop.** A gate is an interactive question (use the +question tool, which returns only when they answer), then END the turn. Do +not summarize or restate a report in chat -- point at the file and stop; a +chat summary invites skipping the review the gate exists for. + +## How to talk about where you are + +Use the stage names with the developer, in plain language about the work -- +never "Phase 2" or any numbered-stage shorthand. The stages, in order: +**Understanding the slice** -> **Agreeing on how it works** -> **Deciding +what to prove** -> **Planning the replacement** -> **Building it** -> +**Proving it works**. + +Input: the legacy slice class name (e.g. `PhoneEnvReferenceSlice`) or the +layout identity (`editor="..."` / `editor="Custom" class="..."`). +Scope: PORT with low-cost improvements; a "redesign" verdict parks the +slice. + +Working documents, resume rule, and ticket attachment are identical to +convert-dialog: `Docs/migration/working//` holding +`-analysis.md`, `-integration-test-plan.md`, +`-design.md`; detect-and-resume on re-invocation; the documents +remain after completion (retention is the developer's call). + +## Understanding the slice (read-only; safe while they explore) + +Source, history, Jira, and layout-XML reading only -- no build, no test run, +no app automation -- so it runs concurrently with the developer's own +exploration. The "before" evidence is NOT part of that concurrent work. + +Everything convert-dialog does when understanding a dialog (source + related +files, file history + Jira chronology, logic and data-member analysis incl. +LCM objects and Units of Work, enabled/disabled cataloging), plus +slice-specific work: + +- Read `Docs/lessons/avalonia-migration/README.md` and any card matching the + slice's capabilities. Record applicable constraints and rejected assumptions + in the analysis, then verify them against current code and legacy behavior; + never use a card as an implementation recipe. + +- Resolve the layout identity: the `editor=`/`class=` attributes and every + layout node that produces this slice. +- Inventory where it appears: which fields, which tools, roughly how many + instances (compose an affected record in the New UI -- the Unsupported + worklist row names the unclaimed class). +- Interaction picture is row-shaped: mouse and keyboard edit interactions, + commit timing (focus-loss autosave vs immediate-commit actions), context + menus, and how the slice behaves inside DataTree (indent, label, + expansion). +- "Before" evidence: the dialog screenshot harness does NOT apply (a slice + renders inside the DataTree), so the capture needs the live app -- which + means it is the DEVELOPER's to do while they explore (ask them to grab the + affected entry's rows and say where they put them). Only drive the app + yourself with their explicit go-ahead that it is closed and yours. + +The analysis document keeps the four-section shape; section 4 (layout) +describes the row: label/value split, sizing, wrapping, and any multi-row +structure. + +## Agreeing on how it works (gate) + +Identical to convert-dialog: announce and STOP ("I've finished my analysis. +Are you ready to align our understanding?"), then point at the file and +correct round by round -- each round its own question and end of turn -- +until the developer gives a clear negative to "any errors, gaps, or +questions?". Never summarize the document in chat. + +## Deciding what to prove (gate) + +Identical process (developer guidance -> `grill-with-docs` -> saved plan). Slice +plans must cover: compose (the field renders with correct values, not +Unsupported), edit -> ONE undo step, validation blocking, re-show after +external PropChanged, and cluster/bidi safety when the slice carries text. + +## Planning the replacement (gate) + +The route decision comes FIRST and is the developer's (present the tree +with a recommendation): + +1. An existing `DetailFieldKind` fits -> the work is classification in + `EditorKindMap`; composer and `SliceFactory` already handle the rest. +2. A genuinely new interaction shape -> new `DetailFieldKind` + a new owned + `Fw*Field` control + a `SliceFactory` case. +3. A custom slice (`editor="Custom"`) -> an `ISlicePlugin` keyed by the + exact legacy `class=` string, registered in `SlicePluginRegistry`; no layout + edits. + +Then map controls/patterns against the exemplar map exactly as convert-dialog +does when planning a replacement (including the no-exemplar catalog, package +search, `grill-with-docs` fill plan, and human-gated promotion row), and produce the +same four-section design report, with section 4 describing the proposed row +layout and every deliberate difference from the legacy slice. + +The children-convert-first dependency gate applies here too: any dialog the +slice opens (choosers, create dialogs) must already have an Avalonia route +(converted, kit-covered, or FwMessageBox) before this slice proceeds. So +does the custom-control discipline: designing a new `Fw*Field` follows the +capability-comparison table with the stock/composed bias (convert-dialog's +custom-control sub-cycle), and the state-manifest + gate re-evaluation +resume rules are shared (`-state.md`). At any gate, the same +choice applies: inform the developer and ask whether to convert the blocker +now in-session (launching the right skill with the class from context) or +pause. + +## Building it (developer's choice) + +**Scaffold** for a slice means: the editor string is classified (or the +plugin registered), a placeholder control composes and renders at the +slice's REAL position in the New UI detail view, tests compose green, and +legacy is untouched -- the row goes from "Unsupported" to "empty but +present". There is no per-slice launcher gate; the detail surface's UIMode +gate already covers it. Merge policy is the same as dialogs: scaffold is +branch-state only; nothing merges until Proving it works passes (an empty row +is worse than an honest Unsupported row for preview users). + +**Implement** builds the control out from the design + test plan: + +- Values project LCModel-free through `DetailValueFactory` / + `IDetailValueProvider` -- never a second projection of an existing recipe. +- Composition wires through `DetailComposer` (the field's + `FieldEditHandler` carries its edit operations). +- Edits stage through the edit context; the fenced session commits ONE undo + step; new edit operations go on a sub-capability interface (the + `IStructuredTextEditing` precedent) -- never widen `IDetailEditContext`. +- Idiom rules per `.claude/skills/fieldworks-avalonia-ui/references/style-system.md`; + WS typography via the multi-WS text exemplar when text is involved; a + null edit context yields read-only display. +- Compose-time snapshot discipline: rows do not live-update; the re-show + does. No ad-hoc refresh plumbing; refresh coordination stays with + `AvaloniaDetailRefreshController`. +- Plugin factories degrade: missing/null/throwing renders the labeled + Unsupported row, never a crash or a blank row. +- Keep `FwAvalonia` LCModel-free (projection and write-back live in + xWorks). The repository comment standard applies throughout. + +## Proving it works + +1. create-integration-test against the plan (tests land per the mirroring + rule: control tests in `FwAvaloniaTests/Detail/`, composer tests in + `xWorksTests/Avalonia/Composer/`). +2. Developer manual test in live FieldWorks, New UI on: the field at its + real position, edit interactions per the analysis document, focus-loss + autosave, single Ctrl+Z per save, cross-surface PropChanged refresh both + directions, tool-switch mid-edit settles cleanly. +3. Land the exemplar-promotion row for anything new (human-gated, same PR). diff --git a/.claude/skills/create-integration-test/SKILL.md b/.claude/skills/create-integration-test/SKILL.md new file mode 100644 index 0000000000..45120d4147 --- /dev/null +++ b/.claude/skills/create-integration-test/SKILL.md @@ -0,0 +1,61 @@ +--- +name: create-integration-test +description: Turn a [class]-integration-test-plan.md into headless integration tests, one per plan item, each driving the scenario, asserting the outcome, and capturing a labeled snapshot. Use for TDD before implementation or as verification after it, whenever a test plan exists under Docs/migration/working/. +--- + +# Create Integration Test + +Consumes the test plan a convert-* skill produced with the developer and +turns it into real tests, one plan item at a time. Plans exist for +dialogs, slices, AND owned controls (a control converted through the +custom-control sub-cycle gets its own plan under its own class name). + +Input: the class name (or find the single plan when only one exists). +Plan location: `Docs/migration/working//-integration-test-plan.md`. +If no plan exists, stop and say so -- the plan is authored with the +developer in the convert-* flow, never invented here. + +## What each plan item becomes + +One headless `[AvaloniaTest]` that: + +1. **Drives** the scenario with the headless input pipeline (typing, focus, + commands) from a constructed dialog/control in the state the item names. +2. **Asserts** the behavioral outcome in code -- this is the pass/fail gate + (enabled state, staged value, validation message, payload content). +3. **Captures** a labeled snapshot as reviewable evidence + (`DialogSnapshot.Capture`, name `-NN-` numbered in + plan order). Snapshots document; assertions gate. + +Known headless limit: pointer hit-testing does not run headless. A plan item +that genuinely requires mouse interaction is implemented as far as the +model/keyboard path allows and flagged in the report as needing the manual +checkpoint. + +## Where tests land + +Derive the location from the SUT by the repository's test-mirroring rule +(tests mirror their SUT's project and subfolder): + +- Dialog View/ViewModel (`Src/Common/FwAvaloniaDialogs/`, flat) -> + `FwAvaloniaDialogsTests/IntegrationTests.cs` +- Owned control in `Src/Common/FwAvaloniaDialogs/Controls/` (dialog + composite) -> `FwAvaloniaDialogsTests/Controls/`; owned control in + `Src/Common/FwAvalonia/` (shared) -> the `FwAvaloniaTests/` mirror of its + subfolder +- Launcher edge (`Src/LexText/LexTextControls/Avalonia/`) -> + `LexTextControlsTests/Avalonia/` +- Slice control (`Src/Common/FwAvalonia/Detail/`) -> + `FwAvaloniaTests/Detail/` (create the folder on first use) +- Composer wiring (`Src/xWorks/Avalonia/Composer/`) -> + `xWorksTests/Avalonia/Composer/` + +## Process + +Work the plan top to bottom, one item at a time: write the test, run it, +and record the result against the plan item (passing, failing-as-expected +for TDD, or blocked-needs-manual). Keep generated code to the repository +comment standard. When the plan is exhausted, run the owning suite in full +and report: items implemented, items flagged manual-only, suite counts, and +any plan item whose wording did not survive contact with the code (report +it back for the developer to re-decide -- do not silently reinterpret). diff --git a/.claude/skills/dialog-update/SKILL.md b/.claude/skills/dialog-update/SKILL.md new file mode 100644 index 0000000000..a896cd95ed --- /dev/null +++ b/.claude/skills/dialog-update/SKILL.md @@ -0,0 +1,141 @@ +--- +name: dialog-update +description: "Keep a FieldWorks dialog's WinForms (old) and Avalonia (new) implementations in sync whenever either is changed. Use whenever you add, edit, or review a control, field, button, validation rule, apply-order step, or string in a dialog that exists in BOTH a WinForms form (e.g. LexOptionsDlg) and its Avalonia replacement (e.g. LexOptionsDlgView + AvaloniaOptionsDialogLauncher) — even for a one-line change. Also use before claiming a migrated dialog is at parity, and when deciding whether a difference between the two is an approved divergence." +--- + +# Dialog Update — Keep Old (WinForms) and New (Avalonia) In Sync + +During the WinForms → Avalonia coexistence, many dialogs exist **twice**: the +legacy WinForms form and the migrated Avalonia view. Both ship, and which one +runs is chosen at launch (usually by `UIMode`). A change to one that is not +mirrored in the other is a **divergence bug**, not a style choice. + +## Non-negotiable rule + +**NEVER diverge the two UIs without explicit product-owner approval.** This +includes behavior, controls, layout, wording, validation, apply order, and +which settings persist. "The new one is nicer this way" is not approval. If you +believe a divergence is warranted, stop and ask; do not encode it and move on. + +Approved divergences must be recorded (see *Divergence register* below) with a +one-line reason and the approver — otherwise the next person reads it as a bug +and "fixes" it, thrashing the code. + +## The dialog pairs (start here) + +| Concern | WinForms (old) | Avalonia (new) | +|---|---|---| +| Tools → Options | `Src/LexText/LexTextControls/LexOptionsDlg.cs` (+ `.Designer.cs`, `.resx`) | `Src/Common/FwAvaloniaDialogs/LexOptionsDlgView.axaml(.cs)` + `LexOptionsDlgViewModel.cs` + `LexOptionsDlgState.cs`; edge: `Src/LexText/LexTextControls/Avalonia/AvaloniaOptionsDialogLauncher.cs` | +| Manage Individual Features | none — Avalonia-only, no WinForms precedent (see the carve-out comment in `LexOptionsDlg.cs` near its UI-mode group construction) | `LexiconFeatureManagerDialog` (`Src/Common/FwAvaloniaDialogs/`) | +| Insert Entry | `Src/LexText/LexTextControls/InsertEntryDlg.cs` | `Src/Common/FwAvaloniaDialogs/InsertEntryDlgView.axaml.cs` + `InsertEntryDlgViewModel.cs` | +| Add New Sense | `Src/LexText/LexTextControls/AddNewSenseDlg.cs` | `Src/Common/FwAvaloniaDialogs/AddNewSenseDlgView.axaml.cs` + `AddNewSenseDlgViewModel.cs` | +| MSA Creator | `Src/LexText/LexTextControls/MsaCreatorDlg.cs` | `Src/Common/FwAvaloniaDialogs/MsaCreatorDlgView.axaml.cs` + `MsaCreatorDlgViewModel.cs` | +| Picture Properties | `Src/FwCoreDlgs/PicturePropertiesDialog.cs` | `Src/Common/FwAvaloniaDialogs/PicturePropertiesDialogView.cs` + `PicturePropertiesDialogViewModel.cs` | +| Phonological Feature Chooser | `Src/LexText/LexTextControls/PhonologicalFeatureChooserDlg.cs` | `Src/Common/FwAvaloniaDialogs/FeatureChooserDialogView.axaml.cs` + `FeatureChooserDialogViewModel.cs` | +| Entry Go (jump to entry) | `Src/LexText/LexTextControls/EntryGoDlg.cs` | `Src/Common/FwAvaloniaDialogs/EntryGoDialogView.axaml.cs` + `EntryGoDialogViewModel.cs` | +| Possibility/list chooser (FilterBar "choose") | `Src/Common/Controls/XMLViews/ReallySimpleListChooser.cs` (+ `Src/Common/Controls/DetailControls/SimpleListChooser.cs`) | `Src/Common/FwAvaloniaDialogs/ChooserDialogView.axaml.cs` + `ChooserDialogViewModel.cs`; edge: a product launcher lands with the ReallySimpleListChooser migration | +| Create feature / add feature value | `Src/LexText/LexTextControls/MasterInflectionFeatureListDlg.cs` / `MasterPhonologicalFeatureListDlg.cs` | `Src/Common/FwAvaloniaDialogs/CreateFeatureDialogView.axaml.cs` + `CreateFeatureDialogViewModel.cs`; edge: `Src/LexText/LexTextControls/Avalonia/LcmCreateFeatureLauncher.cs` | + +Symbols in the rules below use Tools → Options as the worked example. + +The Avalonia side splits into three layers — keep the split when you edit: +- **View (`*.axaml`)** — controls + bindings only. No LCModel, no WinForms. +- **ViewModel (`*ViewModel.cs`)** — edits a plain **state DTO**, exposes + commands. LCModel-free and WinForms-free. +- **Launcher/edge (`Avalonia*Launcher.cs` in LexText)** — the only place that + touches `PropertyTable`/`FwApplicationSettings`/LCModel; builds the state, + applies it on OK, and supplies callbacks (e.g. showing a nested dialog). + +The WinForms form does all of this in one class. So "add a field" means one +edit on the WinForms side and typically **four** on the Avalonia side (view, +view-model, state DTO, launcher build + apply). Missing any one silently drops +the field. + +## What fails when one side is updated but not the other + +Concrete failure modes seen in this codebase — check for each when you touch a +paired dialog: + +1. **Setting silently not saved.** A new field added to WinForms `OK`/apply but + not to the launcher's `Apply()` (or vice versa): the user edits it, closes on + OK, nothing persists. (This is exactly the "checked everything, X'd out, + still Legacy" class of bug.) +2. **Control missing entirely.** A button/checkbox added to one view and not the + other — the user on the missing side simply can't reach the feature. When + the absence is deliberate (e.g. the "Manage Individual Features" selector, + which is Avalonia-only with no WinForms precedent), it must be an explicit, + recorded divergence, not silence. +3. **Behavioral divergence.** One applies live, the other prompts a restart; one + validates, the other doesn't; different apply order → different side effects + (e.g. writing-system change before vs after plugin install). +4. **Visibility/enable drift.** One side gates a control's visibility or enabled + state on a condition (UI mode, platform, settings state) that the other side + doesn't mirror. Check that both sides gate on the same condition, not just + that both sides have a similarly named control. +5. **String/localization drift.** Wording, mnemonics, or the `.resx`/XLIFF key + updated on one side only → inconsistent UI and broken translation memory. + Both sides must carry the same seed English (see `fieldworks-localization-review`). +6. **State DTO / persisted-key mismatch.** The DTO field, the settings property, + and the `PropertyTable` broadcast key must all agree + (e.g., in the Options pair: `UIModeDisabledTools` ↔ + `EditSurfaceResolver.UIModeDisabledToolsPropertyName`). + A rename on one side leaves the other writing a dead key. +7. **Test blind spot.** Headless Avalonia tests pass while the WinForms form (or + the live modal-host input path) is broken, because the tests exercise + bindings, not the real host. Green tests ≠ parity. +8. **Divergence comment rot.** A "sanctioned divergence" note that was never + actually approved (or is now stale) misleads the next migrator into + preserving a bug. Treat undocumented-approver notes as suspect. + +## Concrete ways to keep them in sync + +Do these, in order, on any paired-dialog change: + +1. **Edit both sides in the same commit.** Never land a one-sided change. If the + other side is out of scope, stop and say so explicitly. +2. **Share the source of truth, don't copy it.** Prefer one list/rule both + consume over two hand-maintained copies: + - e.g., in the Options pair: `LexiconFeatureCatalog` is the single catalog + behind the Avalonia `LexiconFeatureManagerDialog` (an Avalonia-only + feature, no legacy WinForms equivalent) — extend it, not a second list. + - Apply/normalize/gate helpers should be shared or mirrored with a pointer + comment (e.g., in the Options pair: `NormalizeUiMode`, + `ParseDisabledTools`/`SerializeDisabledTools`). +3. **Mirror the apply order.** Keep the two sides' apply/OK ordering identical + and cite the counterpart method in a comment when you add a step. Worked + example: the Options launcher's `Apply()` is explicitly written to follow + `LexOptionsDlg.m_btnOK_Click`'s order. +4. **Cross-reference in comments.** Each side names its counterpart + (e.g. `// parity with WinForms LexOptionsDlg m_uiModeBetaWarning`). A grep + for the partner symbol should always find the other side. +5. **Parity tests, not just binding tests.** Assert the *behavior* both dialogs + promise: field persists on OK, control hidden in Legacy/shown in New, + validation blocks OK, disabled-tools round-trips. Put the DTO/launcher apply + under test (see e.g. `AvaloniaOptionsDialogLauncherTests`, `LexOptionsDlgTests`, + `OptionsDialogTests`). +6. **Record approved divergences** in the launcher/class doc as a + `KNOWN GAP`/`APPROVED DIVERGENCE` block with the reason **and the approver**. + No approver ⇒ it's a bug to fix, not a divergence to keep. +7. **Verify in the real host, both modes.** Headless tests can't see the + WinForms-hosted-Avalonia input path. Before claiming done, drive the live + dialog in New mode (and confirm Legacy still uses the WinForms form). + +## Pre-commit checklist for a paired-dialog change + +- [ ] Both implementations edited (WinForms form; Avalonia view + view-model + + state DTO + launcher build & apply). +- [ ] Same controls, visibility gates, validation, and apply order on both. +- [ ] Same seed strings + `.resx`/XLIFF keys on both (localization strategy). +- [ ] DTO field ↔ settings property ↔ `PropertyTable` key all agree. +- [ ] Parity tests assert the behavior (persist / gate / validate), not just a + binding. +- [ ] Any difference is an explicitly approved, documented divergence — else + it's removed. +- [ ] Driven live in New mode; Legacy still routes to the WinForms form. + +## Related skills + +- `fieldworks-winforms-to-avalonia-migration` — the full surface-migration playbook. +- `fieldworks-ui-wiring-review` — which host is active / how a setting reaches a screen. +- `fieldworks-localization-review` — string + `.resx`/XLIFF parity. +- `fieldworks-avalonia-ui` — the Avalonia View/ViewModel/host patterns themselves. diff --git a/.claude/skills/fieldworks-avalonia-ui/SKILL.md b/.claude/skills/fieldworks-avalonia-ui/SKILL.md new file mode 100644 index 0000000000..f976dd43e4 --- /dev/null +++ b/.claude/skills/fieldworks-avalonia-ui/SKILL.md @@ -0,0 +1,189 @@ +--- +name: fieldworks-avalonia-ui +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." +--- + +# FieldWorks Avalonia UI + +## Use This For + +- Avalonia XAML, view models, commands, lifetimes, dispatching, and + resource/style changes. +- New or changed projects under `Src/**/**/*.Avalonia/`, + `Src/Common/FwAvalonia/`, and `Src/Common/FwAvaloniaPreviewHost/`. +- Preview Host module registration, sample data providers, and UI + diagnostics (see `.github/instructions/avalonia.instructions.md` for + build/preview commands and project layout rules). +- UI host wiring that selects between Avalonia and legacy UI — apply + `fieldworks-ui-wiring-review` alongside this skill. + +## Start From the Established Patterns + +Do not design controls or seams from scratch. The migration hub skill +(`fieldworks-winforms-to-avalonia-migration`) documents the decided +architecture; its +`../fieldworks-winforms-to-avalonia-migration/references/architecture-patterns.md` +covers owned controls, writing-system text fields, dialogs/flyouts, +validation, and lifetime. Canonical code to imitate: + +- Owned field controls: `Src/Common/FwAvalonia/Detail/FwFieldControls.cs`, + `FwOptionChooser.cs`, `DetailMenuFlyout.cs`, `HoverReveal.cs` +- Detail view + focus memory: `DataTree.cs`, + `DetailFocusMemory.cs` +- Seams (scheduler, lifetime, clipboard, edit sessions): + `Src/Common/FwAvalonia/Seams/` +- Headless test setup: `Src/Common/FwAvalonia/FwAvaloniaTests/TestAppBuilder.cs`; + examples in `DetailEditingTests.cs`, `VisualParityAndDensityTests.cs` +- Density constants: `Src/Common/FwAvalonia/FwAvaloniaDensity.cs` +- **Dialog kit (XAML + CommunityToolkit.Mvvm + compiled bindings):** + `Src/Common/FwAvaloniaDialogs/` — `LexOptionsDlgView.axaml`/`.axaml.cs` + + `LexOptionsDlgViewModel.cs`; headless tests in `FwAvaloniaDialogsTests/`. + This is the verified template for hand-authored dialogs — see + "Converting a WinForms dialog (MVVM kit)" below. + +**Re-implementing a Phase-1 deferred screen (JIRA tickets).** The full recipe (per-screen +`Docs/migration/.md` on the never-merged `phase1-docs` branch, stub recovery from git +history, which canonical screen to copy, `UIMode=New` re-wiring) is canonical in the migration +hub skill — `.claude/skills/fieldworks-winforms-to-avalonia-migration/SKILL.md`; start there. + +## Converting a WinForms dialog (MVVM kit) + +Hand-authored dialogs/wizards use **XAML + CommunityToolkit.Mvvm + compiled +bindings** — NOT the region/IR pattern (that is only for XML-view-definition +surfaces). Full step-by-step, the working template, and the decision +history/rationale pointer: `references/dialog-conversion.md`. The shape, per +dialog: + +1. **View** `XyzDialogView.axaml` (+ `.axaml.cs`): a `UserControl` (not a + `Window` — see modality below), `x:DataType` set to the view-model, + compiled `{Binding}`s, and a stable `AutomationProperties.AutomationId` + on every interactive control. Reuse owned controls (`FwMultiWsTextField`, + `FwOptionChooser`) for writing-system fields and list pickers. +2. **View-model** `XyzDialogViewModel.cs`: `ObservableObject` with + `[ObservableProperty]` state and `[RelayCommand]` actions; expose the + result (e.g. `Accepted`). Keep it LCModel-free for the view; bind real + settings/domain through the app-settings/edit-session seams. +3. **Tests** `XyzDialogTests.cs` (headless `[AvaloniaTest]`): assert the + compiled bindings propagate both directions and the commands fire, plus + the per-stage PNG captures and subjective checks under "Dialog spacing" + below — together, the per-dialog definition of done. + +### Style system (density + borders, per surface) + +The font/density tokens and the field-border rule are a GLOBAL system, calibrated to WinForms density — +not the roomy Fluent defaults — and applied per-control-tree (the only mechanism that renders in BOTH the +runtime host and the headless tests). Full detail, the calibrated numbers, and the per-surface intent: +**`references/style-system.md`**. Headlines: **dialog inputs are BOXED** (`Border.fwFieldHost`), +**detail/region values are FLAT** with subtle separators, **browse keeps its grid lines** — just denser +font everywhere; one source of truth per family (`DialogTheme.axaml` for dialogs, `FwSurfaceStyles` for +region/browse); anything that must render headlessly uses a **CONCRETE value, never a Fluent +`DynamicResource`**. + +### Dialog spacing + +All dialog spacing/borders come from the shared tokens in +`Src/Common/FwAvaloniaDialogs/DialogTheme.axaml` (applied to each dialog body by +`DialogThemeBootstrap.Apply(this)`, called from every dialog view ctor). See `style-system.md`'s "Dialog +spacing tokens" table for the current calibrated values — that table is the single source of truth; don't +copy the numbers here too. Headlines (full rules + rationale: +`references/dialog-conversion.md` §2a-bis): + +- Every dialog root carries `Classes="fwDialogRoot"` (window padding); no root `Margin` literal. +- No text-bearing or `PART_*Host` control with 0 padding — host borders carry `Classes="fwFieldHost"`. +- OK/Cancel use the standard button-strip gap tokens. +- Never hardcode a margin/spacing literal — use a token; add new tokens to `DialogTheme.axaml`. +- The headless `DialogLayoutAssert.AssertNoCrowding(view)` tripwire gates this in every dialog's + realized-view test. +- **Capture a PNG at EACH interaction stage via `DialogSnapshot.Capture(view, "--")` + (→ flat gitignored folder `Output/Snapshots/--.png`), then Read each PNG and answer + the six subjective-quality questions — a hard rule and part of the per-dialog definition of done, + for region/browse surfaces too.** The canonical checklist, the six questions, and the + capture → run → Read → judge → fix → re-capture loop: `references/visual-snapshot-testing.md`. + +Rules specific to dialogs: + +- **It lives in `Src/Common/FwAvaloniaDialogs/`** (the dedicated XAML project), + never in the pure-C# `FwAvalonia` foundation. Avalonia projects — including + the XAML-compiled ones — are ordinary members of the `FieldWorks.proj` + traversal (the `Src` glob); a new dialog project just needs adding to + `FieldWorks.sln` (restore + VS). Exclude any nested test folder from the + library's compile glob (``). +- **Modality during coexistence:** no Avalonia `Window.ShowDialog` — show the + dialog `UserControl` via **`AvaloniaDialogHost.ShowModal`**; the view-model + implements **`IDialogViewModel`** and raises `CloseRequested(bool)` from + OK/Cancel. Mechanics + code: `references/dialog-conversion.md` §2. +- **Coexistence sync with the WinForms twin:** while both implementations + ship, they are edited together — the apply-order mirroring, divergence + register, and paired-edit rules live in the `dialog-update` skill. +- **Scope:** simple/confirmation/settings dialogs are good junior+AI work; + Views-engine-coupled dialogs (Find/Replace, Styles host `IVwRootSite`) + belong with the document engine (Stage 9), NOT this kit. + +## Required Checks + +- Use current Avalonia docs for uncertain APIs; do not guess dispatcher, + headless, automation, or binding behavior. +- Keep field labels on the StringTable strategy. Product-facing + FieldWorks-owned strings go in the project `.resx` and are consumed via + the string accessor (`FwAvaloniaStrings`/`FwAvaloniaDialogsStrings`), + never hardcoded; the neutral resx is the English source of truth. + Prototype hardcoded strings must be called out as gaps. +- Stamp stable, nonlocalized `AutomationProperties.AutomationId` (derived + from IR `StableId` where applicable) and localized + `AutomationProperties.Name` on user-facing controls. +- UI logic stays in bindings/view models where practical; avoid + logic-heavy code-behind. +- For any Avalonia "select from a list" surface, prefer the shared + `FwOptionChooser` pattern in `Src/Common/FwAvalonia/Detail/FwOptionChooser.cs` + (AutoCompleteBox-based, keyboard-safe, search-capable, compact density) + over ad hoc `ListBox` popups or one-off editable selectors. Reach for a raw + `ComboBox` only when the UX explicitly needs an always-visible inline combo + rather than the shared flyout selector. +- Do not fix Avalonia keyboard, focus, filtering, selection, popup, or + rendering bugs by patching `System.Windows.Forms` hosts, WinForms + interop message handling, or other legacy host-only routes unless the + task explicitly targets interop behavior. Default to fixing the issue + inside the Avalonia control tree or Avalonia-owned seams. +- Marshal to the UI thread through `IUiScheduler` (or Avalonia dispatcher + in non-region code); no hidden `Task.Run`, no sync-over-async. +- Keep preview data lightweight unless the change explicitly opts into + LCModel/project data; product-facing paths use real edit-session/domain + contracts — detached DTO-only models remain preview-only. +- Headless tests: simulate input on `Window`, flush with + `Dispatcher.UIThread.RunJobs()`, and capture visual regression frames + with Skia (`UseHeadlessDrawing=false` + `CaptureRenderedFrame()`). +- Resx satellite assemblies need no runtime bootstrap; only tests that + exercise genuine Chorus-supplied UI need an L10NSharp + LocalizationManager. +- Evidence runs through `./build.ps1` and `./test.ps1` via the normal repo + graph, not branch-only build paths. + +## Review Red Flags + +- A Common project directly references a feature module without an + explicit architecture decision. +- Preview-only code launched from product UI without a feature gate. +- Tests manually call `OnPropertyChanged(...)`, `ShowRecord()`, or similar + instead of proving the real broadcast/wiring path. +- The active Avalonia path drives hidden legacy rendering/menu + infrastructure (see the hub skill's hard rules). +- Sleep-based or timing-sensitive UI tests. +- Claims of accessibility, localization, IME, or keyboard parity without + executable evidence (see the hub skill's + `../fieldworks-winforms-to-avalonia-migration/references/parity-evidence.md` + §"Evidence language"). + +## Handoff + +Report Avalonia docs consulted, tests run, remaining prototype gaps, +whether the change is product-facing or preview-only, and how the live +wiring path was validated for each affected host. For parity work, say +whether visual evidence is control-level headless capture or live desktop +capture, and which automation identities were assigned. + +## Keep This Skill Current + +When a control pattern, headless-test technique, or Avalonia API gotcha +proves out (or a pointer above goes stale), update this skill in the same +PR — and route durable architecture lessons through the protocol in +`../fieldworks-winforms-to-avalonia-migration/references/lessons-learned.md`. diff --git a/.claude/skills/fieldworks-avalonia-ui/references/dialog-conversion.md b/.claude/skills/fieldworks-avalonia-ui/references/dialog-conversion.md new file mode 100644 index 0000000000..306bad3882 --- /dev/null +++ b/.claude/skills/fieldworks-avalonia-ui/references/dialog-conversion.md @@ -0,0 +1,296 @@ +# Converting a WinForms dialog to Avalonia (MVVM kit) + +The verified playbook for migrating a hand-authored WinForms dialog/wizard to Avalonia using the +**XAML + CommunityToolkit.Mvvm + compiled bindings** kit. Proven by the **real** Tools→Options +migration (`Src/Common/FwAvaloniaDialogs/`): four tabs wired to the live settings bus via an +`LexOptionsDlgState` DTO seam, launched New-mode-gated from `LexTextApp`/`WelcomeToFieldWorksDlg` +(`Src/LexText/LexTextControls/Avalonia/AvaloniaOptionsDialogLauncher.cs`), headless tests green on net48 +through `build.ps1`. Decided 2026-06-15; the original rationale doc was relocated to the +`phase1-docs` branch and is not present here — this file is the proven template that decision +produced. + +> Use this for **hand-authored** dialogs/wizards. Do NOT use the region/IR/composer pattern — that is +> only for surfaces driven by FieldWorks XML view-definitions (entry/detail/browse). Dialogs have no +> XML layout to compile. +> +> Do NOT convert Views-engine-coupled dialogs here (e.g. `FwFindReplaceDlg`, `FwStylesDlg` host +> `IVwRootSite`/`SimpleRootSite`) — those go with the document engine (Stage 9), not this kit. + +## 0. Where dialogs live + +All MVVM dialogs go in **`Src/Common/FwAvaloniaDialogs/`** — the dedicated XAML-enabled project. Never +add XAML to the pure-C# `FwAvalonia` foundation. The project enables `EnableDefaultAvaloniaItems` + +`AvaloniaUseCompiledBindingsByDefault` and references `CommunityToolkit.Mvvm` + the foundation. + +## 1. The three artifacts (copy this shape) + +### View-model — `XyzDialogViewModel.cs` +```csharp +using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace FwAvaloniaDialogs +{ + public partial class XyzDialogViewModel : ObservableObject + { + [ObservableProperty] private bool _someFlag; + [ObservableProperty] private string _someChoice = "A"; + public IReadOnlyList Choices { get; } = new[] { "A", "B" }; + + public bool? Accepted { get; private set; } // null until closed; true=OK, false=Cancel + + [RelayCommand] private void Ok() => Accepted = true; + [RelayCommand] private void Cancel() => Accepted = false; + } +} +``` +`[ObservableProperty]` generates the public property + change notification; `[RelayCommand]` generates +`OkCommand`/`CancelCommand`. The class MUST be `partial`. Keep it LCModel-free for the view; bind real +settings/domain through the app-settings/edit-session seams (not directly). + +### View — `XyzDialogView.axaml` (+ `.axaml.cs`) +```xml + + + +