From 8555fed86b7070218a07f329a90da4501b2adb42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Sat, 26 Sep 2020 16:13:03 +0200 Subject: [PATCH 01/11] Add failing tests for #4733 and fix property accessor. --- .../Plugins/InpcPropertyAccessorPlugin.cs | 44 ++++++++++++++++--- .../Core/ExpressionObserverTests_Property.cs | 29 ++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 8fc2a7b77c..3bf6842cd6 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using Avalonia.Utilities; @@ -11,8 +12,11 @@ namespace Avalonia.Data.Core.Plugins /// public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { + private readonly Dictionary<(Type, string), PropertyInfo> _propertyLookup = + new Dictionary<(Type, string), PropertyInfo>(); + /// - public bool Match(object obj, string propertyName) => GetPropertyWithName(obj.GetType(), propertyName) != null; + public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj.GetType(), propertyName) != null; /// /// Starts monitoring the value of a property on an object. @@ -30,7 +34,7 @@ namespace Avalonia.Data.Core.Plugins reference.TryGetTarget(out object instance); - var p = GetPropertyWithName(instance.GetType(), propertyName); + var p = GetFirstPropertyWithName(instance.GetType(), propertyName); if (p != null) { @@ -44,12 +48,40 @@ namespace Avalonia.Data.Core.Plugins } } - private static PropertyInfo GetPropertyWithName(Type type, string propertyName) + private PropertyInfo GetFirstPropertyWithName(Type type, string propertyName) { - const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | - BindingFlags.Static | BindingFlags.Instance; + var key = (type, propertyName); + + if (!_propertyLookup.TryGetValue(key, out PropertyInfo propertyInfo)) + { + propertyInfo = TryFindAndCacheProperty(type, propertyName); + } + + return propertyInfo; + } + + private PropertyInfo TryFindAndCacheProperty(Type type, string propertyName) + { + PropertyInfo found = null; + + const BindingFlags bindingFlags = + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; + + var properties = type.GetProperties(bindingFlags); + + foreach (PropertyInfo propertyInfo in properties) + { + if (propertyInfo.Name == propertyName) + { + found = propertyInfo; + + break; + } + } + + _propertyLookup.Add((type, propertyName), found); - return type.GetProperty(propertyName, bindingFlags); + return found; } private class Accessor : PropertyAccessorBase, IWeakSubscriber diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 784046ac0b..cf98293d6d 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -600,6 +600,24 @@ namespace Avalonia.Base.UnitTests.Data.Core result); } + [Fact] + public void Should_Not_Throw_Exception_On_Duplicate_Properties() + { + // Repro of https://github.com/AvaloniaUI/Avalonia/issues/4733. + var source = new MyViewModel(); + var target = new PropertyAccessorNode("Name", false); + + target.Target = new WeakReference(source); + + var result = new List(); + + target.Subscribe(x => result.Add(x)); + } + + public class MyViewModelBase { public object Name => "Name"; } + + public class MyViewModel : MyViewModelBase { public new string Name => "NewName"; } + private interface INext { int PropertyChangedSubscriptionCount { get; } @@ -664,6 +682,17 @@ namespace Avalonia.Base.UnitTests.Data.Core } } + private class WithNewBar : Class1 + { + private string _bar; + + public new string Bar + { + get { return _bar; } + set { _bar = value; } + } + } + private class Class3 : Class1 { } From 00dbf12cee54609d0b94d0449d2affdeab93ba18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Sat, 26 Sep 2020 16:26:39 +0200 Subject: [PATCH 02/11] Remove not needed code. --- .../Data/Core/ExpressionObserverTests_Property.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index cf98293d6d..32cdd21e04 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -682,17 +682,6 @@ namespace Avalonia.Base.UnitTests.Data.Core } } - private class WithNewBar : Class1 - { - private string _bar; - - public new string Bar - { - get { return _bar; } - set { _bar = value; } - } - } - private class Class3 : Class1 { } From f97d43e76507f35871d4d1ed0b6064b7a5ef6f39 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Sep 2020 23:51:00 +0100 Subject: [PATCH 03/11] add failing unit test. --- .../TreeViewTests.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 7022fbf4c1..f1a346e6aa 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; @@ -13,6 +14,7 @@ using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; +using ReactiveUI; using Xunit; namespace Avalonia.Controls.UnitTests @@ -454,6 +456,43 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Bound_SelectedItem_Should_Not_Be_Cleared_when_Changing_Selection() + { + using (Application()) + { + var dataContext = new TestDataContext(); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + DataContext = dataContext + }; + + target.Bind(TreeView.ItemsProperty, new Binding("Items")); + target.Bind(TreeView.SelectedItemProperty, new Binding("SelectedItem")); + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var children = target.GetLogicalChildren().ToList(); + + var selectedValues = new List(); + + dataContext.WhenAnyValue(x => x.SelectedItem) + .Subscribe(x => selectedValues.Add(x)); + + _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Left); + _mouse.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Left); + + Assert.Equal(4, selectedValues.Count); + Assert.Equal(new[] { null, "Item 0", "Item 2" }, selectedValues.ToArray()); + } + } + [Fact] public void LogicalChildren_Should_Be_Set() { @@ -1288,5 +1327,29 @@ namespace Avalonia.Controls.UnitTests private class DerivedTreeView : TreeView { } + + private class TestDataContext : ReactiveObject + { + private int _counter; + private string _selectedItem; + + public TestDataContext() + { + Items = new ObservableCollection(Enumerable.Range(1, 5).Select(i => GenerateItem())); + } + + private string GenerateItem() => $"Item {_counter++.ToString()}"; + + public ObservableCollection Items { get; } + + public string SelectedItem + { + get { return _selectedItem; } + set + { + this.RaiseAndSetIfChanged(ref _selectedItem, value); + } + } + } } } From 0bb91da4959e46a0a35988dc0583185143f1b1f6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Oct 2020 10:45:02 +0100 Subject: [PATCH 04/11] fix test. --- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index f1a346e6aa..9e22c44f44 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -488,7 +488,7 @@ namespace Avalonia.Controls.UnitTests _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Left); _mouse.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Left); - Assert.Equal(4, selectedValues.Count); + Assert.Equal(3, selectedValues.Count); Assert.Equal(new[] { null, "Item 0", "Item 2" }, selectedValues.ToArray()); } } From ec7ae1fefb8cbe3fae2d228981cec7132dddc0e0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Oct 2020 10:45:33 +0100 Subject: [PATCH 05/11] [TreeView] dont raise change events on the clear stage of setting a single selection. --- src/Avalonia.Controls/TreeView.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b4c30e0149..b9e30620bc 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -219,7 +219,9 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { + _syncingSelectedItems = true; SelectedItems.Clear(); + _syncingSelectedItems = false; SelectedItems.Add(item); } @@ -353,7 +355,7 @@ namespace Avalonia.Controls MarkItemSelected(item, true); } - if (SelectedItem == null && !_syncingSelectedItems) + if (!_syncingSelectedItems) { SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]); } From 4ca07974af61aa0e363aef8570433a9f7124eafc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Oct 2020 11:06:32 +0100 Subject: [PATCH 06/11] remove line that doesnt do anything. --- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 9e22c44f44..d5b3e6350c 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -478,8 +478,6 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); - var children = target.GetLogicalChildren().ToList(); - var selectedValues = new List(); dataContext.WhenAnyValue(x => x.SelectedItem) From 3fd7892cc231e488163f10709d230ae266097dde Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 09:33:18 +0100 Subject: [PATCH 07/11] use suggested implementation. --- src/Avalonia.Controls/TreeView.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b9e30620bc..560cd9e810 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -117,10 +117,8 @@ namespace Avalonia.Controls if (value != null) { if (selectedItems.Count != 1 || selectedItems[0] != value) - { - _syncingSelectedItems = true; - SelectSingleItem(value); - _syncingSelectedItems = false; + { + SelectSingleItem(value); } } else if (SelectedItems.Count > 0) @@ -219,10 +217,13 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { + var oldValue = SelectedItem; _syncingSelectedItems = true; - SelectedItems.Clear(); - _syncingSelectedItems = false; + SelectedItems.Clear(); SelectedItems.Add(item); + _syncingSelectedItems = false; + + this.RaisePropertyChanged(SelectedItemProperty, oldValue, item); } /// @@ -355,7 +356,7 @@ namespace Avalonia.Controls MarkItemSelected(item, true); } - if (!_syncingSelectedItems) + if (SelectedItem == null && !_syncingSelectedItems) { SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]); } From 09069e28b22350e2338d5065848e89a8366a36f6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 10:00:38 +0100 Subject: [PATCH 08/11] fix implementation. --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 560cd9e810..f0e00e1d7e 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -223,7 +223,7 @@ namespace Avalonia.Controls SelectedItems.Add(item); _syncingSelectedItems = false; - this.RaisePropertyChanged(SelectedItemProperty, oldValue, item); + SetAndRaise(SelectedItemProperty, ref _selectedItem, item); } /// From a4e4d6469d686e58fc52bea1c1462bd927a0c2a8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 22:30:19 +0100 Subject: [PATCH 09/11] fix nit in test --- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index d5b3e6350c..b4b14ba409 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1333,11 +1333,9 @@ namespace Avalonia.Controls.UnitTests public TestDataContext() { - Items = new ObservableCollection(Enumerable.Range(1, 5).Select(i => GenerateItem())); + Items = new ObservableCollection(Enumerable.Range(1, 5).Select(i => $"Item {i}")); } - private string GenerateItem() => $"Item {_counter++.ToString()}"; - public ObservableCollection Items { get; } public string SelectedItem From 5a675a073fbeb471bdec1c67e1ea99cc580cfb7f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 23:36:46 +0100 Subject: [PATCH 10/11] fix test. --- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index b4b14ba409..b90b74d794 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1333,7 +1333,7 @@ namespace Avalonia.Controls.UnitTests public TestDataContext() { - Items = new ObservableCollection(Enumerable.Range(1, 5).Select(i => $"Item {i}")); + Items = new ObservableCollection(Enumerable.Range(0, 5).Select(i => $"Item {i}")); } public ObservableCollection Items { get; } From 8f4059654636d52eb5ff3975ebdc5788b3e60652 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Oct 2020 11:59:28 +0100 Subject: [PATCH 11/11] fix nits --- src/Avalonia.Controls/TreeView.cs | 1 - tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index f0e00e1d7e..b2bd5ab2e5 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -217,7 +217,6 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { - var oldValue = SelectedItem; _syncingSelectedItems = true; SelectedItems.Clear(); SelectedItems.Add(item); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index b90b74d794..9253d8a07f 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1328,7 +1328,6 @@ namespace Avalonia.Controls.UnitTests private class TestDataContext : ReactiveObject { - private int _counter; private string _selectedItem; public TestDataContext()