Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/Controls/src/Core/RadioButton/RadioButtonGroupController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public RadioButtonGroupController(Maui.ILayout layout)
_layout = (Element)layout;
_layout.ChildAdded += ChildAdded;
_layout.ChildRemoved += ChildRemoved;
_layout.DescendantAdded += DescendantAdded;
Comment thread
kubaflo marked this conversation as resolved.
_layout.DescendantRemoved += DescendantRemoved;
Comment on lines +25 to +26

Copilot AI Jan 1, 2026

Copy link

Choose a reason for hiding this comment

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

The PR description mentions significant changes to RadioButton.cs, RadioButtonGroup.cs, and test files (Issue33264.xaml, Issue33264.xaml.cs, and Issue33264.cs) that are not present in this diff. The actual changes in this PR are limited to:

  1. RadioButtonGroupController.cs - Event handler changes from ChildAdded/ChildRemoved to DescendantAdded/DescendantRemoved
  2. RadioButtonTests.cs - Two new unit tests

Please verify that all intended changes were included in this pull request, or update the PR description to accurately reflect the actual changes being made.

Copilot uses AI. Check for mistakes.

if (!string.IsNullOrEmpty(_groupName))
{
Expand Down Expand Up @@ -97,6 +99,30 @@ void ChildRemoved(object sender, ElementEventArgs e)
}
}

void DescendantAdded(object sender, ElementEventArgs e)
{
if (string.IsNullOrEmpty(_groupName) || _layout == null)
{
return;
}

if (e.Element is RadioButton radioButton)
{
AddRadioButton(radioButton);
}
}

void DescendantRemoved(object sender, ElementEventArgs e)
{
if (e.Element is RadioButton radioButton)
{
if (groupControllers.TryGetValue(radioButton, out _))
{
groupControllers.Remove(radioButton);
}
}
}

internal void HandleRadioButtonValueChanged(RadioButton radioButton)
{
if (radioButton?.GroupName != _groupName)
Expand Down
90 changes: 90 additions & 0 deletions src/Controls/tests/Core.UnitTests/RadioButtonTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,5 +319,95 @@ public void ValuePropertyCanBeSetToNull()

Assert.Null(radioButton.Value);
}

[Fact]
public void RadioButtonGroupWorksWithDynamicallyAddedDescendants()
{
// Simulates CollectionView scenario where RadioButtons are added as descendants
// rather than direct children (they're inside ItemTemplate)
var layout = new StackLayout();
var groupName = "choices";

// Set up RadioButtonGroup on parent layout
layout.SetValue(RadioButtonGroup.GroupNameProperty, groupName);
layout.SetValue(RadioButtonGroup.SelectedValueProperty, null);

// Create a container that simulates CollectionView item container
var itemContainer = new StackLayout();
layout.Children.Add(itemContainer);

// Create RadioButtons and add them to the nested container
// This triggers DescendantAdded events (like CollectionView does)
var radioButton1 = new RadioButton() { Value = "Choice 1" };
var radioButton2 = new RadioButton() { Value = "Choice 2" };
var radioButton3 = new RadioButton() { Value = "Choice 3" };

itemContainer.Children.Add(radioButton1);
itemContainer.Children.Add(radioButton2);
itemContainer.Children.Add(radioButton3);

// Verify RadioButtons received the group name from ancestor
Assert.Equal(groupName, radioButton1.GroupName);
Assert.Equal(groupName, radioButton2.GroupName);
Assert.Equal(groupName, radioButton3.GroupName);

// Verify SelectedValue is initially null
Assert.Null(layout.GetValue(RadioButtonGroup.SelectedValueProperty));

// Check a RadioButton
radioButton2.IsChecked = true;

// Verify SelectedValue binding is updated
Assert.Equal("Choice 2", layout.GetValue(RadioButtonGroup.SelectedValueProperty));

// Check another RadioButton
radioButton3.IsChecked = true;

// Verify SelectedValue binding updates again
Assert.Equal("Choice 3", layout.GetValue(RadioButtonGroup.SelectedValueProperty));

// Verify only one RadioButton is checked
Assert.False(radioButton1.IsChecked);
Assert.False(radioButton2.IsChecked);
Assert.True(radioButton3.IsChecked);
}

[Fact]
public void RadioButtonGroupSelectedValueBindingWorksWithNestedDescendants()
{
// Tests that setting SelectedValue on the group selects the correct descendant RadioButton
var layout = new StackLayout();
var groupName = "choices";

layout.SetValue(RadioButtonGroup.GroupNameProperty, groupName);

// Nested container simulating CollectionView
var itemContainer = new StackLayout();
layout.Children.Add(itemContainer);

var radioButton1 = new RadioButton() { Value = "Choice 1" };
var radioButton2 = new RadioButton() { Value = "Choice 2" };
var radioButton3 = new RadioButton() { Value = "Choice 3" };

itemContainer.Children.Add(radioButton1);
itemContainer.Children.Add(radioButton2);
itemContainer.Children.Add(radioButton3);

// Set SelectedValue from the group (simulates binding update)
layout.SetValue(RadioButtonGroup.SelectedValueProperty, "Choice 2");

// Verify the correct RadioButton is checked
Assert.False(radioButton1.IsChecked);
Assert.True(radioButton2.IsChecked);
Assert.False(radioButton3.IsChecked);

// Change SelectedValue
layout.SetValue(RadioButtonGroup.SelectedValueProperty, "Choice 3");

// Verify selection updates
Assert.False(radioButton1.IsChecked);
Assert.False(radioButton2.IsChecked);
Assert.True(radioButton3.IsChecked);
}
}
}
23 changes: 23 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue33264.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issue33264"
Title="Issue 33264">

