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);
+ }
}
}