fix: Raise WinUI-faithful ChildAdded/ChildRemoved UIA events by morning4coffe-dev · Pull Request #23599 · unoplatform/uno · GitHub
Skip to content

fix: Raise WinUI-faithful ChildAdded/ChildRemoved UIA events#23599

Draft
morning4coffe-dev wants to merge 2 commits into
masterfrom
dev/doti/win32-uia-childadded-events
Draft

fix: Raise WinUI-faithful ChildAdded/ChildRemoved UIA events#23599
morning4coffe-dev wants to merge 2 commits into
masterfrom
dev/doti/win32-uia-childadded-events

Conversation

@morning4coffe-dev

Copy link
Copy Markdown
Member

GitHub Issue: part of https://github.com/unoplatform/kahua-private/issues/493

PR Type:

🐞 Bugfix

What changed? 🚀

PR Checklist ✅

Copilot AI review requested due to automatic review settings June 29, 2026 20:12
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

return provider;
}

var peer = element.GetOrCreateAutomationPeer();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — peer created as side-effect in a "TryGetExisting" method

GetOrCreateAutomationPeer() can create a new automation peer on an element that is about to be removed from the tree. The callers of this method (only OnChildRemoved today) want to capture the existing runtime ID, not trigger peer construction. The method name TryGetExistingProviderForElement and the comment just above it ("Only an already-existing provider is meaningful here") both signal this intent.

If a non-creating accessor exists (e.g. a GetAutomationPeer() that returns an existing peer or null), use it here. If no such accessor exists, document why GetOrCreate is safe on a departing element, and consider renaming to GetProviderForElement to avoid the misleading contract.

Suggested change
var peer = element.GetOrCreateAutomationPeer();
var peer = element.GetOrCreateAutomationPeer();

(Replace with a non-creating accessor if one is available, or add a comment explaining the intended semantics.)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Skia/Win32 UIA provider implementation to raise WinUI-faithful StructureChanged events, specifically emitting ChildAdded and ChildRemoved (and bulk/coarse variants) instead of always falling back to ChildrenInvalidated, and gates the more expensive event/provider work behind a “clients are listening” check.

Changes:

  • Added UiaClientsAreListening() interop to detect whether UIA clients are subscribed.
  • Reworked structure-change coalescing to track Added/Removed/Invalidated and flush WinUI-faithful event types (including bulk thresholds).
  • Updated child add/remove handling to raise ChildAdded on the added element and ChildRemoved on the container with the removed child’s runtime id.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/Uno.UI.Runtime.Skia.Win32/Accessibility/Win32UIAutomationInterop.cs Adds UiaClientsAreListening() P/Invoke used to gate structure event work.
src/Uno.UI.Runtime.Skia.Win32/Accessibility/Win32Accessibility.cs Implements WinUI-faithful ChildAdded/ChildRemoved structure event queuing, grouping, and flushing (with bulk/coarse fallbacks).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +518 to +539
@@ -396,36 +528,103 @@ private void RaiseStructureChanged(Win32RawElementProvider provider)
{
_structureChangeFlushQueued = false;

// Short-circuit the flush if the window was closed while this callback
// was queued (edge case "Window close during dispatch").
// Short-circuit the flush if the window was closed while this callback was queued.
if (IsDisposed)
{
_pendingStructureChanges.Clear();
return;
}

foreach (var pending in _pendingStructureChanges)
FlushStructureChanges();
});
Comment thread src/Uno.UI.Runtime.Skia.Win32/Accessibility/Win32Accessibility.cs Outdated
Comment on lines +49 to +51
// Matches WinUI's AP_BULK_CHILDREN_LIMIT: more than this many add/remove on one container in a
// single coalescing window collapses to a single ChildrenInvalidated/ChildrenBulk* event.
private const int BulkChildrenLimit = 20;
Comment on lines +298 to +302
var ancestorProvider = FindNearestAncestorProvider(parent);
if (ancestorProvider is not null)
if (ancestorProvider is null)
{
return;
}
Comment on lines 384 to 388
var ancestorProvider = FindNearestAncestorProvider(parent);
if (ancestorProvider is not null)
if (ancestorProvider is null)
{
RaiseStructureChanged(ancestorProvider);
return;
}
Comment on lines +583 to +589
if (group.Invalidated || total > BulkChildrenLimit)
{
RaiseStructureChangedCore(container, StructureChangeType.ChildrenInvalidated, null);
continue;
}

