Skip to content

GridView raises duplicate OnSelectedIndexChanged echoes on two same-frame programmatic SelectedIndex writes (pre-existing, mechanism-independent) #464

@codemonkeychris

Description

@codemonkeychris

Summary

GridView (the simple GridViewElement arm) raises two spurious OnSelectedIndexChanged callbacks when two programmatic SelectedIndex writes are applied through the reconciler without a render / dispatcher drain between them. The selection echo escapes suppression on both writes. ListBox does not exhibit this under the same test.

This was found while investigating PR #455 CR item #1. It is pre-existing (present on main) and mechanism-independent — it reproduces identically under both the §8 value-diff arm and the causal counter (ShouldSuppress/WriteSuppressed), so it is not caused by either echo-suppression mechanism.

Severity / reachability

Low. The normal render loop performs one reconcile pass per render, visiting each element's Update once, so two UpdateChilds on the same control with no intervening dispatcher drain does not occur in production. Filing so it isn't lost — batched/coalesced update paths (or future changes to the render scheduler) could make it reachable, and it points at a GridView-specific extra SelectionChanged fire worth understanding.

Repro (self-test fixture sketch)

var rec = new Reconciler();
int userFire = 0;
var el = new GridViewElement(items) { SelectedIndex = 0, OnSelectedIndexChanged = _ => userFire++ };
var gv = (WinUI.GridView)rec.Mount(el, noOp);
parent.Children.Add(gv);
await Harness.Render();           // realize containers
await Harness.Render();
userFire = 0;

// two reconciler-driven writes, NO render/drain between them
rec.UpdateChild(el  with { SelectedIndex = 2 }, el with { SelectedIndex = 1 }, gv, noOp);
rec.UpdateChild(el  with { SelectedIndex = 1 }, el with { SelectedIndex = 3 }, gv, noOp);
await Harness.Render();

// observed: userFire == 2  (expected 0); gv.SelectedIndex == 3 (correct)

Observed numbers (x64 selftest host, PR #455 probe)

  • GridView double-write: userFire = 2, final index 3 — under both value-diff and counter.
  • ListBox same scenario: userFire = 0 (no leak).
  • Single programmatic write per render: correctly suppressed for both controls (no leak) — this is the production-reachable path and is fine.

Notes / hypothesis

A single post-realization programmatic write fires SelectionChanged exactly once and synchronously (verified). The double-leak therefore suggests GridView emits an additional deferred/coalesced SelectionChanged when the index is changed twice in quick succession, which arrives after the per-write suppression token/arm has already been consumed/cleared. Worth confirming against the WinUI GridView/ListViewBase selection plumbing.

Suggested follow-up

  • Add a permanent regression fixture once the root cause is understood.
  • Decide whether the engine should coalesce multiple same-frame controlled writes to a single suppressed write (would also neutralize this), or whether suppression should tolerate N deferred echoes per N writes (the counter already increments — the gap is the extra GridView fire beyond N).

Found during PR #455 (spec-047 §4.10) review follow-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions