From 434bf0d3db186c94da88e0779890577c761d9b7e Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Tue, 14 Aug 2018 20:28:55 +0900 Subject: [PATCH 1/7] Support invoking a function in the dispatcher. --- src/Avalonia.Base/Threading/Dispatcher.cs | 13 +- src/Avalonia.Base/Threading/IDispatcher.cs | 11 +- src/Avalonia.Base/Threading/JobRunner.cs | 168 +++++++++++++----- .../Avalonia.UnitTests/ImmediateDispatcher.cs | 6 + 4 files changed, 144 insertions(+), 54 deletions(-) diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index cf7acb3e8a..aa2a7a7a8e 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -69,7 +69,7 @@ namespace Avalonia.Threading /// public void RunJobs() { - _jobRunner?.RunJobs(null); + _jobRunner.RunJobs(null); } /// @@ -82,14 +82,21 @@ namespace Avalonia.Threading public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) { Contract.Requires(action != null); - return _jobRunner?.InvokeAsync(action, priority); + return _jobRunner.InvokeAsync(action, priority); + } + + /// + public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + { + Contract.Requires(function != null); + return _jobRunner.InvokeAsync(function, priority); } /// public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) { Contract.Requires(action != null); - _jobRunner?.Post(action, priority); + _jobRunner.Post(action, priority); } /// diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index 4009dcdeab..1fdc9da5fe 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -28,12 +28,17 @@ namespace Avalonia.Threading void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal); /// - /// Post action that will be invoked on main thread + /// Posts an action that will be invoked on the dispatcher thread. /// /// The method. /// The priority with which to invoke the method. - // TODO: The naming of this method is confusing: the Async suffix usually means return a task. - // Remove this and rename InvokeTaskAsync as InvokeAsync. See #816. Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal); + + /// + /// Posts a function that will be invoked on the dispatcher thread. + /// + /// The method. + /// The priority with which to invoke the method. + Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/JobRunner.cs b/src/Avalonia.Base/Threading/JobRunner.cs index c2040a0982..b922370e84 100644 --- a/src/Avalonia.Base/Threading/JobRunner.cs +++ b/src/Avalonia.Base/Threading/JobRunner.cs @@ -14,32 +14,16 @@ namespace Avalonia.Threading /// internal class JobRunner { - - private IPlatformThreadingInterface _platform; - private Queue[] _queues = Enumerable.Range(0, (int) DispatcherPriority.MaxValue + 1) - .Select(_ => new Queue()).ToArray(); + private readonly Queue[] _queues = Enumerable.Range(0, (int) DispatcherPriority.MaxValue + 1) + .Select(_ => new Queue()).ToArray(); public JobRunner(IPlatformThreadingInterface platform) { _platform = platform; } - Job GetNextJob(DispatcherPriority minimumPriority) - { - for (int c = (int) DispatcherPriority.MaxValue; c >= (int) minimumPriority; c--) - { - var q = _queues[c]; - lock (q) - { - if (q.Count > 0) - return q.Dequeue(); - } - } - return null; - } - /// /// Runs continuations pushed on the loop. /// @@ -52,24 +36,8 @@ namespace Avalonia.Threading var job = GetNextJob(minimumPriority); if (job == null) return; - - if (job.TaskCompletionSource == null) - { - job.Action(); - } - else - { - try - { - job.Action(); - job.TaskCompletionSource.SetResult(null); - } - catch (Exception e) - { - job.TaskCompletionSource.SetException(e); - } - } + job.Run(); } } @@ -83,7 +51,20 @@ namespace Avalonia.Threading { var job = new Job(action, priority, false); AddJob(job); - return job.TaskCompletionSource.Task; + return job.Task; + } + + /// + /// Invokes a method on the main loop. + /// + /// The method. + /// The priority with which to invoke the method. + /// A task that can be used to track the method's execution. + public Task InvokeAsync(Func function, DispatcherPriority priority) + { + var job = new Job(function, priority); + AddJob(job); + return job.Task; } /// @@ -105,9 +86,9 @@ namespace Avalonia.Threading _platform = AvaloniaLocator.Current.GetService(); } - private void AddJob(Job job) + private void AddJob(IJob job) { - var needWake = false; + bool needWake; var queue = _queues[(int) job.Priority]; lock (queue) { @@ -118,38 +99,129 @@ namespace Avalonia.Threading _platform?.Signal(job.Priority); } + private IJob GetNextJob(DispatcherPriority minimumPriority) + { + for (int c = (int) DispatcherPriority.MaxValue; c >= (int) minimumPriority; c--) + { + var q = _queues[c]; + lock (q) + { + if (q.Count > 0) + return q.Dequeue(); + } + } + return null; + } + + private interface IJob + { + /// + /// Gets the job priority. + /// + DispatcherPriority Priority { get; } + + /// + /// Runs the job. + /// + void Run(); + } + /// /// A job to run. /// - private class Job + private sealed class Job : IJob { + /// + /// The method to call. + /// + private readonly Action _action; + /// + /// The task completion source. + /// + private readonly TaskCompletionSource _taskCompletionSource; + /// /// Initializes a new instance of the class. /// /// The method to call. /// The job priority. - /// Do not wrap excepption in TaskCompletionSource + /// Do not wrap exception in TaskCompletionSource public Job(Action action, DispatcherPriority priority, bool throwOnUiThread) { - Action = action; + _action = action; Priority = priority; - TaskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); + _taskCompletionSource = throwOnUiThread ? null : new TaskCompletionSource(); } + /// + public DispatcherPriority Priority { get; } + /// - /// Gets the method to call. + /// The task. /// - public Action Action { get; } + public Task Task => _taskCompletionSource?.Task; + + /// + void IJob.Run() + { + if (_taskCompletionSource == null) + { + _action(); + return; + } + try + { + _action(); + _taskCompletionSource.SetResult(null); + } + catch (Exception e) + { + _taskCompletionSource.SetException(e); + } + } + } + + /// + /// A job to run. + /// + private sealed class Job : IJob + { + private readonly Func _function; + private readonly TaskCompletionSource _taskCompletionSource; /// - /// Gets the job priority. + /// Initializes a new instance of the class. /// - public DispatcherPriority Priority { get; } + /// The method to call. + /// The job priority. + public Job(Func function, DispatcherPriority priority) + { + _function = function; + Priority = priority; + _taskCompletionSource = new TaskCompletionSource(); + } + /// + public DispatcherPriority Priority { get; } + /// - /// Gets the task completion source. + /// The task. /// - public TaskCompletionSource TaskCompletionSource { get; } + public Task Task => _taskCompletionSource.Task; + + /// + void IJob.Run() + { + try + { + var result = _function(); + _taskCompletionSource.SetResult(result); + } + catch (Exception e) + { + _taskCompletionSource.SetException(e); + } + } } } } diff --git a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs index 92f64bde6f..44d8c78054 100644 --- a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs +++ b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs @@ -25,6 +25,12 @@ namespace Avalonia.UnitTests return Task.FromResult(null); } + public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + { + var result = function(); + return Task.FromResult(result); + } + public void VerifyAccess() { } From f1e422866a3c4e04defcdbcd1abca98306c4cc02 Mon Sep 17 00:00:00 2001 From: jp2masa Date: Tue, 21 Aug 2018 00:51:20 +0100 Subject: [PATCH 2/7] Implemented UniformGrid. (#1819) --- .../Primitives/UniformGrid.cs | 161 ++++++++++++++++++ .../Primitives/UniformGridTests.cs | 144 ++++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 src/Avalonia.Controls/Primitives/UniformGrid.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs diff --git a/src/Avalonia.Controls/Primitives/UniformGrid.cs b/src/Avalonia.Controls/Primitives/UniformGrid.cs new file mode 100644 index 0000000000..f3580eee10 --- /dev/null +++ b/src/Avalonia.Controls/Primitives/UniformGrid.cs @@ -0,0 +1,161 @@ +using System; + +namespace Avalonia.Controls.Primitives +{ + /// + /// A with uniform column and row sizes. + /// + public class UniformGrid : Panel + { + /// + /// Defines the property. + /// + public static readonly StyledProperty RowsProperty = + AvaloniaProperty.Register(nameof(Rows)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColumnsProperty = + AvaloniaProperty.Register(nameof(Columns)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FirstColumnProperty = + AvaloniaProperty.Register(nameof(FirstColumn)); + + private int _rows; + private int _columns; + + /// + /// Specifies the row count. If set to 0, row count will be calculated automatically. + /// + public int Rows + { + get => GetValue(RowsProperty); + set => SetValue(RowsProperty, value); + } + + /// + /// Specifies the column count. If set to 0, column count will be calculated automatically. + /// + public int Columns + { + get => GetValue(ColumnsProperty); + set => SetValue(ColumnsProperty, value); + } + + /// + /// Specifies, for the first row, the column where the items should start. + /// + public int FirstColumn + { + get => GetValue(FirstColumnProperty); + set => SetValue(FirstColumnProperty, value); + } + + protected override Size MeasureOverride(Size availableSize) + { + UpdateRowsAndColumns(); + + var maxWidth = 0d; + var maxHeight = 0d; + + var childAvailableSize = new Size(availableSize.Width / _columns, availableSize.Height / _rows); + + foreach (var child in Children) + { + child.Measure(childAvailableSize); + + if (child.DesiredSize.Width > maxWidth) + { + maxWidth = child.DesiredSize.Width; + } + + if (child.DesiredSize.Height > maxHeight) + { + maxHeight = child.DesiredSize.Height; + } + } + + return new Size(maxWidth * _columns, maxHeight * _rows); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var x = FirstColumn; + var y = 0; + + var width = finalSize.Width / _columns; + var height = finalSize.Height / _rows; + + foreach (var child in Children) + { + if (!child.IsVisible) + { + continue; + } + + child.Arrange(new Rect(x * width, y * height, width, height)); + + x++; + + if (x >= _columns) + { + x = 0; + y++; + } + } + + return finalSize; + } + + private void UpdateRowsAndColumns() + { + _rows = Rows; + _columns = Columns; + + if (FirstColumn >= Columns) + { + FirstColumn = 0; + } + + var itemCount = FirstColumn; + + foreach (var child in Children) + { + if (child.IsVisible) + { + itemCount++; + } + } + + if (_rows == 0) + { + if (_columns == 0) + { + _rows = _columns = (int)Math.Ceiling(Math.Sqrt(itemCount)); + } + else + { + _rows = Math.DivRem(itemCount, _columns, out int rem); + + if (rem != 0) + { + _rows++; + } + } + } + else if (_columns == 0) + { + _columns = Math.DivRem(itemCount, _rows, out int rem); + + if (rem != 0) + { + _columns++; + } + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs new file mode 100644 index 0000000000..340bd09611 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Primitives/UniformGridTests.cs @@ -0,0 +1,144 @@ +using Avalonia.Controls.Primitives; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Primitives +{ + public class UniformGridTests + { + [Fact] + public void Grid_Columns_Equals_Rows_For_Auto_Columns_And_Rows() + { + var target = new UniformGrid() + { + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90 } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // 2 * 2 grid + Assert.Equal(new Size(2 * 80, 2 * 90), target.Bounds.Size); + } + + [Fact] + public void Grid_Expands_Vertically_For_Columns_With_Auto_Rows() + { + var target = new UniformGrid() + { + Columns = 2, + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90 }, + new Border { Width = 20, Height = 30 }, + new Border { Width = 40, Height = 60 } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // 2 * 3 grid + Assert.Equal(new Size(2 * 80, 3 * 90), target.Bounds.Size); + } + + [Fact] + public void Grid_Extends_For_Columns_And_First_Column_With_Auto_Rows() + { + var target = new UniformGrid() + { + Columns = 3, + FirstColumn = 2, + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90 }, + new Border { Width = 20, Height = 30 }, + new Border { Width = 40, Height = 60 } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // 3 * 3 grid + Assert.Equal(new Size(3 * 80, 3 * 90), target.Bounds.Size); + } + + [Fact] + public void Grid_Expands_Horizontally_For_Rows_With_Auto_Columns() + { + var target = new UniformGrid() + { + Rows = 2, + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90 }, + new Border { Width = 20, Height = 30 }, + new Border { Width = 40, Height = 60 } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // 3 * 2 grid + Assert.Equal(new Size(3 * 80, 2 * 90), target.Bounds.Size); + } + + [Fact] + public void Grid_Size_Is_Limited_By_Rows_And_Columns() + { + var target = new UniformGrid() + { + Columns = 2, + Rows = 2, + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90 }, + new Border { Width = 20, Height = 30 }, + new Border { Width = 40, Height = 60 } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // 2 * 2 grid + Assert.Equal(new Size(2 * 80, 2 * 90), target.Bounds.Size); + } + + [Fact] + public void Not_Visible_Children_Are_Ignored() + { + var target = new UniformGrid() + { + Children = + { + new Border { Width = 50, Height = 70 }, + new Border { Width = 30, Height = 50 }, + new Border { Width = 80, Height = 90, IsVisible = false }, + new Border { Width = 20, Height = 30 }, + new Border { Width = 40, Height = 60 } + } + }; + + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + // 2 * 2 grid + Assert.Equal(new Size(2 * 50, 2 * 70), target.Bounds.Size); + } + } +} From 8c31c71d1e3189921f608b1d9850c0158416efef Mon Sep 17 00:00:00 2001 From: Sergey Date: Tue, 21 Aug 2018 16:47:24 +0300 Subject: [PATCH 3/7] References updates Updates for Rx, ReactiveUI and Microsoft CSharp nuget references. --- build/Microsoft.CSharp.props | 2 +- build/Microsoft.Reactive.Testing.props | 2 +- build/ReactiveUI.props | 2 +- build/Rx.props | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build/Microsoft.CSharp.props b/build/Microsoft.CSharp.props index d0fa63bc3e..4f738dd254 100644 --- a/build/Microsoft.CSharp.props +++ b/build/Microsoft.CSharp.props @@ -1,5 +1,5 @@ - + diff --git a/build/Microsoft.Reactive.Testing.props b/build/Microsoft.Reactive.Testing.props index 5ee6df708e..33b8d8a7ca 100644 --- a/build/Microsoft.Reactive.Testing.props +++ b/build/Microsoft.Reactive.Testing.props @@ -1,5 +1,5 @@  - + diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index 11afefa8ad..acdfdd215a 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + diff --git a/build/Rx.props b/build/Rx.props index 7078e31195..77c7e69c94 100644 --- a/build/Rx.props +++ b/build/Rx.props @@ -1,9 +1,9 @@  - - - - - + + + + + From b13908681df3c01758167e348593636c6579c857 Mon Sep 17 00:00:00 2001 From: Sergey Date: Tue, 21 Aug 2018 19:39:46 +0300 Subject: [PATCH 4/7] Downgrade to Rx 4.0 Because of MonoAndroid we can't use Rx 4.1 (System.Threading.Tasks.Extensions is duplicated in mscorlib) --- build/Microsoft.Reactive.Testing.props | 2 +- build/Rx.props | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build/Microsoft.Reactive.Testing.props b/build/Microsoft.Reactive.Testing.props index 33b8d8a7ca..777bc4bb53 100644 --- a/build/Microsoft.Reactive.Testing.props +++ b/build/Microsoft.Reactive.Testing.props @@ -1,5 +1,5 @@  - + diff --git a/build/Rx.props b/build/Rx.props index 77c7e69c94..f4affcacac 100644 --- a/build/Rx.props +++ b/build/Rx.props @@ -1,9 +1,9 @@  - - - - - + + + + + From 18f36ab3bc34c0a47e93da5089697851e3bcc099 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 21 Aug 2018 15:30:34 +0200 Subject: [PATCH 5/7] Fix #1782: Implement GroupName for RadioButton --- .../ControlCatalog/Pages/RadioButtonPage.xaml | 13 ++ src/Avalonia.Controls/RadioButton.cs | 201 +++++++++++++++++- .../RadioButtonTests.cs | 38 ++++ 3 files changed, 242 insertions(+), 10 deletions(-) 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); + } } } From 9555a914a910c0d1ad482b9ea63053a1fcfc1a25 Mon Sep 17 00:00:00 2001 From: William David Cossey Date: Wed, 22 Aug 2018 20:11:32 +0200 Subject: [PATCH 6/7] OS compatibility check for SetProcessDpiAwareness (Windows 8.1 or newer) (#1816) --- src/Windows/Avalonia.Win32/Win32Platform.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index a5088e794c..9afb1218af 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -50,7 +50,12 @@ namespace Avalonia.Win32 // Declare that this process is aware of per monitor DPI if (UnmanagedMethods.ShCoreAvailable) { - UnmanagedMethods.SetProcessDpiAwareness(UnmanagedMethods.PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE); + var osVersion = Environment.OSVersion.Version; + if (osVersion.Major > 6 || (osVersion.Major == 6 && osVersion.Minor > 2)) + { + UnmanagedMethods.SetProcessDpiAwareness(UnmanagedMethods.PROCESS_DPI_AWARENESS + .PROCESS_PER_MONITOR_DPI_AWARE); + } } CreateMessageWindow(); From 739fdc3b9bebacb01ed83c4919bbf2982a0bf6f6 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 22 Aug 2018 22:10:34 +0200 Subject: [PATCH 7/7] Use ConditionalWeakTable instead of a custom list of (IRenderRoot, RadioButtonGroupManager). --- src/Avalonia.Controls/RadioButton.cs | 35 ++++------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/RadioButton.cs b/src/Avalonia.Controls/RadioButton.cs index f8bcd8b478..a1d353f135 100644 --- a/src/Avalonia.Controls/RadioButton.cs +++ b/src/Avalonia.Controls/RadioButton.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Avalonia.Controls.Primitives; using Avalonia.Rendering; using Avalonia.VisualTree; @@ -15,9 +16,9 @@ namespace Avalonia.Controls 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)>(); - + static readonly ConditionalWeakTable s_registeredVisualRoots + = new ConditionalWeakTable(); + readonly Dictionary>> s_registeredGroups = new Dictionary>>(); @@ -25,33 +26,7 @@ namespace Avalonia.Controls { 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; - } + return s_registeredVisualRoots.GetValue(root, key => new RadioButtonGroupManager()); } public void Add(RadioButton radioButton)