Skip to content

ValueCommandParameter<T>: value radio-sets + live preview, native BarComboBox/BarGallery support#7

Merged
mstrobel merged 5 commits into
developfrom
feat/value-command-parameter
Jul 7, 2026
Merged

ValueCommandParameter<T>: value radio-sets + live preview, native BarComboBox/BarGallery support#7
mstrobel merged 5 commits into
developfrom
feat/value-command-parameter

Conversation

@mstrobel

@mstrobel mstrobel commented Jul 7, 2026

Copy link
Copy Markdown
Owner

ValueCommandParameter<T> + live preview + the BarCommand refactor — the full converged design from the review discussion, everything where we want it in one PR.

Final shape (head 29c96cb)

The two axes are independent: command shape (plain / wrapped / previewable) × parameter shape (none / checkable / value).

Commands (the verbs)

  • IPreviewableCommand : ICommandPreview(object?) + CancelPreview(object?) + bool CanPreview. Dry-run contract: Preview = a dry-run Execute (produce the effect, commit nothing), CancelPreview = unwind byte-exact, Execute = do the thing for real — ordinary execution, zero preview awareness (the session cancels any active dry-run first). CanPreview is the structural gate ("do I support dry-runs at all") vs CanExecute's runtime gate — a deliberate departure from Actipro's Avalonia interface, needed because we wrap (probes must never lie).
  • DelegateCommand — the plain delegate-backed IPreviewableCommand building block (CanPreview = preview delegate supplied; no auto-raise — that's the wrapper's courtesy).
  • BarCommand : UIObject — wraps a bindable Command StyledProperty (<BarCommand Command="{Binding SaveCommand}"/>); delegate ctors are sugar over an inner DelegateCommand; implements IPreviewableCommand explicitly, delegation-only (CanPreview forwards off the inner; CancelPreview never gated). Inner-swap unhooks/rehooks CanExecuteChanged + raises; post-Execute courtesy re-raise preserved (the FB-27 convenience — works even over a mute raw ICommand). All five metadata props (Text/Icon/InputGestureText/IsCheckable/Description) became StyledProperties — source-compatible, whole solution compiles unchanged.

Parameters (the value channel — untouched by the wrapper, they live on the control)

  • ValueCommandParameter<T> : CheckableCommandParameter + non-generic IValueCommandParameter: Value (chosen) + PreviewValue (candidate) — two verbs ↔ two slots (Preview reads PreviewValue, Execute reads Value). No Action enum. Radio sets: CanExecute writes IsChecked = (Value == current); FB-27 reflects/coerces; composes with Handled/Override gating.

Sessions