<StackLayout Padding="20" Spacing="10" RadioButtonGroup.GroupName="choices" RadioButtonGroup.SelectedValue="{Binding SelectedValue}">
<Label Text="RadioButtonGroup not working with CollectionView" FontSize="18" FontAttributes="Bold"/>

<Label Text="Select an option below:" FontSize="14"/>

<CollectionView ItemsSource="{Binding Choices}" AutomationId="ChoicesCollectionView">
<CollectionView.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding}" Value="{Binding}" GroupName="choices" AutomationId="{Binding}"/>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>

<Label Text="Selected value:" FontSize="14" FontAttributes="Bold"/>
<Label Text="{Binding SelectedValueDisplay}" FontSize="16" AutomationId="SelectedValueLabel"/>
</StackLayout>
</ContentPage>
68 changes: 68 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue33264.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Maui.Controls.Sample.Issues;

[Issue(IssueTracker.Github, 33264, "RadioButtonGroup not working with CollectionView", PlatformAffected.All)]
public partial class Issue33264 : ContentPage
{
public Issue33264()
{
InitializeComponent();
BindingContext = new Issue33264ViewModel();
}

protected override void OnAppearing()
{
base.OnAppearing();
Dispatcher.DispatchDelayed(TimeSpan.FromMilliseconds(500), () =>
{
CaptureState("OnAppearing");
});
}

private void CaptureState(string context)
{
var vm = BindingContext as Issue33264ViewModel;
Console.WriteLine($"=== STATE CAPTURE: {context} ===");
Console.WriteLine($"SelectedValue: {vm?.SelectedValue ?? "null"}");
Console.WriteLine("=== END STATE CAPTURE ===");
}
}

public class Issue33264ViewModel : INotifyPropertyChanged
{
private string _selectedValue;

public ObservableCollection<string> Choices { get; } = new ObservableCollection<string>
{
"Choice 1",
"Choice 2",
"Choice 3"
};

public string SelectedValue
{
get => _selectedValue;
set
{
if (_selectedValue != value)
{
Console.WriteLine($"=== BINDING UPDATE: SelectedValue changing from '{_selectedValue}' to '{value}' ===");
_selectedValue = value;
OnPropertyChanged();
OnPropertyChanged(nameof(SelectedValueDisplay));
}
}
}

public string SelectedValueDisplay => string.IsNullOrEmpty(SelectedValue) ? "None" : SelectedValue;

public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues;

public class Issue33264 : _IssuesUITest
{
public override string Issue => "RadioButtonGroup not working with CollectionView";

public Issue33264(TestDevice device) : base(device) { }

[Test]
[Category(UITestCategories.RadioButton)]
public void RadioButtonGroupBindingWorksInsideCollectionView()
{
// Wait for all elements to load
App.WaitForElement("ChoicesCollectionView");
App.WaitForElement("Choice 2");
App.WaitForElement("SelectedValueLabel");

// Initially, SelectedValue should show "None"
var initialValue = App.FindElement("SelectedValueLabel").GetText();
Console.WriteLine($"Initial SelectedValue: '{initialValue}'");
Assert.That(initialValue, Is.EqualTo("None"), "Initial value should be 'None'");

// Tap "Choice 2" radio button
App.Tap("Choice 2");

// Wait for binding update by polling the label text
App.WaitForTextToBePresentInElement("SelectedValueLabel", "Choice 2");

// Verify SelectedValue is updated
var selectedValue = App.FindElement("SelectedValueLabel").GetText();
Console.WriteLine($"After tapping 'Choice 2', SelectedValue: '{selectedValue}'");
Assert.That(selectedValue, Is.EqualTo("Choice 2"), "SelectedValue should be updated via binding when RadioButton is checked");

// Tap "Choice 3" radio button
App.Tap("Choice 3");

// Wait for binding update by polling the label text
App.WaitForTextToBePresentInElement("SelectedValueLabel", "Choice 3");

// Verify SelectedValue is updated again
selectedValue = App.FindElement("SelectedValueLabel").GetText();
Console.WriteLine($"After tapping 'Choice 3', SelectedValue: '{selectedValue}'");
Assert.That(selectedValue, Is.EqualTo("Choice 3"), "SelectedValue should be updated when different RadioButton is checked");
}
}
Loading