From f8e83db6b0367c0ace69d95bcc789bf10e6f1c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Tue, 24 Sep 2019 03:18:44 +0100 Subject: [PATCH 01/70] Update .NET Core SDK to version 3.0.x. --- azure-pipelines.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 92e4afdca8..389f343c54 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,9 +34,17 @@ jobs: pool: vmImage: 'macOS-10.14' steps: - - task: DotNetCoreInstaller@0 + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.0.x' inputs: - version: '2.1.403' + packageType: sdk + version: 3.0.x + + - task: UseDotNet@2 + displayName: 'Use .NET Core Runtime 2.1.x' + inputs: + packageType: runtime + version: 2.1.x - task: CmdLine@2 displayName: 'Install Mono 5.18' From 1cd113182cdb29fb645d54f66477956827548fda Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Nov 2019 20:22:29 +0100 Subject: [PATCH 02/70] Added failing test for #2985. --- .../TreeViewTests.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index a91b7a0701..cfc9daa402 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -893,6 +893,37 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, GetItem(target, 0, 1, 0).Level); } + [Fact] + public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash() + { + // Issue #2985 + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + ExpandAll(target); + + var parent = tree[0]; + var node = parent.Children[1]; + + parent.Children.Remove(node); + parent.Children.Add(node); + + var item = target.ItemContainerGenerator.Index.ContainerFromItem(node); + ApplyTemplates(new[] { item }); + + // #2985 causes ArgumentException here. + node.Children.Add(new Node()); + } + [Fact] public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection() { From 15dfb88fe7b39eb786e37d5e29ca39eeb1df0502 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 Nov 2019 20:24:35 +0100 Subject: [PATCH 03/70] Update tree item container index dynamically. Update `TreeItemContainerGenerator.Index` when a `TreeViewItem` is added to or removed from the logical tree. This ensures that removed `TreeViewItem`s won't try to add duplicate containers to the index. Fixes #2985 --- .../Generators/ITreeItemContainerGenerator.cs | 5 ++ .../Generators/TreeItemContainerGenerator.cs | 49 ++++++++++++++----- src/Avalonia.Controls/TreeView.cs | 3 +- src/Avalonia.Controls/TreeViewItem.cs | 9 ++-- .../TreeViewTests.cs | 7 +++ 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs index e2e591215e..5c931bc771 100644 --- a/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs @@ -12,5 +12,10 @@ namespace Avalonia.Controls.Generators /// Gets the container index for the tree. /// TreeContainerIndex Index { get; } + + /// + /// Updates the index based on the parent . + /// + void UpdateIndex(); } } diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index c06a64443c..9200490668 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Generators { @@ -15,6 +17,8 @@ namespace Avalonia.Controls.Generators public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator where T : class, IControl, new() { + private TreeView _treeView; + /// /// Initializes a new instance of the class. /// @@ -23,31 +27,28 @@ namespace Avalonia.Controls.Generators /// The container's ContentTemplate property. /// The container's Items property. /// The container's IsExpanded property. - /// The container index for the tree public TreeItemContainerGenerator( IControl owner, AvaloniaProperty contentProperty, AvaloniaProperty contentTemplateProperty, AvaloniaProperty itemsProperty, - AvaloniaProperty isExpandedProperty, - TreeContainerIndex index) + AvaloniaProperty isExpandedProperty) : base(owner, contentProperty, contentTemplateProperty) { Contract.Requires(owner != null); Contract.Requires(contentProperty != null); Contract.Requires(itemsProperty != null); Contract.Requires(isExpandedProperty != null); - Contract.Requires(index != null); ItemsProperty = itemsProperty; IsExpandedProperty = isExpandedProperty; - Index = index; + UpdateIndex(); } /// /// Gets the container index for the tree. /// - public TreeContainerIndex Index { get; } + public TreeContainerIndex Index { get; private set; } /// /// Gets the item container's Items property. @@ -70,7 +71,7 @@ namespace Avalonia.Controls.Generators } else if (container != null) { - Index.Add(item, container); + Index?.Add(item, container); return container; } else @@ -92,7 +93,7 @@ namespace Avalonia.Controls.Generators result.DataContext = item; } - Index.Add(item, result); + Index?.Add(item, result); return result; } @@ -101,24 +102,50 @@ namespace Avalonia.Controls.Generators public override IEnumerable Clear() { var items = base.Clear(); - Index.Remove(0, items); + Index?.Remove(0, items); return items; } public override IEnumerable Dematerialize(int startingIndex, int count) { - Index.Remove(startingIndex, GetContainerRange(startingIndex, count)); + Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); return base.Dematerialize(startingIndex, count); } public override IEnumerable RemoveRange(int startingIndex, int count) { - Index.Remove(startingIndex, GetContainerRange(startingIndex, count)); + Index?.Remove(startingIndex, GetContainerRange(startingIndex, count)); return base.RemoveRange(startingIndex, count); } public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; + public void UpdateIndex() + { + if (Owner is TreeView treeViewOwner && Index == null) + { + Index = new TreeContainerIndex(); + _treeView = treeViewOwner; + } + else if (Owner.IsAttachedToLogicalTree) + { + var treeView = Owner.GetSelfAndLogicalAncestors().OfType().FirstOrDefault(); + + if (treeView != _treeView) + { + Clear(); + Index = treeView?.ItemContainerGenerator?.Index; + _treeView = treeView; + } + } + else + { + Clear(); + Index = null; + _treeView = null; + } + } + class WrapperTreeDataTemplate : ITreeDataTemplate { private readonly IDataTemplate _inner; diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 8907137ecb..acfef5117c 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -393,8 +393,7 @@ namespace Avalonia.Controls TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, TreeViewItem.ItemsProperty, - TreeViewItem.IsExpandedProperty, - new TreeContainerIndex()); + TreeViewItem.IsExpandedProperty); result.Index.Materialized += ContainerMaterialized; return result; } diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 07d5497c14..4d24337c3a 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -98,17 +98,18 @@ namespace Avalonia.Controls TreeViewItem.HeaderProperty, TreeViewItem.ItemTemplateProperty, TreeViewItem.ItemsProperty, - TreeViewItem.IsExpandedProperty, - _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex()); + TreeViewItem.IsExpandedProperty); } /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); + _treeView = this.GetLogicalAncestors().OfType().FirstOrDefault(); - + Level = CalculateDistanceFromLogicalParent(this) - 1; + ItemContainerGenerator.UpdateIndex(); if (ItemTemplate == null && _treeView?.ItemTemplate != null) { @@ -119,7 +120,7 @@ namespace Avalonia.Controls protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnDetachedFromLogicalTree(e); - ItemContainerGenerator.Clear(); + ItemContainerGenerator.UpdateIndex(); } protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index cfc9daa402..ed8a39d063 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Data.Core; +using Avalonia.Diagnostics; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; @@ -33,6 +34,8 @@ namespace Avalonia.Controls.UnitTests Items = CreateTestTreeData(), }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); @@ -77,6 +80,8 @@ namespace Avalonia.Controls.UnitTests Items = CreateTestTreeData(), }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); @@ -527,6 +532,8 @@ namespace Avalonia.Controls.UnitTests Items = data, }; + var root = new TestRoot(target); + CreateNodeDataTemplate(target); ApplyTemplates(target); From 57ff7b35f13a88d33657c1a91106413cb91db9a2 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 9 Nov 2019 14:33:58 +0100 Subject: [PATCH 04/70] Remove reflection usage from Shape AffectsGeometry calls. --- src/Avalonia.Controls/Shapes/Shape.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 499dfb5320..1e5cdc9744 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Reflection; using Avalonia.Collections; using Avalonia.Media; @@ -166,10 +165,7 @@ namespace Avalonia.Controls.Shapes { property.Changed.Subscribe(e => { - var senderType = e.Sender.GetType().GetTypeInfo(); - var affectedType = typeof(TShape).GetTypeInfo(); - - if (affectedType.IsAssignableFrom(senderType)) + if (e.Sender is TShape) { AffectsGeometryInvalidate(e); } From 45168de0289f53cf46524e256fb9b4db71ff3d71 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 9 Nov 2019 14:42:28 +0100 Subject: [PATCH 05/70] Remove not needed type check. --- src/Avalonia.Controls/Shapes/Shape.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 1e5cdc9744..7728302aad 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -165,9 +165,9 @@ namespace Avalonia.Controls.Shapes { property.Changed.Subscribe(e => { - if (e.Sender is TShape) + if (e.Sender is TShape shape) { - AffectsGeometryInvalidate(e); + AffectsGeometryInvalidate(shape, e); } }); } @@ -318,13 +318,8 @@ namespace Avalonia.Controls.Shapes return (size, transform); } - private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e) + private static void AffectsGeometryInvalidate(Shape control, AvaloniaPropertyChangedEventArgs e) { - if (!(e.Sender is Shape control)) - { - return; - } - // If the geometry is invalidated when Bounds changes, only invalidate when the Size // portion changes. if (e.Property == BoundsProperty) From 993840a612a1ad61a333cd812f48d83300713926 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 11 Nov 2019 00:25:03 +0100 Subject: [PATCH 06/70] Improve measure benchmark. --- .../ControlHierarchyCreator.cs | 35 +++++++++++++ tests/Avalonia.Benchmarks/Layout/Measure.cs | 51 +++++++------------ tests/Avalonia.Benchmarks/NullRenderer.cs | 44 ++++++++++++++++ 3 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs create mode 100644 tests/Avalonia.Benchmarks/NullRenderer.cs diff --git a/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs b/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs new file mode 100644 index 0000000000..1c9ba4e6ae --- /dev/null +++ b/tests/Avalonia.Benchmarks/ControlHierarchyCreator.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Avalonia.Controls; + +namespace Avalonia.Benchmarks +{ + internal class ControlHierarchyCreator + { + public static List CreateChildren(List controls, IPanel parent, int childCount, int innerCount, int iterations) + { + for (var i = 0; i < childCount; ++i) + { + var control = new StackPanel(); + parent.Children.Add(control); + + for (int j = 0; j < innerCount; ++j) + { + var child = new Button(); + + parent.Children.Add(child); + + controls.Add(child); + } + + if (iterations > 0) + { + CreateChildren(controls, control, childCount, innerCount, iterations - 1); + } + + controls.Add(control); + } + + return controls; + } + } +} diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs index b0490d8a0f..d03d17b4d3 100644 --- a/tests/Avalonia.Benchmarks/Layout/Measure.cs +++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using System.Runtime.CompilerServices; using Avalonia.Controls; -using Avalonia.Layout; using Avalonia.UnitTests; using BenchmarkDotNet.Attributes; @@ -10,48 +9,34 @@ namespace Avalonia.Benchmarks.Layout [MemoryDiagnoser] public class Measure { - private TestRoot root; - private List controls = new List(); + private readonly TestRoot _root; + private readonly List _controls = new List(); public Measure() { var panel = new StackPanel(); - root = new TestRoot { Child = panel }; - controls.Add(panel); - CreateChildren(panel, 3, 5); - root.LayoutManager.ExecuteInitialLayoutPass(root); - } - - [Benchmark] - public void Remeasure_Half() - { - var random = new Random(1); - foreach (var control in controls) + _root = new TestRoot { - if (random.Next(2) == 0) - { - control.InvalidateMeasure(); - } - } + Child = panel, + Renderer = new NullRenderer() + }; - root.LayoutManager.ExecuteLayoutPass(); + _controls.Add(panel); + _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 5); + + _root.LayoutManager.ExecuteInitialLayoutPass(_root); } - private void CreateChildren(IPanel parent, int childCount, int iterations) + [Benchmark, MethodImpl(MethodImplOptions.NoInlining)] + public void Remeasure() { - for (var i = 0; i < childCount; ++i) + foreach (var control in _controls) { - var control = new StackPanel(); - parent.Children.Add(control); - - if (iterations > 0) - { - CreateChildren(control, childCount, iterations - 1); - } - - controls.Add(control); + control.InvalidateMeasure(); } + + _root.LayoutManager.ExecuteLayoutPass(); } } } diff --git a/tests/Avalonia.Benchmarks/NullRenderer.cs b/tests/Avalonia.Benchmarks/NullRenderer.cs new file mode 100644 index 0000000000..7167eafc87 --- /dev/null +++ b/tests/Avalonia.Benchmarks/NullRenderer.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using Avalonia.Rendering; +using Avalonia.VisualTree; + +namespace Avalonia.Benchmarks +{ + internal class NullRenderer : IRenderer + { + public bool DrawFps { get; set; } + public bool DrawDirtyRects { get; set; } + public event EventHandler SceneInvalidated; + + public void AddDirty(IVisual visual) + { + } + + public void Dispose() + { + } + + public IEnumerable HitTest(Point p, IVisual root, Func filter) => null; + + public void Paint(Rect rect) + { + } + + public void RecalculateChildren(IVisual visual) + { + } + + public void Resized(Size size) + { + } + + public void Start() + { + } + + public void Stop() + { + } + } +} From 396c34d82bbe847f2de730017c427a66a1165366 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 12 Nov 2019 10:43:00 +0000 Subject: [PATCH 07/70] fix invalid project reference. --- src/Avalonia.Native/Avalonia.Native.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/Avalonia.Native.csproj b/src/Avalonia.Native/Avalonia.Native.csproj index 88a6da8945..65c2a75b1c 100644 --- a/src/Avalonia.Native/Avalonia.Native.csproj +++ b/src/Avalonia.Native/Avalonia.Native.csproj @@ -22,6 +22,6 @@ - + From 7f722305978c17fdd1d9a879e5148cbb55ad4709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Tue, 12 Nov 2019 14:40:34 +0100 Subject: [PATCH 08/70] Fix ItemsRepeater not clearing focused elements on items source changed. --- .../Pages/ItemsRepeaterPage.xaml | 3 +- .../Pages/ItemsRepeaterPage.xaml.cs | 9 +++++ .../ViewModels/ItemsRepeaterPageViewModel.cs | 36 ++++++++++++++----- .../Repeater/ItemsRepeater.cs | 12 ++++++- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 103c016298..028a294492 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -15,6 +15,7 @@ + - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index decd849adc..6fa9fc515e 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -20,6 +20,7 @@ namespace ControlCatalog.Pages _repeater = this.FindControl("repeater"); _scroller = this.FindControl("scroller"); _repeater.PointerPressed += RepeaterClick; + _repeater.KeyDown += RepeaterOnKeyDown; DataContext = new ItemsRepeaterPageViewModel(); } @@ -77,5 +78,13 @@ namespace ControlCatalog.Pages var item = (e.Source as TextBlock)?.DataContext as ItemsRepeaterPageViewModel.Item; ((ItemsRepeaterPageViewModel)DataContext).SelectedItem = item; } + + private void RepeaterOnKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.F5) + { + ((ItemsRepeaterPageViewModel)DataContext).ResetItems(); + } + } } } diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs index bc2ce80714..de5669123f 100644 --- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs @@ -7,25 +7,27 @@ namespace ControlCatalog.ViewModels { public class ItemsRepeaterPageViewModel : ReactiveObject { - private int newItemIndex = 1; + private int _newItemIndex = 1; + private int _newGenerationIndex = 0; + private ObservableCollection _items; public ItemsRepeaterPageViewModel() { - Items = new ObservableCollection( - Enumerable.Range(1, 100000).Select(i => new Item - { - Text = $"Item {i.ToString()}", - })); + Items = CreateItems(); } - public ObservableCollection Items { get; } + public ObservableCollection Items + { + get => _items; + set => this.RaiseAndSetIfChanged(ref _items, value); + } public Item SelectedItem { get; set; } public void AddItem() { var index = SelectedItem != null ? Items.IndexOf(SelectedItem) : -1; - Items.Insert(index + 1, new Item { Text = $"New Item {newItemIndex++}" }); + Items.Insert(index + 1, new Item { Text = $"New Item {_newItemIndex++}" }); } public void RandomizeHeights() @@ -38,6 +40,24 @@ namespace ControlCatalog.ViewModels } } + public void ResetItems() + { + Items = CreateItems(); + } + + private ObservableCollection CreateItems() + { + var suffix = _newGenerationIndex == 0 ? string.Empty : $"[{_newGenerationIndex.ToString()}]"; + + _newGenerationIndex++; + + return new ObservableCollection( + Enumerable.Range(1, 100000).Select(i => new Item + { + Text = $"Item {i.ToString()} {suffix}" + })); + } + public class Item : ReactiveObject { private double _height = double.NaN; diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 257c1b2399..0e2136a6f3 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -565,7 +565,17 @@ namespace Avalonia.Controls if (Layout is VirtualizingLayout virtualLayout) { var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); - virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + + _processingItemsSourceChange = args; + + try + { + virtualLayout.OnItemsChanged(GetLayoutContext(), newValue, args); + } + finally + { + _processingItemsSourceChange = null; + } } else if (Layout is NonVirtualizingLayout nonVirtualLayout) { From 7a13d819fdbf0cc26b144bda2a97ec59344f39b7 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 14 Nov 2019 01:03:58 +0100 Subject: [PATCH 09/70] Optimize and add new efficient visual tree extensions. --- src/Avalonia.Controls/ItemsControl.cs | 25 +-- .../WindowNotificationManager.cs | 10 +- .../Primitives/SelectingItemsControl.cs | 18 +- src/Avalonia.Controls/TopLevel.cs | 8 +- src/Avalonia.Input/FocusManager.cs | 18 +- src/Avalonia.Input/Pointer.cs | 8 +- src/Avalonia.Layout/Layoutable.cs | 32 +++- src/Avalonia.Visuals/Visual.cs | 20 ++- .../VisualTree/VisualExtensions.cs | 168 +++++++++++++++++- .../Traversal/VisualTreeTraversal.cs | 64 +++++++ .../VisualExtensionsTests.cs | 76 ++++++++ 11 files changed, 392 insertions(+), 55 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index bf22f0a08a..94c34b28d6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -5,7 +5,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; @@ -324,20 +323,24 @@ namespace Avalonia.Controls return; } - var current = focus.Current - .GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.VisualParent == container); + IVisual current = focus.Current; - if (current != null) + while (current != null) { - var next = GetNextControl(container, direction.Value, current, false); - - if (next != null) + if (current.VisualParent == container && current is IInputElement inputElement) { - focus.Focus(next, NavigationMethod.Directional); - e.Handled = true; + IInputElement next = GetNextControl(container, direction.Value, inputElement, false); + + if (next != null) + { + focus.Focus(next, NavigationMethod.Directional); + e.Handled = true; + } + + break; } + + current = current.VisualParent; } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index aa91224572..1a9347e317 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -149,15 +149,9 @@ namespace Avalonia.Controls.Notifications /// The that will be the host. private void Install(Window host) { - var adornerLayer = host.GetVisualDescendants() - .OfType() - .FirstOrDefault() - ?.AdornerLayer; + var adornerLayer = host.FindDescendantOfType()?.AdornerLayer; - if (adornerLayer != null) - { - adornerLayer.Children.Add(this); - } + adornerLayer?.Children.Add(this); } } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 329b086a7c..a5bbcec186 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -13,7 +13,6 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; using Avalonia.Logging; -using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives @@ -269,11 +268,20 @@ namespace Avalonia.Controls.Primitives /// The container or null if the event did not originate in a container. protected IControl GetContainerFromEventSource(IInteractive eventSource) { - var item = ((IVisual)eventSource).GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1); + var parent = (IVisual)eventSource; - return item; + while (parent != null) + { + if (parent is IControl control && control.LogicalParent == this + && ItemContainerGenerator?.IndexFromContainer(control) != -1) + { + return control; + } + + parent = parent.VisualParent; + } + + return null; } /// diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 293809bf51..131a1304d7 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -2,9 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using System.Reactive.Linq; -using Avalonia.Controls.Notifications; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Raw; @@ -15,7 +13,6 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; -using Avalonia.VisualTree; using JetBrains.Annotations; namespace Avalonia.Controls @@ -296,10 +293,7 @@ namespace Avalonia.Controls /// The window scaling. protected virtual void HandleScalingChanged(double scaling) { - foreach (ILayoutable control in this.GetSelfAndVisualDescendants()) - { - control.InvalidateMeasure(); - } + InvalidateSelfAndDescendantsMeasure(); } /// diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index 104ac9cb61..77902a7390 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -180,18 +180,18 @@ namespace Avalonia.Input if (sender == e.Source && ev.MouseButton == MouseButton.Left) { - var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement); + IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement; - if (element == null || !CanFocus(element)) + while (element != null) { - element = element.GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(CanFocus); - } + if (element is IInputElement inputElement && CanFocus(inputElement)) + { + Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers); - if (element != null) - { - Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers); + break; + } + + element = element.VisualParent; } } } diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 819d231b31..00222e92cf 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -55,9 +55,11 @@ namespace Avalonia.Input Captured.DetachedFromVisualTree += OnCaptureDetached; } - IInputElement GetNextCapture(IVisual parent) => - parent as IInputElement ?? parent.GetVisualAncestors().OfType().FirstOrDefault(); - + IInputElement GetNextCapture(IVisual parent) + { + return parent as IInputElement ?? parent.FindAncestorOfType(); + } + private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e) { Capture(GetNextCapture(e.Parent)); diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index 4732808b91..f9e438cbeb 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using Avalonia.Logging; using Avalonia.VisualTree; @@ -693,14 +692,37 @@ namespace Avalonia.Layout return finalSize; } - /// - protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + /// + /// Invalidates measure for this instance and all visual children. + /// + protected void InvalidateSelfAndDescendantsMeasure() { - foreach (ILayoutable i in this.GetSelfAndVisualDescendants()) + void InnerInvalidateMeasure(IVisual target) { - i.InvalidateMeasure(); + if (target is ILayoutable layoutable) + { + layoutable.InvalidateMeasure(); + } + + var visualChildren = target.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (int i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + InnerInvalidateMeasure(child); + } } + InnerInvalidateMeasure(this); + } + + /// + protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent) + { + InvalidateSelfAndDescendantsMeasure(); + base.OnVisualParentChanged(oldParent, newParent); } diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index f4306d3929..c70e9a49fb 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Specialized; using System.Linq; -using System.Reactive.Linq; using Avalonia.Collections; using Avalonia.Data; using Avalonia.Logging; @@ -173,7 +172,22 @@ namespace Avalonia /// public bool IsEffectivelyVisible { - get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); } + get + { + IVisual node = this; + + while (node != null) + { + if (!node.IsVisible) + { + return false; + } + + node = node.VisualParent; + } + + return true; + } } /// @@ -552,7 +566,7 @@ namespace Avalonia if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) { - var root = this.GetVisualAncestors().OfType().FirstOrDefault(); + var root = this.FindAncestorOfType(); var e = new VisualTreeAttachmentEventArgs(_visualParent, root); OnAttachedToVisualTreeCore(e); } diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index 567b676b1e..623e64d136 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -14,7 +14,7 @@ namespace Avalonia.VisualTree public static class VisualExtensions { /// - /// Calculates the distance from a visual's . + /// Calculates the distance from a visual's ancestor. /// /// The visual. /// The ancestor visual. @@ -30,13 +30,39 @@ namespace Avalonia.VisualTree while (visual != null && visual != ancestor) { - ++result; visual = visual.VisualParent; + + result++; } return visual != null ? result : -1; } + /// + /// Calculates the distance from a visual's root. + /// + /// The visual. + /// + /// The number of steps from the visual to the root. + /// + public static int CalculateDistanceFromRoot(IVisual visual) + { + Contract.Requires(visual != null); + + var result = 0; + + visual = visual?.VisualParent; + + while (visual != null) + { + visual = visual.VisualParent; + + result++; + } + + return result; + } + /// /// Tries to get the first common ancestor of two visuals. /// @@ -47,8 +73,64 @@ namespace Avalonia.VisualTree { Contract.Requires(visual != null); - return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors()) - .FirstOrDefault(); + if (target is null) + { + return null; + } + + IVisual GoUpwards(ref IVisual node, int count, IVisual parentCandidate) + { + for (int i = 0; i < count; ++i) + { + node = node.VisualParent; + + // Other node can be our ancestor so we might find it early on. + if (node == parentCandidate) + { + return node; + } + } + + return null; + } + + // We want to find lowest node first, then make sure that both nodes are at the same height. + // By doing that we can sometimes find out that other node is our lowest common ancestor. + var firstHeight = CalculateDistanceFromRoot(visual); + var secondHeight = CalculateDistanceFromRoot(target); + + IVisual found = firstHeight > secondHeight ? + GoUpwards(ref visual, firstHeight - secondHeight, target) : + GoUpwards(ref target, secondHeight - firstHeight, target); + + if (found != null) + { + return found; + } + + if (visual == target) + { + return visual; + } + + while (true) + { + IVisual firstParent = visual.VisualParent; + IVisual secondParent = target.VisualParent; + + if (firstParent == secondParent) + { + return firstParent; + } + + visual = visual.VisualParent; + target = target.VisualParent; + + if (visual == null || target == null) + { + return null; + } + } } /// @@ -69,6 +151,59 @@ namespace Avalonia.VisualTree } } + /// + /// Finds first ancestor of given type. + /// + /// Ancestor type. + /// The visual. + /// If given visual should be included in search. + /// First ancestor of given type. + public static T FindAncestorOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + IVisual parent = includeSelf ? visual : visual.VisualParent; + + while (parent != null) + { + if (parent is T result) + { + return result; + } + + parent = parent.VisualParent; + } + + return null; + } + + /// + /// Finds first descendant of given type. + /// + /// Descendant type. + /// The visual. + /// If given visual should be included in search. + /// First descendant of given type. + public static T FindDescendantOfType(this IVisual visual, bool includeSelf = false) where T : class + { + if (visual is null) + { + return null; + } + + if (includeSelf && visual is T result) + { + return result; + } + + FindDescendantOfTypeCore(visual); + + return null; + } + /// /// Enumerates an and its ancestors in the visual tree. /// @@ -249,6 +384,31 @@ namespace Avalonia.VisualTree .Select(x => x.Element); } + private static T FindDescendantOfTypeCore(IVisual visual) where T : class + { + var visualChildren = visual.VisualChildren; + var visualChildrenCount = visualChildren.Count; + + for (var i = 0; i < visualChildrenCount; i++) + { + IVisual child = visualChildren[i]; + + if (child is T result) + { + return result; + } + + var childResult = FindDescendantOfTypeCore(child); + + if (!(childResult is null)) + { + return childResult; + } + } + + return null; + } + private class ZOrderElement : IComparable { public IVisual Element { get; set; } diff --git a/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs new file mode 100644 index 0000000000..fc2380d670 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Traversal +{ + [MemoryDiagnoser] + public class VisualTreeTraversal + { + private readonly TestRoot _root; + private readonly List _controls = new List(); + private readonly List _shuffledControls; + + public VisualTreeTraversal() + { + var panel = new StackPanel(); + _root = new TestRoot { Child = panel, Renderer = new NullRenderer()}; + _controls.Add(panel); + _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 4); + + var random = new Random(1); + + _shuffledControls = _controls.OrderBy(r => random.Next()).ToList(); + + _root.LayoutManager.ExecuteInitialLayoutPass(_root); + } + + [Benchmark] + public void FindAncestorOfType_Linq() + { + foreach (Control control in _controls) + { + control.GetSelfAndVisualAncestors() + .OfType() + .FirstOrDefault(); + } + } + + [Benchmark] + public void FindAncestorOfType_Optimized() + { + foreach (Control control in _controls) + { + control.FindAncestorOfType(); + } + } + + [Benchmark] + public void FindCommonVisualAncestor() + { + foreach (IVisual first in _controls) + { + foreach (Control second in _shuffledControls) + { + first.FindCommonVisualAncestor(second); + } + } + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs index a8d8c07d8b..51806135af 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs @@ -2,12 +2,88 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Visuals.UnitTests { public class VisualExtensionsTests { + [Fact] + public void FindCommonVisualAncestor_Two_Subtrees_Uniform_Height() + { + Control left, right; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + new Decorator + { + Child = new Decorator + { + Child = left = new Decorator() + } + }, + new Decorator + { + Child = new Decorator + { + Child = right = new Decorator() + } + } + } + } + }; + + var ancestor = left.FindCommonVisualAncestor(right); + Assert.Equal(root.Child, ancestor); + + ancestor = right.FindCommonVisualAncestor(left); + Assert.Equal(root.Child, ancestor); + } + + [Fact] + public void FindCommonVisualAncestor_Two_Subtrees_NonUniform_Height() + { + Control left, right; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + new Decorator + { + Child = new Decorator + { + Child = left = new Decorator() + } + }, + new Decorator + { + Child = new Decorator + { + Child = new Decorator + { + Child = right = new Decorator() + } + } + } + } + } + }; + + var ancestor = left.FindCommonVisualAncestor(right); + Assert.Equal(root.Child, ancestor); + + ancestor = right.FindCommonVisualAncestor(left); + Assert.Equal(root.Child, ancestor); + } + [Fact] public void TranslatePoint_Should_Respect_RenderTransforms() { From 6be21fb36c8fb91e99f58cd071b95fba97b26b55 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 15 Nov 2019 22:27:32 +0100 Subject: [PATCH 10/70] Add more unit tests. --- .../VisualExtensionsTests.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs index 51806135af..c9a3c64a4c 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs @@ -9,6 +9,82 @@ namespace Avalonia.Visuals.UnitTests { public class VisualExtensionsTests { + [Fact] + public void FindAncestorOfType_Finds_Direct_Parent() + { + StackPanel target; + + var root = new TestRoot + { + Child = target = new StackPanel() + }; + + Assert.Equal(root, target.FindAncestorOfType()); + } + + [Fact] + public void FindAncestorOfType_Finds_Ancestor_Of_Nested_Child() + { + Button target; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + (target = new Button()) + } + } + } + } + }; + + Assert.Equal(root, target.FindAncestorOfType()); + } + + [Fact] + public void FindDescendantOfType_Finds_Direct_Child() + { + StackPanel target; + + var root = new TestRoot + { + Child = target = new StackPanel() + }; + + Assert.Equal(target, root.FindDescendantOfType()); + } + + [Fact] + public void FindDescendantOfType_Finds_Nested_Child() + { + Button target; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + new StackPanel + { + Children = + { + (target = new Button()) + } + } + } + } + }; + + Assert.Equal(target, root.FindDescendantOfType