BarComboBox/BarGallery: value session (commit-on-close via Execute, restore-on-dismiss) works with ANY ICommand; the live dry-run activates only on Command is IPreviewableCommand { CanPreview: true }. No default parameter provisioned (T not inferrable; no pair = byte-for-byte today's behavior, test-pinned). ComboBox gained OnDropDownOpened/OnDropDownClosed(bool committed) hooks. Atomicity (your ruling) is structural: mid-session gate flip → dry-run unwound, execution refused, face restored — no in-between.

Verification

Cursorial.UI.Tests 2922 · Cursorial.UI.Bars.Tests 268 · TextBox 75 · 0 warnings. FB-27/ComboBox/Bars matrices untouched-green; probe-honesty, inner-swap re-query, courtesy-raise, and delegate-sugar regression-pin tests added.

Semantics calls flagged for your review

  1. BarCommand.CanExecute is false while Command is null — surfaces grey until a command binds (the swap-raise re-enables). The old class always had delegates, so this state is new.
  2. DelegateCommand has no post-Execute auto-raise (building block); the courtesy raise lives in BarCommand.
  3. Earlier remaining questions: closed-state gesture provenance (deferred), Space-as-accept classification (WPF parity), CheckableCommandParameter.Set protected, stale-check-when-locally-disabled (FB candidate).

🤖 Generated with Claude Code

https://claude.ai/code/session_01UxMEA2t8CHYmYgNg3BZi3w

mstrobel and others added 5 commits July 7, 2026 14:51
…over shared commands

Adds the Actipro-style value-carrying command parameter on top of the FB-27
checkable machinery, so several controls can share ONE command while each
contributes its own value:

- ValueCommandParameter<T> : CheckableCommandParameter — a Value the control
  contributes on Execute, plus the live-preview trio (PreviewValue +
  ValueCommandParameterAction {Commit, Preview, CancelPreview}), all raising
  INotifyPropertyChanged. IValueCommandParameter is the non-generic surface
  framework controls write through without knowing T.
- Radio sets: on each re-query the command's CanExecute receives every bound
  control's own parameter, compares Value to the model, and reflects the match
  (IsChecked at the Bars layer; Override on plain ToggleButton) — exactly the
  matching member shows checked; Execute applies the clicked member's Value.
- ComboBox grows OnDropDownOpened/OnDropDownClosed(bool committed) hooks with a
  commit-flagged close channel (Enter / non-editable Space / item click commit;
  Escape / light-dismiss / focus-out / toggle-close dismiss). Non-editable
  Space now accepts like Enter (WPF parity).
- BarComboBox (and BarGallery, rebased onto it) natively route a value
  parameter: highlight-change previews (apply-without-commit), commit sends
  CancelPreview-then-Commit (one real operation), dismissal cancels and
  restores the pre-open selection. Payload writes and every execute are
  CanExecute-gated; Action always rests at Commit; the combo greys per CD25.
  No default parameter is auto-provisioned (T is not inferrable) — without the
  command+parameter pair, behavior is byte-for-byte as before.

Tests: parameter unit contract, framework + Bars radio sets (incl. FB-27
context-gate composition), gallery/combo preview-cancel-commit flows, and
no-parameter backward-compat. Suites: Cursorial.UI.Tests 2919, Bars 259, all
green; solution builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UxMEA2t8CHYmYgNg3BZi3w
…ough the gate

Mike's ruling on the mid-session gate question: a live preview is ATOMIC —
either committed or entirely rolled back, no in-between. Accordingly:

- BarCommand.Execute exempts a parameter carrying Action=CancelPreview from
  its CanExecute self-gate; Preview and Commit stay gated (a gated command
  refuses NEW tentative state and refuses commits). Custom ICommands with an
  internal gate are documented to honor the same exemption.
- BarComboBox dispatches the session's CancelPreview without a CanExecute
  check (Preview/Commit keep their gates, payload writes included), and a
  refused commit now nets a full rollback: the pre-open selection returns to
  the face, so the face never diverges from the unchanged model.
- Docs: the "keep CanExecute stable across a session" guidance is replaced by
  the atomicity contract on ValueCommandParameterAction.CancelPreview,
  ValueCommandParameter<T>, BarCommand.Execute, and the BarComboBox remarks —
  CancelPreview is a cleanup obligation, always dispatched and executed
  regardless of CanExecute; a command gated mid-session has its active preview
  rolled back and any commit refused.
- Tests: gate flips false mid-preview → dismissal still reverts byte-exact
  (cancel delivered through the gate); gate flips false mid-preview → commit
  gesture nets a full rollback (cancel executed, commit refused un-applied,
  face restored). The shared test command now carries the gate (default open),
  so the whole existing suite pins the stable-gate case unchanged.

Suites: Cursorial.UI.Tests 2919, Bars 261, TextBox 75 — all green; solution
builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UxMEA2t8CHYmYgNg3BZi3w
…-side Action channel

Design revision from Mike, combining the best of Actipro's two preview models:
the WPF ribbon's value-carrying parameter stays as the VALUE channel, while the
preview verbs move to the newer Avalonia Bars command-side shape.

- New IPreviewableCommand : ICommand in Cursorial.UI (mirrors Avalonia's
  Preview(object?)/CancelPreview(object?) exactly), documented as a DRY-RUN
  model: Preview is a dry-run Execute (produce the effect, commit nothing),
  CancelPreview unwinds it byte-exact, and Execute just does the thing for
  real — ordinary execution with zero preview awareness, since a previewing
  control always cancels the active dry-run before executing.
- ValueCommandParameterAction and IValueCommandParameter.Action are DROPPED —
  the verb is now the method, so nothing reads a mode off the parameter and a
  preview can never be mis-executed as the real thing by an unaware command.
- BarCommand implements IPreviewableCommand via optional ctor delegates
  (previewExecute/cancelPreviewExecute); without them the verbs no-op (not
  preview-capable). Preview self-gates on CanExecute (no new tentative state
  on a gated command); CancelPreview is NEVER gated — Mike's atomicity ruling
  is now structural, so the previous Execute-side CancelPreview gate exemption
  is reverted.
- BarComboBox/BarGallery: the value session (commit-on-close via Execute,
  restore-on-dismiss) works with ANY ICommand; the preview verbs dispatch only
  when Command is IPreviewableCommand. Gate-flip semantics unchanged: dry-run
  unwound, execution refused, face restored.
- Tests revised to the delegate shape; new UnawareCommand_NoPreview_
  CommitStillLands pins that a plain ICommand never sees a preview verb yet
  still receives the value commit; both gate-flip atomicity tests now pass
  structurally.

Suites: Cursorial.UI.Tests 2919, Bars 262, TextBox 75 — all green; solution
builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UxMEA2t8CHYmYgNg3BZi3w
…iewableCommand gains CanPreview

Final scope addition folding the BarCommand refactor into this PR (no
intermediate step, per Mike):

- BarCommand : UIObject now wraps a BINDABLE inner command: a Command
  StyledProperty holds the behavior (<BarCommand Command="{Binding Save}"/>)
  while the Bars display metadata stays on the wrapper — Text / Icon /
  InputGestureText / IsCheckable / Description are StyledProperties too
  (bindable; source-compatible with every existing object initializer).
  The delegate ctors become sugar over a new plain DelegateCommand
  (execute/canExecute + optional previewExecute/cancelPreviewExecute)
  assigned to Command.
- Every ICommand member forwards to the inner; CanExecute is false while
  Command is null (surfaces grey until a command binds). The inner's
  CanExecuteChanged surfaces through the wrapper's; an inner SWAP unhooks
  the old, hooks the new, and raises so every bound control re-queries
  immediately; Execute keeps the post-run COURTESY re-raise, so even a raw
  inner ICommand that never raises still re-syncs bound controls (the FB-27
  convenience). The parameters are untouched — they live on the control and
  pass through verbatim.
- IPreviewableCommand gains bool CanPreview — the STRUCTURAL gate ("do I
  support dry-runs at all") vs CanExecute's runtime gate ("may I run now") —
  a deliberate departure from Actipro's Avalonia interface (theirs never
  wraps), so wrapper probes never lie. BarCommand implements the interface
  EXPLICITLY by delegation: CanPreview => inner is IPreviewableCommand
  { CanPreview: true }; Preview forwards self-gated on CanExecute;
  CancelPreview forwards NEVER gated (atomicity ruling unchanged).
  DelegateCommand answers CanPreview from its supplied delegates.
- Probe sites (BarComboBox/BarGallery session start + close cancel) tightened
  to `is IPreviewableCommand { CanPreview: true }`.

Tests: new DelegateCommandTests (structural/runtime gates, ungated cancel, no
auto-raise) and BarCommandTests (null-inner semantics, courtesy raise over a
mute raw inner, inner-forwarded raises, swap re-raise + bound-control
re-query + old-inner unhook, probe honesty ends a wrapped non-previewable
session before it starts while the value commit still lands, full dry-run
session through the wrapper, delegate-ctor sugar regression-pin). The entire
pre-existing suite passed the refactor unmodified.

Suites: Cursorial.UI.Tests 2922, Bars 268, TextBox 75 — all green; solution
builds 0 warnings / 0 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UxMEA2t8CHYmYgNg3BZi3w
@mstrobel mstrobel marked this pull request as draft July 7, 2026 23:29
@mstrobel mstrobel changed the base branch from main to develop July 7, 2026 23:30
@mstrobel mstrobel marked this pull request as ready for review July 7, 2026 23:30
@mstrobel mstrobel merged commit 134ec17 into develop Jul 7, 2026
3 checks passed
@mstrobel mstrobel deleted the feat/value-command-parameter branch July 7, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant