diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml index 0882817a9a..9525f6187e 100644 --- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml +++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml @@ -22,6 +22,19 @@ Three States: Option 3 Disabled + + Group A: Option 1 + Group A: Disabled + Group B: Option 1 + Group B: Option 3 + + + Group A: Option 2 + Group B: Option 2 + Group B: Option 4 + \ No newline at end of file diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index 945335b8f7..f8bcd8b478 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -2,19 +2,144 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Primitives; +using Avalonia.Rendering; using Avalonia.VisualTree; namespace Avalonia.Controls { public class RadioButton : ToggleButton { + private class RadioButtonGroupManager + { + public static readonly RadioButtonGroupManager Default = new RadioButtonGroupManager(); + static readonly List<(WeakReference Root, RadioButtonGroupManager Manager)> s_registeredVisualRoots + = new List<(WeakReference Root, RadioButtonGroupManager Manager)>(); + + readonly Dictionary>> s_registeredGroups + = new Dictionary>>(); + + public static RadioButtonGroupManager GetOrCreateForRoot(IRenderRoot root) + { + if (root == null) + return Default; + lock (s_registeredVisualRoots) + { + int i = 0; + while (i < s_registeredVisualRoots.Count) + { + var item = s_registeredVisualRoots[i].Root; + if (!item.TryGetTarget(out var target)) + { + s_registeredVisualRoots.RemoveAt(i); + continue; + } + if (root == target) + break; + i++; + } + RadioButtonGroupManager manager; + if (i >= s_registeredVisualRoots.Count) + { + manager = new RadioButtonGroupManager(); + s_registeredVisualRoots.Add((new WeakReference(root), manager)); + } + else + { + manager = s_registeredVisualRoots[i].Manager; + } + return manager; + } + } + + public void Add(RadioButton radioButton) + { + lock (s_registeredGroups) + { + string groupName = radioButton.GroupName; + if (!s_registeredGroups.TryGetValue(groupName, out var group)) + { + group = new List>(); + s_registeredGroups.Add(groupName, group); + } + group.Add(new WeakReference(radioButton)); + } + } + + public void Remove(RadioButton radioButton, string oldGroupName) + { + lock (s_registeredGroups) + { + if (!string.IsNullOrEmpty(oldGroupName) && s_registeredGroups.TryGetValue(oldGroupName, out var group)) + { + int i = 0; + while (i < group.Count) + { + if (!group[i].TryGetTarget(out var button) || button == radioButton) + { + group.RemoveAt(i); + continue; + } + i++; + } + if (group.Count == 0) + { + s_registeredGroups.Remove(oldGroupName); + } + } + } + } + + public void SetChecked(RadioButton radioButton) + { + lock (s_registeredGroups) + { + string groupName = radioButton.GroupName; + if (s_registeredGroups.TryGetValue(groupName, out var group)) + { + int i = 0; + while (i < group.Count) + { + if (!group[i].TryGetTarget(out var current)) + { + group.RemoveAt(i); + continue; + } + if (current != radioButton && current.IsChecked.GetValueOrDefault()) + current.IsChecked = false; + i++; + } + if (group.Count == 0) + { + s_registeredGroups.Remove(groupName); + } + } + } + } + } + + public static readonly DirectProperty GroupNameProperty = + AvaloniaProperty.RegisterDirect( + nameof(GroupName), + o => o.GroupName, + (o, v) => o.GroupName = v); + + private string _groupName; + private RadioButtonGroupManager _groupManager; + public RadioButton() { this.GetObservable(IsCheckedProperty).Subscribe(IsCheckedChanged); } + public string GroupName + { + get { return _groupName; } + set { SetGroupName(value); } + } + protected override void Toggle() { if (!IsChecked.GetValueOrDefault()) @@ -23,21 +148,77 @@ namespace Avalonia.Controls } } - private void IsCheckedChanged(bool? value) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - var parent = this.GetVisualParent(); + if (!string.IsNullOrEmpty(GroupName)) + { + var manager = RadioButtonGroupManager.GetOrCreateForRoot(e.Root); + if (manager != _groupManager) + { + _groupManager.Remove(this, _groupName); + _groupManager = manager; + manager.Add(this); + } + } + base.OnAttachedToVisualTree(e); + } - if (value.GetValueOrDefault() && parent != null) + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + if (!string.IsNullOrEmpty(GroupName) && _groupManager != null) { - var siblings = parent - .GetVisualChildren() - .OfType() - .Where(x => x != this); + _groupManager.Remove(this, _groupName); + } + } - foreach (var sibling in siblings) + private void SetGroupName(string newGroupName) + { + string oldGroupName = GroupName; + if (newGroupName != oldGroupName) + { + if (!string.IsNullOrEmpty(oldGroupName) && _groupManager != null) + { + _groupManager.Remove(this, oldGroupName); + } + _groupName = newGroupName; + if (!string.IsNullOrEmpty(newGroupName)) + { + if (_groupManager == null) + { + _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(this.GetVisualRoot()); + } + _groupManager.Add(this); + } + } + } + + private void IsCheckedChanged(bool? value) + { + string groupName = GroupName; + if (string.IsNullOrEmpty(groupName)) + { + var parent = this.GetVisualParent(); + + if (value.GetValueOrDefault() && parent != null) + { + var siblings = parent + .GetVisualChildren() + .OfType() + .Where(x => x != this); + + foreach (var sibling in siblings) + { + if (sibling.IsChecked.GetValueOrDefault()) + sibling.IsChecked = false; + } + } + } + else + { + if (value.GetValueOrDefault() && _groupManager != null) { - if (sibling.IsChecked.GetValueOrDefault()) - sibling.IsChecked = false; + _groupManager.SetChecked(this); } } } diff --git a/tests/Avalonia.Controls.UnitTests/RadioButtonTests.cs b/tests/Avalonia.Controls.UnitTests/RadioButtonTests.cs index 2d9dca93f5..7c5249b2c4 100644 --- a/tests/Avalonia.Controls.UnitTests/RadioButtonTests.cs +++ b/tests/Avalonia.Controls.UnitTests/RadioButtonTests.cs @@ -32,5 +32,43 @@ namespace Avalonia.Controls.UnitTests Assert.True(radioButton1.IsChecked); Assert.Null(radioButton2.IsChecked); } + + [Fact] + public void RadioButton_In_Same_Group_Is_Unchecked() + { + var parent = new Panel(); + + var panel1 = new Panel(); + var panel2 = new Panel(); + + parent.Children.Add(panel1); + parent.Children.Add(panel2); + + var radioButton1 = new RadioButton(); + radioButton1.GroupName = "A"; + radioButton1.IsChecked = false; + + var radioButton2 = new RadioButton(); + radioButton2.GroupName = "A"; + radioButton2.IsChecked = true; + + var radioButton3 = new RadioButton(); + radioButton3.GroupName = "A"; + radioButton3.IsChecked = false; + + panel1.Children.Add(radioButton1); + panel1.Children.Add(radioButton2); + panel2.Children.Add(radioButton3); + + Assert.False(radioButton1.IsChecked); + Assert.True(radioButton2.IsChecked); + Assert.False(radioButton3.IsChecked); + + radioButton3.IsChecked = true; + + Assert.False(radioButton1.IsChecked); + Assert.False(radioButton2.IsChecked); + Assert.True(radioButton3.IsChecked); + } } }