if (group.Added.Count >= BulkChildrenLimit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor — inconsistent bulk threshold operator

The combined total guard uses > BulkChildrenLimit (strict: > 20), but the per-type guards below use >= BulkChildrenLimit (at least 20). This means a container with exactly 20 adds and 0 removes skips the ChildrenInvalidated path and falls through to the ChildrenBulkAdded path — which may or may not match WinUI's AP_BULK_CHILDREN_LIMIT semantics.

The constant comment says "more than this many", implying >, yet the per-type branches use >=. Align the operators and/or add a note explaining the deliberate asymmetry if it's intentional:

// Too many changes (or an explicit coarse request) -> a single ChildrenInvalidated.
if (group.Invalidated || total > BulkChildrenLimit)
{
    RaiseStructureChangedCore(container, StructureChangeType.ChildrenInvalidated, null);
    continue;
}

if (group.Added.Count >= BulkChildrenLimit)  // >= here but > above — document intent

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23599/wasm-skia-net9/index.html

@unodevops

Copy link
Copy Markdown
Contributor

⚠️⚠️ The build 220178 has failed on Uno.UI - CI.

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @morning4coffe-dev's task in 5m 3s —— View job


Review complete

  • Read changed files
  • Analyze diff
  • Review code against AGENTS.md rules
  • Post inline findings
  • Post summary

Summary

The second commit (99b4addb) correctly addresses all three issues raised in the prior review:

  • GetOrCreateAutomationPeer side-effect → fixed to element.CachedAutomationPeer in TryGetExistingProviderForElement
  • Redundant ancestorProvider null-checks → removed; FindNearestAncestorProvider now returns Win32RawElementProvider (non-nullable), with _rootProvider as fallback
  • TryEnqueue return value ignored → now properly resets _structureChangeFlushQueued = false on failure

The core logic (ChildAdded on the entering element, ChildRemoved on the container with the child's runtime ID, bulk thresholding, coalesced dispatcher flush) is correct and faithful to WinUI semantics.

Severity Count
Minor 1

Inline finding: The total > BulkChildrenLimit (strict, line 579) vs. Count >= BulkChildrenLimit (non-strict, lines 585/598) asymmetry is intentional — the updated BulkChildrenLimit comment explains it — but a short // deliberate >= annotation at the per-type sites prevents the next reader from suspecting a typo.

Cross-cutting concerns

Missing runtime tests (unchecked in PR checklist)
The behavioral change (ChildAdded fires on the element, not the container; ChildRemoved carries the removed child's runtime ID; BulkChildrenLimit collapse path) is exactly the kind of subtle contract that silently regresses. At minimum, tests should verify:

  • ChildAdded fires on the added element, not the ancestor container
  • ChildRemoved fires on the container with the correct child runtime ID
  • The BulkChildrenLimit collapse path triggers ChildrenInvalidated at threshold + 1

Empty "What changed?" section in PR description
Given the private-only issue reference, a public description of the bug and how it manifested (screen reader doesn't announce newly revealed content) would help future readers understand the change without access to the private tracker.

continue;
}

if (group.Added.Count >= BulkChildrenLimit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor — deliberate >= / > asymmetry deserves an inline note

The total guard (line 579) uses strict > (total > BulkChildrenLimit), while the per-type guards here use >=. The comment on BulkChildrenLimit was improved in commit 2 to describe "per-type count that reaches this emits Bulk*", which documents the intent — but the single-character difference between > and >= across these three checks is easy to misread as a typo, especially since the constant name and the old comment both said "more than this many". A short annotation at the >= sites would cement it:

Suggested change
if (group.Added.Count >= BulkChildrenLimit)
if (group.Added.Count >= BulkChildrenLimit) // deliberate >=: exactly BulkChildrenLimit → Bulk* (not Invalidated)

Same applies at line 598 for group.Removed.Count >= BulkChildrenLimit.

}
}

if (group.Removed.Count >= BulkChildrenLimit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same >= note as line 585 — a brief comment here would help confirm this is intentional:

Suggested change
if (group.Removed.Count >= BulkChildrenLimit)
if (group.Removed.Count >= BulkChildrenLimit) // deliberate >=: exactly BulkChildrenLimit → Bulk* (not Invalidated)

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-23599/wasm-skia-net9/index.html

@unodevops

Copy link
Copy Markdown
Contributor

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.

3 participants