[Issue-Resolver] Fix #33264 - RadioButtonGroup not working with Collection View#33268
[Issue-Resolver] Fix #33264 - RadioButtonGroup not working with Collection View#33268kubaflo wants to merge 3 commits intodotnet:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes a bug where RadioButtonGroup.SelectedValue binding fails when RadioButtons are inside a CollectionView's ItemTemplate. The issue occurred because RadioButtons in templated contexts never triggered ChildAdded events on the parent layout where RadioButtonGroup properties were set, preventing controller registration.
Key Changes:
- Added dynamic controller discovery when RadioButtons are checked without registration
- Enhanced RadioButtonGroupController to proactively register RadioButtons found via tree traversal
- Exposed controller access via RadioButtonGroup.GetController() for RadioButton to use
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Issue33264.cs | NUnit UI test verifying RadioButtonGroup binding works in CollectionView |
| Issue33264.xaml.cs | Test page code-behind with ViewModel and state logging |
| Issue33264.xaml | Test page XAML demonstrating RadioButtons in CollectionView DataTemplate |
| RadioButtonGroupController.cs | Added RegisterRadioButton(), dynamic discovery in SetSelectedValue()/SetGroupName() |
| RadioButtonGroup.cs | Exposed GetController() for RadioButton access |
| RadioButton.cs | Added FindControllerInAncestors() and dynamic registration in OnGroupSelectionChanged() |
Comments suppressed due to low confidence (1)
src/Controls/src/Core/RadioButton/RadioButtonGroupController.cs:220
- Calling Descendants() on the layout in SetSelectedValue() can be expensive, especially when called frequently during binding updates. Since this method is called every time SelectedValue changes, repeatedly traversing the entire visual tree to discover and register RadioButtons could impact performance. Consider caching discovered RadioButtons or using a more targeted approach when the CollectionView's items change, rather than scanning the entire tree on every value change.
foreach (var child in _layout.Descendants())
{
if (child is RadioButton radioButton && radioButton.GroupName == _groupName)
{
// Register the RadioButton if not already registered
if (!groupControllers.TryGetValue(radioButton, out _))
{
groupControllers.Add(radioButton, this);
}
if (radioButtonValue is not null)
{
if (radioButton.Value is not null && radioButton.Value.Equals(radioButtonValue))
{
radioButton.SetValue(RadioButton.IsCheckedProperty, true, specificity: SetterSpecificity.FromHandler);
}
}
else
{
// Setting null - uncheck the selected radio button in the group
if (radioButton.IsChecked)
{
radioButton.SetValue(RadioButton.IsCheckedProperty, false, specificity: SetterSpecificity.FromHandler);
}
}
}
}
| foreach (var descendant in _layout.Descendants()) | ||
| { | ||
| if (descendant is RadioButton radioButton) | ||
| { | ||
| // Only register RadioButtons that match our group or have no group set | ||
| var rbGroupName = radioButton.GroupName; | ||
| if (rbGroupName == _groupName || (string.IsNullOrEmpty(rbGroupName) && string.IsNullOrEmpty(oldName))) | ||
| { | ||
| if (!groupControllers.TryGetValue(radioButton, out _)) | ||
| { | ||
| groupControllers.Add(radioButton, this); | ||
| } | ||
|
|
||
| // If this RadioButton is checked, update SelectedValue | ||
| if (radioButton.IsChecked && object.Equals(radioButton.Value, _selectedValue) == false) | ||
| { | ||
| _layout.SetValue(RadioButtonGroup.SelectedValueProperty, radioButton.Value); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Calling Descendants() in SetGroupName() after every group name change is expensive and can impact performance. This method traverses the entire visual tree to discover RadioButtons. Consider whether this discovery needs to happen on every group name change, or if it could be optimized by only scanning when necessary (e.g., when the group name is initially set or when known structural changes occur).
| App.WaitForElement("SelectedValueLabel"); | ||
|
|
||
| // Give it a moment to render | ||
| Task.Delay(500).Wait(); |
There was a problem hiding this comment.
Using Task.Delay().Wait() with a hardcoded delay is not a reliable approach for UI testing. This pattern is discouraged in the MAUI test infrastructure. Instead, rely on App.WaitForElement() which has built-in retry logic. If you need additional time for rendering, App.WaitForElement() already handles this with its timeout mechanism.
| App.Tap("Choice 2"); | ||
|
|
||
| // Wait for binding update | ||
| Task.Delay(1000).Wait(); |
There was a problem hiding this comment.
Using Task.Delay().Wait() for hardcoded wait times is unreliable for UI tests. App.WaitForElement() already includes appropriate timeout logic. Remove this line and rely on the WaitForElement calls to handle timing.
| App.Tap("Choice 3"); | ||
|
|
||
| // Wait for binding update | ||
| Task.Delay(1000).Wait(); |
There was a problem hiding this comment.
Using Task.Delay().Wait() for hardcoded wait times is unreliable for UI tests. App.WaitForElement() already includes appropriate timeout logic. Remove this line and rely on the WaitForElement calls to handle timing.
PureWeen
left a comment
There was a problem hiding this comment.
Thoughts?
Review Feedback: PR #33268 - RadioButtonGroup not working with CollectionView
🤖 Copilot Session Summary
This review was conducted through an interactive deep-dive session with the following prompts:
-
"I want to do a deep review of PR 33268 can you please 1) analyze the fix 2) see if you can think of alternative fixes? 3) test those fixes against the UITests that were added"
- Analyzed the PR's code changes and root cause
- Identified the regression from MessagingCenter removal in .NET 10 Preview 5
- Proposed two alternative fixes with different trade-offs
-
"Feel free to test all your alternatives to see if they fix the issue in a better way. It's fine to assume the fix on the PR is wrong and you need to start from your own solution. Exhaust your alternatives."
- Reverted PR fix and validated UI tests fail correctly without fix
- Implemented and tested Alternative 1 (Minimal Ancestor Search - 44 lines)
- Implemented and tested Alternative 2 (DescendantAdded event - 14 lines)
- Both alternatives passed the same UI tests as the PR's fix
-
"Please review the UITests on this PR and see if you think they provide adequate coverage. Please add any additional UITests. Please revert the changes on the PR that fix the problem and validate that the UITests correctly fail with those changes reverted."
- Reviewed existing UI test coverage
- Attempted to add edge case tests (discovered issue number conflict causing app crash)
- Validated tests correctly fail without fix and pass with fix
-
"So all of the UITests added on this PR pass with your suggested changes?"
- Confirmed Alternative 2 (DescendantAdded) passes PR's UI tests with only 14 lines of changes
Recommendation
Required changes:
- Consider using the simpler
DescendantAddedapproach (14 lines) instead of the current ancestor-search approach (93 lines)
Recommended changes:
- Remove redundant registration code from
SetSelectedValue()andSetGroupName()if keeping current approach - Add null check for
parent as BindableObjectinFindControllerInAncestors()to prevent potential null reference
📋 Full PR Review Details
Summary
PR #33268 fixes a regression where RadioButtonGroup.SelectedValue binding doesn't work when RadioButtons are inside a CollectionView's ItemTemplate. The fix is functionally correct but over-engineered. I identified two simpler alternatives that achieve the same result with significantly less code.
Root Cause Analysis
The regression was introduced in commit 6a96e92d50 when MessagingCenter was removed from RadioButtonGroup in .NET 10 Preview 5.
Why it worked before (MessagingCenter approach):
- RadioButtons broadcast selection changes via MessagingCenter
- Any layout with matching GroupName could receive and process the message
- Decoupled from parent-child hierarchy
Why it broke (ConditionalWeakTable approach):
- RadioButtons are registered via
ChildAddedevents on the parent layout - CollectionView creates RadioButtons inside virtualized containers
- These RadioButtons are NOT direct children of the layout where
RadioButtonGroup.GroupNameis set ChildAddedonly fires when CollectionView is added, not when items inside it are created
Fix Comparison
| Approach | Lines Changed | Complexity | Pros | Cons |
|---|---|---|---|---|
| PR's Fix | 93 lines | High | Comprehensive | Redundant code, DRY violation |
| Alternative 1 (Ancestor Search) | 44 lines | Medium | Lazy registration | Runs on every selection |
| Alternative 2 (DescendantAdded) | 14 lines | Low | Proactive, minimal | Event overhead |
PR's Approach (93 lines)
Adds ancestor search in OnGroupSelectionChanged() + redundant registration in 4 other places:
RegisterRadioButton()HandleRadioButtonGroupSelectionChanged()SetSelectedValue()SetGroupName()
Alternative 1: Minimal Ancestor Search (44 lines)
Only adds ancestor search in OnGroupSelectionChanged(). Registration happens lazily when RadioButton is checked.
// In RadioButton.cs
RadioButtonGroupController FindAndRegisterWithAncestorController()
{
var parent = this.Parent;
while (parent != null)
{
if (parent is BindableObject bindable)
{
var parentGroupName = RadioButtonGroup.GetGroupName(bindable);
if (parentGroupName == this.GroupName)
{
var controller = RadioButtonGroup.GetController(bindable);
if (controller != null)
{
RadioButtonGroupController.RegisterRadioButton(this, controller);
return controller;
}
}
}
parent = parent.Parent;
}
return null;
}Alternative 2: DescendantAdded Event (14 lines) - RECOMMENDED
Subscribe to DescendantAdded event which fires for ALL descendants at any depth:
// In RadioButtonGroupController constructor
_layout.DescendantAdded += DescendantAdded;
// New handler
void DescendantAdded(object sender, ElementEventArgs e)
{
if (string.IsNullOrEmpty(_groupName) || _layout == null)
return;
if (e.Element is RadioButton radioButton)
AddRadioButton(radioButton);
}Why Alternative 2 is best:
- Minimal code change (14 lines in 1 file)
- Uses existing infrastructure (
DescendantAddedevent) - Proactive registration (RadioButtons registered as soon as added)
- No ancestor walking needed
- No redundant registration code
Test Coverage
The PR includes a UI test that properly validates the fix:
- ✅ Tests selection change updates binding
- ✅ Tests multiple selection changes
- ✅ Test correctly fails without fix, passes with fix
Testing Results
| Test | Without Fix | With PR Fix | Alt 1 | Alt 2 |
|---|---|---|---|---|
| RadioButtonGroupBindingWorksInsideCollectionView | ❌ FAIL | ✅ PASS | ✅ PASS | ✅ PASS |
Platform: iOS (iPhone Xs, iOS 18.5)
Issues Found
Must Fix
- Redundant registration code - The same registration logic appears in 5 places, violating DRY principle
Should Fix
-
Potential NullReferenceException in
FindControllerInAncestors():var parentGroupName = RadioButtonGroup.GetGroupName(parent as BindableObject);
If
parentis not aBindableObject, this passesnulltoGetGroupName(), which may throw. -
Missing DescendantRemoved handler - If using
DescendantAdded, should also handleDescendantRemovedfor cleanup (though not strictly necessary due toConditionalWeakTable)
Approval Checklist
- Code solves the stated problem
- Minimal, focused changes (over-engineered)
- Appropriate test coverage
- No security concerns
- Follows .NET MAUI conventions
Recommendation
Replace the current implementation with Alternative 2 (DescendantAdded). It:
- Reduces code from 93 lines to 14 lines
- Eliminates redundant registration code
- Uses existing MAUI infrastructure
- Is easier to maintain and understand
Review Metadata
|
/rebase |
… CollectionView
…ing with CollectionView" This reverts commit 8a3d4ca.
Added event handlers for DescendantAdded and DescendantRemoved to ensure radio buttons added or removed as descendants are properly managed by the group controller. This improves dynamic handling of radio button groups when the visual tree changes.
34ce9d4 to
47b8bd5
Compare
PureWeen
left a comment
There was a problem hiding this comment.
could this be a unit test vs a UITest?
|
Why a Unit Test Would Fail A unit test like this would not work: The current UI test is correctly implemented and necessary because it |
yea I think we could still fake the templates getting created or possibly use a different view with the same characteristics? |
|
Reopening |
|
I can't reopen so I've created another one #33343 |
Fix #33264 - RadioButtonGroup not working with CollectionView
Fixes #33264
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Summary
Fixed
RadioButtonGroup.SelectedValuebinding not working when RadioButtons are inside a CollectionView's ItemTemplate while RadioButtonGroup attached properties are set on a parent layout outside the CollectionView.Quick verification:
Root Cause
RadioButtonGroupController only registered RadioButtons through
ChildAddedevents. When RadioButtons were dynamically created inside a CollectionView's ItemTemplate, they never triggeredChildAddedevents on the parent layout whereRadioButtonGroup.GroupNameandRadioButtonGroup.SelectedValuewere set.Without registration, when a RadioButton was checked,
GetGroupController()returned null, soHandleRadioButtonGroupSelectionChanged()was never called and theSelectedValuebinding was never updated.Solution
Enhanced RadioButton to actively search up the visual tree for a matching RadioButtonGroup controller when checked, rather than relying solely on event-based registration:
Key Changes:
RadioButton.OnGroupSelectionChanged()- If no controller registered, searches ancestors for matching group and registers dynamicallyRadioButton.FindControllerInAncestors()- Walks up visual tree to find layout with matchingRadioButtonGroup.GroupNameRadioButtonGroupController.RegisterRadioButton()- Public method to register RadioButton with controllerRadioButtonGroup.GetController()- Exposes controller access for RadioButtonRadioButtonGroupController.SetSelectedValue()andSetGroupName()- Enhanced to proactively register discovered RadioButtonsFiles Changed:
src/Controls/src/Core/RadioButton/RadioButton.cs- Added ancestor search logic (+33 lines)src/Controls/src/Core/RadioButton/RadioButtonGroupController.cs- Added dynamic registration (+55 lines)src/Controls/src/Core/RadioButton/RadioButtonGroup.cs- Exposed controller access (+5 lines)Testing
Before fix (Android):
After fix (Android):
After fix (iOS):
Edge Cases Tested:
Platforms Tested:
Test Coverage
src/Controls/tests/TestCases.HostApp/Issues/Issue33264.xamlsrc/Controls/tests/TestCases.HostApp/Issues/Issue33264.xaml.cssrc/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue33264.csBreaking Changes
None - the fix adds proactive discovery without changing existing behavior for scenarios that already work.