From b91d8781d8d2ca5dcb8971cd2c281511605e7d0b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 14 May 2016 12:43:56 +0200 Subject: [PATCH 001/158] WIP --- .../Views/MainWindow.cs | 11 ++ .../Views/MainWindow.xaml | 11 ++ .../Avalonia.Controls.csproj | 3 + src/Avalonia.Controls/IVirtualizingPanel.cs | 13 ++ src/Avalonia.Controls/Panel.cs | 34 ++-- src/Avalonia.Controls/StackPanel.cs | 18 ++- src/Avalonia.Controls/Thingamybob.cs | 67 ++++++++ .../VirtualizingStackPanel.cs | 148 ++++++++++++++++++ 8 files changed, 284 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.Controls/IVirtualizingPanel.cs create mode 100644 src/Avalonia.Controls/Thingamybob.cs create mode 100644 src/Avalonia.Controls/VirtualizingStackPanel.cs diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.cs b/samples/XamlTestApplicationPcl/Views/MainWindow.cs index 2c0012d65f..3df8bdeea3 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.cs +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.cs @@ -25,6 +25,17 @@ namespace XamlTestApplication.Views AvaloniaXamlLoader.Load(this); _exitMenu = this.FindControl("exitMenu"); _exitMenu.Click += (s, e) => Application.Current.Exit(); + + var vadd = this.FindControl + + + + + diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 6f7dd4bcf8..0bc1e5221e 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -57,6 +57,7 @@ + @@ -161,6 +162,7 @@ + @@ -171,6 +173,7 @@ + diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs new file mode 100644 index 0000000000..6ac9a83f89 --- /dev/null +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -0,0 +1,13 @@ +using System; + +namespace Avalonia.Controls +{ + public interface IVirtualizingPanel : IPanel + { + bool IsFull { get; } + + int OverflowCount { get; } + + Action ArrangeCompleted { get; set; } + } +} diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 55bdbc9dd4..793399841c 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -79,12 +79,28 @@ namespace Avalonia.Controls set { SetValue(BackgroundProperty, value); } } + /// + /// Renders the visual to a . + /// + /// The drawing context. + public override void Render(DrawingContext context) + { + var background = Background; + if (background != null) + { + var renderSize = Bounds.Size; + context.FillRectangle(background, new Rect(renderSize)); + } + + base.Render(context); + } + /// /// Called when the collection changes. /// /// The event sender. /// The event args. - private void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + protected virtual void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) { List controls; @@ -122,21 +138,5 @@ namespace Avalonia.Controls InvalidateMeasure(); } - - /// - /// Renders the visual to a . - /// - /// The drawing context. - public override void Render(DrawingContext context) - { - var background = Background; - if (background != null) - { - var renderSize = Bounds.Size; - context.FillRectangle(background, new Rect(renderSize)); - } - - base.Render(context); - } } } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 1a032d4def..0284d84df3 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -181,6 +181,7 @@ namespace Avalonia.Controls /// The space taken. protected override Size ArrangeOverride(Size finalSize) { + var orientation = Orientation; double arrangedWidth = finalSize.Width; double arrangedHeight = finalSize.Height; double gap = Gap; @@ -199,11 +200,11 @@ namespace Avalonia.Controls double childWidth = child.DesiredSize.Width; double childHeight = child.DesiredSize.Height; - if (Orientation == Orientation.Vertical) + if (orientation == Orientation.Vertical) { double width = Math.Max(childWidth, arrangedWidth); Rect childFinal = new Rect(0, arrangedHeight, width, childHeight); - child.Arrange(childFinal); + ArrangeChild(child, childFinal, finalSize, orientation); arrangedWidth = Math.Max(arrangedWidth, childWidth); arrangedHeight += childHeight + gap; } @@ -211,13 +212,13 @@ namespace Avalonia.Controls { double height = Math.Max(childHeight, arrangedHeight); Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height); - child.Arrange(childFinal); + ArrangeChild(child, childFinal, finalSize, orientation); arrangedWidth += childWidth + gap; arrangedHeight = Math.Max(arrangedHeight, childHeight); } } - if (Orientation == Orientation.Vertical) + if (orientation == Orientation.Vertical) { arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height); } @@ -228,5 +229,14 @@ namespace Avalonia.Controls return new Size(arrangedWidth, arrangedHeight); } + + internal virtual void ArrangeChild( + IControl child, + Rect rect, + Size panelSize, + Orientation orientation) + { + child.Arrange(rect); + } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Thingamybob.cs b/src/Avalonia.Controls/Thingamybob.cs new file mode 100644 index 0000000000..7fd251e143 --- /dev/null +++ b/src/Avalonia.Controls/Thingamybob.cs @@ -0,0 +1,67 @@ +using Avalonia.Media; +using System; + +namespace Avalonia.Controls +{ + public class Thingamybob : Decorator + { + private int _lastIndex; + + public override void ApplyTemplate() + { + if (Child == null) + { + Child = new VirtualizingStackPanel(); + ((IVirtualizingPanel)Child).ArrangeCompleted = CheckPanel; + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + var result = base.ArrangeOverride(finalSize); + CreateItems(); + return result; + } + + private void CreateItems() + { + var panel = Child as IVirtualizingPanel; + var randomColor = Color.FromUInt32( + (uint)(0xff000000 + new Random().Next(0xffffff))); + + while (!panel.IsFull) + { + panel.Children.Add(new TextBlock + { + Text = "Item " + ++_lastIndex, + Background = new SolidColorBrush(randomColor), + }); + } + } + + private void RemoveItems() + { + var panel = Child as IVirtualizingPanel; + var remove = panel.OverflowCount; + + panel.Children.RemoveRange( + panel.Children.Count - remove, + panel.OverflowCount); + _lastIndex -= remove; + } + + private void CheckPanel() + { + var panel = Child as IVirtualizingPanel; + + if (!panel.IsFull) + { + CreateItems(); + } + else if (panel.OverflowCount > 0) + { + RemoveItems(); + } + } + } +} diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs new file mode 100644 index 0000000000..8c45148d2b --- /dev/null +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -0,0 +1,148 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls.Primitives; +using System; +using System.Collections.Specialized; + +namespace Avalonia.Controls +{ + public class VirtualizingStackPanel : StackPanel, IScrollable, IVirtualizingPanel + { + private double _takenSpace; + private int _canBeRemoved; + + bool IVirtualizingPanel.IsFull + { + get + { + return Orientation == Orientation.Horizontal ? + _takenSpace >= Bounds.Width : + _takenSpace >= Bounds.Height; + } + } + + int IVirtualizingPanel.OverflowCount => _canBeRemoved; + + Action IVirtualizingPanel.ArrangeCompleted { get; set; } + + Action IScrollable.InvalidateScroll + { + get; + set; + } + + Size IScrollable.Extent => new Size(_takenSpace, _takenSpace); + + Vector IScrollable.Offset + { + get { return default(Vector); } + set { } + } + + Size IScrollable.Viewport => Bounds.Size; + + Size IScrollable.ScrollSize => new Size(1, 1); + + Size IScrollable.PageScrollSize => new Size(1, 1); + + protected override Size ArrangeOverride(Size finalSize) + { + _canBeRemoved = 0; + _takenSpace = 0; + var result = base.ArrangeOverride(finalSize); + ((IVirtualizingPanel)this).ArrangeCompleted?.Invoke(); + return result; + } + + protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + base.ChildrenChanged(sender, e); + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (IControl control in e.NewItems) + { + UpdatePhysicalSizeForAdd(control); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (IControl control in e.OldItems) + { + UpdatePhysicalSizeForRemove(control); + } + + break; + } + } + + internal override void ArrangeChild( + IControl child, + Rect rect, + Size panelSize, + Orientation orientation) + { + base.ArrangeChild(child, rect, panelSize, orientation); + + if (orientation == Orientation.Horizontal) + { + if (rect.X >= panelSize.Width) + { + ++_canBeRemoved; + } + + if (rect.Right >= _takenSpace) + { + _takenSpace = rect.Right; + } + } + else + { + if (rect.Y >= panelSize.Height) + { + ++_canBeRemoved; + } + + if (rect.Bottom >= _takenSpace) + { + _takenSpace = rect.Bottom; + } + } + } + + private void UpdatePhysicalSizeForAdd(IControl child) + { + var bounds = Bounds; + var gap = Gap; + + child.Measure(bounds.Size); + + if (Orientation == Orientation.Vertical) + { + _takenSpace += child.DesiredSize.Height + gap; + } + else + { + _takenSpace += child.DesiredSize.Width + gap; + } + } + + private void UpdatePhysicalSizeForRemove(IControl child) + { + var bounds = Bounds; + var gap = Gap; + + if (Orientation == Orientation.Vertical) + { + _takenSpace -= child.DesiredSize.Height + gap; + } + else + { + _takenSpace -= child.DesiredSize.Width + gap; + } + } + } +} From 03e37f8f693d3a92b88ac8ec2932c4b345aa45d7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 14 May 2016 20:45:14 +0200 Subject: [PATCH 002/158] WIP: More work on primitive virtualization. --- .../Avalonia.Controls.csproj | 1 + .../Presenters/ThingamybobPresenter.cs | 104 ++++++++++++++++++ src/Avalonia.Controls/Thingamybob.cs | 58 ++-------- 3 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 0bc1e5221e..056fe9e93a 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -69,6 +69,7 @@ + diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs new file mode 100644 index 0000000000..a270896441 --- /dev/null +++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs @@ -0,0 +1,104 @@ +using System; +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace Avalonia.Controls.Presenters +{ + public class ThingamybobPresenter : Decorator, IItemsPresenter, IScrollable + { + private IVirtualizingPanel _panel; + private int _firstIndex; + private int _lastIndex; + + public override void ApplyTemplate() + { + if (_panel == null) + { + _panel = new VirtualizingStackPanel(); + _panel.ArrangeCompleted = CheckPanel; + Child = _panel; + } + } + + public IPanel Panel => _panel; + + Action IScrollable.InvalidateScroll { get; set; } + + Size IScrollable.Extent => new Size(1, 100); + + Vector IScrollable.Offset + { + get + { + return new Vector(0, _firstIndex); + } + + set + { + var count = _lastIndex - _firstIndex; + _firstIndex = (int)Math.Round(value.Y); + _lastIndex = _firstIndex + count; + Renumber(); + } + } + + Size IScrollable.Viewport => new Size(1, _lastIndex - _firstIndex); + Size IScrollable.ScrollSize => new Size(0, 1); + Size IScrollable.PageScrollSize => new Size(0, 1); + + protected override Size ArrangeOverride(Size finalSize) + { + var result = base.ArrangeOverride(finalSize); + CreateItems(); + ((IScrollable)this).InvalidateScroll(); + return result; + } + + private void CreateItems() + { + var randomColor = Color.FromUInt32( + (uint)(0xff000000 + new Random().Next(0xffffff))); + + while (!_panel.IsFull) + { + _panel.Children.Add(new TextBlock + { + Text = "Item " + ++_lastIndex, + Background = new SolidColorBrush(randomColor), + }); + } + } + + private void RemoveItems() + { + var remove = _panel.OverflowCount; + + _panel.Children.RemoveRange( + _panel.Children.Count - remove, + _panel.OverflowCount); + _lastIndex -= remove; + } + + private void Renumber() + { + var index = _firstIndex; + + foreach (TextBlock child in _panel.Children) + { + child.Text = "Item " + ++index; + } + } + + private void CheckPanel() + { + if (!_panel.IsFull) + { + CreateItems(); + } + else if (_panel.OverflowCount > 0) + { + RemoveItems(); + } + } + } +} diff --git a/src/Avalonia.Controls/Thingamybob.cs b/src/Avalonia.Controls/Thingamybob.cs index 7fd251e143..6129b029e8 100644 --- a/src/Avalonia.Controls/Thingamybob.cs +++ b/src/Avalonia.Controls/Thingamybob.cs @@ -1,66 +1,24 @@ using Avalonia.Media; using System; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Presenters; namespace Avalonia.Controls { public class Thingamybob : Decorator { - private int _lastIndex; + private ScrollViewer _scrollViewer; + private ThingamybobPresenter _presenter; public override void ApplyTemplate() { if (Child == null) { - Child = new VirtualizingStackPanel(); - ((IVirtualizingPanel)Child).ArrangeCompleted = CheckPanel; - } - } - - protected override Size ArrangeOverride(Size finalSize) - { - var result = base.ArrangeOverride(finalSize); - CreateItems(); - return result; - } - - private void CreateItems() - { - var panel = Child as IVirtualizingPanel; - var randomColor = Color.FromUInt32( - (uint)(0xff000000 + new Random().Next(0xffffff))); - - while (!panel.IsFull) - { - panel.Children.Add(new TextBlock - { - Text = "Item " + ++_lastIndex, - Background = new SolidColorBrush(randomColor), - }); - } - } - - private void RemoveItems() - { - var panel = Child as IVirtualizingPanel; - var remove = panel.OverflowCount; - - panel.Children.RemoveRange( - panel.Children.Count - remove, - panel.OverflowCount); - _lastIndex -= remove; - } + _scrollViewer = new ScrollViewer(); + _presenter = new ThingamybobPresenter(); + _scrollViewer.Content = _presenter; - private void CheckPanel() - { - var panel = Child as IVirtualizingPanel; - - if (!panel.IsFull) - { - CreateItems(); - } - else if (panel.OverflowCount > 0) - { - RemoveItems(); + Child = _scrollViewer; } } } From 9253e71eea540558da9722c08a32b790ffeac49b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 15 May 2016 09:26:49 +0200 Subject: [PATCH 003/158] Removed IScrollable from VirtualizingStackPanel --- .../VirtualizingStackPanel.cs | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 8c45148d2b..bc21a60aac 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -7,7 +7,7 @@ using System.Collections.Specialized; namespace Avalonia.Controls { - public class VirtualizingStackPanel : StackPanel, IScrollable, IVirtualizingPanel + public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel { private double _takenSpace; private int _canBeRemoved; @@ -26,26 +26,6 @@ namespace Avalonia.Controls Action IVirtualizingPanel.ArrangeCompleted { get; set; } - Action IScrollable.InvalidateScroll - { - get; - set; - } - - Size IScrollable.Extent => new Size(_takenSpace, _takenSpace); - - Vector IScrollable.Offset - { - get { return default(Vector); } - set { } - } - - Size IScrollable.Viewport => Bounds.Size; - - Size IScrollable.ScrollSize => new Size(1, 1); - - Size IScrollable.PageScrollSize => new Size(1, 1); - protected override Size ArrangeOverride(Size finalSize) { _canBeRemoved = 0; From 63a688817ea5c16febc8b0610cbabdd1d219d946 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 15 May 2016 10:52:40 +0200 Subject: [PATCH 004/158] Added pixel offset to virtualizing panel. --- src/Avalonia.Controls/IVirtualizingPanel.cs | 4 + .../Presenters/ThingamybobPresenter.cs | 12 ++- .../VirtualizingStackPanel.cs | 85 +++++++++++++++---- 3 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 6ac9a83f89..f771a6b9fe 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -8,6 +8,10 @@ namespace Avalonia.Controls int OverflowCount { get; } + double AverageItemSize { get; } + + double PixelOffset { get; set; } + Action ArrangeCompleted { get; set; } } } diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs index a270896441..4cb4bcef54 100644 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs @@ -17,6 +17,7 @@ namespace Avalonia.Controls.Presenters _panel = new VirtualizingStackPanel(); _panel.ArrangeCompleted = CheckPanel; Child = _panel; + CheckPanel(); } } @@ -24,28 +25,31 @@ namespace Avalonia.Controls.Presenters Action IScrollable.InvalidateScroll { get; set; } - Size IScrollable.Extent => new Size(1, 100); + Size IScrollable.Extent => new Size(1, 100 * AverageItemSize ); Vector IScrollable.Offset { get { - return new Vector(0, _firstIndex); + return new Vector(0, _firstIndex * AverageItemSize); } set { var count = _lastIndex - _firstIndex; - _firstIndex = (int)Math.Round(value.Y); + _firstIndex = (int)(value.Y / AverageItemSize); _lastIndex = _firstIndex + count; + _panel.PixelOffset = value.Y % AverageItemSize; Renumber(); } } - Size IScrollable.Viewport => new Size(1, _lastIndex - _firstIndex); + Size IScrollable.Viewport => new Size(1, (_lastIndex - _firstIndex) * AverageItemSize); Size IScrollable.ScrollSize => new Size(0, 1); Size IScrollable.PageScrollSize => new Size(0, 1); + private double AverageItemSize => _panel?.AverageItemSize ?? 1; + protected override Size ArrangeOverride(Size finalSize) { var result = base.ArrangeOverride(finalSize); diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index bc21a60aac..1364bb1250 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Controls.Primitives; using System; using System.Collections.Specialized; @@ -11,6 +10,9 @@ namespace Avalonia.Controls { private double _takenSpace; private int _canBeRemoved; + private double _averageItemSize; + private int _averageCount; + private double _pixelOffset; bool IVirtualizingPanel.IsFull { @@ -24,12 +26,30 @@ namespace Avalonia.Controls int IVirtualizingPanel.OverflowCount => _canBeRemoved; + double IVirtualizingPanel.AverageItemSize => _averageItemSize; + + double IVirtualizingPanel.PixelOffset + { + get { return _pixelOffset; } + + set + { + if (_pixelOffset != value) + { + _pixelOffset = value; + InvalidateArrange(); + } + } + } + Action IVirtualizingPanel.ArrangeCompleted { get; set; } protected override Size ArrangeOverride(Size finalSize) { _canBeRemoved = 0; _takenSpace = 0; + _averageItemSize = 0; + _averageCount = 0; var result = base.ArrangeOverride(finalSize); ((IVirtualizingPanel)this).ArrangeCompleted?.Invoke(); return result; @@ -44,7 +64,7 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Add: foreach (IControl control in e.NewItems) { - UpdatePhysicalSizeForAdd(control); + UpdateAdd(control); } break; @@ -52,7 +72,7 @@ namespace Avalonia.Controls case NotifyCollectionChangedAction.Remove: foreach (IControl control in e.OldItems) { - UpdatePhysicalSizeForRemove(control); + UpdateRemove(control); } break; @@ -65,64 +85,93 @@ namespace Avalonia.Controls Size panelSize, Orientation orientation) { - base.ArrangeChild(child, rect, panelSize, orientation); - - if (orientation == Orientation.Horizontal) + if (orientation == Orientation.Vertical) { - if (rect.X >= panelSize.Width) + rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); + child.Arrange(rect); + + if (rect.Y >= panelSize.Height) { ++_canBeRemoved; } - if (rect.Right >= _takenSpace) + if (rect.Bottom >= _takenSpace) { - _takenSpace = rect.Right; + _takenSpace = rect.Bottom; } + + AddToAverageItemSize(rect.Height); } else { - if (rect.Y >= panelSize.Height) + rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); + child.Arrange(rect); + + if (rect.X >= panelSize.Width) { ++_canBeRemoved; } - if (rect.Bottom >= _takenSpace) + if (rect.Right >= _takenSpace) { - _takenSpace = rect.Bottom; + _takenSpace = rect.Right; } + + AddToAverageItemSize(rect.Width); } } - private void UpdatePhysicalSizeForAdd(IControl child) + private void UpdateAdd(IControl child) { var bounds = Bounds; var gap = Gap; child.Measure(bounds.Size); + ++_averageCount; if (Orientation == Orientation.Vertical) { - _takenSpace += child.DesiredSize.Height + gap; + var height = child.DesiredSize.Height; + _takenSpace += height + gap; + AddToAverageItemSize(height); } else { - _takenSpace += child.DesiredSize.Width + gap; + var width = child.DesiredSize.Width; + _takenSpace += width + gap; + AddToAverageItemSize(width); } } - private void UpdatePhysicalSizeForRemove(IControl child) + private void UpdateRemove(IControl child) { var bounds = Bounds; var gap = Gap; if (Orientation == Orientation.Vertical) { - _takenSpace -= child.DesiredSize.Height + gap; + var height = child.DesiredSize.Height; + _takenSpace -= height + gap; + RemoveFromAverageItemSize(height); } else { - _takenSpace -= child.DesiredSize.Width + gap; + var width = child.DesiredSize.Width; + _takenSpace -= width + gap; + RemoveFromAverageItemSize(width); } } + + private void AddToAverageItemSize(double value) + { + ++_averageCount; + _averageItemSize += (value - _averageItemSize) / _averageCount; + } + + private void RemoveFromAverageItemSize(double value) + { + _averageItemSize = ((_averageItemSize * _averageCount) - value) / (_averageCount - 1); + --_averageCount; + } } } From 4568024c2fc17cf8169c9dbb520b35ea63883e0d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 21 May 2016 14:38:17 +0200 Subject: [PATCH 005/158] Leave one container materialized. --- src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs index 4cb4bcef54..73fd0c8cfa 100644 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs @@ -99,7 +99,7 @@ namespace Avalonia.Controls.Presenters { CreateItems(); } - else if (_panel.OverflowCount > 0) + else if (_panel.OverflowCount > 1) { RemoveItems(); } From 362745a55ec7df25a0f4060c47ecb4d89b11ae6e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 21 May 2016 16:53:42 +0200 Subject: [PATCH 006/158] Fix scrolling algorithm --- .../Presenters/ThingamybobPresenter.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs index 73fd0c8cfa..00f0c92535 100644 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs @@ -31,20 +31,26 @@ namespace Avalonia.Controls.Presenters { get { - return new Vector(0, _firstIndex * AverageItemSize); + return new Vector(0, (_firstIndex * AverageItemSize) + (_panel?.PixelOffset ?? 0)); } set { var count = _lastIndex - _firstIndex; - _firstIndex = (int)(value.Y / AverageItemSize); + var firstIndex = (int)(value.Y / AverageItemSize); + var firstIndexChanged = _firstIndex != firstIndex; + _firstIndex = firstIndex; _lastIndex = _firstIndex + count; _panel.PixelOffset = value.Y % AverageItemSize; - Renumber(); + + if (firstIndexChanged) + { + Renumber(); + } } } - Size IScrollable.Viewport => new Size(1, (_lastIndex - _firstIndex) * AverageItemSize); + Size IScrollable.Viewport => new Size(1, _panel?.Bounds.Height ?? 0); Size IScrollable.ScrollSize => new Size(0, 1); Size IScrollable.PageScrollSize => new Size(0, 1); From 7db353e742b96e4c8835102805a48e9bb61d4315 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 21 May 2016 18:02:01 +0200 Subject: [PATCH 007/158] Added IScrollable.IsLogicalScrollEnabled. --- .../XamlTestApplicationPcl/TestScrollable.cs | 1 + .../Presenters/ScrollContentPresenter.cs | 61 +++++++++++----- .../Presenters/ThingamybobPresenter.cs | 1 + .../Primitives/IScrollable.cs | 12 ++++ ...ScrollContentPresenterTests_IScrollable.cs | 69 +++++++++++++++++++ 5 files changed, 125 insertions(+), 19 deletions(-) diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 9ec70a4eb1..76f4cdd106 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -14,6 +14,7 @@ namespace XamlTestApplication private Size _viewport; private Size _lineSize; + public bool IsLogicalScrollEnabled => true; public Action InvalidateScroll { get; set; } Size IScrollable.Extent diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0077497b10..abb606435d 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -69,7 +69,7 @@ namespace Avalonia.Controls.Presenters { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); - this.GetObservable(ChildProperty).Subscribe(ChildChanged); + this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } /// @@ -194,22 +194,25 @@ namespace Avalonia.Controls.Presenters protected override Size ArrangeOverride(Size finalSize) { var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable; - var offset = default(Vector); + var logicalScroll = _scrollableSubscription != null; - if (_scrollableSubscription == null) + if (!logicalScroll) { Viewport = finalSize; Extent = _measuredExtent; - offset = Offset; - } - if (child != null) - { - var size = new Size( + if (child != null) + { + var size = new Size( Math.Max(finalSize.Width, child.DesiredSize.Width), Math.Max(finalSize.Height, child.DesiredSize.Height)); - child.Arrange(new Rect((Point)(-offset), size)); - return finalSize; + child.Arrange(new Rect((Point)(-Offset), size)); + return finalSize; + } + } + else if (child != null) + { + child.Arrange(new Rect(finalSize)); } return new Size(); @@ -222,7 +225,7 @@ namespace Avalonia.Controls.Presenters { var scrollable = Child as IScrollable; - if (scrollable != null) + if (scrollable?.IsLogicalScrollEnabled == true) { var y = Offset.Y + (-e.Delta.Y * scrollable.ScrollSize.Height); y = Math.Max(y, 0); @@ -246,7 +249,7 @@ namespace Avalonia.Controls.Presenters e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); } - private void ChildChanged(IControl child) + private void UpdateScrollableSubscription(IControl child) { var scrollable = child as IScrollable; @@ -256,18 +259,38 @@ namespace Avalonia.Controls.Presenters if (scrollable != null) { scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); - _scrollableSubscription = new CompositeDisposable( - this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), - Disposable.Create(() => scrollable.InvalidateScroll = null)); - UpdateFromScrollable(scrollable); + + if (scrollable?.IsLogicalScrollEnabled == true) + { + _scrollableSubscription = new CompositeDisposable( + this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), + Disposable.Create(() => scrollable.InvalidateScroll = null)); + UpdateFromScrollable(scrollable); + } } } private void UpdateFromScrollable(IScrollable scrollable) { - Viewport = scrollable.Viewport; - Extent = scrollable.Extent; - Offset = scrollable.Offset; + var logicalScroll = _scrollableSubscription != null; + + if (logicalScroll != scrollable.IsLogicalScrollEnabled) + { + UpdateScrollableSubscription(Child); + + if (!scrollable.IsLogicalScrollEnabled) + { + Offset = default(Vector); + InvalidateMeasure(); + } + } + + if (scrollable.IsLogicalScrollEnabled) + { + Viewport = scrollable.Viewport; + Extent = scrollable.Extent; + Offset = scrollable.Offset; + } } } } diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs index 00f0c92535..2171e87c51 100644 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs @@ -23,6 +23,7 @@ namespace Avalonia.Controls.Presenters public IPanel Panel => _panel; + bool IScrollable.IsLogicalScrollEnabled => true; Action IScrollable.InvalidateScroll { get; set; } Size IScrollable.Extent => new Size(1, 100 * AverageItemSize ); diff --git a/src/Avalonia.Controls/Primitives/IScrollable.cs b/src/Avalonia.Controls/Primitives/IScrollable.cs index a5165fcfb4..d37d1fdcca 100644 --- a/src/Avalonia.Controls/Primitives/IScrollable.cs +++ b/src/Avalonia.Controls/Primitives/IScrollable.cs @@ -9,8 +9,20 @@ namespace Avalonia.Controls.Primitives /// Interface implemented by controls that handle their own scrolling when placed inside a /// . /// + /// + /// Controls that implement this interface, when placed inside a + /// can override the physical scrolling behavior of the scroll viewer with logical scrolling. + /// Physical scrolling means that the scroll viewer is a simple viewport onto a larger canvas + /// whereas logical scrolling means that the scrolling is handled by the child control itself + /// and it can choose to do handle the scroll information as it sees fit. + /// public interface IScrollable { + /// + /// Gets a value indicating whether logical scrolling is enabled on the control. + /// + bool IsLogicalScrollEnabled { get; } + /// /// Gets or sets the scroll invalidation method. /// diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs index db3b2a4a19..1aea155eaa 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -5,6 +5,7 @@ using System; using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Layout; using Xunit; namespace Avalonia.Controls.UnitTests @@ -48,6 +49,27 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); } + [Fact] + public void Arrange_Should_Offset_IScrollable_Bounds_When_Logical_Scroll_Disabled() + { + var scrollable = new TestScrollable + { + IsLogicalScrollEnabled = false, + }; + + var target = new ScrollContentPresenter + { + Content = scrollable, + Offset = new Vector(25, 25), + }; + + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Rect(-25, -25, 150, 150), scrollable.Bounds); + } + [Fact] public void Arrange_Should_Not_Set_Viewport_And_Extent_With_IScrollable() { @@ -169,12 +191,59 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(50, 50), scrollable.Offset); } + [Fact] + public void Toggling_IsLogicalScrollEnabled_Should_Update_State() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + Viewport = new Size(25, 25), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable, + }; + + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); + + scrollable.IsLogicalScrollEnabled = false; + scrollable.InvalidateScroll(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Size(150, 150), target.Extent); + Assert.Equal(new Vector(0, 0), target.Offset); + Assert.Equal(new Size(100, 100), target.Viewport); + Assert.Equal(new Rect(0, 0, 150, 150), scrollable.Bounds); + + scrollable.IsLogicalScrollEnabled = true; + scrollable.InvalidateScroll(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); + } + + private class TestScrollable : Control, IScrollable { private Size _extent; private Vector _offset; private Size _viewport; + public bool IsLogicalScrollEnabled { get; set; } = true; public Size AvailableSize { get; private set; } public Action InvalidateScroll { get; set; } From f9b3d2ac0333070de7b4df607d7362f4377e9735 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 21 May 2016 19:38:47 +0200 Subject: [PATCH 008/158] WIP: Adding virtualization to ItemsPresenter. --- .../Avalonia.Controls.csproj | 3 +- .../Generators/IItemContainerGenerator.cs | 22 +-- .../Generators/ItemContainerEventArgs.cs | 6 +- .../Generators/ItemContainerGenerator.cs | 84 +++++------- ...{ItemContainer.cs => ItemContainerInfo.cs} | 6 +- .../Generators/TreeContainerIndex.cs | 6 +- .../Generators/TreeItemContainerGenerator.cs | 6 +- .../ItemVirtualizationMode.cs | 21 +++ .../Presenters/CarouselPresenter.cs | 8 +- .../Presenters/ItemsPresenter.cs | 127 ++++++++++++++---- .../Presenters/ItemsPresenterBase.cs | 2 +- .../Avalonia.Controls.UnitTests.csproj | 1 + .../Generators/ItemContainerGeneratorTests.cs | 31 ++++- .../ItemContainerGeneratorTypedTests.cs | 19 ++- .../ItemsPresenterTests_Virtualization.cs | 103 ++++++++++++++ 15 files changed, 331 insertions(+), 114 deletions(-) rename src/Avalonia.Controls/Generators/{ItemContainer.cs => ItemContainerInfo.cs} (91%) create mode 100644 src/Avalonia.Controls/ItemVirtualizationMode.cs create mode 100644 tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 9b5f8a9c1b..52e329189a 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -50,13 +50,14 @@ - + + diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 8199333f47..2e02bdc647 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls.Generators /// /// Gets the currently realized containers. /// - IEnumerable Containers { get; } + IEnumerable Containers { get; } /// /// Gets or sets the data template used to display the items in the control. @@ -34,17 +34,17 @@ namespace Avalonia.Controls.Generators event EventHandler Dematerialized; /// - /// Creates container controls for a collection of items. + /// Creates a container control for an item. /// - /// - /// The index of the first item of the data in the containing collection. + /// + /// The index of the item of the data in the containing collection. /// - /// The items. + /// The item. /// An optional member selector. /// The created controls. - IEnumerable Materialize( - int startingIndex, - IEnumerable items, + ItemContainerInfo Materialize( + int index, + object item, IMemberSelector selector); /// @@ -55,7 +55,7 @@ namespace Avalonia.Controls.Generators /// /// The the number of items to remove. /// The removed containers. - IEnumerable Dematerialize(int startingIndex, int count); + IEnumerable Dematerialize(int startingIndex, int count); /// /// Inserts space for newly inserted containers in the index. @@ -73,13 +73,13 @@ namespace Avalonia.Controls.Generators /// /// The the number of items to remove. /// The removed containers. - IEnumerable RemoveRange(int startingIndex, int count); + IEnumerable RemoveRange(int startingIndex, int count); /// /// Clears all created containers and returns the removed controls. /// /// The removed controls. - IEnumerable Clear(); + IEnumerable Clear(); /// /// Gets the container control representing the item with the specified index. diff --git a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs index 304e0a99fa..cd26b0ba83 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs @@ -19,7 +19,7 @@ namespace Avalonia.Controls.Generators /// The container. public ItemContainerEventArgs( int startingIndex, - ItemContainer container) + ItemContainerInfo container) { StartingIndex = startingIndex; Containers = new[] { container }; @@ -32,7 +32,7 @@ namespace Avalonia.Controls.Generators /// The containers. public ItemContainerEventArgs( int startingIndex, - IList containers) + IList containers) { StartingIndex = startingIndex; Containers = containers; @@ -41,7 +41,7 @@ namespace Avalonia.Controls.Generators /// /// Gets the containers. /// - public IList Containers { get; } + public IList Containers { get; } /// /// Gets the index of the first container in the source items. diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 114d31c5a6..3bf69d910a 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private List _containers = new List(); + private List _containers = new List(); /// /// Initializes a new instance of the class. @@ -29,7 +29,7 @@ namespace Avalonia.Controls.Generators } /// - public IEnumerable Containers => _containers.Where(x => x != null); + public IEnumerable Containers => _containers.Where(x => x != null); /// public event EventHandler Materialized; @@ -48,33 +48,24 @@ namespace Avalonia.Controls.Generators public IControl Owner { get; } /// - public IEnumerable Materialize( - int startingIndex, - IEnumerable items, + public ItemContainerInfo Materialize( + int index, + object item, IMemberSelector selector) { - Contract.Requires(items != null); + var i = selector != null ? selector.Select(item) : item; + var container = new ItemContainerInfo(CreateContainer(i), item, index); - int index = startingIndex; - var result = new List(); + AddContainer(container); + Materialized?.Invoke(this, new ItemContainerEventArgs(index, container)); - foreach (var item in items) - { - var i = selector != null ? selector.Select(item) : item; - var container = new ItemContainer(CreateContainer(i), item, index++); - result.Add(container); - } - - AddContainers(result); - Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); - - return result.Where(x => x != null).ToList(); + return container; } /// - public virtual IEnumerable Dematerialize(int startingIndex, int count) + public virtual IEnumerable Dematerialize(int startingIndex, int count) { - var result = new List(); + var result = new List(); for (int i = startingIndex; i < startingIndex + count; ++i) { @@ -93,13 +84,13 @@ namespace Avalonia.Controls.Generators /// public virtual void InsertSpace(int index, int count) { - _containers.InsertRange(index, Enumerable.Repeat(null, count)); + _containers.InsertRange(index, Enumerable.Repeat(null, count)); } /// - public virtual IEnumerable RemoveRange(int startingIndex, int count) + public virtual IEnumerable RemoveRange(int startingIndex, int count) { - List result = new List(); + List result = new List(); if (startingIndex < _containers.Count) { @@ -112,10 +103,10 @@ namespace Avalonia.Controls.Generators } /// - public virtual IEnumerable Clear() + public virtual IEnumerable Clear() { var result = _containers.Where(x => x != null).ToList(); - _containers = new List(); + _containers = new List(); if (result.Count > 0) { @@ -172,32 +163,29 @@ namespace Avalonia.Controls.Generators } /// - /// Adds a collection of containers to the index. + /// Adds a container to the index. /// - /// The containers. - protected void AddContainers(IList containers) + /// The container. + protected void AddContainer(ItemContainerInfo container) { - Contract.Requires(containers != null); + Contract.Requires(container != null); - foreach (var c in containers) + while (_containers.Count < container.Index) { - while (_containers.Count < c.Index) - { - _containers.Add(null); - } + _containers.Add(null); + } - if (_containers.Count == c.Index) - { - _containers.Add(c); - } - else if (_containers[c.Index] == null) - { - _containers[c.Index] = c; - } - else - { - throw new InvalidOperationException("Container already created."); - } + if (_containers.Count == container.Index) + { + _containers.Add(container); + } + else if (_containers[container.Index] == null) + { + _containers[container.Index] = container; + } + else + { + throw new InvalidOperationException("Container already created."); } } @@ -207,7 +195,7 @@ namespace Avalonia.Controls.Generators /// The first index. /// The number of elements in the range. /// The containers. - protected IEnumerable GetContainerRange(int index, int count) + protected IEnumerable GetContainerRange(int index, int count) { return _containers.GetRange(index, count); } diff --git a/src/Avalonia.Controls/Generators/ItemContainer.cs b/src/Avalonia.Controls/Generators/ItemContainerInfo.cs similarity index 91% rename from src/Avalonia.Controls/Generators/ItemContainer.cs rename to src/Avalonia.Controls/Generators/ItemContainerInfo.cs index ed2e433a28..b0fcd2867e 100644 --- a/src/Avalonia.Controls/Generators/ItemContainer.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerInfo.cs @@ -7,17 +7,17 @@ namespace Avalonia.Controls.Generators /// Holds information about an item container generated by an /// . /// - public class ItemContainer + public class ItemContainerInfo { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The container control. /// The item that the container represents. /// /// The index of the item in the collection. /// - public ItemContainer(IControl container, object item, int index) + public ItemContainerInfo(IControl container, object item, int index) { ContainerControl = container; Item = item; diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index b7a9bb2dc1..f58f7e019d 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators Materialized?.Invoke( this, - new ItemContainerEventArgs(0, new ItemContainer(container, item, 0))); + new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0))); } /// @@ -62,14 +62,14 @@ namespace Avalonia.Controls.Generators Dematerialized?.Invoke( this, - new ItemContainerEventArgs(0, new ItemContainer(container, item, 0))); + new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0))); } /// /// Removes a set of containers from the index. /// /// The item containers. - public void Remove(IEnumerable containers) + public void Remove(IEnumerable containers) { foreach (var container in containers) { diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 681bea865a..68e410ae19 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -99,20 +99,20 @@ namespace Avalonia.Controls.Generators } } - public override IEnumerable Clear() + public override IEnumerable Clear() { var items = base.Clear(); Index.Remove(items); return items; } - public override IEnumerable Dematerialize(int startingIndex, int count) + public override IEnumerable Dematerialize(int startingIndex, int count) { Index.Remove(GetContainerRange(startingIndex, count)); return base.Dematerialize(startingIndex, count); } - public override IEnumerable RemoveRange(int startingIndex, int count) + public override IEnumerable RemoveRange(int startingIndex, int count) { Index.Remove(GetContainerRange(startingIndex, count)); return base.RemoveRange(startingIndex, count); diff --git a/src/Avalonia.Controls/ItemVirtualizationMode.cs b/src/Avalonia.Controls/ItemVirtualizationMode.cs new file mode 100644 index 0000000000..f17e8c07ad --- /dev/null +++ b/src/Avalonia.Controls/ItemVirtualizationMode.cs @@ -0,0 +1,21 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Controls +{ + /// + /// Describes the item virtualization method to use for a list. + /// + public enum ItemVirtualizationMode + { + /// + /// Do not virtualize items. + /// + None, + + /// + /// Virtualize items without smooth scrolling. + /// + Simple, + } +} diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index a4f9c0cb43..29488c8b80 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -175,12 +175,8 @@ namespace Avalonia.Controls.Presenters if (container == null) { var item = Items.Cast().ElementAt(index); - var materialized = ItemContainerGenerator.Materialize( - index, - new[] { item }, - MemberSelector); - container = materialized.First().ContainerControl; - Panel.Children.Add(container); + var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector); + Panel.Children.Add(materialized.ContainerControl); } return container; diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 8c8ab5c0f8..1c4443811d 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -1,9 +1,12 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Controls.Generators; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; @@ -12,8 +15,18 @@ namespace Avalonia.Controls.Presenters /// /// Displays items inside an . /// - public class ItemsPresenter : ItemsPresenterBase + public class ItemsPresenter : ItemsPresenterBase, IScrollable { + /// + /// Defines the property. + /// + public static readonly StyledProperty VirtualizationModeProperty = + AvaloniaProperty.Register( + nameof(VirtualizationMode), + defaultValue: ItemVirtualizationMode.Simple); + + private VirtualizationInfo _virt; + /// /// Initializes static members of the class. /// @@ -24,11 +37,72 @@ namespace Avalonia.Controls.Presenters KeyboardNavigationMode.Once); } + /// + /// Gets or sets the virtualization mode for the items. + /// + public ItemVirtualizationMode VirtualizationMode + { + get { return GetValue(VirtualizationModeProperty); } + set { SetValue(VirtualizationModeProperty, value); } + } + + /// + bool IScrollable.IsLogicalScrollEnabled + { + get { return _virt != null && VirtualizationMode != ItemVirtualizationMode.None; } + } + + /// + Action IScrollable.InvalidateScroll { get; set; } + + Size IScrollable.Extent + { + get + { + switch (VirtualizationMode) + { + case ItemVirtualizationMode.Simple: + return new Size(0, Items?.Count() ?? 0); + default: + return default(Size); + } + } + } + + Vector IScrollable.Offset { get; set; } + + Size IScrollable.Viewport + { + get + { + throw new NotImplementedException(); + } + } + + Size IScrollable.ScrollSize + { + get + { + throw new NotImplementedException(); + } + } + + Size IScrollable.PageScrollSize + { + get + { + throw new NotImplementedException(); + } + } + /// protected override void CreatePanel() { base.CreatePanel(); + var virtualizingPanel = Panel as IVirtualizingPanel; + _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null; + if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) { KeyboardNavigation.SetDirectionalNavigation( @@ -55,7 +129,7 @@ namespace Avalonia.Controls.Presenters generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); } - AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector)); + AddContainers(e.NewStartingIndex, e.NewItems); break; case NotifyCollectionChangedAction.Remove: @@ -64,8 +138,7 @@ namespace Avalonia.Controls.Presenters case NotifyCollectionChangedAction.Replace: RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); - var containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); - AddContainers(containers); + var containers = AddContainers(e.NewStartingIndex, e.NewItems); var i = e.NewStartingIndex; @@ -83,7 +156,7 @@ namespace Avalonia.Controls.Presenters if (Items != null) { - AddContainers(generator.Materialize(0, Items, MemberSelector)); + AddContainers(0, Items); } break; @@ -92,17 +165,20 @@ namespace Avalonia.Controls.Presenters InvalidateMeasure(); } - private void AddContainersToPanel(IEnumerable items) + private IList AddContainers(int index, IEnumerable items) { - foreach (var i in items) + var generator = ItemContainerGenerator; + var result = new List(); + + foreach (var item in items) { + var i = generator.Materialize(index++, item, MemberSelector); + if (i.ContainerControl != null) { if (i.Index < this.Panel.Children.Count) { - // HACK: This will insert at the wrong place when there are null items, - // but all of this will need to be rewritten when we implement - // virtualization so hope no-one notices until then :) + // TODO: This will insert at the wrong place when there are null items. this.Panel.Children.Insert(i.Index, i.ContainerControl); } else @@ -110,39 +186,34 @@ namespace Avalonia.Controls.Presenters this.Panel.Children.Add(i.ContainerControl); } } + + result.Add(i); } + + return result; } - private void AddContainers(IEnumerable items) + private void RemoveContainers(IEnumerable items) { foreach (var i in items) { if (i.ContainerControl != null) { - if (i.Index < this.Panel.Children.Count) - { - // HACK: This will insert at the wrong place when there are null items, - // but all of this will need to be rewritten when we implement - // virtualization so hope no-one notices until then :) - this.Panel.Children.Insert(i.Index, i.ContainerControl); - } - else - { - this.Panel.Children.Add(i.ContainerControl); - } + this.Panel.Children.Remove(i.ContainerControl); } } } - private void RemoveContainers(IEnumerable items) + private class VirtualizationInfo { - foreach (var i in items) + public VirtualizationInfo(IVirtualizingPanel panel) { - if (i.ContainerControl != null) - { - this.Panel.Children.Remove(i.ContainerControl); - } + Panel = panel; } + + public IVirtualizingPanel Panel { get; } + public int FirstIndex { get; set; } + public int LastIndex { get; set; } } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 91ffec9c4c..718c6f018f 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -206,7 +206,7 @@ namespace Avalonia.Controls.Presenters /// /// Called when the items for the presenter change, either because - /// has been set, or the items collection has been modified. + /// has been set, the items collection has been modified, or the panel has been created. /// /// A description of the change. protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e); diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 173163c849..d1d6e3da86 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -91,6 +91,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index cdff65b35a..ff35d90237 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -1,8 +1,11 @@ // Copyright (c) The Avalonia Project. All rights reserved. // 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.Generators; +using Avalonia.Controls.Templates; using Xunit; namespace Avalonia.Controls.UnitTests.Generators @@ -15,7 +18,7 @@ namespace Avalonia.Controls.UnitTests.Generators var items = new[] { "foo", "bar", "baz" }; var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var containers = target.Materialize(0, items, null); + var containers = Materialize(target, 0, items); var result = containers .Select(x => x.ContainerControl) .OfType() @@ -31,20 +34,36 @@ namespace Avalonia.Controls.UnitTests.Generators var items = new[] { "foo", "bar", "baz" }; var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var containers = target.Materialize(0, items, null).ToList(); + var containers = Materialize(target, 0, items); Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1)); Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); } + private IList Materialize( + IItemContainerGenerator generator, + int index, + string[] items) + { + var result = new List(); + + foreach (var item in items) + { + var container = generator.Materialize(index++, item, null); + result.Add(container); + } + + return result; + } + [Fact] public void IndexFromContainer_Should_Return_Index() { var items = new[] { "foo", "bar", "baz" }; var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var containers = target.Materialize(0, items, null).ToList(); + var containers = Materialize(target, 0, items); Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl)); Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl)); @@ -57,7 +76,7 @@ namespace Avalonia.Controls.UnitTests.Generators var items = new[] { "foo", "bar", "baz" }; var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var containers = target.Materialize(0, items, null).ToList(); + var containers = Materialize(target, 0, items); target.Dematerialize(1, 1); @@ -72,7 +91,7 @@ namespace Avalonia.Controls.UnitTests.Generators var items = new[] { "foo", "bar", "baz" }; var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var containers = target.Materialize(0, items, null); + var containers = Materialize(target, 0, items); var expected = target.Containers.Take(2).ToList(); var result = target.Dematerialize(0, 2); @@ -85,7 +104,7 @@ namespace Avalonia.Controls.UnitTests.Generators var items = new[] { "foo", "bar", "baz" }; var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var containers = target.Materialize(0, items, null).ToList(); + var containers = Materialize(target, 0, items); var removed = target.RemoveRange(1, 1).Single(); diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs index 2522f1ef03..f63c0efbf9 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Generators; using Xunit; @@ -15,7 +16,7 @@ namespace Avalonia.Controls.UnitTests.Generators var items = new[] { "foo", "bar", "baz" }; var owner = new Decorator(); var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null); - var containers = target.Materialize(0, items, null); + var containers = Materialize(target, 0, items); var result = containers .Select(x => x.ContainerControl) .OfType() @@ -24,5 +25,21 @@ namespace Avalonia.Controls.UnitTests.Generators Assert.Equal(items, result); } + + private IList Materialize( + IItemContainerGenerator generator, + int index, + string[] items) + { + var result = new List(); + + foreach (var item in items) + { + var container = generator.Materialize(index++, item, null); + result.Add(container); + } + + return result; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs new file mode 100644 index 0000000000..ff602bbbb4 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -0,0 +1,103 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Presenters +{ + public class ItemsPresenterTests_Virtualization + { + [Fact] + public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel() + { + var target = new ItemsPresenter + { + }; + + target.ApplyTemplate(); + + Assert.False(((IScrollable)target).IsLogicalScrollEnabled); + } + + [Fact] + public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None() + { + var target = new ItemsPresenter + { + ItemsPanel = VirtualizingPanelTemplate(), + VirtualizationMode = ItemVirtualizationMode.None, + }; + + target.ApplyTemplate(); + + Assert.False(((IScrollable)target).IsLogicalScrollEnabled); + } + + [Fact] + public void Should_Return_IsLogicalScrollEnabled_True_When_Has_Virtualizing_Panel() + { + var target = new ItemsPresenter + { + ItemsPanel = VirtualizingPanelTemplate(), + }; + + target.ApplyTemplate(); + + Assert.True(((IScrollable)target).IsLogicalScrollEnabled); + } + + public class Simple + { + [Fact] + public void Should_Return_Items_Count_For_Extent() + { + var target = new ItemsPresenter + { + Items = new string[10], + ItemsPanel = VirtualizingPanelTemplate(), + VirtualizationMode = ItemVirtualizationMode.Simple, + }; + + target.ApplyTemplate(); + + Assert.Equal(new Size(0, 10), ((IScrollable)target).Extent); + } + + [Fact] + public void Should_Have_Number_Of_Visible_Items_As_Viewport() + { + var target = new ItemsPresenter + { + Items = new string[20], + ItemsPanel = VirtualizingPanelTemplate(), + ItemTemplate = ItemTemplate(), + VirtualizationMode = ItemVirtualizationMode.Simple, + }; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(10, ((IScrollable)target).Viewport.Height); + } + } + + private static IDataTemplate ItemTemplate() + { + return new FuncDataTemplate(x => new TextBlock + { + Text = x, + Height = 10, + }); + } + + private static ITemplate VirtualizingPanelTemplate() + { + return new FuncTemplate(() => new VirtualizingStackPanel()); + } + } +} From 38afaf1aca9e64abe3ffec38881b9c8f070c523e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 22 May 2016 20:18:43 +0200 Subject: [PATCH 009/158] More WIP on adding virtualization to ItemsPresenter. --- .../Views/MainWindow.cs | 14 +- .../Views/MainWindow.xaml | 3 +- src/Avalonia.Controls/IVirtualizingPanel.cs | 2 - src/Avalonia.Controls/ItemsControl.cs | 1 - src/Avalonia.Controls/ListBox.cs | 15 ++ .../Presenters/CarouselPresenter.cs | 3 +- .../Presenters/ItemsPresenter.cs | 111 ++++++++++--- .../Presenters/ItemsPresenterBase.cs | 34 ++-- .../Presenters/ScrollContentPresenter.cs | 1 + .../Presenters/ThingamybobPresenter.cs | 1 - .../VirtualizingStackPanel.cs | 3 - .../ItemsPresenterTests_Virtualization.cs | 149 +++++++++++++++--- 12 files changed, 260 insertions(+), 77 deletions(-) diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.cs b/samples/XamlTestApplicationPcl/Views/MainWindow.cs index 3df8bdeea3..ed5cb8f73a 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.cs +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Generic; +using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Diagnostics; @@ -26,16 +28,8 @@ namespace XamlTestApplication.Views _exitMenu = this.FindControl("exitMenu"); _exitMenu.Click += (s, e) => Application.Current.Exit(); - var vadd = this.FindControl - + diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index f771a6b9fe..ca7ffae145 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -11,7 +11,5 @@ namespace Avalonia.Controls double AverageItemSize { get; } double PixelOffset { get; set; } - - Action ArrangeCompleted { get; set; } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 3a77f61f1b..ef090bc697 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -25,7 +25,6 @@ namespace Avalonia.Controls /// /// The default value for the property. /// - [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")] private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 0f32bc91da..f61e2fa8d3 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; @@ -16,6 +17,12 @@ namespace Avalonia.Controls /// public class ListBox : SelectingItemsControl { + /// + /// The default value for the property. + /// + private static readonly FuncTemplate DefaultPanel = + new FuncTemplate(() => new VirtualizingStackPanel()); + /// /// Defines the property. /// @@ -28,6 +35,14 @@ namespace Avalonia.Controls public static readonly new AvaloniaProperty SelectionModeProperty = SelectingItemsControl.SelectionModeProperty; + /// + /// Initializes static members of the class. + /// + static ListBox() + { + ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + } + /// public new IList SelectedItems => base.SelectedItems; diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 29488c8b80..2b7442597b 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -95,9 +95,8 @@ namespace Avalonia.Controls.Presenters } /// - protected override void CreatePanel() + protected override void PanelCreated(IPanel panel) { - base.CreatePanel(); var task = MoveToPage(-1, SelectedIndex); } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 1c4443811d..21d2bcada9 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; @@ -55,6 +56,7 @@ namespace Avalonia.Controls.Presenters /// Action IScrollable.InvalidateScroll { get; set; } + /// Size IScrollable.Extent { get @@ -69,39 +71,53 @@ namespace Avalonia.Controls.Presenters } } + /// Vector IScrollable.Offset { get; set; } + /// Size IScrollable.Viewport { get { - throw new NotImplementedException(); + switch (VirtualizationMode) + { + case ItemVirtualizationMode.Simple: + return new Size(0, (_virt.LastIndex - _virt.FirstIndex) + 1); + default: + return default(Size); + } } } - Size IScrollable.ScrollSize - { - get - { - throw new NotImplementedException(); - } - } + /// + Size IScrollable.ScrollSize => new Size(0, 1); + + /// + Size IScrollable.PageScrollSize => new Size(0, 1); - Size IScrollable.PageScrollSize + /// + protected override Size ArrangeOverride(Size finalSize) { - get + var result = base.ArrangeOverride(finalSize); + + if (_virt != null) { - throw new NotImplementedException(); + CreateRemoveVirtualizedContainers(); + ((IScrollable)this).InvalidateScroll(); + } + + return result; } /// - protected override void CreatePanel() + protected override void PanelCreated(IPanel panel) { - base.CreatePanel(); - - var virtualizingPanel = Panel as IVirtualizingPanel; - _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null; + if (((IScrollable)this).InvalidateScroll != null) + { + var virtualizingPanel = Panel as IVirtualizingPanel; + _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null; + } if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) { @@ -115,8 +131,19 @@ namespace Avalonia.Controls.Presenters KeyboardNavigation.GetTabNavigation(this)); } - /// protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) + { + if (_virt == null) + { + ItemsChangedNonVirtualized(e); + } + else + { + ItemsChangedVirtualized(e); + } + } + + private void ItemsChangedNonVirtualized(NotifyCollectionChangedEventArgs e) { var generator = ItemContainerGenerator; @@ -129,7 +156,7 @@ namespace Avalonia.Controls.Presenters generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); } - AddContainers(e.NewStartingIndex, e.NewItems); + AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems); break; case NotifyCollectionChangedAction.Remove: @@ -138,7 +165,7 @@ namespace Avalonia.Controls.Presenters case NotifyCollectionChangedAction.Replace: RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); - var containers = AddContainers(e.NewStartingIndex, e.NewItems); + var containers = AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems); var i = e.NewStartingIndex; @@ -156,7 +183,7 @@ namespace Avalonia.Controls.Presenters if (Items != null) { - AddContainers(0, Items); + AddContainersNonVirtualized(0, Items); } break; @@ -165,7 +192,11 @@ namespace Avalonia.Controls.Presenters InvalidateMeasure(); } - private IList AddContainers(int index, IEnumerable items) + private void ItemsChangedVirtualized(NotifyCollectionChangedEventArgs e) + { + } + + private IList AddContainersNonVirtualized(int index, IEnumerable items) { var generator = ItemContainerGenerator; var result = new List(); @@ -193,6 +224,42 @@ namespace Avalonia.Controls.Presenters return result; } + private void CreateRemoveVirtualizedContainers() + { + var generator = ItemContainerGenerator; + var panel = _virt.Panel; + + if (!panel.IsFull) + { + var index = _virt.LastIndex + 1; + var items = Items.Cast().Skip(index); + var memberSelector = MemberSelector; + + foreach (var item in items) + { + var materialized = generator.Materialize(index++, item, memberSelector); + panel.Children.Add(materialized.ContainerControl); + + if (panel.IsFull) + { + break; + } + } + + _virt.LastIndex = index - 1; + } + + if (panel.OverflowCount > 0) + { + var remove = panel.OverflowCount; + + panel.Children.RemoveRange( + panel.Children.Count - remove, + panel.OverflowCount); + _virt.LastIndex -= remove; + } + } + private void RemoveContainers(IEnumerable items) { foreach (var i in items) @@ -213,7 +280,7 @@ namespace Avalonia.Controls.Presenters public IVirtualizingPanel Panel { get; } public int FirstIndex { get; set; } - public int LastIndex { get; set; } + public int LastIndex { get; set; } = -1; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 718c6f018f..5fbed4d553 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -102,7 +102,13 @@ namespace Avalonia.Controls.Presenters if (_generator == null) { var i = TemplatedParent as ItemsControl; - _generator = (i?.ItemContainerGenerator) ?? new ItemContainerGenerator(this); + _generator = i?.ItemContainerGenerator; + + if (_generator == null) + { + _generator = new ItemContainerGenerator(this); + _generator.ItemTemplate = ItemTemplate; + } } return _generator; @@ -178,11 +184,26 @@ namespace Avalonia.Controls.Presenters return finalSize; } + /// + /// Called when the is created. + /// + /// The panel. + protected virtual void PanelCreated(IPanel panel) + { + } + + /// + /// Called when the items for the presenter change, either because + /// has been set, the items collection has been modified, or the panel has been created. + /// + /// A description of the change. + protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e); + /// /// Creates the when is called for the first /// time. /// - protected virtual void CreatePanel() + private void CreatePanel() { Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); @@ -201,16 +222,11 @@ namespace Avalonia.Controls.Presenters incc.CollectionChanged += ItemsCollectionChanged; } + PanelCreated(Panel); + ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } - /// - /// Called when the items for the presenter change, either because - /// has been set, the items collection has been modified, or the panel has been created. - /// - /// A description of the change. - protected abstract void ItemsChanged(NotifyCollectionChangedEventArgs e); - /// /// Called when the collection changes. /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index abb606435d..5d295d7fc8 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -213,6 +213,7 @@ namespace Avalonia.Controls.Presenters else if (child != null) { child.Arrange(new Rect(finalSize)); + return finalSize; } return new Size(); diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs index 2171e87c51..c4d9dc34c8 100644 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs @@ -15,7 +15,6 @@ namespace Avalonia.Controls.Presenters if (_panel == null) { _panel = new VirtualizingStackPanel(); - _panel.ArrangeCompleted = CheckPanel; Child = _panel; CheckPanel(); } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 1364bb1250..865ad5d895 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -42,8 +42,6 @@ namespace Avalonia.Controls } } - Action IVirtualizingPanel.ArrangeCompleted { get; set; } - protected override Size ArrangeOverride(Size finalSize) { _canBeRemoved = 0; @@ -51,7 +49,6 @@ namespace Avalonia.Controls _averageItemSize = 0; _averageCount = 0; var result = base.ArrangeOverride(finalSize); - ((IVirtualizingPanel)this).ArrangeCompleted?.Invoke(); return result; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index ff602bbbb4..aba9dbb7cf 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections.Generic; +using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -14,9 +16,8 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Should_Return_IsLogicalScrollEnabled_False_When_Has_No_Virtualizing_Panel() { - var target = new ItemsPresenter - { - }; + var target = CreateTarget(); + target.ClearValue(ItemsPresenter.ItemsPanelProperty); target.ApplyTemplate(); @@ -26,11 +27,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Should_Return_IsLogicalScrollEnabled_False_When_VirtualizationMode_None() { - var target = new ItemsPresenter - { - ItemsPanel = VirtualizingPanelTemplate(), - VirtualizationMode = ItemVirtualizationMode.None, - }; + var target = CreateTarget(ItemVirtualizationMode.None); target.ApplyTemplate(); @@ -38,45 +35,108 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Return_IsLogicalScrollEnabled_True_When_Has_Virtualizing_Panel() + public void Should_Return_IsLogicalScrollEnabled_False_When_Doesnt_Have_ScrollPresenter_Parent() { var target = new ItemsPresenter { ItemsPanel = VirtualizingPanelTemplate(), + ItemTemplate = ItemTemplate(), + VirtualizationMode = ItemVirtualizationMode.Simple, }; target.ApplyTemplate(); + Assert.False(((IScrollable)target).IsLogicalScrollEnabled); + } + + [Fact] + public void Should_Return_IsLogicalScrollEnabled_True() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + Assert.True(((IScrollable)target).IsLogicalScrollEnabled); } + [Fact] + public void Should_Fill_Panel_With_Containers() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(10, target.Panel.Children.Count); + } + + [Fact] + public void Should_Only_Create_Enough_Containers_To_Display_All_Items() + { + var target = CreateTarget(itemCount: 2); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(2, target.Panel.Children.Count); + } + + [Fact] + public void Initial_Item_DataContexts_Should_Be_Correct() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i], target.Panel.Children[i].DataContext); + } + } + + [Fact] + public void Should_Add_New_Items_When_Control_Is_Enlarged() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(10, target.Panel.Children.Count); + + target.Arrange(new Rect(0, 0, 100, 120)); + + Assert.Equal(12, target.Panel.Children.Count); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i], target.Panel.Children[i].DataContext); + } + } + public class Simple { [Fact] public void Should_Return_Items_Count_For_Extent() { - var target = new ItemsPresenter - { - Items = new string[10], - ItemsPanel = VirtualizingPanelTemplate(), - VirtualizationMode = ItemVirtualizationMode.Simple, - }; + var target = CreateTarget(); target.ApplyTemplate(); - Assert.Equal(new Size(0, 10), ((IScrollable)target).Extent); + Assert.Equal(new Size(0, 20), ((IScrollable)target).Extent); } [Fact] public void Should_Have_Number_Of_Visible_Items_As_Viewport() { - var target = new ItemsPresenter - { - Items = new string[20], - ItemsPanel = VirtualizingPanelTemplate(), - ItemTemplate = ItemTemplate(), - VirtualizationMode = ItemVirtualizationMode.Simple, - }; + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -84,13 +144,52 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(10, ((IScrollable)target).Viewport.Height); } + + [Fact] + public void Should_Remove_Items_When_Control_Is_Shrank() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(10, target.Panel.Children.Count); + + target.Arrange(new Rect(0, 0, 100, 80)); + + Assert.Equal(8, target.Panel.Children.Count); + } + } + + private static ItemsPresenter CreateTarget( + ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, + int itemCount = 20) + { + ItemsPresenter result; + var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList(); + + var scroller = new ScrollContentPresenter + { + Content = result = new ItemsPresenter + { + Items = items, + ItemsPanel = VirtualizingPanelTemplate(), + ItemTemplate = ItemTemplate(), + VirtualizationMode = mode, + } + }; + + scroller.UpdateChild(); + + return result; } private static IDataTemplate ItemTemplate() { - return new FuncDataTemplate(x => new TextBlock + return new FuncDataTemplate(x => new Canvas { - Text = x, Height = 10, }); } From 850dfdea4809fd5d458686de0d05cbbb74ff398e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 22 May 2016 20:32:34 +0200 Subject: [PATCH 010/158] Dematerialize items when removed. --- src/Avalonia.Controls/Presenters/ItemsPresenter.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 21d2bcada9..23fe8d0a69 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -251,12 +251,13 @@ namespace Avalonia.Controls.Presenters if (panel.OverflowCount > 0) { - var remove = panel.OverflowCount; + var count = panel.OverflowCount; + var index = panel.Children.Count - count; - panel.Children.RemoveRange( - panel.Children.Count - remove, - panel.OverflowCount); - _virt.LastIndex -= remove; + panel.Children.RemoveRange(index, count); + generator.Dematerialize(index, count); + + _virt.LastIndex -= count; } } From 91e2f2a0ca786d4d57b7a875c405b8bc7a97b52f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 May 2016 09:56:03 +0200 Subject: [PATCH 011/158] Refactored virtualization handling into classes Also take into account the scroll direction of the panel. --- .../Avalonia.Controls.csproj | 3 + src/Avalonia.Controls/IVirtualizingPanel.cs | 25 ++- .../Presenters/ItemVirtualizer.cs | 52 +++++ .../Presenters/ItemVirtualizerNone.cs | 137 ++++++++++++ .../Presenters/ItemVirtualizerSimple.cs | 93 +++++++++ .../Presenters/ItemsPresenter.cs | 197 +----------------- .../Utils/IEnumerableUtils.cs | 19 +- .../VirtualizingStackPanel.cs | 2 + ...lonia.Controls.UnitTests.v2.ncrunchproject | 7 +- .../ItemsPresenterTests_Virtualization.cs | 41 +++- 10 files changed, 368 insertions(+), 208 deletions(-) create mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizer.cs create mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs create mode 100644 src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 52e329189a..9fe4fecc68 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -70,7 +70,10 @@ + + + diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index ca7ffae145..2aa356d38c 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -1,15 +1,38 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; namespace Avalonia.Controls { + /// + /// A panel that can be used to virtualize items. + /// public interface IVirtualizingPanel : IPanel { + /// + /// Gets a value indicating whether the panel is full. + /// bool IsFull { get; } + /// + /// Gets the number of items that can be removed while keeping the panel full. + /// int OverflowCount { get; } + /// + /// Gets the direction of scroll. + /// + Orientation ScrollDirection { get; } + + /// + /// Gets the average size of the materialized items in the direction of scroll. + /// double AverageItemSize { get; } + /// + /// Gets or sets the current pixel offset of the items in the direction of scroll. + /// double PixelOffset { get; set; } } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs new file mode 100644 index 0000000000..b95511e635 --- /dev/null +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -0,0 +1,52 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Specialized; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Controls.Presenters +{ + internal abstract class ItemVirtualizer + { + public ItemVirtualizer(ItemsPresenter owner) + { + Owner = owner; + } + + public ItemsPresenter Owner { get; } + public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel; + public IEnumerable Items { get; private set; } + public int FirstIndex { get; set; } + public int LastIndex { get; set; } = -1; + + public abstract bool IsLogicalScrollEnabled { get; } + public abstract Size Extent { get; } + public abstract Size Viewport { get; } + + public static ItemVirtualizer Create(ItemsPresenter owner) + { + var virtualizingPanel = owner.Panel as IVirtualizingPanel; + var scrollable = (IScrollable)owner; + + if (virtualizingPanel != null && scrollable.InvalidateScroll != null) + { + switch (owner.VirtualizationMode) + { + case ItemVirtualizationMode.Simple: + return new ItemVirtualizerSimple(owner); + } + } + + return new ItemVirtualizerNone(owner); + } + + public abstract void Arranging(Size finalSize); + + public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) + { + Items = items; + } + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs new file mode 100644 index 0000000000..c5ccb2ec0b --- /dev/null +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -0,0 +1,137 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls.Presenters +{ + internal class ItemVirtualizerNone : ItemVirtualizer + { + public ItemVirtualizerNone(ItemsPresenter owner) + : base(owner) + { + } + + public override bool IsLogicalScrollEnabled => false; + + public override Size Extent + { + get + { + throw new NotSupportedException(); + } + } + + public override Size Viewport + { + get + { + throw new NotSupportedException(); + } + } + + public override void Arranging(Size finalSize) + { + // We don't need to do anything here. + } + + public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) + { + base.ItemsChanged(items, e); + + var generator = Owner.ItemContainerGenerator; + var panel = Owner.Panel; + + // TODO: Handle Move and Replace etc. + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewStartingIndex + e.NewItems.Count < Items.Count()) + { + generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); + } + + AddContainers(e.NewStartingIndex, e.NewItems); + break; + + case NotifyCollectionChangedAction.Remove: + RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); + break; + + case NotifyCollectionChangedAction.Replace: + RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); + var containers = AddContainers(e.NewStartingIndex, e.NewItems); + + var i = e.NewStartingIndex; + + foreach (var container in containers) + { + panel.Children[i++] = container.ContainerControl; + } + + break; + + case NotifyCollectionChangedAction.Move: + // TODO: Implement Move in a more efficient manner. + case NotifyCollectionChangedAction.Reset: + RemoveContainers(generator.Clear()); + + if (Items != null) + { + AddContainers(0, Items); + } + + break; + } + + Owner.InvalidateMeasure(); + } + + private IList AddContainers(int index, IEnumerable items) + { + var generator = Owner.ItemContainerGenerator; + var result = new List(); + var panel = Owner.Panel; + + foreach (var item in items) + { + var i = generator.Materialize(index++, item, Owner.MemberSelector); + + if (i.ContainerControl != null) + { + if (i.Index < panel.Children.Count) + { + // TODO: This will insert at the wrong place when there are null items. + panel.Children.Insert(i.Index, i.ContainerControl); + } + else + { + panel.Children.Add(i.ContainerControl); + } + } + + result.Add(i); + } + + return result; + } + + private void RemoveContainers(IEnumerable items) + { + var panel = Owner.Panel; + + foreach (var i in items) + { + if (i.ContainerControl != null) + { + panel.Children.Remove(i.ContainerControl); + } + } + } + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs new file mode 100644 index 0000000000..fb40960778 --- /dev/null +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -0,0 +1,93 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Linq; +using Avalonia.Controls.Utils; + +namespace Avalonia.Controls.Presenters +{ + internal class ItemVirtualizerSimple : ItemVirtualizer + { + public ItemVirtualizerSimple(ItemsPresenter owner) + : base(owner) + { + } + + public override bool IsLogicalScrollEnabled => true; + + public override Size Extent + { + get + { + if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + { + return new Size(0, Items.Count()); + } + else + { + return new Size(Items.Count(), 0); + } + } + } + + public override Size Viewport + { + get + { + var panel = VirtualizingPanel; + + if (panel.ScrollDirection == Orientation.Vertical) + { + return new Size(0, panel.Children.Count); + } + else + { + return new Size(panel.Children.Count, 0); + } + } + } + + public override void Arranging(Size finalSize) + { + CreateRemoveContainers(); + } + + private void CreateRemoveContainers() + { + var generator = Owner.ItemContainerGenerator; + var panel = VirtualizingPanel; + + if (!panel.IsFull) + { + var index = LastIndex + 1; + var items = Items.Cast().Skip(index); + var memberSelector = Owner.MemberSelector; + + foreach (var item in items) + { + var materialized = generator.Materialize(index++, item, memberSelector); + panel.Children.Add(materialized.ContainerControl); + + if (panel.IsFull) + { + break; + } + } + + LastIndex = index - 1; + } + + if (panel.OverflowCount > 0) + { + var count = panel.OverflowCount; + var index = panel.Children.Count - count; + + panel.Children.RemoveRange(index, count); + generator.Dematerialize(index, count); + + LastIndex -= count; + } + } + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 23fe8d0a69..fa05d1bbd0 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls.Presenters nameof(VirtualizationMode), defaultValue: ItemVirtualizationMode.Simple); - private VirtualizationInfo _virt; + private ItemVirtualizer _virtualizer; /// /// Initializes static members of the class. @@ -50,44 +50,20 @@ namespace Avalonia.Controls.Presenters /// bool IScrollable.IsLogicalScrollEnabled { - get { return _virt != null && VirtualizationMode != ItemVirtualizationMode.None; } + get { return _virtualizer?.IsLogicalScrollEnabled ?? false; } } /// Action IScrollable.InvalidateScroll { get; set; } /// - Size IScrollable.Extent - { - get - { - switch (VirtualizationMode) - { - case ItemVirtualizationMode.Simple: - return new Size(0, Items?.Count() ?? 0); - default: - return default(Size); - } - } - } + Size IScrollable.Extent => _virtualizer.Extent; /// Vector IScrollable.Offset { get; set; } /// - Size IScrollable.Viewport - { - get - { - switch (VirtualizationMode) - { - case ItemVirtualizationMode.Simple: - return new Size(0, (_virt.LastIndex - _virt.FirstIndex) + 1); - default: - return default(Size); - } - } - } + Size IScrollable.Viewport => _virtualizer.Viewport; /// Size IScrollable.ScrollSize => new Size(0, 1); @@ -99,25 +75,14 @@ namespace Avalonia.Controls.Presenters protected override Size ArrangeOverride(Size finalSize) { var result = base.ArrangeOverride(finalSize); - - if (_virt != null) - { - CreateRemoveVirtualizedContainers(); - ((IScrollable)this).InvalidateScroll(); - - } - + _virtualizer.Arranging(finalSize); return result; } /// protected override void PanelCreated(IPanel panel) { - if (((IScrollable)this).InvalidateScroll != null) - { - var virtualizingPanel = Panel as IVirtualizingPanel; - _virt = virtualizingPanel != null ? new VirtualizationInfo(virtualizingPanel) : null; - } + _virtualizer = ItemVirtualizer.Create(this); if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) { @@ -133,155 +98,7 @@ namespace Avalonia.Controls.Presenters protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) { - if (_virt == null) - { - ItemsChangedNonVirtualized(e); - } - else - { - ItemsChangedVirtualized(e); - } - } - - private void ItemsChangedNonVirtualized(NotifyCollectionChangedEventArgs e) - { - var generator = ItemContainerGenerator; - - // TODO: Handle Move and Replace etc. - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - if (e.NewStartingIndex + e.NewItems.Count < Items.Count()) - { - generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); - } - - AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems); - break; - - case NotifyCollectionChangedAction.Remove: - RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); - break; - - case NotifyCollectionChangedAction.Replace: - RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); - var containers = AddContainersNonVirtualized(e.NewStartingIndex, e.NewItems); - - var i = e.NewStartingIndex; - - foreach (var container in containers) - { - Panel.Children[i++] = container.ContainerControl; - } - - break; - - case NotifyCollectionChangedAction.Move: - // TODO: Implement Move in a more efficient manner. - case NotifyCollectionChangedAction.Reset: - RemoveContainers(generator.Clear()); - - if (Items != null) - { - AddContainersNonVirtualized(0, Items); - } - - break; - } - - InvalidateMeasure(); - } - - private void ItemsChangedVirtualized(NotifyCollectionChangedEventArgs e) - { - } - - private IList AddContainersNonVirtualized(int index, IEnumerable items) - { - var generator = ItemContainerGenerator; - var result = new List(); - - foreach (var item in items) - { - var i = generator.Materialize(index++, item, MemberSelector); - - if (i.ContainerControl != null) - { - if (i.Index < this.Panel.Children.Count) - { - // TODO: This will insert at the wrong place when there are null items. - this.Panel.Children.Insert(i.Index, i.ContainerControl); - } - else - { - this.Panel.Children.Add(i.ContainerControl); - } - } - - result.Add(i); - } - - return result; - } - - private void CreateRemoveVirtualizedContainers() - { - var generator = ItemContainerGenerator; - var panel = _virt.Panel; - - if (!panel.IsFull) - { - var index = _virt.LastIndex + 1; - var items = Items.Cast().Skip(index); - var memberSelector = MemberSelector; - - foreach (var item in items) - { - var materialized = generator.Materialize(index++, item, memberSelector); - panel.Children.Add(materialized.ContainerControl); - - if (panel.IsFull) - { - break; - } - } - - _virt.LastIndex = index - 1; - } - - if (panel.OverflowCount > 0) - { - var count = panel.OverflowCount; - var index = panel.Children.Count - count; - - panel.Children.RemoveRange(index, count); - generator.Dematerialize(index, count); - - _virt.LastIndex -= count; - } - } - - private void RemoveContainers(IEnumerable items) - { - foreach (var i in items) - { - if (i.ContainerControl != null) - { - this.Panel.Children.Remove(i.ContainerControl); - } - } - } - - private class VirtualizationInfo - { - public VirtualizationInfo(IVirtualizingPanel panel) - { - Panel = panel; - } - - public IVirtualizingPanel Panel { get; } - public int FirstIndex { get; set; } - public int LastIndex { get; set; } = -1; + _virtualizer?.ItemsChanged(Items, e); } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs index 1e05c23e5a..ad95b0269a 100644 --- a/src/Avalonia.Controls/Utils/IEnumerableUtils.cs +++ b/src/Avalonia.Controls/Utils/IEnumerableUtils.cs @@ -17,17 +17,22 @@ namespace Avalonia.Controls.Utils public static int Count(this IEnumerable items) { - Contract.Requires(items != null); - - var collection = items as ICollection; - - if (collection != null) + if (items != null) { - return collection.Count; + var collection = items as ICollection; + + if (collection != null) + { + return collection.Count; + } + else + { + return Enumerable.Count(items.Cast()); + } } else { - return Enumerable.Count(items.Cast()); + return 0; } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 865ad5d895..afb691d2e5 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -26,6 +26,8 @@ namespace Avalonia.Controls int IVirtualizingPanel.OverflowCount => _canBeRemoved; + Orientation IVirtualizingPanel.ScrollDirection => Orientation; + double IVirtualizingPanel.AverageItemSize => _averageItemSize; double IVirtualizingPanel.PixelOffset diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject index 30815b1937..b5cd70b13f 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.v2.ncrunchproject @@ -17,10 +17,11 @@ true true 60000 - - - + + + AutoDetect STA x86 + LongTestTimesWithoutParallelExecution \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index aba9dbb7cf..097ce08171 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -6,7 +6,6 @@ using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; -using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters @@ -124,7 +123,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public class Simple { [Fact] - public void Should_Return_Items_Count_For_Extent() + public void Should_Return_Items_Count_For_Extent_Vertical() { var target = CreateTarget(); @@ -134,7 +133,17 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Have_Number_Of_Visible_Items_As_Viewport() + public void Should_Return_Items_Count_For_Extent_Horizontal() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + + Assert.Equal(new Size(20, 0), ((IScrollable)target).Extent); + } + + [Fact] + public void Should_Have_Number_Of_Visible_Items_As_Viewport_Vertical() { var target = CreateTarget(); @@ -142,7 +151,19 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - Assert.Equal(10, ((IScrollable)target).Viewport.Height); + Assert.Equal(new Size(0, 10), ((IScrollable)target).Viewport); + } + + [Fact] + public void Should_Have_Number_Of_Visible_Items_As_Viewport_Horizontal() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Size(10, 0), ((IScrollable)target).Viewport); } [Fact] @@ -165,6 +186,7 @@ namespace Avalonia.Controls.UnitTests.Presenters private static ItemsPresenter CreateTarget( ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, + Orientation orientation = Orientation.Vertical, int itemCount = 20) { ItemsPresenter result; @@ -175,7 +197,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Content = result = new ItemsPresenter { Items = items, - ItemsPanel = VirtualizingPanelTemplate(), + ItemsPanel = VirtualizingPanelTemplate(orientation), ItemTemplate = ItemTemplate(), VirtualizationMode = mode, } @@ -190,13 +212,18 @@ namespace Avalonia.Controls.UnitTests.Presenters { return new FuncDataTemplate(x => new Canvas { + Width = 10, Height = 10, }); } - private static ITemplate VirtualizingPanelTemplate() + private static ITemplate VirtualizingPanelTemplate( + Orientation orientation = Orientation.Vertical) { - return new FuncTemplate(() => new VirtualizingStackPanel()); + return new FuncTemplate(() => new VirtualizingStackPanel + { + Orientation = orientation, + }); } } } From 1c7a99dc5e93bd90a57b60303dc4865cf98a57bf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 May 2016 15:11:58 +0200 Subject: [PATCH 012/158] WIP Trying to get virt scrolling working. --- .../Presenters/ItemVirtualizer.cs | 1 + .../Presenters/ItemVirtualizerNone.cs | 16 ++--- .../Presenters/ItemVirtualizerSimple.cs | 69 +++++++++++++++++++ .../Presenters/ItemsPresenter.cs | 21 ++++-- .../ItemsPresenterTests_Virtualization.cs | 62 +++++++++++++++++ 5 files changed, 155 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index b95511e635..b45ed8065b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -23,6 +23,7 @@ namespace Avalonia.Controls.Presenters public abstract bool IsLogicalScrollEnabled { get; } public abstract Size Extent { get; } + public abstract Vector Offset { get; set; } public abstract Size Viewport { get; } public static ItemVirtualizer Create(ItemsPresenter owner) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index c5ccb2ec0b..919bde7e0f 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -21,18 +21,18 @@ namespace Avalonia.Controls.Presenters public override Size Extent { - get - { - throw new NotSupportedException(); - } + get { throw new NotSupportedException(); } + } + + public override Vector Offset + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } } public override Size Viewport { - get - { - throw new NotSupportedException(); - } + get { throw new NotSupportedException(); } } public override void Arranging(Size finalSize) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index fb40960778..722bd9c15d 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -31,6 +31,75 @@ namespace Avalonia.Controls.Presenters } } + public override Vector Offset + { + get + { + if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + { + return new Vector(0, FirstIndex); + } + else + { + return new Vector(FirstIndex, 0); + } + } + + set + { + var scroll = (VirtualizingPanel.ScrollDirection == Orientation.Vertical) ? + value.Y : value.X; + var delta = (int)(scroll - FirstIndex); + var panel = VirtualizingPanel; + + if (delta != 0) + { + if (delta >= panel.Children.Count) + { + var index = FirstIndex + delta; + + foreach (var container in panel.Children) + { + container.DataContext = Items.ElementAt(index++); + } + } + else if (delta > 0) + { + var containers = panel.Children.GetRange(0, delta).ToList(); + panel.Children.RemoveRange(0, delta); + + var index = LastIndex + 1; + + foreach (var container in containers) + { + container.DataContext = Items.ElementAt(index++); + } + + panel.Children.AddRange(containers); + } + else + { + var first = panel.Children.Count + delta; + var count = -delta; + var containers = panel.Children.GetRange(first, count).ToList(); + panel.Children.RemoveRange(first, count); + + var index = FirstIndex + delta; + + foreach (var container in containers) + { + container.DataContext = Items.ElementAt(index++); + } + + panel.Children.InsertRange(0, containers); + } + + FirstIndex += delta; + LastIndex += delta; + } + } + } + public override Size Viewport { get diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index fa05d1bbd0..fc20add96c 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -2,14 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; -using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Utils; using Avalonia.Input; +using static Avalonia.Utilities.MathUtilities; namespace Avalonia.Controls.Presenters { @@ -60,7 +56,11 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Extent => _virtualizer.Extent; /// - Vector IScrollable.Offset { get; set; } + Vector IScrollable.Offset + { + get { return _virtualizer.Offset; } + set { _virtualizer.Offset = CoerceOffset(value); } + } /// Size IScrollable.Viewport => _virtualizer.Viewport; @@ -83,6 +83,7 @@ namespace Avalonia.Controls.Presenters protected override void PanelCreated(IPanel panel) { _virtualizer = ItemVirtualizer.Create(this); + ((IScrollable)this).InvalidateScroll?.Invoke(); if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) { @@ -100,5 +101,13 @@ namespace Avalonia.Controls.Presenters { _virtualizer?.ItemsChanged(Items, e); } + + private Vector CoerceOffset(Vector value) + { + var scrollable = (IScrollable)this; + var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0); + var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); + return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); + } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 097ce08171..fe56cd95ac 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -182,6 +182,68 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(8, target.Panel.Children.Count); } + + [Fact] + public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var containers = target.Panel.Children.ToList(); + var scroller = (ScrollContentPresenter)target.Parent; + + scroller.Offset = new Vector(0, 5); + + var scrolledContainers = containers + .Skip(5) + .Take(5) + .Concat(containers.Take(5)).ToList(); + + Assert.Equal(new Vector(0, 5), ((IScrollable)target).Offset); + Assert.Equal(scrolledContainers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); + } + + scroller.Offset = new Vector(0, 0); + Assert.Equal(new Vector(0, 0), ((IScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i], target.Panel.Children[i].DataContext); + } + } + + [Fact] + public void Scrolling_More_Than_A_Page_Should_Recycle_Items() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var containers = target.Panel.Children.ToList(); + var scroller = (ScrollContentPresenter)target.Parent; + + scroller.Offset = new Vector(0, 10); + + Assert.Equal(new Vector(0, 10), ((IScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 10], target.Panel.Children[i].DataContext); + } + } } private static ItemsPresenter CreateTarget( From cce2ab603f33742e22e22e198aed9ae0b381c37e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 May 2016 19:16:58 +0200 Subject: [PATCH 013/158] Ensure to update scroll info when items changed. --- .../Presenters/ItemVirtualizerSimple.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 722bd9c15d..fa2b987a07 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -2,7 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; +using System.Collections.Specialized; using System.Linq; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; namespace Avalonia.Controls.Presenters @@ -122,6 +125,12 @@ namespace Avalonia.Controls.Presenters CreateRemoveContainers(); } + public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) + { + base.ItemsChanged(items, e); + ((IScrollable)Owner).InvalidateScroll(); + } + private void CreateRemoveContainers() { var generator = Owner.ItemContainerGenerator; From 1d83126d20f4b1d5375816de82f74efea371a6ce Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 May 2016 21:25:51 +0200 Subject: [PATCH 014/158] More WIP on virtualization. Kinda nearly working in the test app. --- .../Generators/IItemContainerGenerator.cs | 12 ++- .../Generators/ItemContainerGenerator.cs | 26 ++++++ .../Generators/ItemContainerGenerator`1.cs | 22 ++++++ .../Generators/TreeItemContainerGenerator.cs | 5 ++ .../Presenters/ItemVirtualizerSimple.cs | 79 +++++++++---------- .../Presenters/ItemsPresenterBase.cs | 29 +++++-- .../ItemsPresenterTests_Virtualization.cs | 34 +++++++- 7 files changed, 153 insertions(+), 54 deletions(-) diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 2e02bdc647..b5ac8aef6e 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.Generators /// Creates a container control for an item. /// /// - /// The index of the item of the data in the containing collection. + /// The index of the item of data in the control's items. /// /// The item. /// An optional member selector. @@ -51,7 +51,7 @@ namespace Avalonia.Controls.Generators /// Removes a set of created containers. /// /// - /// The index of the first item of the data in the containing collection. + /// The index of the first item in the control's items. /// /// The the number of items to remove. /// The removed containers. @@ -69,12 +69,18 @@ namespace Avalonia.Controls.Generators /// the gap. /// /// - /// The index of the first item of the data in the containing collection. + /// The index of the first item in the control's items. /// /// The the number of items to remove. /// The removed containers. IEnumerable RemoveRange(int startingIndex, int count); + bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector); + /// /// Clears all created containers and returns the removed controls. /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 3bf69d910a..801f237804 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; namespace Avalonia.Controls.Generators { @@ -102,6 +103,16 @@ namespace Avalonia.Controls.Generators return result; } + /// + public virtual bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector) + { + return false; + } + /// public virtual IEnumerable Clear() { @@ -189,6 +200,21 @@ namespace Avalonia.Controls.Generators } } + /// + /// Moves a container. + /// + /// The old index. + /// The new index. + /// The new item. + /// The container info. + protected void MoveContainer(int oldIndex, int newIndex, object item) + { + var container = _containers[oldIndex]; + var newContainer = new ItemContainerInfo(container.ContainerControl, item, newIndex); + _containers[oldIndex] = null; + AddContainer(newContainer); + } + /// /// Gets all containers with an index that fall within a range. /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index c4bc730e15..76922bfc55 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -75,5 +75,27 @@ namespace Avalonia.Controls.Generators return result; } } + + /// + public override bool TryRecycle( + int oldIndex, + int newIndex, + object item, + IMemberSelector selector) + { + var container = ContainerFromIndex(oldIndex); + var i = selector != null ? selector.Select(item) : item; + + container.SetValue(ContentProperty, i); + + if (!(item is IControl)) + { + container.DataContext = i; + } + + MoveContainer(oldIndex, newIndex, i); + + return true; + } } } diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 68e410ae19..e26a4fb0d6 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -118,6 +118,11 @@ namespace Avalonia.Controls.Generators return base.RemoveRange(startingIndex, count); } + public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector) + { + return false; + } + /// /// Gets the data template for the specified item. /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index fa2b987a07..d2572e4088 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -53,50 +53,10 @@ namespace Avalonia.Controls.Presenters var scroll = (VirtualizingPanel.ScrollDirection == Orientation.Vertical) ? value.Y : value.X; var delta = (int)(scroll - FirstIndex); - var panel = VirtualizingPanel; if (delta != 0) { - if (delta >= panel.Children.Count) - { - var index = FirstIndex + delta; - - foreach (var container in panel.Children) - { - container.DataContext = Items.ElementAt(index++); - } - } - else if (delta > 0) - { - var containers = panel.Children.GetRange(0, delta).ToList(); - panel.Children.RemoveRange(0, delta); - - var index = LastIndex + 1; - - foreach (var container in containers) - { - container.DataContext = Items.ElementAt(index++); - } - - panel.Children.AddRange(containers); - } - else - { - var first = panel.Children.Count + delta; - var count = -delta; - var containers = panel.Children.GetRange(first, count).ToList(); - panel.Children.RemoveRange(first, count); - - var index = FirstIndex + delta; - - foreach (var container in containers) - { - container.DataContext = Items.ElementAt(index++); - } - - panel.Children.InsertRange(0, containers); - } - + RecycleContainers(delta); FirstIndex += delta; LastIndex += delta; } @@ -167,5 +127,42 @@ namespace Avalonia.Controls.Presenters LastIndex -= count; } } + + private void RecycleContainers(int delta) + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var selector = Owner.MemberSelector; + var sign = delta < 0 ? -1 : 1; + var first = delta < 0 ? panel.Children.Count + delta : 0; + var count = Math.Abs(delta); + var containers = panel.Children.GetRange(first, count).ToList(); + + for (var i = 0; i < containers.Count; ++i) + { + var oldItemIndex = FirstIndex + first + i; + var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); + var item = Items.ElementAt(newItemIndex); + + if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) + { + throw new NotImplementedException(); + } + } + + if (delta < panel.Children.Count) + { + panel.Children.RemoveRange(first, count); + + if (delta > 0) + { + panel.Children.AddRange(containers); + } + else + { + panel.Children.InsertRange(0, containers); + } + } + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 5fbed4d553..abebe85080 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -101,14 +101,7 @@ namespace Avalonia.Controls.Presenters { if (_generator == null) { - var i = TemplatedParent as ItemsControl; - _generator = i?.ItemContainerGenerator; - - if (_generator == null) - { - _generator = new ItemContainerGenerator(this); - _generator.ItemTemplate = ItemTemplate; - } + _generator = CreateItemContainerGenerator(); } return _generator; @@ -170,6 +163,26 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Creates the for the control. + /// + /// + /// An or null. + /// + protected virtual IItemContainerGenerator CreateItemContainerGenerator() + { + var i = TemplatedParent as ItemsControl; + var result = i?.ItemContainerGenerator; + + if (result == null) + { + result = new ItemContainerGenerator(this); + result.ItemTemplate = ItemTemplate; + } + + return result; + } + /// protected override Size MeasureOverride(Size availableSize) { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index fe56cd95ac..3072770127 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -205,7 +206,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Vector(0, 5), ((IScrollable)target).Offset); Assert.Equal(scrolledContainers, target.Panel.Children); - + for (var i = 0; i < target.Panel.Children.Count; ++i) { Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); @@ -215,6 +216,8 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Vector(0, 0), ((IScrollable)target).Offset); Assert.Equal(containers, target.Panel.Children); + var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); + for (var i = 0; i < target.Panel.Children.Count; ++i) { Assert.Equal(items[i], target.Panel.Children[i].DataContext); @@ -249,6 +252,7 @@ namespace Avalonia.Controls.UnitTests.Presenters private static ItemsPresenter CreateTarget( ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, Orientation orientation = Orientation.Vertical, + bool useContainers = true, int itemCount = 20) { ItemsPresenter result; @@ -256,7 +260,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var scroller = new ScrollContentPresenter { - Content = result = new ItemsPresenter + Content = result = new TestItemsPresenter(useContainers) { Items = items, ItemsPanel = VirtualizingPanelTemplate(orientation), @@ -287,5 +291,31 @@ namespace Avalonia.Controls.UnitTests.Presenters Orientation = orientation, }); } + + private class TestItemsPresenter : ItemsPresenter + { + private bool _useContainers; + + public TestItemsPresenter(bool useContainers) + { + _useContainers = useContainers; + } + + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return _useContainers ? + new ItemContainerGenerator(this, TestContainer.ContentProperty, null) : + new ItemContainerGenerator(this); + } + } + + private class TestContainer : ContentControl + { + public TestContainer() + { + Width = 10; + Height = 10; + } + } } } From f527d4ef1ac2490093edff9235df2f90d39ddd42 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 May 2016 21:30:00 +0200 Subject: [PATCH 015/158] Update scroll after arrange. --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index d2572e4088..a568f9455e 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -83,6 +83,7 @@ namespace Avalonia.Controls.Presenters public override void Arranging(Size finalSize) { CreateRemoveContainers(); + ((IScrollable)Owner).InvalidateScroll(); } public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) From fa3550882ad8412b18c5c3ffb2225f3a43283b72 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 May 2016 21:43:29 +0200 Subject: [PATCH 016/158] Handle scrolling >1 page. --- .../Presenters/ItemVirtualizerSimple.cs | 7 +- .../ItemsPresenterTests_Virtualization.cs | 95 ++++++++++--------- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index a568f9455e..538db9fa42 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -135,8 +135,9 @@ namespace Avalonia.Controls.Presenters var generator = Owner.ItemContainerGenerator; var selector = Owner.MemberSelector; var sign = delta < 0 ? -1 : 1; - var first = delta < 0 ? panel.Children.Count + delta : 0; - var count = Math.Abs(delta); + var move = delta < panel.Children.Count; + var first = delta < 0 && move ? panel.Children.Count + delta : 0; + var count = Math.Min(Math.Abs(delta), panel.Children.Count); var containers = panel.Children.GetRange(first, count).ToList(); for (var i = 0; i < containers.Count; ++i) @@ -151,7 +152,7 @@ namespace Avalonia.Controls.Presenters } } - if (delta < panel.Children.Count) + if (move) { panel.Children.RemoveRange(first, count); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 3072770127..2bdead4c7e 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -184,67 +184,70 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(8, target.Panel.Children.Count); } - [Fact] - public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items() + public class WithContainers { - var target = CreateTarget(); - var items = (IList)target.Items; + [Fact] + public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items() + { + var target = CreateTarget(); + var items = (IList)target.Items; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); - var containers = target.Panel.Children.ToList(); - var scroller = (ScrollContentPresenter)target.Parent; + var containers = target.Panel.Children.ToList(); + var scroller = (ScrollContentPresenter)target.Parent; - scroller.Offset = new Vector(0, 5); + scroller.Offset = new Vector(0, 5); - var scrolledContainers = containers - .Skip(5) - .Take(5) - .Concat(containers.Take(5)).ToList(); + var scrolledContainers = containers + .Skip(5) + .Take(5) + .Concat(containers.Take(5)).ToList(); - Assert.Equal(new Vector(0, 5), ((IScrollable)target).Offset); - Assert.Equal(scrolledContainers, target.Panel.Children); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); - } - - scroller.Offset = new Vector(0, 0); - Assert.Equal(new Vector(0, 0), ((IScrollable)target).Offset); - Assert.Equal(containers, target.Panel.Children); + Assert.Equal(new Vector(0, 5), ((IScrollable)target).Offset); + Assert.Equal(scrolledContainers, target.Panel.Children); - var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); + } - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i], target.Panel.Children[i].DataContext); + scroller.Offset = new Vector(0, 0); + Assert.Equal(new Vector(0, 0), ((IScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i], target.Panel.Children[i].DataContext); + } } - } - [Fact] - public void Scrolling_More_Than_A_Page_Should_Recycle_Items() - { - var target = CreateTarget(); - var items = (IList)target.Items; + [Fact] + public void Scrolling_More_Than_A_Page_Should_Recycle_Items() + { + var target = CreateTarget(itemCount: 50); + var items = (IList)target.Items; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); - var containers = target.Panel.Children.ToList(); - var scroller = (ScrollContentPresenter)target.Parent; + var containers = target.Panel.Children.ToList(); + var scroller = (ScrollContentPresenter)target.Parent; - scroller.Offset = new Vector(0, 10); + scroller.Offset = new Vector(0, 20); - Assert.Equal(new Vector(0, 10), ((IScrollable)target).Offset); - Assert.Equal(containers, target.Panel.Children); + Assert.Equal(new Vector(0, 20), ((IScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i + 10], target.Panel.Children[i].DataContext); + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 20], target.Panel.Children[i].DataContext); + } } } } From f12435731b364a9d38f36f3e4e98a851bfb50d6a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 10:57:02 +0200 Subject: [PATCH 017/158] Removed Thingamybob. --- .../Avalonia.Controls.csproj | 2 - .../Presenters/ThingamybobPresenter.cs | 114 ------------------ src/Avalonia.Controls/Thingamybob.cs | 25 ---- 3 files changed, 141 deletions(-) delete mode 100644 src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs delete mode 100644 src/Avalonia.Controls/Thingamybob.cs diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 9fe4fecc68..f3d7f10029 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -72,7 +72,6 @@ - @@ -167,7 +166,6 @@ - diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs deleted file mode 100644 index c4d9dc34c8..0000000000 --- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using Avalonia.Controls.Primitives; -using Avalonia.Media; - -namespace Avalonia.Controls.Presenters -{ - public class ThingamybobPresenter : Decorator, IItemsPresenter, IScrollable - { - private IVirtualizingPanel _panel; - private int _firstIndex; - private int _lastIndex; - - public override void ApplyTemplate() - { - if (_panel == null) - { - _panel = new VirtualizingStackPanel(); - Child = _panel; - CheckPanel(); - } - } - - public IPanel Panel => _panel; - - bool IScrollable.IsLogicalScrollEnabled => true; - Action IScrollable.InvalidateScroll { get; set; } - - Size IScrollable.Extent => new Size(1, 100 * AverageItemSize ); - - Vector IScrollable.Offset - { - get - { - return new Vector(0, (_firstIndex * AverageItemSize) + (_panel?.PixelOffset ?? 0)); - } - - set - { - var count = _lastIndex - _firstIndex; - var firstIndex = (int)(value.Y / AverageItemSize); - var firstIndexChanged = _firstIndex != firstIndex; - _firstIndex = firstIndex; - _lastIndex = _firstIndex + count; - _panel.PixelOffset = value.Y % AverageItemSize; - - if (firstIndexChanged) - { - Renumber(); - } - } - } - - Size IScrollable.Viewport => new Size(1, _panel?.Bounds.Height ?? 0); - Size IScrollable.ScrollSize => new Size(0, 1); - Size IScrollable.PageScrollSize => new Size(0, 1); - - private double AverageItemSize => _panel?.AverageItemSize ?? 1; - - protected override Size ArrangeOverride(Size finalSize) - { - var result = base.ArrangeOverride(finalSize); - CreateItems(); - ((IScrollable)this).InvalidateScroll(); - return result; - } - - private void CreateItems() - { - var randomColor = Color.FromUInt32( - (uint)(0xff000000 + new Random().Next(0xffffff))); - - while (!_panel.IsFull) - { - _panel.Children.Add(new TextBlock - { - Text = "Item " + ++_lastIndex, - Background = new SolidColorBrush(randomColor), - }); - } - } - - private void RemoveItems() - { - var remove = _panel.OverflowCount; - - _panel.Children.RemoveRange( - _panel.Children.Count - remove, - _panel.OverflowCount); - _lastIndex -= remove; - } - - private void Renumber() - { - var index = _firstIndex; - - foreach (TextBlock child in _panel.Children) - { - child.Text = "Item " + ++index; - } - } - - private void CheckPanel() - { - if (!_panel.IsFull) - { - CreateItems(); - } - else if (_panel.OverflowCount > 1) - { - RemoveItems(); - } - } - } -} diff --git a/src/Avalonia.Controls/Thingamybob.cs b/src/Avalonia.Controls/Thingamybob.cs deleted file mode 100644 index 6129b029e8..0000000000 --- a/src/Avalonia.Controls/Thingamybob.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Avalonia.Media; -using System; -using Avalonia.Controls.Primitives; -using Avalonia.Controls.Presenters; - -namespace Avalonia.Controls -{ - public class Thingamybob : Decorator - { - private ScrollViewer _scrollViewer; - private ThingamybobPresenter _presenter; - - public override void ApplyTemplate() - { - if (Child == null) - { - _scrollViewer = new ScrollViewer(); - _presenter = new ThingamybobPresenter(); - _scrollViewer.Content = _presenter; - - Child = _scrollViewer; - } - } - } -} From 03c138ff90ed8ccd3df403acecd2547ef56fca41 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 11:31:11 +0200 Subject: [PATCH 018/158] Delegate ScrollIntoView to ItemVirtualizer. This doesn't make sense for ItemVirtualizationMode.Simple so we do nothing here. --- .../XamlTestApplicationPcl/TestScrollable.cs | 46 +++++++++++-------- .../Presenters/ItemVirtualizer.cs | 6 +++ .../Presenters/ItemsPresenter.cs | 7 +++ .../Presenters/ScrollContentPresenter.cs | 7 +++ .../Primitives/IScrollable.cs | 9 ++++ ...ScrollContentPresenterTests_IScrollable.cs | 7 ++- 6 files changed, 61 insertions(+), 21 deletions(-) diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 76f4cdd106..1f0961af27 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Media; +using Avalonia.VisualTree; namespace XamlTestApplication { @@ -54,6 +55,31 @@ namespace XamlTestApplication } } + public override void Render(DrawingContext context) + { + var y = 0.0; + + for (var i = (int)_offset.Y; i < itemCount; ++i) + { + using (var line = new FormattedText( + "Item " + (i + 1), + TextBlock.GetFontFamily(this), + TextBlock.GetFontSize(this), + TextBlock.GetFontStyle(this), + TextAlignment.Left, + TextBlock.GetFontWeight(this))) + { + context.DrawText(Brushes.Black, new Point(-_offset.X, y), line); + y += _lineSize.Height; + } + } + } + + public bool BringIntoView(IVisual target, Rect targetRect) + { + throw new NotImplementedException(); + } + protected override Size MeasureOverride(Size availableSize) { using (var line = new FormattedText( @@ -77,25 +103,5 @@ namespace XamlTestApplication InvalidateScroll?.Invoke(); return finalSize; } - - public override void Render(DrawingContext context) - { - var y = 0.0; - - for (var i = (int)_offset.Y; i < itemCount; ++i) - { - using (var line = new FormattedText( - "Item " + (i + 1), - TextBlock.GetFontFamily(this), - TextBlock.GetFontSize(this), - TextBlock.GetFontStyle(this), - TextAlignment.Left, - TextBlock.GetFontWeight(this))) - { - context.DrawText(Brushes.Black, new Point(-_offset.X, y), line); - y += _lineSize.Height; - } - } - } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index b45ed8065b..63d4503df7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Primitives; +using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters { @@ -45,6 +46,11 @@ namespace Avalonia.Controls.Presenters public abstract void Arranging(Size finalSize); + public virtual bool BringIntoView(IVisual target, Rect targetRect) + { + return false; + } + public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { Items = items; diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index fc20add96c..86636b1837 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Specialized; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.VisualTree; using static Avalonia.Utilities.MathUtilities; namespace Avalonia.Controls.Presenters @@ -71,6 +72,12 @@ namespace Avalonia.Controls.Presenters /// Size IScrollable.PageScrollSize => new Size(0, 1); + /// + bool IScrollable.BringIntoView(IVisual target, Rect targetRect) + { + return _virtualizer?.BringIntoView(target, targetRect) ?? false; + } + /// protected override Size ArrangeOverride(Size finalSize) { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 5d295d7fc8..4c7a00f6fd 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -117,6 +117,13 @@ namespace Avalonia.Controls.Presenters return false; } + var scrollable = Child as IScrollable; + + if (scrollable?.IsLogicalScrollEnabled == true) + { + return scrollable.BringIntoView(target, targetRect); + } + var transform = target.TransformToVisual(Child); if (transform == null) diff --git a/src/Avalonia.Controls/Primitives/IScrollable.cs b/src/Avalonia.Controls/Primitives/IScrollable.cs index d37d1fdcca..b9155054b8 100644 --- a/src/Avalonia.Controls/Primitives/IScrollable.cs +++ b/src/Avalonia.Controls/Primitives/IScrollable.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -62,5 +63,13 @@ namespace Avalonia.Controls.Primitives /// Gets the size to page by, in logical units. /// Size PageScrollSize { get; } + + /// + /// Attempts to bring a portion of the target visual into view by scrolling the content. + /// + /// The target visual. + /// The portion of the target visual to bring into view. + /// True if the scroll offset was changed; otherwise false. + bool BringIntoView(IVisual target, Rect targetRect); } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs index 1aea155eaa..a690522e89 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Layout; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests @@ -236,7 +237,6 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); } - private class TestScrollable : Control, IScrollable { private Size _extent; @@ -293,6 +293,11 @@ namespace Avalonia.Controls.UnitTests } } + public bool BringIntoView(IVisual target, Rect targetRect) + { + throw new NotImplementedException(); + } + protected override Size MeasureOverride(Size availableSize) { AvailableSize = availableSize; From b44589e0cb7544ab47b7919d120d759057a9a8c9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 11:43:06 +0200 Subject: [PATCH 019/158] Renamed IScrollable -> ILogicalScrollable We're going to need IScrollable as an interface to ScrollViewer-type controls. --- Avalonia.sln | 4 +++- .../XamlTestApplicationPcl/TestScrollable.cs | 8 +++---- .../Avalonia.Controls.csproj | 2 +- .../Presenters/ItemVirtualizer.cs | 2 +- .../Presenters/ItemVirtualizerSimple.cs | 4 ++-- .../Presenters/ItemsPresenter.cs | 22 +++++++++---------- .../Presenters/ScrollContentPresenter.cs | 8 +++---- .../{IScrollable.cs => ILogicalScrollable.cs} | 4 ++-- .../ItemsPresenterTests_Virtualization.cs | 22 +++++++++---------- ...ScrollContentPresenterTests_IScrollable.cs | 2 +- 10 files changed, 40 insertions(+), 38 deletions(-) rename src/Avalonia.Controls/Primitives/{IScrollable.cs => ILogicalScrollable.cs} (96%) diff --git a/Avalonia.sln b/Avalonia.sln index d9ef7a7137..c06996ba0e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -172,6 +172,8 @@ Global src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4 + src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 + src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e1aa3dbf-9056-4530-9376-18119a7a3ffe}*SharedItemsImports = 4 EndGlobalSection diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 1f0961af27..2b16899ee1 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -7,7 +7,7 @@ using Avalonia.VisualTree; namespace XamlTestApplication { - public class TestScrollable : Control, IScrollable + public class TestScrollable : Control, ILogicalScrollable { private int itemCount = 100; private Size _extent; @@ -18,12 +18,12 @@ namespace XamlTestApplication public bool IsLogicalScrollEnabled => true; public Action InvalidateScroll { get; set; } - Size IScrollable.Extent + Size ILogicalScrollable.Extent { get { return _extent; } } - Vector IScrollable.Offset + Vector ILogicalScrollable.Offset { get { return _offset; } @@ -34,7 +34,7 @@ namespace XamlTestApplication } } - Size IScrollable.Viewport + Size ILogicalScrollable.Viewport { get { return _viewport; } } diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index f3d7f10029..11f8979a43 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -74,7 +74,7 @@ - + diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 63d4503df7..a92990b3ba 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -30,7 +30,7 @@ namespace Avalonia.Controls.Presenters public static ItemVirtualizer Create(ItemsPresenter owner) { var virtualizingPanel = owner.Panel as IVirtualizingPanel; - var scrollable = (IScrollable)owner; + var scrollable = (ILogicalScrollable)owner; if (virtualizingPanel != null && scrollable.InvalidateScroll != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 538db9fa42..a5e04ad6d5 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -83,13 +83,13 @@ namespace Avalonia.Controls.Presenters public override void Arranging(Size finalSize) { CreateRemoveContainers(); - ((IScrollable)Owner).InvalidateScroll(); + ((ILogicalScrollable)Owner).InvalidateScroll(); } public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { base.ItemsChanged(items, e); - ((IScrollable)Owner).InvalidateScroll(); + ((ILogicalScrollable)Owner).InvalidateScroll(); } private void CreateRemoveContainers() diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 86636b1837..8c05067fc7 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Presenters /// /// Displays items inside an . /// - public class ItemsPresenter : ItemsPresenterBase, IScrollable + public class ItemsPresenter : ItemsPresenterBase, ILogicalScrollable { /// /// Defines the property. @@ -45,35 +45,35 @@ namespace Avalonia.Controls.Presenters } /// - bool IScrollable.IsLogicalScrollEnabled + bool ILogicalScrollable.IsLogicalScrollEnabled { get { return _virtualizer?.IsLogicalScrollEnabled ?? false; } } /// - Action IScrollable.InvalidateScroll { get; set; } + Action ILogicalScrollable.InvalidateScroll { get; set; } /// - Size IScrollable.Extent => _virtualizer.Extent; + Size ILogicalScrollable.Extent => _virtualizer.Extent; /// - Vector IScrollable.Offset + Vector ILogicalScrollable.Offset { get { return _virtualizer.Offset; } set { _virtualizer.Offset = CoerceOffset(value); } } /// - Size IScrollable.Viewport => _virtualizer.Viewport; + Size ILogicalScrollable.Viewport => _virtualizer.Viewport; /// - Size IScrollable.ScrollSize => new Size(0, 1); + Size ILogicalScrollable.ScrollSize => new Size(0, 1); /// - Size IScrollable.PageScrollSize => new Size(0, 1); + Size ILogicalScrollable.PageScrollSize => new Size(0, 1); /// - bool IScrollable.BringIntoView(IVisual target, Rect targetRect) + bool ILogicalScrollable.BringIntoView(IVisual target, Rect targetRect) { return _virtualizer?.BringIntoView(target, targetRect) ?? false; } @@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters protected override void PanelCreated(IPanel panel) { _virtualizer = ItemVirtualizer.Create(this); - ((IScrollable)this).InvalidateScroll?.Invoke(); + ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) { @@ -111,7 +111,7 @@ namespace Avalonia.Controls.Presenters private Vector CoerceOffset(Vector value) { - var scrollable = (IScrollable)this; + var scrollable = (ILogicalScrollable)this; var maxX = Math.Max(scrollable.Extent.Width - scrollable.Viewport.Width, 0); var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 4c7a00f6fd..4594e31ac8 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -117,7 +117,7 @@ namespace Avalonia.Controls.Presenters return false; } - var scrollable = Child as IScrollable; + var scrollable = Child as ILogicalScrollable; if (scrollable?.IsLogicalScrollEnabled == true) { @@ -231,7 +231,7 @@ namespace Avalonia.Controls.Presenters { if (Extent.Height > Viewport.Height) { - var scrollable = Child as IScrollable; + var scrollable = Child as ILogicalScrollable; if (scrollable?.IsLogicalScrollEnabled == true) { @@ -259,7 +259,7 @@ namespace Avalonia.Controls.Presenters private void UpdateScrollableSubscription(IControl child) { - var scrollable = child as IScrollable; + var scrollable = child as ILogicalScrollable; _scrollableSubscription?.Dispose(); _scrollableSubscription = null; @@ -278,7 +278,7 @@ namespace Avalonia.Controls.Presenters } } - private void UpdateFromScrollable(IScrollable scrollable) + private void UpdateFromScrollable(ILogicalScrollable scrollable) { var logicalScroll = _scrollableSubscription != null; diff --git a/src/Avalonia.Controls/Primitives/IScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs similarity index 96% rename from src/Avalonia.Controls/Primitives/IScrollable.cs rename to src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index b9155054b8..8c55e1d4a2 100644 --- a/src/Avalonia.Controls/Primitives/IScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Primitives /// whereas logical scrolling means that the scrolling is handled by the child control itself /// and it can choose to do handle the scroll information as it sees fit. /// - public interface IScrollable + public interface ILogicalScrollable { /// /// Gets a value indicating whether logical scrolling is enabled on the control. @@ -34,7 +34,7 @@ namespace Avalonia.Controls.Primitives /// /// /// This property is set by the parent when the - /// is placed inside it. + /// is placed inside it. /// /// Action InvalidateScroll { get; set; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 2bdead4c7e..66bf70efb4 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -21,7 +21,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.False(((IScrollable)target).IsLogicalScrollEnabled); + Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); } [Fact] @@ -31,7 +31,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.False(((IScrollable)target).IsLogicalScrollEnabled); + Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); } [Fact] @@ -46,7 +46,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.False(((IScrollable)target).IsLogicalScrollEnabled); + Assert.False(((ILogicalScrollable)target).IsLogicalScrollEnabled); } [Fact] @@ -56,7 +56,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.True(((IScrollable)target).IsLogicalScrollEnabled); + Assert.True(((ILogicalScrollable)target).IsLogicalScrollEnabled); } [Fact] @@ -130,7 +130,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.Equal(new Size(0, 20), ((IScrollable)target).Extent); + Assert.Equal(new Size(0, 20), ((ILogicalScrollable)target).Extent); } [Fact] @@ -140,7 +140,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.Equal(new Size(20, 0), ((IScrollable)target).Extent); + Assert.Equal(new Size(20, 0), ((ILogicalScrollable)target).Extent); } [Fact] @@ -152,7 +152,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - Assert.Equal(new Size(0, 10), ((IScrollable)target).Viewport); + Assert.Equal(new Size(0, 10), ((ILogicalScrollable)target).Viewport); } [Fact] @@ -164,7 +164,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - Assert.Equal(new Size(10, 0), ((IScrollable)target).Viewport); + Assert.Equal(new Size(10, 0), ((ILogicalScrollable)target).Viewport); } [Fact] @@ -206,7 +206,7 @@ namespace Avalonia.Controls.UnitTests.Presenters .Take(5) .Concat(containers.Take(5)).ToList(); - Assert.Equal(new Vector(0, 5), ((IScrollable)target).Offset); + Assert.Equal(new Vector(0, 5), ((ILogicalScrollable)target).Offset); Assert.Equal(scrolledContainers, target.Panel.Children); for (var i = 0; i < target.Panel.Children.Count; ++i) @@ -215,7 +215,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } scroller.Offset = new Vector(0, 0); - Assert.Equal(new Vector(0, 0), ((IScrollable)target).Offset); + Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); Assert.Equal(containers, target.Panel.Children); var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); @@ -241,7 +241,7 @@ namespace Avalonia.Controls.UnitTests.Presenters scroller.Offset = new Vector(0, 20); - Assert.Equal(new Vector(0, 20), ((IScrollable)target).Offset); + Assert.Equal(new Vector(0, 20), ((ILogicalScrollable)target).Offset); Assert.Equal(containers, target.Panel.Children); for (var i = 0; i < target.Panel.Children.Count; ++i) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs index a690522e89..d0c6a386ac 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -237,7 +237,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); } - private class TestScrollable : Control, IScrollable + private class TestScrollable : Control, ILogicalScrollable { private Size _extent; private Vector _offset; From d346b35ab7133208f72c40c65e7bfe14863c27a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 11:50:14 +0200 Subject: [PATCH 020/158] Added IScrollable interface. This is now the basic Extent/Offset/Viewport interface. Implemented by ScrollViewer and ScrollContentPresenter. --- .../XamlTestApplicationPcl/TestScrollable.cs | 6 ++-- .../Avalonia.Controls.csproj | 1 + src/Avalonia.Controls/IScrollable.cs | 29 +++++++++++++++++++ .../Presenters/ItemsPresenter.cs | 12 ++++---- .../Presenters/ScrollContentPresenter.cs | 2 +- .../Primitives/ILogicalScrollable.cs | 20 ++----------- src/Avalonia.Controls/ScrollViewer.cs | 2 +- 7 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 src/Avalonia.Controls/IScrollable.cs diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 2b16899ee1..39e708d043 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -18,12 +18,12 @@ namespace XamlTestApplication public bool IsLogicalScrollEnabled => true; public Action InvalidateScroll { get; set; } - Size ILogicalScrollable.Extent + Size IScrollable.Extent { get { return _extent; } } - Vector ILogicalScrollable.Offset + Vector IScrollable.Offset { get { return _offset; } @@ -34,7 +34,7 @@ namespace XamlTestApplication } } - Size ILogicalScrollable.Viewport + Size IScrollable.Viewport { get { return _viewport; } } diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 11f8979a43..80d29b14ab 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -54,6 +54,7 @@ + diff --git a/src/Avalonia.Controls/IScrollable.cs b/src/Avalonia.Controls/IScrollable.cs new file mode 100644 index 0000000000..9bbd0d8518 --- /dev/null +++ b/src/Avalonia.Controls/IScrollable.cs @@ -0,0 +1,29 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.VisualTree; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Interface implemented by scrollable controls. + /// + public interface IScrollable + { + /// + /// Gets the extent of the scrollable content, in logical units + /// + Size Extent { get; } + + /// + /// Gets or sets the current scroll offset, in logical units. + /// + Vector Offset { get; set; } + + /// + /// Gets the size of the viewport, in logical units. + /// + Size Viewport { get; } + } +} diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 8c05067fc7..817d734506 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -51,20 +51,20 @@ namespace Avalonia.Controls.Presenters } /// - Action ILogicalScrollable.InvalidateScroll { get; set; } - - /// - Size ILogicalScrollable.Extent => _virtualizer.Extent; + Size IScrollable.Extent => _virtualizer.Extent; /// - Vector ILogicalScrollable.Offset + Vector IScrollable.Offset { get { return _virtualizer.Offset; } set { _virtualizer.Offset = CoerceOffset(value); } } /// - Size ILogicalScrollable.Viewport => _virtualizer.Viewport; + Size IScrollable.Viewport => _virtualizer.Viewport; + + /// + Action ILogicalScrollable.InvalidateScroll { get; set; } /// Size ILogicalScrollable.ScrollSize => new Size(0, 1); diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 4594e31ac8..9e64694f8e 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls.Presenters /// /// Presents a scrolling view of content inside a . /// - public class ScrollContentPresenter : ContentPresenter, IPresenter + public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable { /// /// Defines the property. diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index 8c55e1d4a2..b8b90f83a9 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Primitives /// whereas logical scrolling means that the scrolling is handled by the child control itself /// and it can choose to do handle the scroll information as it sees fit. /// - public interface ILogicalScrollable + public interface ILogicalScrollable : IScrollable { /// /// Gets a value indicating whether logical scrolling is enabled on the control. @@ -30,7 +30,8 @@ namespace Avalonia.Controls.Primitives /// /// /// This method notifies the attached of a change in - /// the , or properties. + /// the , or + /// properties. /// /// /// This property is set by the parent when the @@ -39,21 +40,6 @@ namespace Avalonia.Controls.Primitives /// Action InvalidateScroll { get; set; } - /// - /// Gets the extent of the scrollable content, in logical units - /// - Size Extent { get; } - - /// - /// Gets or sets the current scroll offset, in logical units. - /// - Vector Offset { get; set; } - - /// - /// Gets the size of the viewport, in logical units. - /// - Size Viewport { get; } - /// /// Gets the size to scroll by, in logical units. /// diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 4f5cf304b2..e9d1e56b3a 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls /// /// A control scrolls its content if the content is bigger than the space available. /// - public class ScrollViewer : ContentControl + public class ScrollViewer : ContentControl, IScrollable { /// /// Defines the property. From e930c358827e95eb8e1de94289848d01bd4d571c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 11:57:40 +0200 Subject: [PATCH 021/158] Expose IScrollable in ListBox. To allow programmatic control of the ListBox's scroll. --- src/Avalonia.Controls/ListBox.cs | 14 +++++++++++--- src/Avalonia.Themes.Default/ListBox.xaml | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f61e2fa8d3..95c980974b 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -2,13 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections; -using System.Collections.Generic; -using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; -using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -43,6 +40,11 @@ namespace Avalonia.Controls ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); } + /// + /// Gets the scroll information for the . + /// + public IScrollable Scroll { get; set; } + /// public new IList SelectedItems => base.SelectedItems; @@ -90,5 +92,11 @@ namespace Avalonia.Controls (e.InputModifiers & InputModifiers.Control) != 0); } } + + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + Scroll = e.NameScope.Find("PART_ScrollViewer"); + } } } diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index 782cc36107..9454f3eb31 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -7,7 +7,7 @@ - + Date: Tue, 24 May 2016 12:20:57 +0200 Subject: [PATCH 022/158] Simplify ListBox unit test template creation. --- .../ListBoxTests.cs | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index d5fd59d1fc..dadbd23bbe 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -19,7 +19,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), + Template = CreateListBoxTemplate(), ItemTemplate = new FuncDataTemplate(_ => new Canvas()), }; @@ -40,7 +40,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), + Template = CreateListBoxTemplate(), }; ApplyTemplate(target); @@ -54,7 +54,7 @@ namespace Avalonia.Controls.UnitTests var items = new[] { "Foo", "Bar", "Baz " }; var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), + Template = CreateListBoxTemplate(), Items = items, }; @@ -77,7 +77,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), + Template = CreateListBoxTemplate(), Items = new[] { "Foo", "Bar", "Baz " }, }; @@ -104,7 +104,7 @@ namespace Avalonia.Controls.UnitTests var target = new ListBox { - Template = new FuncControlTemplate(CreateListBoxTemplate), + Template = CreateListBoxTemplate(), DataContext = "Base", DataTemplates = new DataTemplates { @@ -125,17 +125,19 @@ namespace Avalonia.Controls.UnitTests dataContexts); } - private Control CreateListBoxTemplate(ITemplatedControl parent) + private FuncControlTemplate CreateListBoxTemplate() { - return new ScrollViewer - { - Template = new FuncControlTemplate(CreateScrollViewerTemplate), - Content = new ItemsPresenter + return new FuncControlTemplate(parent => + new ScrollViewer { - Name = "PART_ItemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty), - } - }; + Name = "PART_ScrollViewer", + Template = new FuncControlTemplate(CreateScrollViewerTemplate), + Content = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty), + } + }); } private FuncControlTemplate ListBoxItemTemplate() From 2d5a41a72901dfaad2c47cc20602304edde56144 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 13:04:20 +0200 Subject: [PATCH 023/158] Update ListBoxTests Now ListBox is virtualized it needs more setup for unit testing. --- .../ListBoxTests.cs | 134 +++++++++++------- tests/Avalonia.UnitTests/TestServices.cs | 3 + 2 files changed, 83 insertions(+), 54 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index dadbd23bbe..ab0d4030d5 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Styling; +using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -20,18 +21,13 @@ namespace Avalonia.Controls.UnitTests var target = new ListBox { Template = CreateListBoxTemplate(), + Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate(_ => new Canvas()), }; - target.Items = new[] { "Foo" }; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); + Prepare(target); var container = (ListBoxItem)target.Presenter.Panel.Children[0]; - container.Template = ListBoxItemTemplate(); - container.ApplyTemplate(); - ((ContentPresenter)container.Presenter).UpdateChild(); - Assert.IsType(container.Presenter.Child); } @@ -43,7 +39,7 @@ namespace Avalonia.Controls.UnitTests Template = CreateListBoxTemplate(), }; - ApplyTemplate(target); + Prepare(target); Assert.IsType(target.Presenter); } @@ -51,78 +47,85 @@ namespace Avalonia.Controls.UnitTests [Fact] public void ListBoxItem_Containers_Should_Be_Generated() { - var items = new[] { "Foo", "Bar", "Baz " }; - var target = new ListBox + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - Template = CreateListBoxTemplate(), - Items = items, - }; + var items = new[] { "Foo", "Bar", "Baz " }; + var target = new ListBox + { + Template = CreateListBoxTemplate(), + Items = items, + }; - ApplyTemplate(target); + Prepare(target); - var text = target.Presenter.Panel.Children - .OfType() - .Do(x => x.Template = ListBoxItemTemplate()) - .Do(x => { x.ApplyTemplate(); ((ContentPresenter)x.Presenter).UpdateChild(); }) - .Select(x => x.Presenter.Child) - .OfType() - .Select(x => x.Text) - .ToList(); + var text = target.Presenter.Panel.Children + .OfType() + .Select(x => x.Presenter.Child) + .OfType() + .Select(x => x.Text) + .ToList(); - Assert.Equal(items, text); + Assert.Equal(items, text); + } } [Fact] public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items() { - var target = new ListBox + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - Template = CreateListBoxTemplate(), - Items = new[] { "Foo", "Bar", "Baz " }, - }; + var target = new ListBox + { + Template = CreateListBoxTemplate(), + Items = new[] { "Foo", "Bar", "Baz " }, + }; - ApplyTemplate(target); + Prepare(target); - Assert.Equal(3, target.GetLogicalChildren().Count()); + Assert.Equal(3, target.GetLogicalChildren().Count()); - foreach (var child in target.GetLogicalChildren()) - { - Assert.IsType(child); + foreach (var child in target.GetLogicalChildren()) + { + Assert.IsType(child); + } } } [Fact] public void DataContexts_Should_Be_Correctly_Set() { - var items = new object[] + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) { - "Foo", - new Item("Bar"), - new TextBlock { Text = "Baz" }, - new ListBoxItem { Content = "Qux" }, - }; + var items = new object[] + { + "Foo", + new Item("Bar"), + new TextBlock { Text = "Baz" }, + new ListBoxItem { Content = "Qux" }, + }; - var target = new ListBox - { - Template = CreateListBoxTemplate(), - DataContext = "Base", - DataTemplates = new DataTemplates + var target = new ListBox + { + Template = CreateListBoxTemplate(), + DataContext = "Base", + DataTemplates = new DataTemplates { new FuncDataTemplate(x => new Button { Content = x }) }, - Items = items, - }; + Items = items, + }; - ApplyTemplate(target); + Prepare(target); - var dataContexts = target.Presenter.Panel.Children - .Cast() - .Select(x => x.DataContext) - .ToList(); + var dataContexts = target.Presenter.Panel.Children + .Cast() + .Select(x => x.DataContext) + .ToList(); - Assert.Equal( - new object[] { items[0], items[1], "Base", "Base" }, - dataContexts); + Assert.Equal( + new object[] { items[0], items[1], "Base", "Base" }, + dataContexts); + } } private FuncControlTemplate CreateListBoxTemplate() @@ -136,6 +139,7 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty), + [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty), } }); } @@ -159,7 +163,7 @@ namespace Avalonia.Controls.UnitTests }; } - private void ApplyTemplate(ListBox target) + private void Prepare(ListBox target) { // Apply the template to the ListBox itself. target.ApplyTemplate(); @@ -173,6 +177,28 @@ namespace Avalonia.Controls.UnitTests // Now the ItemsPresenter should be reigstered, so apply its template. target.Presenter.ApplyTemplate(); + + // Because ListBox items are virtualized we need to do a layout to make them appear. + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Now set and apply the item templates. + foreach (ListBoxItem item in target.Presenter.Panel.Children) + { + item.Template = ListBoxItemTemplate(); + item.ApplyTemplate(); + item.Presenter.ApplyTemplate(); + ((ContentPresenter)item.Presenter).UpdateChild(); + } + + // The items were created before the template was applied, so now we need to go back + // and re-arrange everything. + foreach (IControl i in target.GetSelfAndVisualDescendents()) + { + i.InvalidateArrange(); + } + + target.Arrange(new Rect(0, 0, 100, 100)); } private class Item diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index db0f600a0a..9ea13e07ff 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -27,6 +27,9 @@ namespace Avalonia.UnitTests threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true), windowingPlatform: new MockWindowingPlatform()); + public static readonly TestServices MockPlatformRenderInterface = new TestServices( + renderInterface: CreateRenderInterfaceMock()); + public static readonly TestServices MockPlatformWrapper = new TestServices( platformWrapper: Mock.Of()); From 2c8d8179e5e16f4dc54bfc1fb1223bbd5090ea4b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 14:01:53 +0200 Subject: [PATCH 024/158] Fix virtualized item selection. So that recycled items' selection state is set correctly. --- .../Generators/IItemContainerGenerator.cs | 6 +- .../Generators/ItemContainerEventArgs.cs | 7 +- .../Generators/ItemContainerGenerator.cs | 17 ++++- .../Generators/ItemContainerGenerator`1.cs | 3 +- .../Generators/TreeContainerIndex.cs | 4 +- src/Avalonia.Controls/ItemsControl.cs | 23 +++++++ .../Primitives/SelectingItemsControl.cs | 13 ++++ .../ListBoxTests.cs | 67 +++++++++++++------ 8 files changed, 110 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index b5ac8aef6e..ba584e33b9 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.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.Collections; using System.Collections.Generic; using Avalonia.Controls.Templates; @@ -33,6 +32,11 @@ namespace Avalonia.Controls.Generators /// event EventHandler Dematerialized; + /// + /// Event raised whenever containers are recycled. + /// + event EventHandler Recycled; + /// /// Creates a container control for an item. /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs index cd26b0ba83..6821a842e3 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs @@ -15,13 +15,10 @@ namespace Avalonia.Controls.Generators /// /// Initializes a new instance of the class. /// - /// The index of the first container in the source items. /// The container. - public ItemContainerEventArgs( - int startingIndex, - ItemContainerInfo container) + public ItemContainerEventArgs(ItemContainerInfo container) { - StartingIndex = startingIndex; + StartingIndex = container.Index; Containers = new[] { container }; } diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 801f237804..b5c176cf33 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -38,6 +38,9 @@ namespace Avalonia.Controls.Generators /// public event EventHandler Dematerialized; + /// + public event EventHandler Recycled; + /// /// Gets or sets the data template used to display the items in the control. /// @@ -58,7 +61,7 @@ namespace Avalonia.Controls.Generators var container = new ItemContainerInfo(CreateContainer(i), item, index); AddContainer(container); - Materialized?.Invoke(this, new ItemContainerEventArgs(index, container)); + Materialized?.Invoke(this, new ItemContainerEventArgs(container)); return container; } @@ -207,12 +210,13 @@ namespace Avalonia.Controls.Generators /// The new index. /// The new item. /// The container info. - protected void MoveContainer(int oldIndex, int newIndex, object item) + protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item) { var container = _containers[oldIndex]; var newContainer = new ItemContainerInfo(container.ContainerControl, item, newIndex); _containers[oldIndex] = null; AddContainer(newContainer); + return newContainer; } /// @@ -225,5 +229,14 @@ namespace Avalonia.Controls.Generators { return _containers.GetRange(index, count); } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseRecycled(ItemContainerEventArgs e) + { + Recycled?.Invoke(this, e); + } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 76922bfc55..c514e0e85f 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -93,7 +93,8 @@ namespace Avalonia.Controls.Generators container.DataContext = i; } - MoveContainer(oldIndex, newIndex, i); + var info = MoveContainer(oldIndex, newIndex, i); + RaiseRecycled(new ItemContainerEventArgs(info)); return true; } diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index f58f7e019d..e0c52beb11 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Generators Materialized?.Invoke( this, - new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0))); + new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } /// @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Generators Dematerialized?.Invoke( this, - new ItemContainerEventArgs(0, new ItemContainerInfo(container, item, 0))); + new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index ef090bc697..8e8603ca24 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -89,6 +89,7 @@ namespace Avalonia.Controls _itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); + _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); } } @@ -264,6 +265,28 @@ namespace Avalonia.Controls LogicalChildren.RemoveAll(toRemove); } + /// + /// Called when containers are recycled for the by its + /// . + /// + /// The details of the containers. + protected virtual void OnContainersRecycled(ItemContainerEventArgs e) + { + var toRemove = new List(); + + foreach (var container in e.Containers) + { + // If the item is its own container, then it will be removed from the logical tree + // when it is removed from the Items collection. + if (container?.ContainerControl != container?.Item) + { + toRemove.Add(container.ContainerControl); + } + } + + LogicalChildren.RemoveAll(toRemove); + } + /// protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 018507bbc8..a46aa8d853 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -394,6 +394,19 @@ namespace Avalonia.Controls.Primitives } } + protected override void OnContainersRecycled(ItemContainerEventArgs e) + { + foreach (var i in e.Containers) + { + if (i.ContainerControl != null && i.Item != null) + { + MarkContainerSelected( + i.ContainerControl, + SelectedItems.Contains(i.Item)); + } + } + } + /// protected override void OnDataContextChanging() { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index ab0d4030d5..d1443fb2ae 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), Items = new[] { "Foo" }, ItemTemplate = new FuncDataTemplate(_ => new Canvas()), }; @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), }; Prepare(target); @@ -52,7 +52,7 @@ namespace Avalonia.Controls.UnitTests var items = new[] { "Foo", "Bar", "Baz " }; var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), Items = items, }; @@ -76,7 +76,7 @@ namespace Avalonia.Controls.UnitTests { var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), Items = new[] { "Foo", "Bar", "Baz " }, }; @@ -106,7 +106,7 @@ namespace Avalonia.Controls.UnitTests var target = new ListBox { - Template = CreateListBoxTemplate(), + Template = ListBoxTemplate(), DataContext = "Base", DataTemplates = new DataTemplates { @@ -128,13 +128,37 @@ namespace Avalonia.Controls.UnitTests } } - private FuncControlTemplate CreateListBoxTemplate() + [Fact] + public void Selection_Should_Be_Cleared_On_Recycled_Items() + { + var target = new ListBox + { + Template = ListBoxTemplate(), + Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), + ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }), + SelectedIndex = 0, + }; + + Prepare(target); + + // Make sure we're virtualized and first item is selected. + Assert.Equal(10, target.Presenter.Panel.Children.Count); + Assert.True(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + + // Scroll down a page. + target.Scroll.Offset = new Vector(0, 10); + + // Make sure recycled item isn't now selected. + Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected); + } + + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate(parent => new ScrollViewer { Name = "PART_ScrollViewer", - Template = new FuncControlTemplate(CreateScrollViewerTemplate), + Template = ScrollViewerTemplate(), Content = new ItemsPresenter { Name = "PART_ItemsPresenter", @@ -146,21 +170,26 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ListBoxItemTemplate() { - return new FuncControlTemplate(parent => new ContentPresenter - { - Name = "PART_ContentPresenter", - [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], - [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty], - }); + return new FuncControlTemplate(parent => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], + [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty], + }); } - private Control CreateScrollViewerTemplate(ITemplatedControl parent) + private FuncControlTemplate ScrollViewerTemplate() { - return new ScrollContentPresenter - { - Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty), - }; + return new FuncControlTemplate(parent => + new ScrollContentPresenter + { + Name = "PART_ContentPresenter", + [~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty), + [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], + [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], + [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], + }); } private void Prepare(ListBox target) From 6cab63fb7046d8d0ddb6c79d46310a709cc798ac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 19:31:19 +0200 Subject: [PATCH 025/158] Don't try to dematerialize null containers. --- src/Avalonia.Controls/Generators/ItemContainerGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index b5c176cf33..bbbe8528b4 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -73,7 +73,7 @@ namespace Avalonia.Controls.Generators for (int i = startingIndex; i < startingIndex + count; ++i) { - if (i < _containers.Count) + if (i < _containers.Count &&_containers[i] != null) { result.Add(_containers[i]); _containers[i] = null; From 69ba9810d82034a8e284a4f3268d03ae7002954a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 19:32:11 +0200 Subject: [PATCH 026/158] Added ListBox.VirtualizationMode --- src/Avalonia.Controls/ListBox.cs | 16 ++++++++++++++++ src/Avalonia.Themes.Default/ListBox.xaml | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 95c980974b..eddadc2846 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -3,6 +3,7 @@ using System.Collections; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -32,6 +33,12 @@ namespace Avalonia.Controls public static readonly new AvaloniaProperty SelectionModeProperty = SelectingItemsControl.SelectionModeProperty; + /// + /// Defines the property. + /// + public static readonly AvaloniaProperty VirtualizationModeProperty = + ItemsPresenter.VirtualizationModeProperty.AddOwner(); + /// /// Initializes static members of the class. /// @@ -55,6 +62,15 @@ namespace Avalonia.Controls set { base.SelectionMode = value; } } + /// + /// Gets or sets the virtualization mode for the items. + /// + public ItemVirtualizationMode VirtualizationMode + { + get { return GetValue(VirtualizationModeProperty); } + set { SetValue(VirtualizationModeProperty, value); } + } + /// protected override IItemContainerGenerator CreateItemContainerGenerator() { diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index 9454f3eb31..eecca02384 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -13,7 +13,8 @@ ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" Margin="{TemplateBinding Padding}" - MemberSelector="{TemplateBinding MemberSelector}"/> + MemberSelector="{TemplateBinding MemberSelector}" + VirtualizationMode="{TemplateBinding VirtualizationMode}"/> From 5582750bb6aa96045ac4033c299c622072b0285b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 20:02:18 +0200 Subject: [PATCH 027/158] Added virtualization test app. --- Avalonia.sln | 35 ++++ samples/VirtualizationTest/App.config | 6 + samples/VirtualizationTest/App.xaml | 6 + samples/VirtualizationTest/App.xaml.cs | 16 ++ samples/VirtualizationTest/MainWindow.xaml | 18 ++ samples/VirtualizationTest/MainWindow.xaml.cs | 25 +++ samples/VirtualizationTest/Program.cs | 20 +++ .../Properties/AssemblyInfo.cs | 36 ++++ .../ViewModels/ItemViewModel.cs | 19 ++ .../ViewModels/MainWindowViewModel.cs | 36 ++++ .../VirtualizationTest.csproj | 168 ++++++++++++++++++ .../VirtualizationTest.v2.ncrunchproject | 26 +++ samples/VirtualizationTest/packages.config | 9 + .../Presenters/ItemVirtualizerSimple.cs | 2 +- 14 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 samples/VirtualizationTest/App.config create mode 100644 samples/VirtualizationTest/App.xaml create mode 100644 samples/VirtualizationTest/App.xaml.cs create mode 100644 samples/VirtualizationTest/MainWindow.xaml create mode 100644 samples/VirtualizationTest/MainWindow.xaml.cs create mode 100644 samples/VirtualizationTest/Program.cs create mode 100644 samples/VirtualizationTest/Properties/AssemblyInfo.cs create mode 100644 samples/VirtualizationTest/ViewModels/ItemViewModel.cs create mode 100644 samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs create mode 100644 samples/VirtualizationTest/VirtualizationTest.csproj create mode 100644 samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject create mode 100644 samples/VirtualizationTest/packages.config diff --git a/Avalonia.sln b/Avalonia.sln index c06996ba0e..928ba71ec8 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -156,6 +156,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 @@ -1899,6 +1901,38 @@ Global {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|x86.ActiveCfg = Release|Any CPU {F1381F98-4D24-409A-A6C5-1C5B1E08BB08}.Release|x86.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Ad-Hoc|x86.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|Any CPU.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhone.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|x86.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.AppStore|x86.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhone.Build.0 = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|x86.ActiveCfg = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Debug|x86.Build.0 = Debug|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Any CPU.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhone.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.ActiveCfg = Release|Any CPU + {FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1949,5 +1983,6 @@ Global {D35A9F3D-8BB0-496E-BF72-444038A7DEBB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {52F55355-D120-42AC-8116-8410A7D602FA} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection EndGlobal diff --git a/samples/VirtualizationTest/App.config b/samples/VirtualizationTest/App.config new file mode 100644 index 0000000000..88fa4027bd --- /dev/null +++ b/samples/VirtualizationTest/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/VirtualizationTest/App.xaml b/samples/VirtualizationTest/App.xaml new file mode 100644 index 0000000000..d9630eef58 --- /dev/null +++ b/samples/VirtualizationTest/App.xaml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/VirtualizationTest/App.xaml.cs b/samples/VirtualizationTest/App.xaml.cs new file mode 100644 index 0000000000..14ab5b3f84 --- /dev/null +++ b/samples/VirtualizationTest/App.xaml.cs @@ -0,0 +1,16 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia; +using Avalonia.Markup.Xaml; + +namespace VirtualizationTest +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml new file mode 100644 index 0000000000..3fd25eb176 --- /dev/null +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/VirtualizationTest/MainWindow.xaml.cs b/samples/VirtualizationTest/MainWindow.xaml.cs new file mode 100644 index 0000000000..952383dffb --- /dev/null +++ b/samples/VirtualizationTest/MainWindow.xaml.cs @@ -0,0 +1,25 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using VirtualizationTest.ViewModels; + +namespace VirtualizationTest +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.AttachDevTools(); + DataContext = new MainWindowViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationTest/Program.cs new file mode 100644 index 0000000000..5bbf01f428 --- /dev/null +++ b/samples/VirtualizationTest/Program.cs @@ -0,0 +1,20 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia; +using Avalonia.Controls; + +namespace VirtualizationTest +{ + class Program + { + static void Main(string[] args) + { + AppBuilder.Configure() + .UseWin32() + .UseDirect2D1() + .Start(); + } + } +} diff --git a/samples/VirtualizationTest/Properties/AssemblyInfo.cs b/samples/VirtualizationTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..e6af64555b --- /dev/null +++ b/samples/VirtualizationTest/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("VirtualizationTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("VirtualizationTest")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fbcaf3d0-2808-4934-8e96-3f607594517b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs new file mode 100644 index 0000000000..f0cf69c23e --- /dev/null +++ b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace VirtualizationTest.ViewModels +{ + internal class ItemViewModel + { + private int _index; + + public ItemViewModel(int index) + { + _index = index; + } + + public string Header => $"Item {_index}"; + } +} diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..916568f7eb --- /dev/null +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,36 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Linq; +using ReactiveUI; + +namespace VirtualizationTest.ViewModels +{ + internal class MainWindowViewModel : ReactiveObject + { + private int _itemCount = 200; + + public MainWindowViewModel() + { + this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); + } + + public int ItemCount + { + get { return _itemCount; } + set { this.RaiseAndSetIfChanged(ref _itemCount, value); } + } + + public IReactiveList Items { get; private set; } + + private void ResizeItems(int count) + { + if (Items == null) + { + var items = Enumerable.Range(0, count).Select(x => new ItemViewModel(x)); + Items = new ReactiveList(items); + } + } + } +} diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationTest/VirtualizationTest.csproj new file mode 100644 index 0000000000..bbe5882de9 --- /dev/null +++ b/samples/VirtualizationTest/VirtualizationTest.csproj @@ -0,0 +1,168 @@ + + + + + Debug + AnyCPU + {FBCAF3D0-2808-4934-8E96-3F607594517B} + WinExe + Properties + VirtualizationTest + VirtualizationTest + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + ..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll + True + + + + + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll + True + + + + + + + + + + + App.xaml + + + MainWindow.xaml + + + + + + + + + + + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Avalonia.Animation + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Avalonia.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Avalonia.Controls + + + {799a7bb5-3c2c-48b6-85a7-406a12c420da} + Avalonia.DesignerSupport + + + {7062ae20-5dcc-4442-9645-8195bdece63e} + Avalonia.Diagnostics + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Avalonia.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Avalonia.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Avalonia.Layout + + + {6417b24e-49c2-4985-8db2-3ab9d898ec91} + Avalonia.ReactiveUI + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Avalonia.SceneGraph + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Avalonia.Styling + + + {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} + Avalonia.Themes.Default + + + {3e53a01a-b331-47f3-b828-4a5717e77a24} + Avalonia.Markup.Xaml + + + {6417e941-21bc-467b-a771-0de389353ce6} + Avalonia.Markup + + + {3e908f67-5543-4879-a1dc-08eace79b3cd} + Avalonia.Direct2D1 + + + {811a76cf-1cf6-440f-963b-bbe31bd72a82} + Avalonia.Win32 + + + + + Designer + + + + + Designer + + + + + \ No newline at end of file diff --git a/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject b/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject new file mode 100644 index 0000000000..f744eecae0 --- /dev/null +++ b/samples/VirtualizationTest/VirtualizationTest.v2.ncrunchproject @@ -0,0 +1,26 @@ + + true + 1000 + false + false + false + true + false + false + true + false + false + true + true + false + true + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/samples/VirtualizationTest/packages.config b/samples/VirtualizationTest/packages.config new file mode 100644 index 0000000000..9613b3fccb --- /dev/null +++ b/samples/VirtualizationTest/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index a5e04ad6d5..d938153d3c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Presenters var generator = Owner.ItemContainerGenerator; var panel = VirtualizingPanel; - if (!panel.IsFull) + if (!panel.IsFull && Items != null) { var index = LastIndex + 1; var items = Items.Cast().Skip(index); From 43d9fa5724fca2502968252c57d480092f124864 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 20:27:33 +0200 Subject: [PATCH 028/158] Show scroll info in virt test app. --- samples/VirtualizationTest/MainWindow.xaml | 16 ++++++++++++++-- samples/VirtualizationTest/Program.cs | 14 ++++++++++++++ .../ViewModels/ItemViewModel.cs | 3 ++- .../VirtualizationTest/VirtualizationTest.csproj | 12 ++++++++++++ samples/VirtualizationTest/packages.config | 1 + src/Avalonia.Controls/ListBox.cs | 14 +++++++++++++- 6 files changed, 56 insertions(+), 4 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index 3fd25eb176..db256a032d 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -1,13 +1,25 @@ - + + + + - + diff --git a/samples/VirtualizationTest/Program.cs b/samples/VirtualizationTest/Program.cs index 5bbf01f428..8958605a3b 100644 --- a/samples/VirtualizationTest/Program.cs +++ b/samples/VirtualizationTest/Program.cs @@ -4,6 +4,8 @@ using System; using Avalonia; using Avalonia.Controls; +using Avalonia.Logging.Serilog; +using Serilog; namespace VirtualizationTest { @@ -11,10 +13,22 @@ namespace VirtualizationTest { static void Main(string[] args) { + InitializeLogging(); + AppBuilder.Configure() .UseWin32() .UseDirect2D1() .Start(); } + + private static void InitializeLogging() + { +#if DEBUG + SerilogLogger.Initialize(new LoggerConfiguration() + .MinimumLevel.Warning() + .WriteTo.Trace(outputTemplate: "{Area}: {Message}") + .CreateLogger()); +#endif + } } } diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs index f0cf69c23e..32acdd4336 100644 --- a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs @@ -2,10 +2,11 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using ReactiveUI; namespace VirtualizationTest.ViewModels { - internal class ItemViewModel + internal class ItemViewModel : ReactiveObject { private int _index; diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationTest/VirtualizationTest.csproj index bbe5882de9..ed7a340234 100644 --- a/samples/VirtualizationTest/VirtualizationTest.csproj +++ b/samples/VirtualizationTest/VirtualizationTest.csproj @@ -36,6 +36,14 @@ + + ..\..\packages\Serilog.1.5.14\lib\net45\Serilog.dll + True + + + ..\..\packages\Serilog.1.5.14\lib\net45\Serilog.FullNetFx.dll + True + ..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll True @@ -114,6 +122,10 @@ {42472427-4774-4c81-8aff-9f27b8e31721} Avalonia.Layout + + {B61B66A3-B82D-4875-8001-89D3394FE0C9} + Avalonia.Logging.Serilog + {6417b24e-49c2-4985-8db2-3ab9d898ec91} Avalonia.ReactiveUI diff --git a/samples/VirtualizationTest/packages.config b/samples/VirtualizationTest/packages.config index 9613b3fccb..782e7fbf2c 100644 --- a/samples/VirtualizationTest/packages.config +++ b/samples/VirtualizationTest/packages.config @@ -5,5 +5,6 @@ + \ No newline at end of file diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index eddadc2846..84e4751d37 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -21,6 +21,12 @@ namespace Avalonia.Controls private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new VirtualizingStackPanel()); + /// + /// Defines the property. + /// + public static readonly DirectProperty ScrollProperty = + AvaloniaProperty.RegisterDirect(nameof(Scroll), o => o.Scroll); + /// /// Defines the property. /// @@ -39,6 +45,8 @@ namespace Avalonia.Controls public static readonly AvaloniaProperty VirtualizationModeProperty = ItemsPresenter.VirtualizationModeProperty.AddOwner(); + private IScrollable _scroll; + /// /// Initializes static members of the class. /// @@ -50,7 +58,11 @@ namespace Avalonia.Controls /// /// Gets the scroll information for the . /// - public IScrollable Scroll { get; set; } + public IScrollable Scroll + { + get { return _scroll; } + private set { SetAndRaise(ScrollProperty, ref _scroll, value); } + } /// public new IList SelectedItems => base.SelectedItems; From 55f05defd1208caa58d8fad4a7855a05f02c4dc2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 23:01:13 +0200 Subject: [PATCH 029/158] Make scrolling up >= 1 page work --- .../Presenters/ItemVirtualizerSimple.cs | 4 ++-- .../Presenters/ItemsPresenterTests_Virtualization.cs | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index d938153d3c..afbd501fc2 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -135,9 +135,9 @@ namespace Avalonia.Controls.Presenters var generator = Owner.ItemContainerGenerator; var selector = Owner.MemberSelector; var sign = delta < 0 ? -1 : 1; - var move = delta < panel.Children.Count; - var first = delta < 0 && move ? panel.Children.Count + delta : 0; var count = Math.Min(Math.Abs(delta), panel.Children.Count); + var move = count < panel.Children.Count; + var first = delta < 0 && move ? panel.Children.Count + delta : 0; var containers = panel.Children.GetRange(first, count).ToList(); for (var i = 0; i < containers.Count; ++i) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 66bf70efb4..a3fc24583c 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -248,6 +248,16 @@ namespace Avalonia.Controls.UnitTests.Presenters { Assert.Equal(items[i + 20], target.Panel.Children[i].DataContext); } + + scroller.Offset = new Vector(0, 0); + + Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i], target.Panel.Children[i].DataContext); + } } } } From 2d4151ddfea8024b3cc7e58ff9172d1f7b5cadab Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 23:54:16 +0200 Subject: [PATCH 030/158] Update containers when Items changes. --- .../Presenters/ItemVirtualizerSimple.cs | 12 ++++++++++++ .../ItemsPresenterTests_Virtualization.cs | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index afbd501fc2..32fa967793 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -89,6 +89,18 @@ namespace Avalonia.Controls.Presenters public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { base.ItemsChanged(items, e); + + if (e.Action == NotifyCollectionChangedAction.Reset) + { + // We could recycle items here if this proves to be inefficient, but + // Reset indicates a large change and should (?) be quite rare. + VirtualizingPanel.Children.Clear(); + Owner.ItemContainerGenerator.Clear(); + FirstIndex = 0; + LastIndex = -1; + CreateRemoveContainers(); + } + ((ILogicalScrollable)Owner).InvalidateScroll(); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index a3fc24583c..3275c1bfba 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -184,6 +184,20 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(8, target.Panel.Children.Count); } + [Fact] + public void Should_Update_Containers_When_Items_Changes() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + target.Items = new[] { "foo", "bar", "baz" }; + + Assert.Equal(3, target.Panel.Children.Count); + } + public class WithContainers { [Fact] From 9c791dbbc1f7cf99713fb447cc7d9a207e700e57 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 24 May 2016 23:59:37 +0200 Subject: [PATCH 031/158] Clarified variable name. --- .../Presenters/ScrollContentPresenter.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 9e64694f8e..0aeb61a529 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -50,7 +50,7 @@ namespace Avalonia.Controls.Presenters private Size _extent; private Size _measuredExtent; private Vector _offset; - private IDisposable _scrollableSubscription; + private IDisposable _logicalScrollSubscription; private Size _viewport; /// @@ -176,7 +176,7 @@ namespace Avalonia.Controls.Presenters { var measureSize = availableSize; - if (_scrollableSubscription == null) + if (_logicalScrollSubscription == null) { measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); @@ -201,7 +201,7 @@ namespace Avalonia.Controls.Presenters protected override Size ArrangeOverride(Size finalSize) { var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable; - var logicalScroll = _scrollableSubscription != null; + var logicalScroll = _logicalScrollSubscription != null; if (!logicalScroll) { @@ -261,8 +261,8 @@ namespace Avalonia.Controls.Presenters { var scrollable = child as ILogicalScrollable; - _scrollableSubscription?.Dispose(); - _scrollableSubscription = null; + _logicalScrollSubscription?.Dispose(); + _logicalScrollSubscription = null; if (scrollable != null) { @@ -270,7 +270,7 @@ namespace Avalonia.Controls.Presenters if (scrollable?.IsLogicalScrollEnabled == true) { - _scrollableSubscription = new CompositeDisposable( + _logicalScrollSubscription = new CompositeDisposable( this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), Disposable.Create(() => scrollable.InvalidateScroll = null)); UpdateFromScrollable(scrollable); @@ -280,7 +280,7 @@ namespace Avalonia.Controls.Presenters private void UpdateFromScrollable(ILogicalScrollable scrollable) { - var logicalScroll = _scrollableSubscription != null; + var logicalScroll = _logicalScrollSubscription != null; if (logicalScroll != scrollable.IsLogicalScrollEnabled) { From 2ea322f54f8bfaf283c9bc02e965d197eef54c27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 25 May 2016 00:01:24 +0200 Subject: [PATCH 032/158] Added "Recreate" button to virtualization test app. --- samples/VirtualizationTest/MainWindow.xaml | 1 + .../ViewModels/ItemViewModel.cs | 6 ++-- .../ViewModels/MainWindowViewModel.cs | 33 +++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index db256a032d..b8fffe6688 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -17,6 +17,7 @@ + diff --git a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs index 32acdd4336..75777012c1 100644 --- a/samples/VirtualizationTest/ViewModels/ItemViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/ItemViewModel.cs @@ -8,13 +8,15 @@ namespace VirtualizationTest.ViewModels { internal class ItemViewModel : ReactiveObject { + private string _prefix; private int _index; - public ItemViewModel(int index) + public ItemViewModel(int index, string prefix = "Item") { + _prefix = prefix; _index = index; } - public string Header => $"Item {_index}"; + public string Header => $"{_prefix} {_index}"; } } diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 916568f7eb..192182798b 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -10,10 +10,14 @@ namespace VirtualizationTest.ViewModels internal class MainWindowViewModel : ReactiveObject { private int _itemCount = 200; + private IReactiveList _items; + private string _prefix = "Item"; public MainWindowViewModel() { this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); + RecreateCommand = ReactiveCommand.Create(); + RecreateCommand.Subscribe(_ => Recreate()); } public int ItemCount @@ -22,15 +26,40 @@ namespace VirtualizationTest.ViewModels set { this.RaiseAndSetIfChanged(ref _itemCount, value); } } - public IReactiveList Items { get; private set; } + public IReactiveList Items + { + get { return _items; } + private set { this.RaiseAndSetIfChanged(ref _items, value); } + } + + public ReactiveCommand RecreateCommand { get; private set; } private void ResizeItems(int count) { if (Items == null) { - var items = Enumerable.Range(0, count).Select(x => new ItemViewModel(x)); + var items = Enumerable.Range(0, count) + .Select(x => new ItemViewModel(x)); Items = new ReactiveList(items); } + else if (count > Items.Count) + { + var items = Enumerable.Range(Items.Count, count - Items.Count) + .Select(x => new ItemViewModel(x)); + Items.AddRange(items); + } + else if (count < Items.Count) + { + Items.RemoveRange(count, Items.Count - count - 1); + } + } + + private void Recreate() + { + _prefix = _prefix == "Item" ? "Recreated" : "Item"; + var items = Enumerable.Range(0, _itemCount) + .Select(x => new ItemViewModel(x, _prefix)); + Items = new ReactiveList(items); } } } From 780ff4c0b4db960792f3da3a4a2a4e9ed5a3e8e3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 25 May 2016 00:38:56 +0200 Subject: [PATCH 033/158] Correctly resize items. --- samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 192182798b..9b0565401a 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -50,7 +50,7 @@ namespace VirtualizationTest.ViewModels } else if (count < Items.Count) { - Items.RemoveRange(count, Items.Count - count - 1); + Items.RemoveRange(count, Items.Count - count); } } From be78eb2ed118f5d4d5d721e78230965e63aa4f81 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 25 May 2016 17:31:32 +0200 Subject: [PATCH 034/158] Dematerialize correct items. --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 32fa967793..41e7c1b072 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -135,7 +135,7 @@ namespace Avalonia.Controls.Presenters var index = panel.Children.Count - count; panel.Children.RemoveRange(index, count); - generator.Dematerialize(index, count); + generator.Dematerialize(FirstIndex + index, count); LastIndex -= count; } From 749c32ace654d0987739b4a83ad27e8f609b054b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 25 May 2016 21:54:51 +0200 Subject: [PATCH 035/158] Added failing partial item tests. --- .../ItemsPresenterTests_Virtualization.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 3275c1bfba..9a7f53def0 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -198,6 +198,40 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(3, target.Panel.Children.Count); } + [Fact] + public void Should_Decrease_The_Viewport_Size_By_One_If_There_Is_A_Partial_Item() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + + Assert.Equal(new Size(0, 9), ((ILogicalScrollable)target).Viewport); + } + + [Fact] + public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 91); + + var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); + Assert.Equal(90, minIndex); + Assert.Equal(6, ((IVirtualizingPanel)target.Panel).PixelOffset); + + ((ILogicalScrollable)target).Offset = new Vector(0, 90); + + minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); + Assert.Equal(90, minIndex); + Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset); + } + public class WithContainers { [Fact] From 0c2060c07fdc76c9c7299d9dcc8ddc4a411e36b6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 26 May 2016 01:04:04 +0100 Subject: [PATCH 036/158] added horizontal and vertical offset properties to popup. --- src/Avalonia.Controls/Primitives/Popup.cs | 43 +++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index c24d1cef2d..6bb4f31a56 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -39,6 +39,18 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty PlacementModeProperty = AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); + /// + /// Defines the property. + /// + public static readonly StyledProperty HorizontalOffsetProperty = + AvaloniaProperty.Register(nameof(HorizontalOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty VerticalOffsetProperty = + AvaloniaProperty.Register(nameof(VerticalOffset)); + /// /// Defines the property. /// @@ -122,6 +134,24 @@ namespace Avalonia.Controls.Primitives set { SetValue(PlacementModeProperty, value); } } + /// + /// Gets or sets the Horizontal offset of the popup in relation to the + /// + public double HorizontalOffset + { + get { return GetValue(HorizontalOffsetProperty); } + set { SetValue(HorizontalOffsetProperty, value); } + } + + /// + /// Gets or sets the Vertical offset of the popup in relation to the + /// + public double VerticalOffset + { + get { return GetValue(VerticalOffsetProperty); } + set { SetValue(VerticalOffsetProperty, value); } + } + /// /// Gets or sets the control that is used to determine the popup's position. /// @@ -292,13 +322,20 @@ namespace Avalonia.Controls.Primitives switch (mode) { case PlacementMode.Pointer: - return MouseDevice.Instance?.Position ?? default(Point); + if (MouseDevice.Instance != null) + { + var offset = new Point(HorizontalOffset, VerticalOffset); + + return new Point(MouseDevice.Instance.Position.X + offset.X, MouseDevice.Instance.Position.Y + offset.Y); + } + + return default(Point); case PlacementMode.Bottom: - return target?.PointToScreen(new Point(0, target.Bounds.Height)) ?? zero; + return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero; case PlacementMode.Right: - return target?.PointToScreen(new Point(target.Bounds.Width, 0)) ?? zero; + return target?.PointToScreen(new Point(target.Bounds.Width + HorizontalOffset, 0 + VerticalOffset)) ?? zero; default: throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); From 7f09154020ab54ccb0a64bd9528c6b4144995336 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 26 May 2016 17:54:35 +0200 Subject: [PATCH 037/158] Correctly handle partially obscured items. And move logical for selecting horizontal/vertical components of extent/offset/viewport into `ItemVirtualizer` base class. --- src/Avalonia.Controls/IVirtualizingPanel.cs | 10 +++ .../Presenters/ItemVirtualizer.cs | 28 +++++- .../Presenters/ItemVirtualizerNone.cs | 6 +- .../Presenters/ItemVirtualizerSimple.cs | 85 +++++++++---------- .../VirtualizingStackPanel.cs | 10 +++ .../ItemsPresenterTests_Virtualization.cs | 21 +++-- 6 files changed, 102 insertions(+), 58 deletions(-) diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 2aa356d38c..ce320c0da7 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -30,6 +30,16 @@ namespace Avalonia.Controls /// double AverageItemSize { get; } + /// + /// Gets or sets a size in pixels by which the content is overflowing the panel, in the + /// direction of scroll. + /// + /// + /// This may be non-zero even when is zero if the last item + /// overflows the panel bounds. + /// + double PixelOverflow { get; } + /// /// Gets or sets the current pixel offset of the items in the direction of scroll. /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index a92990b3ba..a7d1de5777 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Utils; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -19,14 +20,32 @@ namespace Avalonia.Controls.Presenters public ItemsPresenter Owner { get; } public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel; public IEnumerable Items { get; private set; } + public int ItemCount { get; private set; } public int FirstIndex { get; set; } - public int LastIndex { get; set; } = -1; + public int NextIndex { get; set; } + public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical; public abstract bool IsLogicalScrollEnabled { get; } - public abstract Size Extent { get; } - public abstract Vector Offset { get; set; } - public abstract Size Viewport { get; } + public abstract double ExtentValue { get; } + public abstract double OffsetValue { get; set; } + public abstract double ViewportValue { get; } + public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0); + public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0); + + public Vector Offset + { + get + { + return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0); + } + + set + { + OffsetValue = Vertical ? value.Y : value.X; + } + } + public static ItemVirtualizer Create(ItemsPresenter owner) { var virtualizingPanel = owner.Panel as IVirtualizingPanel; @@ -54,6 +73,7 @@ namespace Avalonia.Controls.Presenters public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { Items = items; + ItemCount = items.Count(); } } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 919bde7e0f..a8c24fcf72 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -19,18 +19,18 @@ namespace Avalonia.Controls.Presenters public override bool IsLogicalScrollEnabled => false; - public override Size Extent + public override double ExtentValue { get { throw new NotSupportedException(); } } - public override Vector Offset + public override double OffsetValue { get { throw new NotSupportedException(); } set { throw new NotSupportedException(); } } - public override Size Viewport + public override double ViewportValue { get { throw new NotSupportedException(); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 41e7c1b072..45c1c927e9 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -19,64 +19,59 @@ namespace Avalonia.Controls.Presenters public override bool IsLogicalScrollEnabled => true; - public override Size Extent - { - get - { - if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) - { - return new Size(0, Items.Count()); - } - else - { - return new Size(Items.Count(), 0); - } - } - } + public override double ExtentValue => ItemCount; - public override Vector Offset + public override double OffsetValue { get { - if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) - { - return new Vector(0, FirstIndex); - } - else - { - return new Vector(FirstIndex, 0); - } + var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; + return FirstIndex + offset; } set { - var scroll = (VirtualizingPanel.ScrollDirection == Orientation.Vertical) ? - value.Y : value.X; - var delta = (int)(scroll - FirstIndex); + var panel = VirtualizingPanel; + var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0; + var delta = (int)(value - (FirstIndex + offset)); if (delta != 0) { - RecycleContainers(delta); - FirstIndex += delta; - LastIndex += delta; + if ((NextIndex - 1) + delta < ItemCount) + { + if (panel.PixelOffset > 0) + { + panel.PixelOffset = 0; + delta += 1; + } + + if (delta != 0) + { + RecycleContainers(delta); + FirstIndex += delta; + NextIndex += delta; + } + } + else + { + // We're moving to a partially obscured item at the end of the list. + var firstIndex = ItemCount - panel.Children.Count; + RecycleContainers(firstIndex - FirstIndex); + NextIndex = ItemCount; + FirstIndex = NextIndex - panel.Children.Count; + panel.PixelOffset = VirtualizingPanel.PixelOverflow; + } } } } - public override Size Viewport + public override double ViewportValue { get { - var panel = VirtualizingPanel; - - if (panel.ScrollDirection == Orientation.Vertical) - { - return new Size(0, panel.Children.Count); - } - else - { - return new Size(panel.Children.Count, 0); - } + // If we can't fit the last item in the panel fully, subtract 1 from the viewport. + var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0; + return VirtualizingPanel.Children.Count - overflow; } } @@ -96,8 +91,7 @@ namespace Avalonia.Controls.Presenters // Reset indicates a large change and should (?) be quite rare. VirtualizingPanel.Children.Clear(); Owner.ItemContainerGenerator.Clear(); - FirstIndex = 0; - LastIndex = -1; + FirstIndex = NextIndex = 0; CreateRemoveContainers(); } @@ -111,7 +105,7 @@ namespace Avalonia.Controls.Presenters if (!panel.IsFull && Items != null) { - var index = LastIndex + 1; + var index = NextIndex; var items = Items.Cast().Skip(index); var memberSelector = Owner.MemberSelector; @@ -126,7 +120,7 @@ namespace Avalonia.Controls.Presenters } } - LastIndex = index - 1; + NextIndex = index; } if (panel.OverflowCount > 0) @@ -137,7 +131,7 @@ namespace Avalonia.Controls.Presenters panel.Children.RemoveRange(index, count); generator.Dematerialize(FirstIndex + index, count); - LastIndex -= count; + NextIndex -= count; } } @@ -156,6 +150,7 @@ namespace Avalonia.Controls.Presenters { var oldItemIndex = FirstIndex + first + i; var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); + var item = Items.ElementAt(newItemIndex); if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index afb691d2e5..eccada7796 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -30,6 +30,16 @@ namespace Avalonia.Controls double IVirtualizingPanel.AverageItemSize => _averageItemSize; + double IVirtualizingPanel.PixelOverflow + { + get + { + var bounds = Orientation == Orientation.Horizontal ? + Bounds.Width : Bounds.Height; + return Math.Max(0, (_takenSpace - _pixelOffset) - bounds); + } + } + double IVirtualizingPanel.PixelOffset { get { return _pixelOffset; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 9a7f53def0..09143215d2 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -213,23 +213,32 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() { - var target = CreateTarget(); + var target = CreateTarget(itemCount: 20); target.ApplyTemplate(); target.Measure(new Size(100, 95)); target.Arrange(new Rect(0, 0, 100, 95)); - ((ILogicalScrollable)target).Offset = new Vector(0, 91); + ((ILogicalScrollable)target).Offset = new Vector(0, 11); var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(90, minIndex); - Assert.Equal(6, ((IVirtualizingPanel)target.Panel).PixelOffset); + Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); + Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); - ((ILogicalScrollable)target).Offset = new Vector(0, 90); + ((ILogicalScrollable)target).Offset = new Vector(0, 10); minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(90, minIndex); + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset); + + ((ILogicalScrollable)target).Offset = new Vector(0, 11); + + minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); + Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); + Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); } public class WithContainers From 6df5ff4917cf59d8e1a7867538a873b9bcf80069 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 27 May 2016 00:16:33 +0200 Subject: [PATCH 038/158] Split virtualization tests in two One for base virtualization tests and one for simple mode tests. --- .../Avalonia.Controls.UnitTests.csproj | 1 + .../ItemsPresenterTests_Virtualization.cs | 198 ------------- ...emsPresenterTests_Virtualization_Simple.cs | 279 ++++++++++++++++++ 3 files changed, 280 insertions(+), 198 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index d1d6e3da86..0581568878 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -91,6 +91,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 09143215d2..29d32398b4 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -121,204 +121,6 @@ namespace Avalonia.Controls.UnitTests.Presenters } } - public class Simple - { - [Fact] - public void Should_Return_Items_Count_For_Extent_Vertical() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - - Assert.Equal(new Size(0, 20), ((ILogicalScrollable)target).Extent); - } - - [Fact] - public void Should_Return_Items_Count_For_Extent_Horizontal() - { - var target = CreateTarget(orientation: Orientation.Horizontal); - - target.ApplyTemplate(); - - Assert.Equal(new Size(20, 0), ((ILogicalScrollable)target).Extent); - } - - [Fact] - public void Should_Have_Number_Of_Visible_Items_As_Viewport_Vertical() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(new Size(0, 10), ((ILogicalScrollable)target).Viewport); - } - - [Fact] - public void Should_Have_Number_Of_Visible_Items_As_Viewport_Horizontal() - { - var target = CreateTarget(orientation: Orientation.Horizontal); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(new Size(10, 0), ((ILogicalScrollable)target).Viewport); - } - - [Fact] - public void Should_Remove_Items_When_Control_Is_Shrank() - { - var target = CreateTarget(); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - Assert.Equal(10, target.Panel.Children.Count); - - target.Arrange(new Rect(0, 0, 100, 80)); - - Assert.Equal(8, target.Panel.Children.Count); - } - - [Fact] - public void Should_Update_Containers_When_Items_Changes() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - target.Items = new[] { "foo", "bar", "baz" }; - - Assert.Equal(3, target.Panel.Children.Count); - } - - [Fact] - public void Should_Decrease_The_Viewport_Size_By_One_If_There_Is_A_Partial_Item() - { - var target = CreateTarget(); - - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); - - Assert.Equal(new Size(0, 9), ((ILogicalScrollable)target).Viewport); - } - - [Fact] - public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() - { - var target = CreateTarget(itemCount: 20); - - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 11); - - var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); - Assert.Equal(10, minIndex); - Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); - - ((ILogicalScrollable)target).Offset = new Vector(0, 10); - - minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); - Assert.Equal(10, minIndex); - Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset); - - ((ILogicalScrollable)target).Offset = new Vector(0, 11); - - minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); - Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); - Assert.Equal(10, minIndex); - Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); - } - - public class WithContainers - { - [Fact] - public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items() - { - var target = CreateTarget(); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var containers = target.Panel.Children.ToList(); - var scroller = (ScrollContentPresenter)target.Parent; - - scroller.Offset = new Vector(0, 5); - - var scrolledContainers = containers - .Skip(5) - .Take(5) - .Concat(containers.Take(5)).ToList(); - - Assert.Equal(new Vector(0, 5), ((ILogicalScrollable)target).Offset); - Assert.Equal(scrolledContainers, target.Panel.Children); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); - } - - scroller.Offset = new Vector(0, 0); - Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); - Assert.Equal(containers, target.Panel.Children); - - var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i], target.Panel.Children[i].DataContext); - } - } - - [Fact] - public void Scrolling_More_Than_A_Page_Should_Recycle_Items() - { - var target = CreateTarget(itemCount: 50); - var items = (IList)target.Items; - - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); - - var containers = target.Panel.Children.ToList(); - var scroller = (ScrollContentPresenter)target.Parent; - - scroller.Offset = new Vector(0, 20); - - Assert.Equal(new Vector(0, 20), ((ILogicalScrollable)target).Offset); - Assert.Equal(containers, target.Panel.Children); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i + 20], target.Panel.Children[i].DataContext); - } - - scroller.Offset = new Vector(0, 0); - - Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); - Assert.Equal(containers, target.Panel.Children); - - for (var i = 0; i < target.Panel.Children.Count; ++i) - { - Assert.Equal(items[i], target.Panel.Children[i].DataContext); - } - } - } - } - private static ItemsPresenter CreateTarget( ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, Orientation orientation = Orientation.Vertical, diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs new file mode 100644 index 0000000000..3404b65f52 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -0,0 +1,279 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Presenters +{ + public class ItemsPresenterTests_Virtualization_Simple + { + [Fact] + public void Should_Return_Items_Count_For_Extent_Vertical() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + + Assert.Equal(new Size(0, 20), ((ILogicalScrollable)target).Extent); + } + + [Fact] + public void Should_Return_Items_Count_For_Extent_Horizontal() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + + Assert.Equal(new Size(20, 0), ((ILogicalScrollable)target).Extent); + } + + [Fact] + public void Should_Have_Number_Of_Visible_Items_As_Viewport_Vertical() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Size(0, 10), ((ILogicalScrollable)target).Viewport); + } + + [Fact] + public void Should_Have_Number_Of_Visible_Items_As_Viewport_Horizontal() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Size(10, 0), ((ILogicalScrollable)target).Viewport); + } + + [Fact] + public void Should_Remove_Items_When_Control_Is_Shrank() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(10, target.Panel.Children.Count); + + target.Arrange(new Rect(0, 0, 100, 80)); + + Assert.Equal(8, target.Panel.Children.Count); + } + + [Fact] + public void Should_Update_Containers_When_Items_Changes() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + target.Items = new[] { "foo", "bar", "baz" }; + + Assert.Equal(3, target.Panel.Children.Count); + } + + [Fact] + public void Should_Decrease_The_Viewport_Size_By_One_If_There_Is_A_Partial_Item() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + + Assert.Equal(new Size(0, 9), ((ILogicalScrollable)target).Viewport); + } + + [Fact] + public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 11); + + var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); + Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); + Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); + + ((ILogicalScrollable)target).Offset = new Vector(0, 10); + + minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); + Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset); + + ((ILogicalScrollable)target).Offset = new Vector(0, 11); + + minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); + Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); + Assert.Equal(10, minIndex); + Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); + } + + public class WithContainers + { + [Fact] + public void Scrolling_Less_Than_A_Page_Should_Move_Recycled_Items() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var containers = target.Panel.Children.ToList(); + var scroller = (ScrollContentPresenter)target.Parent; + + scroller.Offset = new Vector(0, 5); + + var scrolledContainers = containers + .Skip(5) + .Take(5) + .Concat(containers.Take(5)).ToList(); + + Assert.Equal(new Vector(0, 5), ((ILogicalScrollable)target).Offset); + Assert.Equal(scrolledContainers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 5], target.Panel.Children[i].DataContext); + } + + scroller.Offset = new Vector(0, 0); + Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + var dcs = target.Panel.Children.Select(x => x.DataContext).ToList(); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i], target.Panel.Children[i].DataContext); + } + } + + [Fact] + public void Scrolling_More_Than_A_Page_Should_Recycle_Items() + { + var target = CreateTarget(itemCount: 50); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var containers = target.Panel.Children.ToList(); + var scroller = (ScrollContentPresenter)target.Parent; + + scroller.Offset = new Vector(0, 20); + + Assert.Equal(new Vector(0, 20), ((ILogicalScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 20], target.Panel.Children[i].DataContext); + } + + scroller.Offset = new Vector(0, 0); + + Assert.Equal(new Vector(0, 0), ((ILogicalScrollable)target).Offset); + Assert.Equal(containers, target.Panel.Children); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i], target.Panel.Children[i].DataContext); + } + } + } + + private static ItemsPresenter CreateTarget( + Orientation orientation = Orientation.Vertical, + bool useContainers = true, + int itemCount = 20) + { + ItemsPresenter result; + var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList(); + + var scroller = new ScrollContentPresenter + { + Content = result = new TestItemsPresenter(useContainers) + { + Items = items, + ItemsPanel = VirtualizingPanelTemplate(orientation), + ItemTemplate = ItemTemplate(), + VirtualizationMode = ItemVirtualizationMode.Simple, + } + }; + + scroller.UpdateChild(); + + return result; + } + + private static IDataTemplate ItemTemplate() + { + return new FuncDataTemplate(x => new Canvas + { + Width = 10, + Height = 10, + }); + } + + private static ITemplate VirtualizingPanelTemplate( + Orientation orientation = Orientation.Vertical) + { + return new FuncTemplate(() => new VirtualizingStackPanel + { + Orientation = orientation, + }); + } + + private class TestItemsPresenter : ItemsPresenter + { + private bool _useContainers; + + public TestItemsPresenter(bool useContainers) + { + _useContainers = useContainers; + } + + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return _useContainers ? + new ItemContainerGenerator(this, TestContainer.ContentProperty, null) : + new ItemContainerGenerator(this); + } + } + + private class TestContainer : ContentControl + { + public TestContainer() + { + Width = 10; + Height = 10; + } + } + } +} From 3eada7eb202b3df24a8bc75f650c0af65268669a Mon Sep 17 00:00:00 2001 From: donandren Date: Fri, 27 May 2016 16:18:56 +0300 Subject: [PATCH 039/158] Fixes memory leak in hit testing simplifies hit testing not to use geometry, but simple transforms --- src/Avalonia.Input/InputExtensions.cs | 6 ++--- src/Avalonia.SceneGraph/Rect.cs | 4 ++-- .../VisualTree/TransformedBounds.cs | 22 +++++++++---------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index a2321f3493..5d80653322 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -25,7 +25,6 @@ namespace Avalonia.Input { Contract.Requires(element != null); var transformedBounds = BoundsTracker.GetTransformedBounds((Visual)element); - var geometry = transformedBounds.GetTransformedBoundsGeometry(); if (element.IsVisible && element.IsHitTestVisible && @@ -42,7 +41,7 @@ namespace Avalonia.Input } } - if (geometry.FillContains(p)) + if (transformedBounds.Contains(p)) { yield return element; } @@ -71,7 +70,6 @@ namespace Avalonia.Input }) .OrderBy(x => x, null) .Select(x => x.Element); - } private class ZOrderElement : IComparable @@ -95,4 +93,4 @@ namespace Avalonia.Input } } } -} +} \ No newline at end of file diff --git a/src/Avalonia.SceneGraph/Rect.cs b/src/Avalonia.SceneGraph/Rect.cs index de4d13890b..806ad39b53 100644 --- a/src/Avalonia.SceneGraph/Rect.cs +++ b/src/Avalonia.SceneGraph/Rect.cs @@ -230,8 +230,8 @@ namespace Avalonia /// true if the point is in the bounds of the rectangle; otherwise false. public bool Contains(Point p) { - return p.X >= _x && p.X < _x + _width && - p.Y >= _y && p.Y < _y + _height; + return p.X >= _x && p.X <= _x + _width && + p.Y >= _y && p.Y <= _y + _height; } /// diff --git a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs index da8ed01ccc..4c548669bd 100644 --- a/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs +++ b/src/Avalonia.SceneGraph/VisualTree/TransformedBounds.cs @@ -38,20 +38,18 @@ namespace Avalonia.VisualTree /// public Matrix Transform { get; } - public Geometry GetTransformedBoundsGeometry() + public bool Contains(Point point) { - StreamGeometry geometry = new StreamGeometry(); - using (var context = geometry.Open()) + if (Transform.HasInverse) { - context.SetFillRule(FillRule.EvenOdd); - context.BeginFigure(Bounds.TopLeft * Transform, true); - context.LineTo(Bounds.TopRight * Transform); - context.LineTo(Bounds.BottomRight * Transform); - context.LineTo(Bounds.BottomLeft * Transform); - context.LineTo(Bounds.TopLeft * Transform); - context.EndFigure(true); + Point trPoint = point * Transform.Invert(); + + return Bounds.Contains(trPoint); + } + else + { + return Bounds.Contains(point); } - return geometry; } } -} +} \ No newline at end of file From 31147af9d30a0680b4f606f4f9cdc0936b24dfa8 Mon Sep 17 00:00:00 2001 From: donandren Date: Fri, 27 May 2016 16:50:28 +0300 Subject: [PATCH 040/158] renamed Visual.TransformOrigin -> RenderTransformOrigin --- src/Avalonia.Controls/LayoutTransformControl.cs | 2 +- src/Avalonia.Controls/Primitives/AdornerLayer.cs | 2 +- src/Avalonia.SceneGraph/Rendering/RendererMixin.cs | 4 ++-- src/Avalonia.SceneGraph/Visual.cs | 12 ++++++------ src/Avalonia.SceneGraph/VisualTree/IVisual.cs | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs index ded75d708a..ed7a50d9b0 100644 --- a/src/Avalonia.Controls/LayoutTransformControl.cs +++ b/src/Avalonia.Controls/LayoutTransformControl.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls if (null != TransformRoot) { TransformRoot.RenderTransform = _matrixTransform; - TransformRoot.TransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); + TransformRoot.RenderTransformOrigin = new RelativePoint(0, 0, RelativeUnit.Absolute); } ApplyLayoutTransform(); diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index b732387cf5..1dfa2d7af2 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -60,7 +60,7 @@ namespace Avalonia.Controls.Primitives if (info != null) { child.RenderTransform = new MatrixTransform(info.Bounds.Transform); - child.TransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); + child.RenderTransformOrigin = new RelativePoint(new Point(0,0), RelativeUnit.Absolute); child.Arrange(info.Bounds.Bounds); } else diff --git a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs index b26d825064..fbd5550c95 100644 --- a/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs +++ b/src/Avalonia.SceneGraph/Rendering/RendererMixin.cs @@ -104,7 +104,7 @@ namespace Avalonia.Rendering if (visual.RenderTransform != null) { - var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(origin); renderTransform = (-offset) * visual.RenderTransform.Value * (offset); } @@ -171,7 +171,7 @@ namespace Avalonia.Rendering } else { - var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(visual.Bounds.Position + origin); var m = (-offset) * visual.RenderTransform.Value * (offset); return visual.Bounds.TransformToAABB(m); diff --git a/src/Avalonia.SceneGraph/Visual.cs b/src/Avalonia.SceneGraph/Visual.cs index 8bdbfecb4a..95cae794af 100644 --- a/src/Avalonia.SceneGraph/Visual.cs +++ b/src/Avalonia.SceneGraph/Visual.cs @@ -64,10 +64,10 @@ namespace Avalonia AvaloniaProperty.Register(nameof(RenderTransform)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty TransformOriginProperty = - AvaloniaProperty.Register(nameof(TransformOrigin), defaultValue: RelativePoint.Center); + public static readonly StyledProperty RenderTransformOriginProperty = + AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center); /// /// Defines the property. @@ -180,10 +180,10 @@ namespace Avalonia /// /// Gets the transform origin of the scene graph node. /// - public RelativePoint TransformOrigin + public RelativePoint RenderTransformOrigin { - get { return GetValue(TransformOriginProperty); } - set { SetValue(TransformOriginProperty, value); } + get { return GetValue(RenderTransformOriginProperty); } + set { SetValue(RenderTransformOriginProperty, value); } } /// diff --git a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs index fb4416f78e..3a01ac0afc 100644 --- a/src/Avalonia.SceneGraph/VisualTree/IVisual.cs +++ b/src/Avalonia.SceneGraph/VisualTree/IVisual.cs @@ -72,9 +72,9 @@ namespace Avalonia.VisualTree Transform RenderTransform { get; set; } /// - /// Gets or sets the transform origin of the scene graph node. + /// Gets or sets the render transform origin of the scene graph node. /// - RelativePoint TransformOrigin { get; set; } + RelativePoint RenderTransformOrigin { get; set; } /// /// Gets the scene graph node's child nodes. From e931bcf79c30fe9345c8ca1d56123775ca318acb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 27 May 2016 22:41:10 +0100 Subject: [PATCH 041/158] correctly scale offsets for pointer placement mode. --- src/Avalonia.Controls/Primitives/Popup.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 6bb4f31a56..81d01ff74f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -10,6 +10,7 @@ using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.Rendering; using Avalonia.VisualTree; +using Avalonia.Layout; namespace Avalonia.Controls.Primitives { @@ -317,21 +318,22 @@ namespace Avalonia.Controls.Primitives if (target?.GetVisualRoot() == null) { mode = PlacementMode.Pointer; - } + } switch (mode) { case PlacementMode.Pointer: if (MouseDevice.Instance != null) { - var offset = new Point(HorizontalOffset, VerticalOffset); - - return new Point(MouseDevice.Instance.Position.X + offset.X, MouseDevice.Instance.Position.Y + offset.Y); + // Scales the Horizontal and Vertical offset to screen co-ordinates. + var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling); + return MouseDevice.Instance.Position + screenOffset; } return default(Point); case PlacementMode.Bottom: + return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero; case PlacementMode.Right: From f70bed953b44f49b83c9f8aff6b8a5665ac5de8c Mon Sep 17 00:00:00 2001 From: Stano Turza Date: Sat, 28 May 2016 10:11:04 +0200 Subject: [PATCH 042/158] Fix broken Gitter link --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 1c77a38c08..63047fdc85 100644 --- a/readme.md +++ b/readme.md @@ -45,7 +45,7 @@ framework. As mentioned above, Avalonia is still in alpha and as such there's not much documentation yet. You can take a look at the [getting started page](docs/tutorial/gettingstarted.md) for an overview of how to get started but probably the best thing to do for now is to already know a little bit -about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/Avalonia/Avalonia). +about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia). There's also a high-level [architecture document](docs/spec/architecture.md) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/. From 722a329106bff57a65f5606aef7272f8728395fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 29 May 2016 12:42:05 +0200 Subject: [PATCH 043/158] Add containers at beginning when scrolled to end. --- .../Generators/ItemContainerGenerator`1.cs | 6 +++ .../Presenters/ItemVirtualizerSimple.cs | 42 +++++++++++++++---- ...emsPresenterTests_Virtualization_Simple.cs | 23 ++++++++++ 3 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index c514e0e85f..35107a84fc 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -84,6 +84,12 @@ namespace Avalonia.Controls.Generators IMemberSelector selector) { var container = ContainerFromIndex(oldIndex); + + if (container == null) + { + throw new IndexOutOfRangeException("Could not recycle container: not materialized."); + } + var i = selector != null ? selector.Select(item) : item; container.SetValue(ContentProperty, i); diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 45c1c927e9..3c972e922f 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -105,22 +105,48 @@ namespace Avalonia.Controls.Presenters if (!panel.IsFull && Items != null) { - var index = NextIndex; - var items = Items.Cast().Skip(index); var memberSelector = Owner.MemberSelector; + var index = NextIndex; + var step = 1; - foreach (var item in items) + while (!panel.IsFull) { - var materialized = generator.Materialize(index++, item, memberSelector); - panel.Children.Add(materialized.ContainerControl); + if (index == ItemCount) + { + if (FirstIndex == 0) + { + break; + } + else + { + index = FirstIndex - 1; + step = -1; + } + } + + var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector); - if (panel.IsFull) + if (step == 1) { - break; + panel.Children.Add(materialized.ContainerControl); } + else + { + panel.Children.Insert(0, materialized.ContainerControl); + } + + index += step; } - NextIndex = index; + if (step == 1) + { + NextIndex = index; + } + else + { + NextIndex = ItemCount; + FirstIndex = index + 1; + } } if (panel.OverflowCount > 0) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 3404b65f52..013487f8fc 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -74,6 +74,29 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(8, target.Panel.Children.Count); } + [Fact] + public void Should_Add_New_Items_At_Top_When_Control_Is_Scrolled_To_Bottom_And_Enlarged() + { + var target = CreateTarget(); + var items = (IList)target.Items; + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(10, target.Panel.Children.Count); + + ((IScrollable)target).Offset = new Vector(0, 10); + target.Arrange(new Rect(0, 0, 100, 120)); + + Assert.Equal(12, target.Panel.Children.Count); + + for (var i = 0; i < target.Panel.Children.Count; ++i) + { + Assert.Equal(items[i + 8], target.Panel.Children[i].DataContext); + } + } + [Fact] public void Should_Update_Containers_When_Items_Changes() { From 3e8a8c6d7cfba1439b538354c9336db35ee1f150 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 31 May 2016 14:57:08 +0200 Subject: [PATCH 044/158] WIP: INCC virtualization support. --- samples/VirtualizationTest/MainWindow.xaml | 4 ++ .../Presenters/ItemVirtualizerSimple.cs | 56 +++++++++++++++---- ...emsPresenterTests_Virtualization_Simple.cs | 31 +++++++++- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index b8fffe6688..a6fd6918ef 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -17,6 +17,10 @@ + + diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 3c972e922f..1ff85c50ac 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -47,7 +47,7 @@ namespace Avalonia.Controls.Presenters if (delta != 0) { - RecycleContainers(delta); + RecycleMoveContainers(delta); FirstIndex += delta; NextIndex += delta; } @@ -56,7 +56,7 @@ namespace Avalonia.Controls.Presenters { // We're moving to a partially obscured item at the end of the list. var firstIndex = ItemCount - panel.Children.Count; - RecycleContainers(firstIndex - FirstIndex); + RecycleMoveContainers(firstIndex - FirstIndex); NextIndex = ItemCount; FirstIndex = NextIndex - panel.Children.Count; panel.PixelOffset = VirtualizingPanel.PixelOverflow; @@ -85,14 +85,26 @@ namespace Avalonia.Controls.Presenters { base.ItemsChanged(items, e); - if (e.Action == NotifyCollectionChangedAction.Reset) + switch (e.Action) { - // We could recycle items here if this proves to be inefficient, but - // Reset indicates a large change and should (?) be quite rare. - VirtualizingPanel.Children.Clear(); - Owner.ItemContainerGenerator.Clear(); - FirstIndex = NextIndex = 0; - CreateRemoveContainers(); + case NotifyCollectionChangedAction.Add: + if (e.NewStartingIndex >= FirstIndex && + e.NewStartingIndex + e.NewItems.Count < NextIndex) + { + CreateRemoveContainers(); + RecycleContainers(); + } + + break; + + case NotifyCollectionChangedAction.Reset: + // We could recycle items here if this proves to be inefficient, but + // Reset indicates a large change and should (?) be quite rare. + VirtualizingPanel.Children.Clear(); + Owner.ItemContainerGenerator.Clear(); + FirstIndex = NextIndex = 0; + CreateRemoveContainers(); + break; } ((ILogicalScrollable)Owner).InvalidateScroll(); @@ -161,7 +173,31 @@ namespace Avalonia.Controls.Presenters } } - private void RecycleContainers(int delta) + private void RecycleContainers() + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var selector = Owner.MemberSelector; + var containers = generator.Containers.ToList(); + var itemIndex = FirstIndex; + + foreach (var container in containers) + { + var item = Items.ElementAt(itemIndex); + + if (!object.Equals(container.Item, item)) + { + if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + { + throw new NotImplementedException(); + } + } + + ++itemIndex; + } + } + + private void RecycleMoveContainers(int delta) { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 013487f8fc..036ecdce6f 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; @@ -154,6 +155,32 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); } + [Fact] + public void Inserting_Items_Should_Update_Containers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 5); + + var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + + Assert.Equal( + expected, + target.Panel.Children.Select(x => x.DataContext)); + + items.Insert(6, "Inserted"); + expected.Insert(1, "Inserted"); + expected.RemoveAt(expected.Count - 1); + + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + public class WithContainers { [Fact] @@ -237,7 +264,9 @@ namespace Avalonia.Controls.UnitTests.Presenters int itemCount = 20) { ItemsPresenter result; - var items = Enumerable.Range(0, itemCount).Select(x => $"Item {x}").ToList(); + + var items = new ObservableCollection( + Enumerable.Range(0, itemCount).Select(x => $"Item {x}")); var scroller = new ScrollContentPresenter { From 35fe93c9c2971ed7e91399e02e6702dbd6c4a4b2 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Tue, 31 May 2016 14:59:31 +0100 Subject: [PATCH 045/158] added support for add items and remove items in virtualization sample app. --- samples/VirtualizationTest/MainWindow.xaml | 3 +- .../ViewModels/MainWindowViewModel.cs | 37 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index a6fd6918ef..988bd8bef5 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -21,10 +21,11 @@ UseFloatingWatermark="True" Text="{Binding NewItemString}"/> + - + diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 9b0565401a..239eab2451 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -10,7 +10,9 @@ namespace VirtualizationTest.ViewModels internal class MainWindowViewModel : ReactiveObject { private int _itemCount = 200; + private string _newItemString; private IReactiveList _items; + private ItemViewModel _selectedItem; private string _prefix = "Item"; public MainWindowViewModel() @@ -18,6 +20,18 @@ namespace VirtualizationTest.ViewModels this.WhenAnyValue(x => x.ItemCount).Subscribe(ResizeItems); RecreateCommand = ReactiveCommand.Create(); RecreateCommand.Subscribe(_ => Recreate()); + + NewItemCommand = ReactiveCommand.Create(); + NewItemCommand.Subscribe(_ => Create()); + + RemoveItemCommand = ReactiveCommand.Create(); + RemoveItemCommand.Subscribe(_ => Remove()); + } + + public string NewItemString + { + get { return _newItemString; } + set { this.RaiseAndSetIfChanged(ref _newItemString, value); } } public int ItemCount @@ -26,14 +40,24 @@ namespace VirtualizationTest.ViewModels set { this.RaiseAndSetIfChanged(ref _itemCount, value); } } + public ItemViewModel SelectedItem + { + get { return _selectedItem; } + set { this.RaiseAndSetIfChanged(ref _selectedItem, value); } + } + public IReactiveList Items { get { return _items; } private set { this.RaiseAndSetIfChanged(ref _items, value); } } + public ReactiveCommand NewItemCommand { get; private set; } + public ReactiveCommand RecreateCommand { get; private set; } + public ReactiveCommand RemoveItemCommand { get; private set; } + private void ResizeItems(int count) { if (Items == null) @@ -54,6 +78,19 @@ namespace VirtualizationTest.ViewModels } } + private void Create() + { + Items.Add(new ItemViewModel(Items.Count, NewItemString)); + } + + private void Remove() + { + if (SelectedItem != null) + { + Items.Remove(SelectedItem); + } + } + private void Recreate() { _prefix = _prefix == "Item" ? "Recreated" : "Item"; From defae349a7589912b2278213fe67cd59f5c74284 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Tue, 31 May 2016 15:07:31 +0100 Subject: [PATCH 046/158] added button to prepend and append items to list. --- samples/VirtualizationTest/MainWindow.xaml | 3 ++- .../ViewModels/MainWindowViewModel.cs | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index 988bd8bef5..8595383bdf 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -20,7 +20,8 @@ - + + diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 239eab2451..5b4a51f626 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -21,8 +21,11 @@ namespace VirtualizationTest.ViewModels RecreateCommand = ReactiveCommand.Create(); RecreateCommand.Subscribe(_ => Recreate()); - NewItemCommand = ReactiveCommand.Create(); - NewItemCommand.Subscribe(_ => Create()); + AppendItemCommand = ReactiveCommand.Create(); + AppendItemCommand.Subscribe(_ => Append()); + + PrePendItemCommand = ReactiveCommand.Create(); + PrePendItemCommand.Subscribe(_ => PrePend()); RemoveItemCommand = ReactiveCommand.Create(); RemoveItemCommand.Subscribe(_ => Remove()); @@ -52,7 +55,9 @@ namespace VirtualizationTest.ViewModels private set { this.RaiseAndSetIfChanged(ref _items, value); } } - public ReactiveCommand NewItemCommand { get; private set; } + public ReactiveCommand AppendItemCommand { get; private set; } + + public ReactiveCommand PrePendItemCommand { get; private set; } public ReactiveCommand RecreateCommand { get; private set; } @@ -78,11 +83,16 @@ namespace VirtualizationTest.ViewModels } } - private void Create() + private void Append() { Items.Add(new ItemViewModel(Items.Count, NewItemString)); } + private void PrePend () + { + Items.Insert(0, new ItemViewModel(0, NewItemString)); + } + private void Remove() { if (SelectedItem != null) From 94f8abbc4db4ee2ea7c1b3b387affbd68363e2c7 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Tue, 31 May 2016 15:38:15 +0100 Subject: [PATCH 047/158] add items at selected index, or at end if no selection. --- samples/VirtualizationTest/MainWindow.xaml | 3 +-- .../ViewModels/MainWindowViewModel.cs | 25 ++++++++----------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index 8595383bdf..aa882c052a 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -20,8 +20,7 @@ - - + diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 5b4a51f626..dde3bd357d 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -21,11 +21,8 @@ namespace VirtualizationTest.ViewModels RecreateCommand = ReactiveCommand.Create(); RecreateCommand.Subscribe(_ => Recreate()); - AppendItemCommand = ReactiveCommand.Create(); - AppendItemCommand.Subscribe(_ => Append()); - - PrePendItemCommand = ReactiveCommand.Create(); - PrePendItemCommand.Subscribe(_ => PrePend()); + AddItemCommand = ReactiveCommand.Create(); + AddItemCommand.Subscribe(_ => AddItem()); RemoveItemCommand = ReactiveCommand.Create(); RemoveItemCommand.Subscribe(_ => Remove()); @@ -55,9 +52,7 @@ namespace VirtualizationTest.ViewModels private set { this.RaiseAndSetIfChanged(ref _items, value); } } - public ReactiveCommand AppendItemCommand { get; private set; } - - public ReactiveCommand PrePendItemCommand { get; private set; } + public ReactiveCommand AddItemCommand { get; private set; } public ReactiveCommand RecreateCommand { get; private set; } @@ -83,14 +78,16 @@ namespace VirtualizationTest.ViewModels } } - private void Append() + private void AddItem() { - Items.Add(new ItemViewModel(Items.Count, NewItemString)); - } + var index = Items.Count; - private void PrePend () - { - Items.Insert(0, new ItemViewModel(0, NewItemString)); + if (SelectedItem != null) + { + index = Items.IndexOf(SelectedItem); + } + + Items.Insert(index, new ItemViewModel(index, NewItemString)); } private void Remove() From e015fc15b267501b3f65d70291836993f9248183 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Tue, 31 May 2016 16:03:29 +0100 Subject: [PATCH 048/158] add items, adds after selected item. --- samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index dde3bd357d..953f533065 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -84,7 +84,7 @@ namespace VirtualizationTest.ViewModels if (SelectedItem != null) { - index = Items.IndexOf(SelectedItem); + index = Items.IndexOf(SelectedItem) + 1; } Items.Insert(index, new ItemViewModel(index, NewItemString)); From e2fe94cafb3aa45d667e611cb44a21f06278b255 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Tue, 31 May 2016 16:03:57 +0100 Subject: [PATCH 049/158] Added test for removing last item in the list, when scrolled to end. --- ...emsPresenterTests_Virtualization_Simple.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 036ecdce6f..32cd0c2825 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -181,6 +181,31 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(expected, actual); } + [Fact] + public void Removing_last_item_when_visible_should_UpdateContainers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 195)); + target.Arrange(new Rect(0, 0, 100, 195)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 5); + + var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + + Assert.Equal( + expected, + target.Panel.Children.Select(x => x.DataContext)); + + items.Remove(items.Last()); + expected.Remove(expected.Last()); + + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + public class WithContainers { [Fact] From 2e9c19beaf650d746a3283464e653609e37bab81 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Tue, 31 May 2016 17:12:52 +0100 Subject: [PATCH 050/158] Throw not implemented exception placeholder. --- .../Presenters/ItemVirtualizerSimple.cs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 1ff85c50ac..b2c72174d3 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -42,7 +42,7 @@ namespace Avalonia.Controls.Presenters if (panel.PixelOffset > 0) { panel.PixelOffset = 0; - delta += 1; + delta += 1; } if (delta != 0) @@ -87,6 +87,18 @@ namespace Avalonia.Controls.Presenters switch (e.Action) { + case NotifyCollectionChangedAction.Remove: + if (e.OldStartingIndex == ItemCount) + { + NextIndex = ItemCount - 1; + + throw new NotImplementedException("Remove the last item from the panel."); + } + + CreateRemoveContainers(); + RecycleContainers(); + break; + case NotifyCollectionChangedAction.Add: if (e.NewStartingIndex >= FirstIndex && e.NewStartingIndex + e.NewItems.Count < NextIndex) @@ -183,17 +195,25 @@ namespace Avalonia.Controls.Presenters foreach (var container in containers) { - var item = Items.ElementAt(itemIndex); - - if (!object.Equals(container.Item, item)) + if (itemIndex < ItemCount) { - if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + var item = Items.ElementAt(itemIndex); + + if (!object.Equals(container.Item, item)) { - throw new NotImplementedException(); + if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + { + throw new NotImplementedException(); + } } } + else + { + panel.Children.RemoveAt(panel.Children.Count - 1); + } ++itemIndex; + } } From 1417aab362ed98b64bc48cfc450fc53c2aab2feb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 1 Jun 2016 23:19:29 +0100 Subject: [PATCH 051/158] added some more test cases for simple virtualization. --- ...emsPresenterTests_Virtualization_Simple.cs | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 32cd0c2825..36d43cd6fe 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -182,7 +182,63 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Removing_last_item_when_visible_should_UpdateContainers() + public void Removing_First_Item_When_Visible_Should_UpdateContainers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 195)); + target.Arrange(new Rect(0, 0, 100, 195)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 5); + + var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + + Assert.Equal( + expected, + target.Panel.Children.Select(x => x.DataContext)); + + items.Remove(items.First()); + expected.Remove(expected.First()); + + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + + [Fact] + public void Removing_Items_From_Middle_Should_Update_Containers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 195)); + target.Arrange(new Rect(0, 0, 100, 195)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 5); + + var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + + Assert.Equal( + expected, + target.Panel.Children.Select(x => x.DataContext)); + + items.RemoveAt(2); + expected.RemoveAt(2); + + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + + items.RemoveAt(items.Count - 2); + expected.RemoveAt(expected.Count -2); + + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + + [Fact] + public void Removing_Last_Item_When_Visible_Should_UpdateContainers() { var target = CreateTarget(itemCount: 20); From ac8185b8c3075d7f08675ca1f38b696722f9cab6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 1 Jun 2016 23:24:46 +0100 Subject: [PATCH 052/158] added implementation for removing first and last elements. --- .../Presenters/ItemVirtualizerSimple.cs | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index b2c72174d3..5f32f5d10c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -88,15 +88,44 @@ namespace Avalonia.Controls.Presenters switch (e.Action) { case NotifyCollectionChangedAction.Remove: - if (e.OldStartingIndex == ItemCount) + if(e.OldStartingIndex >= FirstIndex && + e.OldStartingIndex + e.OldItems.Count <= NextIndex) { - NextIndex = ItemCount - 1; - - throw new NotImplementedException("Remove the last item from the panel."); - } - - CreateRemoveContainers(); - RecycleContainers(); + if (e.OldStartingIndex == FirstIndex) + { + // We are removing the first in the list. + VirtualizingPanel.Children.RemoveAt(e.OldStartingIndex - FirstIndex); + Owner.ItemContainerGenerator.Dematerialize(e.OldStartingIndex - FirstIndex, 1); + FirstIndex++; // This may not be necessary, but cant get to work without this. + + // If all items are visible we need to reduce the NextIndex too. + if(NextIndex > ItemCount) + { + NextIndex = ItemCount; + } + + CreateRemoveContainers(); + RecycleContainers(); + } + else if (e.OldStartingIndex + e.OldItems.Count == NextIndex) + { + // We are removing the last one in the list. + VirtualizingPanel.Children.RemoveAt(e.OldStartingIndex - FirstIndex); + Owner.ItemContainerGenerator.Dematerialize(e.OldStartingIndex - FirstIndex, 1); + NextIndex--; + } + else + { + // If all items are visible we need to reduce the NextIndex too. + if (NextIndex > ItemCount) + { + NextIndex = ItemCount; + } + + CreateRemoveContainers(); + RecycleContainers(); + } + } break; case NotifyCollectionChangedAction.Add: From 72ea9f02c792aeca2b224ff5238c2c63e9dd9261 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 15:06:37 +0200 Subject: [PATCH 053/158] More work on virtualization with INCC. Got removes working a bit better. --- .../Presenters/ItemVirtualizerSimple.cs | 169 +++++++++--------- ...emsPresenterTests_Virtualization_Simple.cs | 123 ++++++++----- 2 files changed, 166 insertions(+), 126 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 5f32f5d10c..4d0cf4ac3b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -47,18 +47,14 @@ namespace Avalonia.Controls.Presenters if (delta != 0) { - RecycleMoveContainers(delta); - FirstIndex += delta; - NextIndex += delta; + RecycleContainersForMove(delta); } } else { // We're moving to a partially obscured item at the end of the list. var firstIndex = ItemCount - panel.Children.Count; - RecycleMoveContainers(firstIndex - FirstIndex); - NextIndex = ItemCount; - FirstIndex = NextIndex - panel.Children.Count; + RecycleContainersForMove(firstIndex - FirstIndex); panel.PixelOffset = VirtualizingPanel.PixelOverflow; } } @@ -77,7 +73,7 @@ namespace Avalonia.Controls.Presenters public override void Arranging(Size finalSize) { - CreateRemoveContainers(); + CreateAndRemoveContainers(); ((ILogicalScrollable)Owner).InvalidateScroll(); } @@ -85,73 +81,44 @@ namespace Avalonia.Controls.Presenters { base.ItemsChanged(items, e); - switch (e.Action) + if (items != null) { - case NotifyCollectionChangedAction.Remove: - if(e.OldStartingIndex >= FirstIndex && - e.OldStartingIndex + e.OldItems.Count <= NextIndex) - { - if (e.OldStartingIndex == FirstIndex) + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewStartingIndex >= FirstIndex && + e.NewStartingIndex + e.NewItems.Count <= NextIndex) { - // We are removing the first in the list. - VirtualizingPanel.Children.RemoveAt(e.OldStartingIndex - FirstIndex); - Owner.ItemContainerGenerator.Dematerialize(e.OldStartingIndex - FirstIndex, 1); - FirstIndex++; // This may not be necessary, but cant get to work without this. - - // If all items are visible we need to reduce the NextIndex too. - if(NextIndex > ItemCount) - { - NextIndex = ItemCount; - } - - CreateRemoveContainers(); + CreateAndRemoveContainers(); RecycleContainers(); } - else if (e.OldStartingIndex + e.OldItems.Count == NextIndex) - { - // We are removing the last one in the list. - VirtualizingPanel.Children.RemoveAt(e.OldStartingIndex - FirstIndex); - Owner.ItemContainerGenerator.Dematerialize(e.OldStartingIndex - FirstIndex, 1); - NextIndex--; - } - else - { - // If all items are visible we need to reduce the NextIndex too. - if (NextIndex > ItemCount) - { - NextIndex = ItemCount; - } - CreateRemoveContainers(); - RecycleContainers(); - } - } - break; + break; - case NotifyCollectionChangedAction.Add: - if (e.NewStartingIndex >= FirstIndex && - e.NewStartingIndex + e.NewItems.Count < NextIndex) - { - CreateRemoveContainers(); - RecycleContainers(); - } + case NotifyCollectionChangedAction.Remove: + if (e.OldStartingIndex >= FirstIndex && + e.OldStartingIndex + e.OldItems.Count <= NextIndex) + { + RecycleContainersOnRemove(); + } - break; + break; - case NotifyCollectionChangedAction.Reset: - // We could recycle items here if this proves to be inefficient, but - // Reset indicates a large change and should (?) be quite rare. - VirtualizingPanel.Children.Clear(); - Owner.ItemContainerGenerator.Clear(); - FirstIndex = NextIndex = 0; - CreateRemoveContainers(); - break; + case NotifyCollectionChangedAction.Reset: + RecycleContainersOnRemove(); + break; + } + } + else + { + Owner.ItemContainerGenerator.Clear(); + VirtualizingPanel.Children.Clear(); } ((ILogicalScrollable)Owner).InvalidateScroll(); } - private void CreateRemoveContainers() + private void CreateAndRemoveContainers() { var generator = Owner.ItemContainerGenerator; var panel = VirtualizingPanel; @@ -164,8 +131,12 @@ namespace Avalonia.Controls.Presenters while (!panel.IsFull) { - if (index == ItemCount) + if (index >= ItemCount) { + // We can fit more containers in the panel, but we're at the end of the + // items. If we're scrolled to the top (FirstIndex == 0), then there are + // no more items to create. Otherwise, go backwards adding containers to + // the beginning of the panel. if (FirstIndex == 0) { break; @@ -204,13 +175,7 @@ namespace Avalonia.Controls.Presenters if (panel.OverflowCount > 0) { - var count = panel.OverflowCount; - var index = panel.Children.Count - count; - - panel.Children.RemoveRange(index, count); - generator.Dematerialize(FirstIndex + index, count); - - NextIndex -= count; + RemoveContainers(panel.OverflowCount); } } @@ -224,29 +189,21 @@ namespace Avalonia.Controls.Presenters foreach (var container in containers) { - if (itemIndex < ItemCount) - { - var item = Items.ElementAt(itemIndex); + var item = Items.ElementAt(itemIndex); - if (!object.Equals(container.Item, item)) + if (!object.Equals(container.Item, item)) + { + if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) { - if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } - else - { - panel.Children.RemoveAt(panel.Children.Count - 1); - } ++itemIndex; - } } - private void RecycleMoveContainers(int delta) + private void RecycleContainersForMove(int delta) { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; @@ -283,6 +240,52 @@ namespace Avalonia.Controls.Presenters panel.Children.InsertRange(0, containers); } } + + FirstIndex += delta; + NextIndex += delta; + } + + private void RecycleContainersOnRemove() + { + var panel = VirtualizingPanel; + + if (NextIndex <= ItemCount) + { + // Items have been removed but FirstIndex..NextIndex is still a valid range in the + // items, so just recycle the containers to adapt to the new state. + RecycleContainers(); + } + else + { + // Items have been removed and now the range FirstIndex..NextIndex goes out of + // the item bounds. Try to scroll up and then remove any excess containers. + var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount)); + var delta = newFirstIndex - FirstIndex; + var newNextIndex = NextIndex + delta; + + if (newNextIndex > ItemCount) + { + RemoveContainers(newNextIndex - ItemCount); + } + + if (delta != 0) + { + RecycleContainersForMove(delta); + } + else + { + RecycleContainers(); + } + } + } + + private void RemoveContainers(int count) + { + var index = VirtualizingPanel.Children.Count - count; + + VirtualizingPanel.Children.RemoveRange(index, count); + Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count); + NextIndex -= count; } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 36d43cd6fe..f409865ada 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -1,9 +1,11 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -76,7 +78,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_Add_New_Items_At_Top_When_Control_Is_Scrolled_To_Bottom_And_Enlarged() + public void Should_Add_New_Containers_At_Top_When_Control_Is_Scrolled_To_Bottom_And_Enlarged() { var target = CreateTarget(); var items = (IList)target.Items; @@ -161,107 +163,140 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = CreateTarget(itemCount: 20); target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); ((ILogicalScrollable)target).Offset = new Vector(0, 5); var expected = Enumerable.Range(5, 10).Select(x => $"Item {x}").ToList(); var items = (ObservableCollection)target.Items; + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal( - expected, - target.Panel.Children.Select(x => x.DataContext)); + Assert.Equal(expected, actual); items.Insert(6, "Inserted"); expected.Insert(1, "Inserted"); expected.RemoveAt(expected.Count - 1); - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); Assert.Equal(expected, actual); } [Fact] - public void Removing_First_Item_When_Visible_Should_UpdateContainers() + public void Removing_First_Materialized_Item_Should_Update_Containers() { var target = CreateTarget(itemCount: 20); target.ApplyTemplate(); - target.Measure(new Size(100, 195)); - target.Arrange(new Rect(0, 0, 100, 195)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 5); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); - var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); + var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); var items = (ObservableCollection)target.Items; + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal( - expected, - target.Panel.Children.Select(x => x.DataContext)); + Assert.Equal(expected, actual); - items.Remove(items.First()); - expected.Remove(expected.First()); + items.RemoveAt(0); + expected = Enumerable.Range(1, 10).Select(x => $"Item {x}").ToList(); - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); Assert.Equal(expected, actual); } [Fact] - public void Removing_Items_From_Middle_Should_Update_Containers() + public void Removing_Items_From_Middle_Should_Update_Containers_When_All_Items_Visible() { var target = CreateTarget(itemCount: 20); target.ApplyTemplate(); - target.Measure(new Size(100, 195)); - target.Arrange(new Rect(0, 0, 100, 195)); - - ((ILogicalScrollable)target).Offset = new Vector(0, 5); + target.Measure(new Size(100, 200)); + target.Arrange(new Rect(0, 0, 100, 200)); - var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); var items = (ObservableCollection)target.Items; + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal( - expected, - target.Panel.Children.Select(x => x.DataContext)); + Assert.Equal(items, actual); items.RemoveAt(2); - expected.RemoveAt(2); - var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(items, actual); items.RemoveAt(items.Count - 2); - expected.RemoveAt(expected.Count -2); actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal(expected, actual); + Assert.Equal(items, actual); } [Fact] - public void Removing_Last_Item_When_Visible_Should_UpdateContainers() + public void Removing_Last_Item_Should_Update_Containers_When_All_Items_Visible() { var target = CreateTarget(itemCount: 20); target.ApplyTemplate(); - target.Measure(new Size(100, 195)); - target.Arrange(new Rect(0, 0, 100, 195)); + target.Measure(new Size(100, 200)); + target.Arrange(new Rect(0, 0, 100, 200)); ((ILogicalScrollable)target).Offset = new Vector(0, 5); var expected = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(); var items = (ObservableCollection)target.Items; + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); - Assert.Equal( - expected, - target.Panel.Children.Select(x => x.DataContext)); + Assert.Equal(expected, actual); items.Remove(items.Last()); expected.Remove(expected.Last()); + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + + [Fact] + public void Removing_Items_When_Scrolled_To_End_Should_Add_Containers_At_Top() + { + var target = CreateTarget(itemCount: 20, useAvaloniaList: true); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 10); + + var expected = Enumerable.Range(10, 10).Select(x => $"Item {x}").ToList(); + var items = (AvaloniaList)target.Items; var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + + Assert.Equal(expected, actual); + + items.RemoveRange(18, 2); + expected = Enumerable.Range(8, 10).Select(x => $"Item {x}").ToList(); + + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); Assert.Equal(expected, actual); } + [Fact] + public void Setting_Items_To_Null_Should_Remove_Containers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + + Assert.Equal(expected, actual); + + target.Items = null; + + Assert.Empty(target.Panel.Children); + } + public class WithContainers { [Fact] @@ -342,12 +377,14 @@ namespace Avalonia.Controls.UnitTests.Presenters private static ItemsPresenter CreateTarget( Orientation orientation = Orientation.Vertical, bool useContainers = true, - int itemCount = 20) + int itemCount = 20, + bool useAvaloniaList = false) { ItemsPresenter result; - - var items = new ObservableCollection( - Enumerable.Range(0, itemCount).Select(x => $"Item {x}")); + var itemsSource = Enumerable.Range(0, itemCount).Select(x => $"Item {x}"); + var items = useAvaloniaList ? + (IEnumerable)new AvaloniaList(itemsSource) : + (IEnumerable)new ObservableCollection(itemsSource); var scroller = new ScrollContentPresenter { From b6dc913b20bd28b069cb994ec1189647d99aecfd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 15:50:51 +0200 Subject: [PATCH 054/158] Allow multiple selection in virt test app. --- samples/VirtualizationTest/MainWindow.xaml | 5 ++++- .../ViewModels/MainWindowViewModel.cs | 17 +++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index aa882c052a..94a0fc1b68 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -25,7 +25,10 @@ - + diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 953f533065..98f1735849 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Avalonia.Collections; using ReactiveUI; namespace VirtualizationTest.ViewModels @@ -12,7 +13,6 @@ namespace VirtualizationTest.ViewModels private int _itemCount = 200; private string _newItemString; private IReactiveList _items; - private ItemViewModel _selectedItem; private string _prefix = "Item"; public MainWindowViewModel() @@ -40,11 +40,8 @@ namespace VirtualizationTest.ViewModels set { this.RaiseAndSetIfChanged(ref _itemCount, value); } } - public ItemViewModel SelectedItem - { - get { return _selectedItem; } - set { this.RaiseAndSetIfChanged(ref _selectedItem, value); } - } + public AvaloniaList SelectedItems { get; } + = new AvaloniaList(); public IReactiveList Items { @@ -82,9 +79,9 @@ namespace VirtualizationTest.ViewModels { var index = Items.Count; - if (SelectedItem != null) + if (SelectedItems.Count > 0) { - index = Items.IndexOf(SelectedItem) + 1; + index = Items.IndexOf(SelectedItems[0]) + 1; } Items.Insert(index, new ItemViewModel(index, NewItemString)); @@ -92,9 +89,9 @@ namespace VirtualizationTest.ViewModels private void Remove() { - if (SelectedItem != null) + if (SelectedItems.Count > 0) { - Items.Remove(SelectedItem); + Items.RemoveAll(SelectedItems); } } From 7ca7f5353795081f7c115b659cc5c88972c955c0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 15:51:43 +0200 Subject: [PATCH 055/158] More work on items removed. --- .../Presenters/ItemVirtualizerSimple.cs | 6 ++-- ...emsPresenterTests_Virtualization_Simple.cs | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 4d0cf4ac3b..26b7ccf65f 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -272,10 +272,8 @@ namespace Avalonia.Controls.Presenters { RecycleContainersForMove(delta); } - else - { - RecycleContainers(); - } + + RecycleContainers(); } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index f409865ada..042060b9f6 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -254,7 +254,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Removing_Items_When_Scrolled_To_End_Should_Add_Containers_At_Top() + public void Removing_Items_When_Scrolled_To_End_Should_Recyle_Containers_At_Top() { var target = CreateTarget(itemCount: 20, useAvaloniaList: true); @@ -277,6 +277,32 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(expected, actual); } + [Fact] + public void Removing_Items_When_Scrolled_To_Near_End_Should_Recycle_Containers_At_Bottom_And_Top() + { + var target = CreateTarget(itemCount: 20, useAvaloniaList: true); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + ((ILogicalScrollable)target).Offset = new Vector(0, 9); + + var expected = Enumerable.Range(9, 10).Select(x => $"Item {x}").ToList(); + var items = (AvaloniaList)target.Items; + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + + Assert.Equal(expected, actual); + + items.RemoveRange(15, 3); + expected = Enumerable.Range(7, 8).Select(x => $"Item {x}") + .Concat(Enumerable.Range(18, 2).Select(x => $"Item {x}")) + .ToList(); + + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + [Fact] public void Setting_Items_To_Null_Should_Remove_Containers() { From c718f65e4c6c7b4fd038d724ba7df2cf6543cd64 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 15:57:33 +0200 Subject: [PATCH 056/158] Make inserting items more user friendly. --- .../VirtualizationTest/ViewModels/MainWindowViewModel.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 98f1735849..0e1f56fa07 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -11,7 +11,8 @@ namespace VirtualizationTest.ViewModels internal class MainWindowViewModel : ReactiveObject { private int _itemCount = 200; - private string _newItemString; + private string _newItemString = "New Item"; + private int _newItemIndex; private IReactiveList _items; private string _prefix = "Item"; @@ -81,10 +82,10 @@ namespace VirtualizationTest.ViewModels if (SelectedItems.Count > 0) { - index = Items.IndexOf(SelectedItems[0]) + 1; + index = Items.IndexOf(SelectedItems[0]); } - Items.Insert(index, new ItemViewModel(index, NewItemString)); + Items.Insert(index, new ItemViewModel(_newItemIndex++, NewItemString)); } private void Remove() From f9e730e705faf61eb9881a9f592faca433b35bd3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 15:59:46 +0200 Subject: [PATCH 057/158] Tweaked comments. --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 26b7ccf65f..fc7e36a2a1 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -258,7 +258,8 @@ namespace Avalonia.Controls.Presenters else { // Items have been removed and now the range FirstIndex..NextIndex goes out of - // the item bounds. Try to scroll up and then remove any excess containers. + // the item bounds. Remove any excess containers, try to scroll up and then recycle + // the containers to make sure they point to the correct item. var newFirstIndex = Math.Max(0, FirstIndex - (NextIndex - ItemCount)); var delta = newFirstIndex - FirstIndex; var newNextIndex = NextIndex + delta; From 6e73244a1efb3c5ee4be370cadee79e790dad8af Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 16:48:55 +0200 Subject: [PATCH 058/158] Handle move and replace in virtualized lists. --- .../Presenters/ItemVirtualizerSimple.cs | 5 +++ ...emsPresenterTests_Virtualization_Simple.cs | 45 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index fc7e36a2a1..01b1b24e43 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -104,6 +104,11 @@ namespace Avalonia.Controls.Presenters break; + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Replace: + RecycleContainers(); + break; + case NotifyCollectionChangedAction.Reset: RecycleContainersOnRemove(); break; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 042060b9f6..09abb85ba7 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -303,6 +303,51 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(expected, actual); } + [Fact] + public void Replacing_Items_Should_Update_Containers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + + Assert.Equal(expected, actual); + + items[4] = expected[4] = "Replaced"; + + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + + [Fact] + public void Moving_Items_Should_Update_Containers() + { + var target = CreateTarget(itemCount: 20); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var expected = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToList(); + var items = (ObservableCollection)target.Items; + var actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + + Assert.Equal(expected, actual); + + items.Move(4, 8); + var i = expected[4]; + expected.RemoveAt(4); + expected.Insert(8, i); + + actual = target.Panel.Children.Select(x => x.DataContext).ToList(); + Assert.Equal(expected, actual); + } + [Fact] public void Setting_Items_To_Null_Should_Remove_Containers() { From aeab538162f005fa86cadca5b40029f2923f5cfd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 17:28:49 +0200 Subject: [PATCH 059/158] Fix CarouselPresenter. Forgot to assign to return value! --- src/Avalonia.Controls/Presenters/CarouselPresenter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 2b7442597b..8b408002a1 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -176,6 +176,7 @@ namespace Avalonia.Controls.Presenters var item = Items.Cast().ElementAt(index); var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector); Panel.Children.Add(materialized.ContainerControl); + container = materialized.ContainerControl; } return container; From 20847b19609fa1bfc413e84ae973ea70ed4fc7e8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 18:05:09 +0200 Subject: [PATCH 060/158] Added documentation for item virtualizers. --- .../Presenters/ItemVirtualizer.cs | 86 ++++++++++++++++++- .../Presenters/ItemVirtualizerNone.cs | 24 +++++- .../Presenters/ItemVirtualizerSimple.cs | 47 ++++++++++ 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index a7d1de5777..dd7cf3bf1d 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -10,29 +10,88 @@ using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters { + /// + /// Base class for classes which handle virtualization for an . + /// internal abstract class ItemVirtualizer { + /// + /// Initializes a new instance of the class. + /// + /// public ItemVirtualizer(ItemsPresenter owner) { Owner = owner; } + /// + /// Gets the which owns the virtualizer. + /// public ItemsPresenter Owner { get; } + + /// + /// Gets the which will host the items. + /// public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel; + + /// + /// Gets the items to display. + /// public IEnumerable Items { get; private set; } + + /// + /// Gets the number of items in . + /// public int ItemCount { get; private set; } - public int FirstIndex { get; set; } - public int NextIndex { get; set; } + + /// + /// Gets or sets the index of the first item displayed in the panel. + /// + public int FirstIndex { get; protected set; } + + /// + /// Gets or sets the index of the first item beyond those displayed in the panel. + /// + public int NextIndex { get; protected set; } + + /// + /// Gets a value indicating whether the items should be scroll horizontally or vertically. + /// public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical; + /// + /// Gets a value indicating whether logical scrolling is enabled. + /// public abstract bool IsLogicalScrollEnabled { get; } + + /// + /// Gets the value of the scroll extent. + /// public abstract double ExtentValue { get; } + + /// + /// Gets or sets the value of the current scroll offset. + /// public abstract double OffsetValue { get; set; } + + /// + /// Gets the value of the scrollable viewport. + /// public abstract double ViewportValue { get; } + /// + /// Gets the as a . + /// public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0); + + /// + /// Gets the as a . + /// public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0); + /// + /// Gets or sets the as a . + /// public Vector Offset { get @@ -46,6 +105,12 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Creates an based on an item presenter's + /// . + /// + /// The items presenter. + /// An . public static ItemVirtualizer Create(ItemsPresenter owner) { var virtualizingPanel = owner.Panel as IVirtualizingPanel; @@ -63,13 +128,30 @@ namespace Avalonia.Controls.Presenters return new ItemVirtualizerNone(owner); } + /// + /// Called by the when it carries out an arrange. + /// + /// The final size passed to the arrange. public abstract void Arranging(Size finalSize); + /// + /// Called when a request is made to bring an item into view. + /// + /// The item to bring into view. + /// The rect on the item to bring into view. + /// True if the request was handled; otherwise false. public virtual bool BringIntoView(IVisual target, Rect targetRect) { return false; } + /// + /// Called when the items for the presenter change, either because + /// has been set, the items collection has been + /// modified, or the panel has been created. + /// + /// The items. + /// A description of the change. public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { Items = items; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index a8c24fcf72..303c7442c2 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -10,6 +10,10 @@ using Avalonia.Controls.Utils; namespace Avalonia.Controls.Presenters { + /// + /// Represents an item virtualizer for an that doesn't actually + /// virtualize items - it just creates a container for every item. + /// internal class ItemVirtualizerNone : ItemVirtualizer { public ItemVirtualizerNone(ItemsPresenter owner) @@ -17,29 +21,44 @@ namespace Avalonia.Controls.Presenters { } + /// public override bool IsLogicalScrollEnabled => false; + /// + /// This property should never be accessed because is + /// false. + /// public override double ExtentValue { get { throw new NotSupportedException(); } } + /// + /// This property should never be accessed because is + /// false. + /// public override double OffsetValue { get { throw new NotSupportedException(); } set { throw new NotSupportedException(); } } + /// + /// This property should never be accessed because is + /// false. + /// public override double ViewportValue { get { throw new NotSupportedException(); } } + /// public override void Arranging(Size finalSize) { // We don't need to do anything here. } + /// public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { base.ItemsChanged(items, e); @@ -47,7 +66,6 @@ namespace Avalonia.Controls.Presenters var generator = Owner.ItemContainerGenerator; var panel = Owner.Panel; - // TODO: Handle Move and Replace etc. switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -77,7 +95,9 @@ namespace Avalonia.Controls.Presenters break; case NotifyCollectionChangedAction.Move: - // TODO: Implement Move in a more efficient manner. + // TODO: Handle move in a more efficient manner. At the moment we just + // drop through to Reset to recreate all the containers. + case NotifyCollectionChangedAction.Reset: RemoveContainers(generator.Clear()); diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 01b1b24e43..69141fdbea 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -10,17 +10,28 @@ using Avalonia.Controls.Utils; namespace Avalonia.Controls.Presenters { + /// + /// Handles virtualization in an for + /// . + /// internal class ItemVirtualizerSimple : ItemVirtualizer { + /// + /// Initializes a new instance of the class. + /// + /// public ItemVirtualizerSimple(ItemsPresenter owner) : base(owner) { } + /// public override bool IsLogicalScrollEnabled => true; + /// public override double ExtentValue => ItemCount; + /// public override double OffsetValue { get @@ -61,6 +72,7 @@ namespace Avalonia.Controls.Presenters } } + /// public override double ViewportValue { get @@ -71,12 +83,14 @@ namespace Avalonia.Controls.Presenters } } + /// public override void Arranging(Size finalSize) { CreateAndRemoveContainers(); ((ILogicalScrollable)Owner).InvalidateScroll(); } + /// public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { base.ItemsChanged(items, e); @@ -123,6 +137,10 @@ namespace Avalonia.Controls.Presenters ((ILogicalScrollable)Owner).InvalidateScroll(); } + /// + /// Creates and removes containers such that we have at most enough containers to fill + /// the panel. + /// private void CreateAndRemoveContainers() { var generator = Owner.ItemContainerGenerator; @@ -184,6 +202,14 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Updates the containers in the panel to make sure they are displaying the correct item + /// based on . + /// + /// + /// This method requires that + the number of + /// materialized containers is not more than . + /// private void RecycleContainers() { var panel = VirtualizingPanel; @@ -208,6 +234,19 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Recycles containers when a move occurs. + /// + /// The delta of the move. + /// + /// If the move is less than a page, then this method moves the containers for the items + /// that are still visible to the correct place, and recyles and moves the others. For + /// example: if there are 20 items and 10 containers visible and the user scrolls 5 + /// items down, then the bottom 5 containers will be moved to the top and the top 5 will + /// be moved to the bottom and recycled to display the newly visible item. Updates + /// and + /// with their new values. + /// private void RecycleContainersForMove(int delta) { var panel = VirtualizingPanel; @@ -250,6 +289,9 @@ namespace Avalonia.Controls.Presenters NextIndex += delta; } + /// + /// Recycles containers due to items being removed. + /// private void RecycleContainersOnRemove() { var panel = VirtualizingPanel; @@ -283,6 +325,11 @@ namespace Avalonia.Controls.Presenters } } + /// + /// Removes the specified number of containers from the end of the panel and updates + /// . + /// + /// The number of containers to remove. private void RemoveContainers(int count) { var index = VirtualizingPanel.Children.Count - count; From e686786959cef3c91d8150df8c495af3f37d8b63 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Jun 2016 20:13:08 +0200 Subject: [PATCH 061/158] Use dictionary to track item containers. Because even when virtualized we were still creating a list the size of the Items collection to store the containers. Using Dictionary here still isn't ideal - we'd ideally use some sort of sparse array but that can be optimized later. --- .../Generators/ItemContainerGenerator.cs | 111 ++++++++---------- .../Generators/ItemContainerInfo.cs | 4 +- .../Generators/ItemContainerGeneratorTests.cs | 49 +++++--- .../Presenters/CarouselPresenterTests.cs | 4 +- 4 files changed, 87 insertions(+), 81 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index bbbe8528b4..fd730c5293 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -16,7 +16,7 @@ namespace Avalonia.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private List _containers = new List(); + private Dictionary _containers = new Dictionary(); /// /// Initializes a new instance of the class. @@ -30,7 +30,7 @@ namespace Avalonia.Controls.Generators } /// - public IEnumerable Containers => _containers.Where(x => x != null); + public IEnumerable Containers => _containers.Values; /// public event EventHandler Materialized; @@ -60,7 +60,7 @@ namespace Avalonia.Controls.Generators var i = selector != null ? selector.Select(item) : item; var container = new ItemContainerInfo(CreateContainer(i), item, index); - AddContainer(container); + _containers.Add(container.Index, container); Materialized?.Invoke(this, new ItemContainerEventArgs(container)); return container; @@ -73,11 +73,8 @@ namespace Avalonia.Controls.Generators for (int i = startingIndex; i < startingIndex + count; ++i) { - if (i < _containers.Count &&_containers[i] != null) - { - result.Add(_containers[i]); - _containers[i] = null; - } + result.Add(_containers[i]); + _containers.Remove(i); } Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); @@ -88,18 +85,47 @@ namespace Avalonia.Controls.Generators /// public virtual void InsertSpace(int index, int count) { - _containers.InsertRange(index, Enumerable.Repeat(null, count)); + if (count > 0) + { + var toMove = _containers.Where(x => x.Key >= index).ToList(); + + foreach (var i in toMove) + { + _containers.Remove(i.Key); + i.Value.Index += count; + _containers[i.Value.Index] = i.Value; + } + } } /// public virtual IEnumerable RemoveRange(int startingIndex, int count) { - List result = new List(); + var result = new List(); - if (startingIndex < _containers.Count) + if (count > 0) { - result.AddRange(_containers.GetRange(startingIndex, count)); - _containers.RemoveRange(startingIndex, count); + for (var i = startingIndex; i < startingIndex + count; ++i) + { + ItemContainerInfo found; + + if (_containers.TryGetValue(i, out found)) + { + result.Add(found); + } + + _containers.Remove(i); + } + + var toMove = _containers.Where(x => x.Key >= startingIndex).ToList(); + + foreach (var i in toMove) + { + _containers.Remove(i.Key); + i.Value.Index -= count; + _containers.Add(i.Value.Index, i.Value); + } + Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); } @@ -119,8 +145,8 @@ namespace Avalonia.Controls.Generators /// public virtual IEnumerable Clear() { - var result = _containers.Where(x => x != null).ToList(); - _containers = new List(); + var result = Containers.ToList(); + _containers.Clear(); if (result.Count > 0) { @@ -133,27 +159,20 @@ namespace Avalonia.Controls.Generators /// public IControl ContainerFromIndex(int index) { - if (index < _containers.Count) - { - return _containers[index]?.ContainerControl; - } - - return null; + ItemContainerInfo result; + _containers.TryGetValue(index, out result); + return result?.ContainerControl; } /// public int IndexFromContainer(IControl container) { - var index = 0; - foreach (var i in _containers) { - if (i?.ContainerControl == container) + if (i.Value.ContainerControl == container) { - return index; + return i.Key; } - - ++index; } return -1; @@ -176,33 +195,6 @@ namespace Avalonia.Controls.Generators return result; } - /// - /// Adds a container to the index. - /// - /// The container. - protected void AddContainer(ItemContainerInfo container) - { - Contract.Requires(container != null); - - while (_containers.Count < container.Index) - { - _containers.Add(null); - } - - if (_containers.Count == container.Index) - { - _containers.Add(container); - } - else if (_containers[container.Index] == null) - { - _containers[container.Index] = container; - } - else - { - throw new InvalidOperationException("Container already created."); - } - } - /// /// Moves a container. /// @@ -213,10 +205,11 @@ namespace Avalonia.Controls.Generators protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item) { var container = _containers[oldIndex]; - var newContainer = new ItemContainerInfo(container.ContainerControl, item, newIndex); - _containers[oldIndex] = null; - AddContainer(newContainer); - return newContainer; + container.Index = newIndex; + container.Item = item; + _containers.Remove(oldIndex); + _containers.Add(newIndex, container); + return container; } /// @@ -227,7 +220,7 @@ namespace Avalonia.Controls.Generators /// The containers. protected IEnumerable GetContainerRange(int index, int count) { - return _containers.GetRange(index, count); + return _containers.Where(x => x.Key >= index && x.Key <= index + count).Select(x => x.Value); } /// diff --git a/src/Avalonia.Controls/Generators/ItemContainerInfo.cs b/src/Avalonia.Controls/Generators/ItemContainerInfo.cs index b0fcd2867e..b9387f1022 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerInfo.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerInfo.cs @@ -35,11 +35,11 @@ namespace Avalonia.Controls.Generators /// /// Gets the item that the container represents. /// - public object Item { get; } + public object Item { get; internal set; } /// /// Gets the index of the item in the collection. /// - public int Index { get; } + public int Index { get; internal set; } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index ff35d90237..a111515848 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -1,11 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // 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.Generators; -using Avalonia.Controls.Templates; using Xunit; namespace Avalonia.Controls.UnitTests.Generators @@ -41,22 +39,6 @@ namespace Avalonia.Controls.UnitTests.Generators Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); } - private IList Materialize( - IItemContainerGenerator generator, - int index, - string[] items) - { - var result = new List(); - - foreach (var item in items) - { - var container = generator.Materialize(index++, item, null); - result.Add(container); - } - - return result; - } - [Fact] public void IndexFromContainer_Should_Return_Index() { @@ -98,6 +80,20 @@ namespace Avalonia.Controls.UnitTests.Generators Assert.Equal(expected, result); } + [Fact] + public void InsertSpace_Should_Alter_Successive_Container_Indexes() + { + var items = new[] { "foo", "bar", "baz" }; + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var containers = Materialize(target, 0, items); + + target.InsertSpace(1, 3); + + Assert.Equal(3, target.Containers.Count()); + Assert.Equal(new[] { 0, 4, 5 }, target.Containers.Select(x => x.Index)); + } + [Fact] public void RemoveRange_Should_Alter_Successive_Container_Indexes() { @@ -111,6 +107,23 @@ namespace Avalonia.Controls.UnitTests.Generators Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(1)); Assert.Equal(containers[1], removed); + Assert.Equal(new[] { 0, 1 }, target.Containers.Select(x => x.Index)); + } + + private IList Materialize( + IItemContainerGenerator generator, + int index, + string[] items) + { + var result = new List(); + + foreach (var item in items) + { + var container = generator.Materialize(index++, item, null); + result.Add(container); + } + + return result; } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs index 33bbf83140..cdc2fa65a5 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs @@ -155,7 +155,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Should_have_correct_index_itemscontainer() + public void Should_Have_Correct_ItemsContainer_Index() { ObservableCollection items = new ObservableCollection(); @@ -186,7 +186,7 @@ namespace Avalonia.Controls.UnitTests.Presenters items.Remove(items[0]); Assert.Equal(1, target.ItemContainerGenerator.Containers.Count()); Assert.Equal(1, target.Panel.Children.Count); - Assert.Equal(1, target.ItemContainerGenerator.Containers.First().Index); + Assert.Equal(0, target.ItemContainerGenerator.Containers.First().Index); items.Remove(items[0]); Assert.Equal(0, target.ItemContainerGenerator.Containers.Count()); From fbb7ea759b6f9670a9a9911a0fc468bcc95b0410 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Jun 2016 19:51:39 +0200 Subject: [PATCH 062/158] Make ItemContainerGenerator create containers Make the untyped ItemContainerGenerator create `ContentPresenter` containers when the data isn't a control. This is required for correct binding to `DataContext` in a data template. --- .../Generators/ItemContainerGenerator.cs | 14 +++--- .../Avalonia.Controls.UnitTests.csproj | 1 - .../CarouselTests.cs | 4 +- .../Generators/ItemContainerGeneratorTests.cs | 5 ++- .../ItemsControlTests.cs | 27 ++++++----- .../Presenters/CarouselPresenterTests.cs | 8 ++-- .../Presenters/ItemsPresenterTests.cs | 45 ++++++++++--------- .../TabControlTests.cs | 16 ++++--- 8 files changed, 68 insertions(+), 52 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index fd730c5293..45f7dff49c 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -2,12 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Reactive.Subjects; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; -using Avalonia.Controls.Utils; namespace Avalonia.Controls.Generators { @@ -185,11 +183,15 @@ namespace Avalonia.Controls.Generators /// The created container control. protected virtual IControl CreateContainer(object item) { - var result = Owner.MaterializeDataTemplate(item, ItemTemplate); + var result = item as IControl; - if (result != null && !(item is IControl)) + if (result == null) { - result.DataContext = item; + result = new ContentPresenter + { + ContentTemplate = ItemTemplate, + Content = item, + }; } return result; diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 0581568878..ec0b19308c 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -102,7 +102,6 @@ - diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 554aab68c3..0e3c6f5953 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -50,8 +50,8 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, target.GetLogicalChildren().Count()); var child = target.GetLogicalChildren().Single(); - Assert.IsType(child); - Assert.Equal("Foo", ((TextBlock)child).Text); + Assert.IsType(child); + Assert.Equal("Foo", ((ContentPresenter)child).Content); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index a111515848..5d2269e73c 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Xunit; namespace Avalonia.Controls.UnitTests.Generators @@ -19,8 +20,8 @@ namespace Avalonia.Controls.UnitTests.Generators var containers = Materialize(target, 0, items); var result = containers .Select(x => x.ContainerControl) - .OfType() - .Select(x => x.Text) + .OfType() + .Select(x => x.Content) .ToList(); Assert.Equal(items, result); diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 92c89039f5..ce2dc4ab6c 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -27,7 +27,10 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - Assert.IsType(target.Presenter.Panel.Children[0]); + var container = (ContentPresenter)target.Presenter.Panel.Children[0]; + container.UpdateChild(); + + Assert.IsType(container.Child); } [Fact] @@ -44,7 +47,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Item_Should_Have_TemplatedParent_Set_To_Null() + public void Container_Should_Have_TemplatedParent_Set_To_Null() { var target = new ItemsControl(); @@ -53,9 +56,9 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = (TextBlock)target.Presenter.Panel.GetVisualChildren().First(); + var container = (ContentPresenter)target.Presenter.Panel.Children[0]; - Assert.Null(item.TemplatedParent); + Assert.Null(container.TemplatedParent); } [Fact] @@ -135,7 +138,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Adding_String_Item_Should_Make_TextBlock_Appear_In_LogicalChildren() + public void Adding_String_Item_Should_Make_ContentPresenter_Appear_In_LogicalChildren() { var target = new ItemsControl(); var child = new Control(); @@ -147,7 +150,7 @@ namespace Avalonia.Controls.UnitTests var logical = (ILogical)target; Assert.Equal(1, logical.LogicalChildren.Count); - Assert.IsType(logical.LogicalChildren[0]); + Assert.IsType(logical.LogicalChildren[0]); } [Fact] @@ -390,8 +393,8 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); var text = target.Presenter.Panel.Children - .Cast() - .Select(x => x.Text) + .Cast() + .Select(x => x.Content) .ToList(); Assert.Equal(new[] { "Foo", "Bar" }, text); @@ -419,7 +422,7 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void DataTemplate_Created_Item_Should_Be_NameScope() + public void DataTemplate_Created_Content_Should_Be_NameScope() { var items = new object[] { @@ -435,8 +438,10 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; - Assert.NotNull(NameScope.GetNameScope((TextBlock)item)); + var container = (ContentPresenter)target.Presenter.Panel.LogicalChildren[0]; + container.UpdateChild(); + + Assert.NotNull(NameScope.GetNameScope((TextBlock)container.Child)); } private class Item diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs index cdc2fa65a5..260c070406 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs @@ -60,8 +60,8 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.IsType(target.Panel.Children[0]); - Assert.Equal("foo", ((TextBlock)target.Panel.Children[0]).Text); + Assert.IsType(target.Panel.Children[0]); + Assert.Equal("foo", ((ContentPresenter)target.Panel.Children[0]).Content); } [Fact] @@ -76,8 +76,8 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); target.SelectedIndex = 1; - Assert.IsType(target.Panel.Children[0]); - Assert.Equal("bar", ((TextBlock)target.Panel.Children[0]).Text); + Assert.IsType(target.Panel.Children[0]); + Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[0]).Content); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 519b9b3d1d..9f76767ec1 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -38,10 +38,10 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); Assert.Equal(2, target.Panel.Children.Count); - Assert.IsType(target.Panel.Children[0]); - Assert.IsType(target.Panel.Children[1]); - Assert.Equal("foo", ((TextBlock)target.Panel.Children[0]).Text); - Assert.Equal("bar", ((TextBlock)target.Panel.Children[1]).Text); + Assert.IsType(target.Panel.Children[0]); + Assert.IsType(target.Panel.Children[1]); + Assert.Equal("foo", ((ContentPresenter)target.Panel.Children[0]).Content); + Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[1]).Content); } [Fact] @@ -88,8 +88,8 @@ namespace Avalonia.Controls.UnitTests.Presenters items.RemoveAt(0); Assert.Equal(1, target.Panel.Children.Count); - Assert.Equal("bar", ((TextBlock)target.Panel.Children[0]).Text); - Assert.Equal("bar", ((TextBlock)target.ItemContainerGenerator.ContainerFromIndex(0)).Text); + Assert.Equal("bar", ((ContentPresenter)target.Panel.Children[0]).Content); + Assert.Equal("bar", ((ContentPresenter)target.ItemContainerGenerator.ContainerFromIndex(0)).Content); } [Fact] @@ -121,8 +121,8 @@ namespace Avalonia.Controls.UnitTests.Presenters items[1] = "baz"; var text = target.Panel.Children - .OfType() - .Select(x => x.Text) + .OfType() + .Select(x => x.Content) .ToList(); Assert.Equal(new[] { "foo", "baz", "baz" }, text); @@ -141,8 +141,8 @@ namespace Avalonia.Controls.UnitTests.Presenters items.Move(2, 1); var text = target.Panel.Children - .OfType() - .Select(x => x.Text) + .OfType() + .Select(x => x.Content) .ToList(); Assert.Equal(new[] { "foo", "baz", "bar" }, text); @@ -161,8 +161,8 @@ namespace Avalonia.Controls.UnitTests.Presenters items.Insert(2, "insert"); var text = target.Panel.Children - .OfType() - .Select(x => x.Text) + .OfType() + .Select(x => x.Content) .ToList(); Assert.Equal(new[] { "foo", "bar", "insert", "baz" }, text); @@ -195,16 +195,16 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); - var text = target.Panel.Children.Cast().Select(x => x.Text).ToList(); + var text = target.Panel.Children.Cast().Select(x => x.Content).ToList(); - Assert.Equal(new[] { "foo", "bar" }, text); + Assert.Equal(new[] { "foo", null, "bar" }, text); Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0)); - Assert.Null(target.ItemContainerGenerator.ContainerFromIndex(1)); + Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(1)); Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(2)); items.RemoveAt(1); - text = target.Panel.Children.Cast().Select(x => x.Text).ToList(); + text = target.Panel.Children.Cast().Select(x => x.Content).ToList(); Assert.Equal(new[] { "foo", "bar" }, text); Assert.NotNull(target.ItemContainerGenerator.ContainerFromIndex(0)); @@ -224,8 +224,11 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); items.RemoveAt(2); - var text = target.Panel.Children.OfType().Select(x => x.Text); - Assert.Equal(new[] { "1", "2" }, text); + var numbers = target.Panel.Children + .OfType() + .Select(x => x.Content) + .Cast(); + Assert.Equal(new[] { 1, 2 }, numbers); } [Fact] @@ -288,8 +291,8 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); var text = target.Panel.Children - .Cast() - .Select(x => x.Text) + .Cast() + .Select(x => x.Content) .ToList(); Assert.Equal(new[] { "Foo", "Bar" }, text); @@ -308,7 +311,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.ApplyTemplate(); var dataContexts = target.Panel.Children - .Cast() + .Cast() .Select(x => x.DataContext) .ToList(); diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 7840b55aae..2bbd08bf42 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -184,23 +184,29 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); var carousel = (Carousel)target.Pages; - var dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; + var container = (ContentPresenter)carousel.Presenter.Panel.Children.Single(); + container.UpdateChild(); + var dataContext = ((TextBlock)container.Child).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - dataContext = ((Button)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; + container = (ContentPresenter)carousel.Presenter.Panel.Children.Single(); + container.UpdateChild(); + dataContext = ((Button)container.Child).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; + dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; + container = (ContentPresenter)carousel.Presenter.Panel.Children[0]; + container.UpdateChild(); + dataContext = ((TextBlock)container.Child).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; + dataContext = ((TextBlock)carousel.Presenter.Panel.Children.Single()).DataContext; Assert.Equal("Base", dataContext); } From 89ca4857f0957315fe1c114ec5cf108e4e5858d8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Jun 2016 20:05:02 +0200 Subject: [PATCH 063/158] Tweaked a couple of tests. --- .../Presenters/ContentPresenterTests.cs | 2 +- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index 2639220023..d3d9101e07 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -152,7 +152,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal("foo", target.DataContext); } - private class TestContentControl : TemplatedControl + private class TestContentControl : ContentControl { public IControl Child { get; set; } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 7bfd3c0b59..f79d12cfde 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -77,6 +77,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var canvas = (Canvas)target.Presenter.Child; Assert.Same(viewModel, target.DataContext); + Assert.Same(viewModel.Child, target.Presenter.DataContext); Assert.Same(viewModel.Child.Child, canvas.DataContext); } } From 35c4835ae420de9985aee16e9ba26510b6213356 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Jun 2016 21:35:21 +0200 Subject: [PATCH 064/158] Recycle DataTemplates. If a `ContentPresenter`s content is assigned a value which matches the current `DataTemplate` then just change the `ContentPresenter.DataContext` and the existing item will update itself. --- src/Avalonia.Base/AvaloniaObject.cs | 4 +- .../Avalonia.Controls.csproj | 1 + src/Avalonia.Controls/Control.cs | 11 ++- .../ISetInheritanceParent.cs | 22 ++++++ .../Presenters/ContentPresenter.cs | 70 +++++++++++++++---- .../Templates/DataTemplateExtensions.cs | 48 ------------- .../Templates/FuncDataTemplate.cs | 20 +++++- .../Templates/DataTemplate.cs | 7 +- .../ControlTests.cs | 2 +- .../Presenters/ContentPresenterTests.cs | 24 +++++++ 10 files changed, 134 insertions(+), 75 deletions(-) create mode 100644 src/Avalonia.Controls/ISetInheritanceParent.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index dd047db279..b0db89f7ea 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -26,7 +26,7 @@ namespace Avalonia /// /// The parent object that inherited values are inherited from. /// - private AvaloniaObject _inheritanceParent; + private IAvaloniaObject _inheritanceParent; /// /// The set values/bindings on this object. @@ -120,7 +120,7 @@ namespace Avalonia /// /// The inheritance parent. /// - protected AvaloniaObject InheritanceParent + protected IAvaloniaObject InheritanceParent { get { diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 80d29b14ab..22d17bf633 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -58,6 +58,7 @@ + diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index e246635906..0bede31fb4 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls /// - Implements to allow styling to work on the control. /// - Implements to form part of a logical tree. /// - public class Control : InputElement, IControl, INamed, ISetLogicalParent, ISupportInitialize + public class Control : InputElement, IControl, INamed, ISetInheritanceParent, ISetLogicalParent, ISupportInitialize { /// /// Defines the property. @@ -455,6 +455,15 @@ namespace Avalonia.Controls } } + /// + /// Sets the control's inheritance parent. + /// + /// The parent. + void ISetInheritanceParent.SetParent(IAvaloniaObject parent) + { + InheritanceParent = parent; + } + /// /// Adds a pseudo-class to be set when a property is true. /// diff --git a/src/Avalonia.Controls/ISetInheritanceParent.cs b/src/Avalonia.Controls/ISetInheritanceParent.cs new file mode 100644 index 0000000000..788ab77246 --- /dev/null +++ b/src/Avalonia.Controls/ISetInheritanceParent.cs @@ -0,0 +1,22 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Controls +{ + /// + /// Defines an interface through which a 's inheritance parent can be set. + /// + /// + /// You should not usually need to use this interface - it is for advanced scenarios only. + /// Additionally, also sets the inheritance parent; this + /// interface is only needed where the logical and inheritance parents differ. + /// + public interface ISetInheritanceParent + { + /// + /// Sets the control's inheritance parent. + /// + /// The parent. + void SetParent(IAvaloniaObject parent); + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 3470a43c1c..87435319c7 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -80,6 +80,7 @@ namespace Avalonia.Controls.Presenters private IControl _child; private bool _createdChild; + private IDataTemplate _dataTemplate; /// /// Initializes static members of the class. @@ -200,6 +201,13 @@ namespace Avalonia.Controls.Presenters } } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _dataTemplate = null; + } + /// /// Updates the control based on the control's . /// @@ -215,32 +223,64 @@ namespace Avalonia.Controls.Presenters { var old = Child; var content = Content; - var result = this.MaterializeDataTemplate(content, ContentTemplate); + var result = content as IControl; - if (old != null) + if (result == null) { - VisualChildren.Remove(old); + DataContext = content; + + if (content != null) + { + if (old != null && _dataTemplate?.Match(content) == true) + { + result = old; + } + else + { + _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + result = _dataTemplate.Build(content); + + var controlResult = result as Control; + + if (controlResult != null) + { + NameScope.SetNameScope(controlResult, new NameScope()); + } + } + } + else + { + _dataTemplate = null; + } + } + else + { + _dataTemplate = null; } - if (result != null) + if (result != old) { - if (!(content is IControl)) + if (old != null) { - result.DataContext = content; + VisualChildren.Remove(old); } - Child = result; + if (result != null) + { + Child = result; - if (result.Parent == null) + if (result.Parent == null) + { + ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); + } + + ((ISetInheritanceParent)result).SetParent(this); + VisualChildren.Add(result); + } + else { - ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); + Child = null; } - - VisualChildren.Add(result); - } - else - { - Child = null; } _createdChild = true; diff --git a/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs b/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs index 9eff4243b1..df25733524 100644 --- a/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs +++ b/src/Avalonia.Controls/Templates/DataTemplateExtensions.cs @@ -11,54 +11,6 @@ namespace Avalonia.Controls.Templates /// public static class DataTemplateExtensions { - /// - /// Materializes a piece of data based on a data template. - /// - /// The control materializing the data template. - /// The data. - /// - /// An optional primary template that can will be tried before the - /// in the tree are searched. - /// - /// The data materialized as a control. - public static IControl MaterializeDataTemplate( - this IControl control, - object data, - IDataTemplate primary = null) - { - if (data == null) - { - return null; - } - else - { - var asControl = data as IControl; - - if (asControl != null) - { - return asControl; - } - else - { - IDataTemplate template = control.FindDataTemplate(data, primary); - IControl result; - - if (template != null) - { - result = template.Build(data); - } - else - { - result = FuncDataTemplate.Default.Build(data); - } - - NameScope.SetNameScope((Control)result, new NameScope()); - - return result; - } - } - } - /// /// Find a data template that matches a piece of data. /// diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index 8ce3f69f36..bdf0732492 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Linq; using System.Reflection; namespace Avalonia.Controls.Templates @@ -12,10 +13,25 @@ namespace Avalonia.Controls.Templates public class FuncDataTemplate : FuncTemplate, IDataTemplate { /// - /// The default data template used in the case where not matching data template is found. + /// The default data template used in the case where no matching data template is found. /// public static readonly FuncDataTemplate Default = - new FuncDataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null); + new FuncDataTemplate( + data => + { + if (data != null) + { + var result = new TextBlock(); + result.Bind( + TextBlock.TextProperty, + result.GetObservable(Control.DataContextProperty).Select(x => x?.ToString())); + return result; + } + else + { + return null; + } + }); /// /// The implementation of the method. diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 2886b63c55..d90dbfda7f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -28,11 +28,6 @@ namespace Avalonia.Markup.Xaml.Templates } } - public IControl Build(object data) - { - var visualTreeForItem = Content.Load(); - visualTreeForItem.DataContext = data; - return visualTreeForItem; - } + public IControl Build(object data) => Content.Load(); } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests.cs b/tests/Avalonia.Controls.UnitTests/ControlTests.cs index 4871f84e82..038641b8b3 100644 --- a/tests/Avalonia.Controls.UnitTests/ControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ControlTests.cs @@ -262,7 +262,7 @@ namespace Avalonia.Controls.UnitTests private class TestControl : Control { - public new AvaloniaObject InheritanceParent => base.InheritanceParent; + public new IAvaloniaObject InheritanceParent => base.InheritanceParent; } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index d3d9101e07..198c19f60f 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -9,6 +9,7 @@ using Avalonia.Controls.Templates; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; +using System; namespace Avalonia.Controls.UnitTests.Presenters { @@ -152,6 +153,29 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal("foo", target.DataContext); } + [Fact] + public void Tries_To_Recycle_DataTemplate() + { + var target = new ContentPresenter + { + DataTemplates = new DataTemplates + { + new FuncDataTemplate(_ => new Border()), + }, + Content = "foo", + }; + + target.UpdateChild(); + var control = target.Child; + + Assert.IsType(control); + + target.Content = "bar"; + target.UpdateChild(); + + Assert.Same(control, target.Child); + } + private class TestContentControl : ContentControl { public IControl Child { get; set; } From d717b461126fbe3e678e576977abbe1e6a96dd15 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2016 11:14:21 +0200 Subject: [PATCH 065/158] Fixed DevTools. - Set `InheritanceParent` before Parent so that control doesn't initially inherit the wrong DataContext - Don't set `InheritanceParent` when setting `Parent` if `InheritanceParent` already set - Added `IDataTemplate.SupportsRecycling` --- src/Avalonia.Controls/Control.cs | 6 +++- .../Presenters/ContentPresenter.cs | 8 +++-- .../Templates/FuncDataTemplate.cs | 21 +++++++++--- .../Templates/FuncDataTemplate`1.cs | 13 +++++--- .../Templates/IDataTemplate.cs | 6 ++++ src/Avalonia.Diagnostics/ViewLocator.cs | 2 ++ .../Templates/DataTemplate.cs | 2 ++ .../Templates/TreeDataTemplate.cs | 2 ++ .../ControlTests.cs | 33 +++++++++++++++++-- .../Presenters/ContentPresenterTests.cs | 2 +- .../TreeViewTests.cs | 2 ++ 11 files changed, 83 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 0bede31fb4..944f6b82ac 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -435,7 +435,11 @@ namespace Avalonia.Controls OnDetachedFromLogicalTreeCore(e); } - InheritanceParent = parent as AvaloniaObject; + if (InheritanceParent == null || parent == null) + { + InheritanceParent = parent as AvaloniaObject; + } + _parent = (IControl)parent; if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 87435319c7..81ed48f5b1 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -231,7 +231,10 @@ namespace Avalonia.Controls.Presenters if (content != null) { - if (old != null && _dataTemplate?.Match(content) == true) + if (old != null && + _dataTemplate != null && + _dataTemplate.SupportsRecycling && + _dataTemplate.Match(content)) { result = old; } @@ -267,6 +270,8 @@ namespace Avalonia.Controls.Presenters if (result != null) { + ((ISetInheritanceParent)result).SetParent(this); + Child = result; if (result.Parent == null) @@ -274,7 +279,6 @@ namespace Avalonia.Controls.Presenters ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); } - ((ISetInheritanceParent)result).SetParent(this); VisualChildren.Add(result); } else diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index bdf0732492..1d90fcd01e 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -31,7 +31,8 @@ namespace Avalonia.Controls.Templates { return null; } - }); + }, + true); /// /// The implementation of the method. @@ -45,8 +46,12 @@ namespace Avalonia.Controls.Templates /// /// A function which when passed an object of returns a control. /// - public FuncDataTemplate(Type type, Func build) - : this(o => IsInstance(o, type), build) + /// Whether the control can be recycled. + public FuncDataTemplate( + Type type, + Func build, + bool supportsRecycling = false) + : this(o => IsInstance(o, type), build, supportsRecycling) { } @@ -59,14 +64,22 @@ namespace Avalonia.Controls.Templates /// /// A function which returns a control for matching data. /// - public FuncDataTemplate(Func match, Func build) + /// Whether the control can be recycled. + public FuncDataTemplate( + Func match, + Func build, + bool supportsRecycling = false) : base(build) { Contract.Requires(match != null); _match = match; + SupportsRecycling = supportsRecycling; } + /// + public bool SupportsRecycling { get; } + /// /// Checks to see if this data template matches the specified data. /// diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index cb2d064605..7154c0e558 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -17,8 +17,9 @@ namespace Avalonia.Controls.Templates /// /// A function which when passed an object of returns a control. /// - public FuncDataTemplate(Func build) - : base(typeof(T), CastBuild(build)) + /// Whether the control can be recycled. + public FuncDataTemplate(Func build, bool supportsRecycling = false) + : base(typeof(T), CastBuild(build), supportsRecycling) { } @@ -31,8 +32,12 @@ namespace Avalonia.Controls.Templates /// /// A function which when passed an object of returns a control. /// - public FuncDataTemplate(Func match, Func build) - : base(CastMatch(match), CastBuild(build)) + /// Whether the control can be recycled. + public FuncDataTemplate( + Func match, + Func build, + bool supportsRecycling = false) + : base(CastMatch(match), CastBuild(build), supportsRecycling) { } diff --git a/src/Avalonia.Controls/Templates/IDataTemplate.cs b/src/Avalonia.Controls/Templates/IDataTemplate.cs index ee490da4de..f9c97e55f3 100644 --- a/src/Avalonia.Controls/Templates/IDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/IDataTemplate.cs @@ -8,6 +8,12 @@ namespace Avalonia.Controls.Templates /// public interface IDataTemplate : ITemplate { + /// + /// Gets a value indicating whether the data template supports recycling of the generated + /// control. + /// + bool SupportsRecycling { get; } + /// /// Checks to see if this data template matches the specified data. /// diff --git a/src/Avalonia.Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/ViewLocator.cs index ddaa11b092..b107338aec 100644 --- a/src/Avalonia.Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/ViewLocator.cs @@ -9,6 +9,8 @@ namespace Avalonia.Diagnostics { public class ViewLocator : IDataTemplate { + public bool SupportsRecycling => false; + public IControl Build(object data) { var name = data.GetType().FullName.Replace("ViewModel", "View"); diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index d90dbfda7f..1feaed1b27 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -16,6 +16,8 @@ namespace Avalonia.Markup.Xaml.Templates [Content] public TemplateContent Content { get; set; } + public bool SupportsRecycling => true; + public bool Match(object data) { if (DataType == null) diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index 3c4d3c5baa..a640f00c65 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -24,6 +24,8 @@ namespace Avalonia.Markup.Xaml.Templates [AssignBinding] public Binding ItemsSource { get; set; } + public bool SupportsRecycling => true; + public bool Match(object data) { if (DataType == null) diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests.cs b/tests/Avalonia.Controls.UnitTests/ControlTests.cs index 038641b8b3..605406518d 100644 --- a/tests/Avalonia.Controls.UnitTests/ControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ControlTests.cs @@ -22,22 +22,51 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void LogicalParent_Should_Be_Set_To_Parent() + public void Setting_Parent_Should_Also_Set_InheritanceParent() { var parent = new Decorator(); var target = new TestControl(); parent.Child = target; + Assert.Equal(parent, target.Parent); Assert.Equal(parent, target.InheritanceParent); } [Fact] - public void LogicalParent_Should_Be_Cleared_When_Removed_From_Parent() + public void Setting_Parent_Should_Not_Set_InheritanceParent_If_Already_Set() { var parent = new Decorator(); + var inheritanceParent = new Decorator(); var target = new TestControl(); + ((ISetInheritanceParent)target).SetParent(inheritanceParent); + parent.Child = target; + + Assert.Equal(parent, target.Parent); + Assert.Equal(inheritanceParent, target.InheritanceParent); + } + + [Fact] + public void InheritanceParent_Should_Be_Cleared_When_Removed_From_Parent() + { + var parent = new Decorator(); + var target = new TestControl(); + + parent.Child = target; + parent.Child = null; + + Assert.Null(target.InheritanceParent); + } + + [Fact] + public void InheritanceParent_Should_Be_Cleared_When_Removed_From_Parent_When_Has_Different_InheritanceParent() + { + var parent = new Decorator(); + var inheritanceParent = new Decorator(); + var target = new TestControl(); + + ((ISetInheritanceParent)target).SetParent(inheritanceParent); parent.Child = target; parent.Child = null; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index 198c19f60f..cd24631661 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -160,7 +160,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { DataTemplates = new DataTemplates { - new FuncDataTemplate(_ => new Border()), + new FuncDataTemplate(_ => new Border(), true), }, Content = "foo", }; diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 7b19a6a4e2..ef8a9ca871 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -462,6 +462,8 @@ namespace Avalonia.Controls.UnitTests return new TextBlock { Text = node.Value }; } + public bool SupportsRecycling => false; + public InstancedBinding ItemsSelector(object item) { var obs = new ExpressionObserver(item, nameof(Node.Children)); From 05f836c4c3ab5e59bdb49949a9a0153d4e3df45e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2016 14:12:16 +0200 Subject: [PATCH 066/158] Added Move and MoveRange to AvaloniaList. Also added test for replace operation. --- src/Avalonia.Base/Collections/AvaloniaList.cs | 51 ++++++++++ .../Collections/AvaloniaListTests.cs | 99 +++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index e893fcb75b..65b559482d 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -319,6 +319,57 @@ namespace Avalonia.Collections } } + /// + /// Moves an item to a new index. + /// + /// The index of the item to move. + /// The index to move the item to. + public void Move(int oldIndex, int newIndex) + { + var item = this[oldIndex]; + _inner.RemoveAt(oldIndex); + _inner.Insert(newIndex, item); + + if (_collectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Move, + item, + newIndex, + oldIndex); + _collectionChanged(this, e); + } + } + + /// + /// Moves multiple items to a new index. + /// + /// The first index of the items to move. + /// The number of items to move. + /// The index to move the items to. + public void MoveRange(int oldIndex, int count, int newIndex) + { + var items = _inner.GetRange(oldIndex, count); + _inner.RemoveRange(oldIndex, count); + + if (newIndex > oldIndex) + { + newIndex -= count; + } + + _inner.InsertRange(newIndex, items); + + if (_collectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Move, + items, + newIndex, + oldIndex); + _collectionChanged(this, e); + } + } + /// /// Removes an item from the collection. /// diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index 2cd92c219b..587816b07b 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -53,6 +53,36 @@ namespace Avalonia.Base.UnitTests.Collections Assert.Throws(() => target.InsertRange(1, new List() { 1 })); } + [Fact] + public void Move_Should_Update_Collection() + { + var target = new AvaloniaList(new[] { 1, 2, 3 }); + + target.Move(2, 0); + + Assert.Equal(new[] { 3, 1, 2 }, target); + } + + [Fact] + public void MoveRange_Should_Update_Collection() + { + var target = new AvaloniaList(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + + target.MoveRange(4, 3, 0); + + Assert.Equal(new[] { 5, 6, 7, 1, 2, 3, 4, 8, 9, 10 }, target); + } + + [Fact] + public void MoveRange_Can_Move_To_End() + { + var target = new AvaloniaList(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + + target.MoveRange(0, 5, 10); + + Assert.Equal(new[] { 6, 7, 8, 9, 10, 1, 2, 3, 4, 5 }, target); + } + [Fact] public void Adding_Item_Should_Raise_CollectionChanged() { @@ -95,6 +125,29 @@ namespace Avalonia.Base.UnitTests.Collections Assert.True(raised); } + [Fact] + public void Replacing_Item_Should_Raise_CollectionChanged() + { + var target = new AvaloniaList(new[] { 1, 2 }); + var raised = false; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(target, s); + Assert.Equal(NotifyCollectionChangedAction.Replace, e.Action); + Assert.Equal(new[] { 2 }, e.OldItems.Cast()); + Assert.Equal(new[] { 3 }, e.NewItems.Cast()); + Assert.Equal(1, e.OldStartingIndex); + Assert.Equal(1, e.NewStartingIndex); + + raised = true; + }; + + target[1] = 3; + + Assert.True(raised); + } + [Fact] public void Inserting_Item_Should_Raise_CollectionChanged() { @@ -158,6 +211,52 @@ namespace Avalonia.Base.UnitTests.Collections Assert.True(raised); } + [Fact] + public void Moving_Item_Should_Raise_CollectionChanged() + { + var target = new AvaloniaList(new[] { 1, 2, 3 }); + var raised = false; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(target, s); + Assert.Equal(NotifyCollectionChangedAction.Move, e.Action); + Assert.Equal(new[] { 3 }, e.OldItems.Cast()); + Assert.Equal(new[] { 3 }, e.NewItems.Cast()); + Assert.Equal(2, e.OldStartingIndex); + Assert.Equal(0, e.NewStartingIndex); + + raised = true; + }; + + target.Move(2, 0); + + Assert.True(raised); + } + + [Fact] + public void Moving_Items_Should_Raise_CollectionChanged() + { + var target = new AvaloniaList(new[] { 1, 2, 3 }); + var raised = false; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(target, s); + Assert.Equal(NotifyCollectionChangedAction.Move, e.Action); + Assert.Equal(new[] { 2, 3 }, e.OldItems.Cast()); + Assert.Equal(new[] { 2, 3 }, e.NewItems.Cast()); + Assert.Equal(1, e.OldStartingIndex); + Assert.Equal(0, e.NewStartingIndex); + + raised = true; + }; + + target.MoveRange(1, 2, 0); + + Assert.True(raised); + } + [Fact] public void Clearing_Items_Should_Raise_CollectionChanged_Reset() { From 2cca2509161aa926a8ba1f0ed303070bcf90bec5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2016 14:13:23 +0200 Subject: [PATCH 067/158] Move items in panel Children collection Instead of removing and re-adding them as this causes a removal and re-addition to the logical tree, with all the overhead that involves. --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 69141fdbea..7750ed7f4a 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -258,7 +258,7 @@ namespace Avalonia.Controls.Presenters var first = delta < 0 && move ? panel.Children.Count + delta : 0; var containers = panel.Children.GetRange(first, count).ToList(); - for (var i = 0; i < containers.Count; ++i) + for (var i = 0; i < count; ++i) { var oldItemIndex = FirstIndex + first + i; var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign); @@ -273,15 +273,13 @@ namespace Avalonia.Controls.Presenters if (move) { - panel.Children.RemoveRange(first, count); - if (delta > 0) { - panel.Children.AddRange(containers); + panel.Children.MoveRange(first, count, panel.Children.Count); } else { - panel.Children.InsertRange(0, containers); + panel.Children.MoveRange(first, count, 0); } } From f3c7ea27a1e98dbcd777e27b9bc2a5eb1fffbdff Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2016 14:45:41 +0200 Subject: [PATCH 068/158] Set ContentPresenter.DataContext in UpdateChild. Thus delaying any changes that changing the data context would make until we know we have the correct child. --- .../Presenters/ContentPresenter.cs | 92 +++++++++---------- .../ItemsControlTests.cs | 1 + .../Presenters/ContentPresenterTests.cs | 4 +- .../Presenters/ItemsPresenterTests.cs | 1 + 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 81ed48f5b1..54d7e76d73 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -96,10 +96,6 @@ namespace Avalonia.Controls.Presenters /// public ContentPresenter() { - var dataContext = this.GetObservable(ContentProperty) - .Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x); - - Bind(Control.DataContextProperty, dataContext); } /// @@ -221,39 +217,36 @@ namespace Avalonia.Controls.Presenters /// public void UpdateChild() { - var old = Child; var content = Content; - var result = content as IControl; + var oldChild = Child; + var newChild = content as IControl; - if (result == null) + if (content != null && newChild == null) { - DataContext = content; - - if (content != null) + // We have content and it isn't a control, so first try to recycle the existing + // child control to display the new data by querying if the template that created + // the child can recycle items and that it also matches the new data. + if (oldChild != null && + _dataTemplate != null && + _dataTemplate.SupportsRecycling && + _dataTemplate.Match(content)) { - if (old != null && - _dataTemplate != null && - _dataTemplate.SupportsRecycling && - _dataTemplate.Match(content)) - { - result = old; - } - else - { - _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; - result = _dataTemplate.Build(content); - - var controlResult = result as Control; - - if (controlResult != null) - { - NameScope.SetNameScope(controlResult, new NameScope()); - } - } + newChild = oldChild; } else { - _dataTemplate = null; + // We couldn't recycle an existing control so find a data template for the data + // and use it to create a control. + _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + newChild = _dataTemplate.Build(content); + + // Try to give the new control its own name scope. + var controlResult = newChild as Control; + + if (controlResult != null) + { + NameScope.SetNameScope(controlResult, new NameScope()); + } } } else @@ -261,30 +254,35 @@ namespace Avalonia.Controls.Presenters _dataTemplate = null; } - if (result != old) + // Remove the old child if we're not recycling it. + if (oldChild != null && newChild != oldChild) { - if (old != null) - { - VisualChildren.Remove(old); - } + VisualChildren.Remove(oldChild); + } - if (result != null) - { - ((ISetInheritanceParent)result).SetParent(this); + // Set the DataContext if the data isn't a control. + if (!(content is IControl)) + { + DataContext = content; + } - Child = result; + // Update the Child. + if (newChild == null) + { + Child = null; + } + else if (newChild != oldChild) + { + ((ISetInheritanceParent)newChild).SetParent(this); - if (result.Parent == null) - { - ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); - } + Child = newChild; - VisualChildren.Add(result); - } - else + if (newChild.Parent == null) { - Child = null; + ((ISetLogicalParent)newChild).SetParent((ILogical)this.TemplatedParent ?? this); } + + VisualChildren.Add(newChild); } _createdChild = true; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index ce2dc4ab6c..dcbc71b9a1 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -370,6 +370,7 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); var dataContexts = target.Presenter.Panel.Children + .Do(x => (x as ContentPresenter)?.UpdateChild()) .Cast() .Select(x => x.DataContext) .ToList(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index cd24631661..5c26ba5a5e 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -143,13 +143,15 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Assigning_NonControl_To_Content_Should_Set_DataContext() + public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() { var target = new ContentPresenter { Content = "foo", }; + target.UpdateChild(); + Assert.Equal("foo", target.DataContext); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 9f76767ec1..da1558439f 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -312,6 +312,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var dataContexts = target.Panel.Children .Cast() + .Do(x => x.UpdateChild()) .Select(x => x.DataContext) .ToList(); From 71b334309768c00f1fe0dcc6804ba5386ef7ff93 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2016 16:15:01 +0200 Subject: [PATCH 069/158] Allow virtualized list to take up available space. --- .../VirtualizingStackPanel.cs | 14 +++-- .../Avalonia.Controls.UnitTests.csproj | 1 + .../ItemsPresenterTests_Virtualization.cs | 1 + ...emsPresenterTests_Virtualization_Simple.cs | 2 + .../VirtualizingStackPanelTests.cs | 52 +++++++++++++++++++ 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index eccada7796..2915ff07d5 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Specialized; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -19,8 +20,8 @@ namespace Avalonia.Controls get { return Orientation == Orientation.Horizontal ? - _takenSpace >= Bounds.Width : - _takenSpace >= Bounds.Height; + _takenSpace >= AvailableSpace.Width : + _takenSpace >= AvailableSpace.Height; } } @@ -54,6 +55,9 @@ namespace Avalonia.Controls } } + // TODO: We need to put a reasonable limit on this, probably based on the max window size. + private Size AvailableSpace => ((ILayoutable)this).PreviousMeasure ?? Bounds.Size; + protected override Size ArrangeOverride(Size finalSize) { _canBeRemoved = 0; @@ -99,7 +103,7 @@ namespace Avalonia.Controls rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); child.Arrange(rect); - if (rect.Y >= panelSize.Height) + if (rect.Y >= AvailableSpace.Height) { ++_canBeRemoved; } @@ -116,7 +120,7 @@ namespace Avalonia.Controls rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); child.Arrange(rect); - if (rect.X >= panelSize.Width) + if (rect.X >= AvailableSpace.Width) { ++_canBeRemoved; } @@ -135,7 +139,7 @@ namespace Avalonia.Controls var bounds = Bounds; var gap = Gap; - child.Measure(bounds.Size); + child.Measure(AvailableSpace); ++_averageCount; if (Orientation == Orientation.Vertical) diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index ec0b19308c..d3a48c02b0 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -143,6 +143,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 29d32398b4..72ec2e6618 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -111,6 +111,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(10, target.Panel.Children.Count); + target.Measure(new Size(120, 120)); target.Arrange(new Rect(0, 0, 100, 120)); Assert.Equal(12, target.Panel.Children.Count); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 09abb85ba7..1c7a08bdb4 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -72,6 +72,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(10, target.Panel.Children.Count); + target.Measure(new Size(100, 80)); target.Arrange(new Rect(0, 0, 100, 80)); Assert.Equal(8, target.Panel.Children.Count); @@ -90,6 +91,7 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(10, target.Panel.Children.Count); ((IScrollable)target).Offset = new Vector(0, 10); + target.Measure(new Size(120, 120)); target.Arrange(new Rect(0, 0, 100, 120)); Assert.Equal(12, target.Panel.Children.Count); diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs new file mode 100644 index 0000000000..900f098198 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class VirtualizingStackPanelTests + { + public class Vertical + { + [Fact] + public void Reports_IsFull_False_Until_Measure_Height_Is_Reached() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(0, 0), target.Bounds.Size); + + Assert.False(target.IsFull); + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + Assert.False(target.IsFull); + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + Assert.True(target.IsFull); + } + + [Fact] + public void Reports_Overflow_Only_After_Arrange() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(0, 0), target.Bounds.Size); + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + Assert.Equal(0, target.OverflowCount); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(2, target.OverflowCount); + } + } + } +} \ No newline at end of file From 9c4ce4efdc94da12b4ef335aebc40490d6a57a1b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Jun 2016 16:51:25 +0200 Subject: [PATCH 070/158] Fix formatting. --- src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs b/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs index 5b84b0ddd3..3473c4a094 100644 --- a/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.SceneGraph/Media/Imaging/Bitmap.cs @@ -55,7 +55,8 @@ namespace Avalonia.Media.Imaging /// public IBitmapImpl PlatformImpl { - get; } + get; + } /// /// Saves the bitmap to a file. From f660da41c73cca5a8852a9c383927a1958a7d4c7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 20:15:07 +0200 Subject: [PATCH 071/158] Fix virtualized list to taking up available space. Previous commit was slightly wrong: too many controls were being created for the list in VirtualizingTest. --- .../Avalonia.Controls.csproj | 1 + .../IVirtualizingController.cs | 22 +++++++++ src/Avalonia.Controls/IVirtualizingPanel.cs | 12 +++++ .../Presenters/ItemVirtualizer.cs | 45 ++++++++++++------- .../Presenters/ItemVirtualizerNone.cs | 6 --- .../Presenters/ItemVirtualizerSimple.cs | 2 +- .../Presenters/ItemsPresenter.cs | 8 ---- .../VirtualizingStackPanel.cs | 38 ++++++++++++---- .../ListBoxTests.cs | 2 +- .../ItemsPresenterTests_Virtualization.cs | 18 +++++++- .../VirtualizingStackPanelTests.cs | 32 ++++++++++++- 11 files changed, 141 insertions(+), 45 deletions(-) create mode 100644 src/Avalonia.Controls/IVirtualizingController.cs diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 22d17bf633..47a56fcc82 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -60,6 +60,7 @@ + diff --git a/src/Avalonia.Controls/IVirtualizingController.cs b/src/Avalonia.Controls/IVirtualizingController.cs new file mode 100644 index 0000000000..0b997f4948 --- /dev/null +++ b/src/Avalonia.Controls/IVirtualizingController.cs @@ -0,0 +1,22 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia.Controls +{ + /// + /// Interface implemented by controls that act as controllers for an + /// . + /// + public interface IVirtualizingController + { + /// + /// Called when the 's controls should be updated. + /// + /// + /// The controller should respond to this method being called by either adding + /// children up until becomes true or + /// removing controls. + /// + void UpdateControls(); + } +} diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index ce320c0da7..ca75517240 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Controls.Primitives; namespace Avalonia.Controls { @@ -10,6 +11,17 @@ namespace Avalonia.Controls /// public interface IVirtualizingPanel : IPanel { + /// + /// Gets or sets the controller for the virtualizing panel. + /// + /// + /// A virtualizing controller is responsible for maintaing the controls in the virtualizing + /// panel. This property will be set by the controller when virtualization is initialized. + /// Note that this property may remain null if the panel is added to a control that does + /// not act as a virtualizing controller. + /// + IVirtualizingController Controller { get; set; } + /// /// Gets a value indicating whether the panel is full. /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index dd7cf3bf1d..8f77cff32b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Presenters /// /// Base class for classes which handle virtualization for an . /// - internal abstract class ItemVirtualizer + internal abstract class ItemVirtualizer : IVirtualizingController { /// /// Initializes a new instance of the class. @@ -115,34 +115,34 @@ namespace Avalonia.Controls.Presenters { var virtualizingPanel = owner.Panel as IVirtualizingPanel; var scrollable = (ILogicalScrollable)owner; + ItemVirtualizer result = null; if (virtualizingPanel != null && scrollable.InvalidateScroll != null) { switch (owner.VirtualizationMode) { case ItemVirtualizationMode.Simple: - return new ItemVirtualizerSimple(owner); + result = new ItemVirtualizerSimple(owner); + break; } } - return new ItemVirtualizerNone(owner); - } + if (result == null) + { + result = new ItemVirtualizerNone(owner); + } - /// - /// Called by the when it carries out an arrange. - /// - /// The final size passed to the arrange. - public abstract void Arranging(Size finalSize); + if (virtualizingPanel != null) + { + virtualizingPanel.Controller = result; + } - /// - /// Called when a request is made to bring an item into view. - /// - /// The item to bring into view. - /// The rect on the item to bring into view. - /// True if the request was handled; otherwise false. - public virtual bool BringIntoView(IVisual target, Rect targetRect) + return result; + } + + /// + public virtual void UpdateControls() { - return false; } /// @@ -157,5 +157,16 @@ namespace Avalonia.Controls.Presenters Items = items; ItemCount = items.Count(); } + + /// + /// Called when a request is made to bring an item into view. + /// + /// The item to bring into view. + /// The rect on the item to bring into view. + /// True if the request was handled; otherwise false. + public virtual bool BringIntoView(IVisual target, Rect targetRect) + { + return false; + } } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 303c7442c2..7450fdb2b4 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -52,12 +52,6 @@ namespace Avalonia.Controls.Presenters get { throw new NotSupportedException(); } } - /// - public override void Arranging(Size finalSize) - { - // We don't need to do anything here. - } - /// public override void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 7750ed7f4a..78ac75e428 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -84,7 +84,7 @@ namespace Avalonia.Controls.Presenters } /// - public override void Arranging(Size finalSize) + public override void UpdateControls() { CreateAndRemoveContainers(); ((ILogicalScrollable)Owner).InvalidateScroll(); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 817d734506..b442c5bc61 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -78,14 +78,6 @@ namespace Avalonia.Controls.Presenters return _virtualizer?.BringIntoView(target, targetRect) ?? false; } - /// - protected override Size ArrangeOverride(Size finalSize) - { - var result = base.ArrangeOverride(finalSize); - _virtualizer.Arranging(finalSize); - return result; - } - /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 2915ff07d5..77aae2548d 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Specialized; using Avalonia.Layout; +using Avalonia.VisualTree; namespace Avalonia.Controls { public class VirtualizingStackPanel : StackPanel, IVirtualizingPanel { + private Size _availableSpace; private double _takenSpace; private int _canBeRemoved; private double _averageItemSize; @@ -20,15 +22,14 @@ namespace Avalonia.Controls get { return Orientation == Orientation.Horizontal ? - _takenSpace >= AvailableSpace.Width : - _takenSpace >= AvailableSpace.Height; + _takenSpace >= _availableSpace.Width : + _takenSpace >= _availableSpace.Height; } } + IVirtualizingController IVirtualizingPanel.Controller { get; set; } int IVirtualizingPanel.OverflowCount => _canBeRemoved; - Orientation IVirtualizingPanel.ScrollDirection => Orientation; - double IVirtualizingPanel.AverageItemSize => _averageItemSize; double IVirtualizingPanel.PixelOverflow @@ -55,16 +56,30 @@ namespace Avalonia.Controls } } - // TODO: We need to put a reasonable limit on this, probably based on the max window size. - private Size AvailableSpace => ((ILayoutable)this).PreviousMeasure ?? Bounds.Size; + private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; + + protected override Size MeasureOverride(Size availableSize) + { + if (availableSize != ((ILayoutable)this).PreviousMeasure) + { + // TODO: We need to put a reasonable limit on this, probably based on the max + // window size. + _availableSpace = availableSize; + Controller?.UpdateControls(); + } + + return base.MeasureOverride(availableSize); + } protected override Size ArrangeOverride(Size finalSize) { + _availableSpace = finalSize; _canBeRemoved = 0; _takenSpace = 0; _averageItemSize = 0; _averageCount = 0; var result = base.ArrangeOverride(finalSize); + Controller?.UpdateControls(); return result; } @@ -103,7 +118,7 @@ namespace Avalonia.Controls rect = new Rect(rect.X, rect.Y - _pixelOffset, rect.Width, rect.Height); child.Arrange(rect); - if (rect.Y >= AvailableSpace.Height) + if (rect.Y >= _availableSpace.Height) { ++_canBeRemoved; } @@ -120,7 +135,7 @@ namespace Avalonia.Controls rect = new Rect(rect.X - _pixelOffset, rect.Y, rect.Width, rect.Height); child.Arrange(rect); - if (rect.X >= AvailableSpace.Width) + if (rect.X >= _availableSpace.Width) { ++_canBeRemoved; } @@ -139,7 +154,7 @@ namespace Avalonia.Controls var bounds = Bounds; var gap = Gap; - child.Measure(AvailableSpace); + child.Measure(_availableSpace); ++_averageCount; if (Orientation == Orientation.Vertical) @@ -173,6 +188,11 @@ namespace Avalonia.Controls _takenSpace -= width + gap; RemoveFromAverageItemSize(width); } + + if (_canBeRemoved > 0) + { + --_canBeRemoved; + } } private void AddToAverageItemSize(double value) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index d1443fb2ae..db2bb0f886 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests // and re-arrange everything. foreach (IControl i in target.GetSelfAndVisualDescendents()) { - i.InvalidateArrange(); + i.InvalidateMeasure(); } target.Arrange(new Rect(0, 0, 100, 100)); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 72ec2e6618..ef3297b5c3 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -65,9 +65,11 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = CreateTarget(); target.ApplyTemplate(); + target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + Assert.Equal(10, target.Panel.Children.Count); + target.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(10, target.Panel.Children.Count); } @@ -83,6 +85,20 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(2, target.Panel.Children.Count); } + [Fact] + public void Should_Expand_To_Fit_Containers_When_Flexible_Size() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(10, 200), target.DesiredSize); + Assert.Equal(new Size(10, 200), target.Bounds.Size); + Assert.Equal(20, target.Panel.Children.Count); + } + [Fact] public void Initial_Item_DataContexts_Should_Be_Correct() { diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 900f098198..d5217e007b 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests @@ -9,25 +10,52 @@ namespace Avalonia.Controls.UnitTests { public class Vertical { + [Fact] + public void Measure_Invokes_Controller_UpdateControls() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + + controller.Verify(x => x.UpdateControls()); + } + + [Fact] + public void Arrange_Invokes_Controller_UpdateControls() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + + target.Controller = controller.Object; + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 110, 110)); + + controller.Verify(x => x.UpdateControls()); + } + [Fact] public void Reports_IsFull_False_Until_Measure_Height_Is_Reached() { var target = (IVirtualizingPanel)new VirtualizingStackPanel(); target.Measure(new Size(100, 100)); - target.Arrange(new Rect(target.DesiredSize)); Assert.Equal(new Size(0, 0), target.Bounds.Size); Assert.False(target.IsFull); + Assert.Equal(0, target.OverflowCount); target.Children.Add(new Canvas { Width = 50, Height = 50 }); Assert.False(target.IsFull); + Assert.Equal(0, target.OverflowCount); target.Children.Add(new Canvas { Width = 50, Height = 50 }); Assert.True(target.IsFull); + Assert.Equal(0, target.OverflowCount); } [Fact] - public void Reports_Overflow_Only_After_Arrange() + public void Reports_Overflow_After_Arrange() { var target = (IVirtualizingPanel)new VirtualizingStackPanel(); From acdf599dec7c7932815fa20c58e8f904cbe5f3fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 20:40:23 +0200 Subject: [PATCH 072/158] Initial impl. of scrolling with arrow keys. - Currently only vertical implemented - Doesn't handle partially visible items at end of list --- Avalonia.sln | 4 ++ src/Avalonia.Controls/IVirtualizingPanel.cs | 13 ++++-- .../Presenters/ItemVirtualizer.cs | 17 +++++++ .../Presenters/ItemVirtualizerSimple.cs | 46 +++++++++++++++++-- .../Presenters/ItemsPresenter.cs | 9 +++- .../Presenters/ScrollContentPresenter.cs | 5 +- .../Primitives/ILogicalScrollable.cs | 12 ++++- src/Avalonia.Controls/StackPanel.cs | 12 +++++ .../VirtualizingStackPanel.cs | 18 ++++++++ ...ScrollContentPresenterTests_IScrollable.cs | 8 +++- .../VirtualizingStackPanelTests.cs | 20 ++++++++ 11 files changed, 151 insertions(+), 13 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 928ba71ec8..9740e31ce2 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -161,6 +161,8 @@ EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{fb05ac90-89ba-4f2f-a924-f37875fb547c}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{db070a10-bf39-4752-8456-86e9d5928478}*SharedItemsImports = 4 @@ -168,11 +170,13 @@ Global src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{925dd807-b651-475f-9f7c-cbeb974ce43d}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{78345174-5b52-4a14-b9fd-d5f2428137f0}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{54f237d5-a70a-4752-9656-0c70b1a7b047}*SharedItemsImports = 4 + samples\TestApplicationShared\TestApplicationShared.projitems*{ff69b927-c545-49ae-8e16-3d14d621aa12}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\PlatformSupport\PlatformSupport.projitems*{811a76cf-1cf6-440f-963b-bbe31bd72a82}*SharedItemsImports = 4 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{47be08a7-5985-410b-9ffc-2264b8ea595f}*SharedItemsImports = 4 + samples\TestApplicationShared\TestApplicationShared.projitems*{8c923867-8a8f-4f6b-8b80-47d9e8436166}*SharedItemsImports = 4 samples\TestApplicationShared\TestApplicationShared.projitems*{e3a1060b-50d0-44e8-88b6-f44ef2e5bd72}*SharedItemsImports = 4 src\Shared\RenderHelpers\RenderHelpers.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{bd43f7c0-396b-4aa1-bad9-dfde54d51298}*SharedItemsImports = 4 diff --git a/src/Avalonia.Controls/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index ca75517240..5d35fa1ec8 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -1,9 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using Avalonia.Controls.Primitives; - namespace Avalonia.Controls { /// @@ -25,11 +22,21 @@ namespace Avalonia.Controls /// /// Gets a value indicating whether the panel is full. /// + /// + /// This property should return false until enough children are added to fill the space + /// passed into the last measure in the direction of scroll. It should be updated + /// immediately after a child is added or removed. + /// bool IsFull { get; } /// /// Gets the number of items that can be removed while keeping the panel full. /// + /// + /// This property should return the number of children that are completely out of the + /// panel's current bounds in the direction of scroll. It should be updated after an + /// arrange. + /// int OverflowCount { get; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 8f77cff32b..d690d4768b 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; +using Avalonia.Input; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -145,6 +146,17 @@ namespace Avalonia.Controls.Presenters { } + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + public virtual IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + return null; + } + /// /// Called when the items for the presenter change, either because /// has been set, the items collection has been @@ -168,5 +180,10 @@ namespace Avalonia.Controls.Presenters { return false; } + + /// + /// Invalidates the current scroll. + /// + protected void InvalidateScroll() => ((ILogicalScrollable)Owner).InvalidateScroll(); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 78ac75e428..29094789a4 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -5,8 +5,8 @@ using System; using System.Collections; using System.Collections.Specialized; using System.Linq; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; +using Avalonia.Input; namespace Avalonia.Controls.Presenters { @@ -87,7 +87,7 @@ namespace Avalonia.Controls.Presenters public override void UpdateControls() { CreateAndRemoveContainers(); - ((ILogicalScrollable)Owner).InvalidateScroll(); + InvalidateScroll(); } /// @@ -134,7 +134,47 @@ namespace Avalonia.Controls.Presenters VirtualizingPanel.Children.Clear(); } - ((ILogicalScrollable)Owner).InvalidateScroll(); + InvalidateScroll(); + } + + public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + var generator = Owner.ItemContainerGenerator; + var itemIndex = generator.IndexFromContainer(from); + + if (itemIndex == -1) + { + return null; + } + + var newItemIndex = -1; + + if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + { + switch (direction) + { + case FocusNavigationDirection.Up: + newItemIndex = itemIndex - 1; + break; + case FocusNavigationDirection.Down: + newItemIndex = itemIndex + 1; + break; + } + } + + if (newItemIndex >= 0 && newItemIndex < ItemCount) + { + + if (newItemIndex < FirstIndex || newItemIndex >= NextIndex) + { + OffsetValue += newItemIndex - itemIndex; + InvalidateScroll(); + } + + return generator.ContainerFromIndex(newItemIndex); + } + + return null; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index b442c5bc61..21580a7cf2 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Specialized; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.VisualTree; using static Avalonia.Utilities.MathUtilities; namespace Avalonia.Controls.Presenters @@ -73,11 +72,17 @@ namespace Avalonia.Controls.Presenters Size ILogicalScrollable.PageScrollSize => new Size(0, 1); /// - bool ILogicalScrollable.BringIntoView(IVisual target, Rect targetRect) + bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) { return _virtualizer?.BringIntoView(target, targetRect) ?? false; } + /// + IControl ILogicalScrollable.GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + return _virtualizer?.GetControlInDirection(direction, from); + } + /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 0aeb61a529..c7a302f71d 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -118,10 +118,11 @@ namespace Avalonia.Controls.Presenters } var scrollable = Child as ILogicalScrollable; + var control = target as IControl; - if (scrollable?.IsLogicalScrollEnabled == true) + if (scrollable?.IsLogicalScrollEnabled == true && control != null) { - return scrollable.BringIntoView(target, targetRect); + return scrollable.BringIntoView(control, targetRect); } var transform = target.TransformToVisual(Child); diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index b8b90f83a9..3fb201affc 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Avalonia.VisualTree; +using Avalonia.Input; namespace Avalonia.Controls.Primitives { @@ -56,6 +56,14 @@ namespace Avalonia.Controls.Primitives /// The target visual. /// The portion of the target visual to bring into view. /// True if the scroll offset was changed; otherwise false. - bool BringIntoView(IVisual target, Rect targetRect); + bool BringIntoView(IControl target, Rect targetRect); + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + IControl GetControlInDirection(FocusNavigationDirection direction, IControl from); } } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 0284d84df3..c52b738d8b 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -73,6 +73,18 @@ namespace Avalonia.Controls /// The control from which movement begins. /// The control. IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + { + var fromControl = from as IControl; + return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null; + } + + /// + /// Gets the next control in the specified direction. + /// + /// The movement direction. + /// The control from which movement begins. + /// The control. + protected virtual IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 77aae2548d..e4f639bdcb 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Specialized; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.VisualTree; @@ -107,6 +110,21 @@ namespace Avalonia.Controls } } + protected override IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + var logicalScrollable = Parent as ILogicalScrollable; + var fromControl = from as IControl; + + if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null) + { + return logicalScrollable.GetControlInDirection(direction, fromControl); + } + else + { + return base.GetControlInDirection(direction, from); + } + } + internal override void ArrangeChild( IControl child, Rect rect, diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs index d0c6a386ac..142df9c6eb 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -5,6 +5,7 @@ using System; using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.VisualTree; using Xunit; @@ -293,7 +294,7 @@ namespace Avalonia.Controls.UnitTests } } - public bool BringIntoView(IVisual target, Rect targetRect) + public bool BringIntoView(IControl target, Rect targetRect) { throw new NotImplementedException(); } @@ -303,6 +304,11 @@ namespace Avalonia.Controls.UnitTests AvailableSize = availableSize; return new Size(150, 150); } + + public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + { + throw new NotImplementedException(); + } } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index d5217e007b..9034076444 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -1,6 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.LogicalTree; using Moq; using Xunit; @@ -42,6 +45,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(new Size(100, 100)); + Assert.Equal(new Size(0, 0), target.DesiredSize); Assert.Equal(new Size(0, 0), target.Bounds.Size); Assert.False(target.IsFull); @@ -75,6 +79,22 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, target.OverflowCount); } + + [Fact] + public void Passes_Navigation_Request_To_ILogicalScrollable_Parent() + { + var presenter = new Mock().As(); + var scrollable = presenter.As(); + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var from = new Canvas(); + + scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); + + ((ISetLogicalParent)target).SetParent(presenter.Object); + ((INavigableContainer)target).GetControl(FocusNavigationDirection.Next, from); + + scrollable.Verify(x => x.GetControlInDirection(FocusNavigationDirection.Next, from)); + } } } } \ No newline at end of file From 7a718ef9c29f19ab0aa39b59a075c6e913c06bb4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 23:54:17 +0200 Subject: [PATCH 073/158] Updated overflow algorithm. --- .../VirtualizingStackPanel.cs | 3 +- .../VirtualizingStackPanelTests.cs | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index e4f639bdcb..9b2e1ffe21 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -41,7 +41,7 @@ namespace Avalonia.Controls { var bounds = Orientation == Orientation.Horizontal ? Bounds.Width : Bounds.Height; - return Math.Max(0, (_takenSpace - _pixelOffset) - bounds); + return Math.Max(0, _takenSpace - bounds); } } @@ -82,6 +82,7 @@ namespace Avalonia.Controls _averageItemSize = 0; _averageCount = 0; var result = base.ArrangeOverride(finalSize); + _takenSpace += _pixelOffset; Controller?.UpdateControls(); return result; } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 9034076444..a024fb5ec5 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -80,6 +80,52 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, target.OverflowCount); } + [Fact] + public void Reports_PixelOverflow_After_Arrange() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(2, target.PixelOverflow); + } + + [Fact] + public void Reports_PixelOverflow_With_PixelOffset() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + target.PixelOffset = 2; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(2, target.PixelOverflow); + } + + [Fact] + public void PixelOffset_Can_Be_More_Than_Child_Without_Affecting_IsFull() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + target.PixelOffset = 55; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(55, target.PixelOffset); + Assert.Equal(2, target.PixelOverflow); + Assert.True(target.IsFull); + } + [Fact] public void Passes_Navigation_Request_To_ILogicalScrollable_Parent() { From 81b66596c01da02d855ad77796ac4cacf4681c96 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Jun 2016 23:58:44 +0200 Subject: [PATCH 074/158] Use a full item's offset for overflow. --- .../Presenters/ItemVirtualizerSimple.cs | 9 ++++++--- .../ItemsPresenterTests_Virtualization_Simple.cs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 29094789a4..e165389c79 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -48,7 +48,9 @@ namespace Avalonia.Controls.Presenters if (delta != 0) { - if ((NextIndex - 1) + delta < ItemCount) + var newLastIndex = (NextIndex - 1) + delta; + + if (newLastIndex < ItemCount) { if (panel.PixelOffset > 0) { @@ -63,10 +65,11 @@ namespace Avalonia.Controls.Presenters } else { - // We're moving to a partially obscured item at the end of the list. + // We're moving to a partially obscured item at the end of the list so + // offset the panel by the height of the first item. var firstIndex = ItemCount - panel.Children.Count; RecycleContainersForMove(firstIndex - FirstIndex); - panel.PixelOffset = VirtualizingPanel.PixelOverflow; + panel.PixelOffset = panel.Children[0].Bounds.Height; } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 1c7a08bdb4..085d420d16 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -142,7 +142,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); Assert.Equal(10, minIndex); - Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); + Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); ((ILogicalScrollable)target).Offset = new Vector(0, 10); @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Presenters minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index); Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset); Assert.Equal(10, minIndex); - Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset); + Assert.Equal(10, ((IVirtualizingPanel)target.Panel).PixelOffset); } [Fact] From cba5200645d1e8b669608f8512b1d846b13c457f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 00:29:13 +0200 Subject: [PATCH 075/158] Fix keyboard nav with partially visible items. --- .../Presenters/ItemVirtualizerSimple.cs | 7 +- ...emsPresenterTests_Virtualization_Simple.cs | 96 +++++++++++++++++-- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index e165389c79..788ed10379 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -143,6 +143,7 @@ namespace Avalonia.Controls.Presenters public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) { var generator = Owner.ItemContainerGenerator; + var panel = VirtualizingPanel; var itemIndex = generator.IndexFromContainer(from); if (itemIndex == -1) @@ -167,8 +168,12 @@ namespace Avalonia.Controls.Presenters if (newItemIndex >= 0 && newItemIndex < ItemCount) { + // Get the index of the first and last fully visible items (i.e. excluding any + // partially visible item at the beginning or end). + var firstIndex = panel.PixelOffset == 0 ? FirstIndex : FirstIndex + 1; + var lastIndex = (FirstIndex + ViewportValue) - 1; - if (newItemIndex < FirstIndex || newItemIndex >= NextIndex) + if (newItemIndex < firstIndex || newItemIndex > lastIndex) { OffsetValue += newItemIndex - itemIndex; InvalidateScroll(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 085d420d16..8c1b0cfa57 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Input; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters @@ -131,7 +132,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 95)); @@ -162,7 +163,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Inserting_Items_Should_Update_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -187,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_First_Materialized_Item_Should_Update_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -209,7 +210,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_Items_From_Middle_Should_Update_Containers_When_All_Items_Visible() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 200)); @@ -234,7 +235,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_Last_Item_Should_Update_Containers_When_All_Items_Visible() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 200)); @@ -258,7 +259,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_Items_When_Scrolled_To_End_Should_Recyle_Containers_At_Top() { - var target = CreateTarget(itemCount: 20, useAvaloniaList: true); + var target = CreateTarget(useAvaloniaList: true); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -282,7 +283,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Removing_Items_When_Scrolled_To_Near_End_Should_Recycle_Containers_At_Bottom_And_Top() { - var target = CreateTarget(itemCount: 20, useAvaloniaList: true); + var target = CreateTarget(useAvaloniaList: true); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -308,7 +309,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Replacing_Items_Should_Update_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -329,7 +330,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Moving_Items_Should_Update_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -353,7 +354,7 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void Setting_Items_To_Null_Should_Remove_Containers() { - var target = CreateTarget(itemCount: 20); + var target = CreateTarget(); target.ApplyTemplate(); target.Measure(new Size(100, 100)); @@ -370,6 +371,81 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Empty(target.Panel.Children); } + public class Vertical + { + [Fact] + public void GetControlInDirection_Down_Should_Return_Existing_Container_If_Materialized() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var from = target.Panel.Children[5]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Down, + from); + + Assert.Same(target.Panel.Children[6], result); + } + + [Fact] + public void GetControlInDirection_Down_Should_Scroll_If_Necessary() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var from = target.Panel.Children[9]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Down, + from); + + Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[9], result); + } + + [Fact] + public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Down, + from); + + Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); + } + + [Fact] + public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Is_Currently_Shown() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 95)); + target.Arrange(new Rect(0, 0, 100, 95)); + ((ILogicalScrollable)target).Offset = new Vector(0, 11); + + var from = target.Panel.Children[1]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Up, + from); + + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[0], result); + } + } + public class WithContainers { [Fact] From e8dd81fa201aa8030b79037c0196af6c1b979b87 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 21:50:43 +0200 Subject: [PATCH 076/158] Support horiz keyboard movement. --- .../Presenters/ItemVirtualizerSimple.cs | 12 +++ ...emsPresenterTests_Virtualization_Simple.cs | 77 ++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 788ed10379..a6ca5dc18c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -165,6 +165,18 @@ namespace Avalonia.Controls.Presenters break; } } + else + { + switch (direction) + { + case FocusNavigationDirection.Left: + newItemIndex = itemIndex - 1; + break; + case FocusNavigationDirection.Right: + newItemIndex = itemIndex + 1; + break; + } + } if (newItemIndex >= 0 && newItemIndex < ItemCount) { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 8c1b0cfa57..f70915ad3c 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -427,7 +427,7 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Is_Currently_Shown() + public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { var target = CreateTarget(); @@ -446,6 +446,81 @@ namespace Avalonia.Controls.UnitTests.Presenters } } + public class Horizontal + { + [Fact] + public void GetControlInDirection_Right_Should_Return_Existing_Container_If_Materialized() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var from = target.Panel.Children[5]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Right, + from); + + Assert.Same(target.Panel.Children[6], result); + } + + [Fact] + public void GetControlInDirection_Right_Should_Scroll_If_Necessary() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var from = target.Panel.Children[9]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Right, + from); + + Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[9], result); + } + + [Fact] + public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(95, 100)); + target.Arrange(new Rect(0, 0, 95, 100)); + + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Right, + from); + + Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); + } + + [Fact] + public void GetControlInDirection_Left_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() + { + var target = CreateTarget(orientation: Orientation.Horizontal); + + target.ApplyTemplate(); + target.Measure(new Size(95, 100)); + target.Arrange(new Rect(0, 0, 95, 100)); + ((ILogicalScrollable)target).Offset = new Vector(11, 0); + + var from = target.Panel.Children[1]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + FocusNavigationDirection.Left, + from); + + Assert.Equal(new Vector(10, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[0], result); + } + } + public class WithContainers { [Fact] From 635ee4fdb2b3566924843367372284da3b390143 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 22:19:48 +0200 Subject: [PATCH 077/158] Fix compile error. --- samples/XamlTestApplicationPcl/TestScrollable.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index 39e708d043..c2bf2f005a 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -2,6 +2,7 @@ using System; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.Media; using Avalonia.VisualTree; @@ -75,7 +76,12 @@ namespace XamlTestApplication } } - public bool BringIntoView(IVisual target, Rect targetRect) + public bool BringIntoView(IControl target, Rect targetRect) + { + throw new NotImplementedException(); + } + + public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) { throw new NotImplementedException(); } From ae2ea31c6e6f6ef14539fe7b089e54078cec15fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 23:44:21 +0200 Subject: [PATCH 078/158] Allow changing virtualization mode in test app. Not yet working correctly. --- samples/VirtualizationTest/MainWindow.xaml | 5 ++- .../ViewModels/MainWindowViewModel.cs | 12 +++++++ .../Presenters/ItemVirtualizer.cs | 12 ++++++- .../Presenters/ItemsPresenter.cs | 18 ++++++++++ .../ItemsPresenterTests_Virtualization.cs | 35 +++++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index 94a0fc1b68..681de0a815 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -5,6 +5,8 @@ Margin="16 0 0 0" MinWidth="150" Gap="4"> + @@ -28,7 +30,8 @@ + SelectionMode="Multiple" + VirtualizationMode="{Binding VirtualizationMode}"> diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 0e1f56fa07..3e07aa54e9 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -2,8 +2,10 @@ // 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.Collections; +using Avalonia.Controls; using ReactiveUI; namespace VirtualizationTest.ViewModels @@ -15,6 +17,7 @@ namespace VirtualizationTest.ViewModels private int _newItemIndex; private IReactiveList _items; private string _prefix = "Item"; + private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple; public MainWindowViewModel() { @@ -50,6 +53,15 @@ namespace VirtualizationTest.ViewModels private set { this.RaiseAndSetIfChanged(ref _items, value); } } + public ItemVirtualizationMode VirtualizationMode + { + get { return _virtualizationMode; } + set { this.RaiseAndSetIfChanged(ref _virtualizationMode, value); } + } + + public IEnumerable VirtualizationModes => + Enum.GetValues(typeof(ItemVirtualizationMode)).Cast(); + public ReactiveCommand AddItemCommand { get; private set; } public ReactiveCommand RecreateCommand { get; private set; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index d690d4768b..c32e225bb0 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -14,8 +14,10 @@ namespace Avalonia.Controls.Presenters /// /// Base class for classes which handle virtualization for an . /// - internal abstract class ItemVirtualizer : IVirtualizingController + internal abstract class ItemVirtualizer : IVirtualizingController, IDisposable { + private bool disposedValue; + /// /// Initializes a new instance of the class. /// @@ -181,6 +183,14 @@ namespace Avalonia.Controls.Presenters return false; } + /// + public virtual void Dispose() + { + VirtualizingPanel.Controller = null; + VirtualizingPanel.Children.Clear(); + Owner.ItemContainerGenerator.Clear(); + } + /// /// Invalidates the current scroll. /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 21580a7cf2..d06d083220 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -32,6 +32,9 @@ namespace Avalonia.Controls.Presenters KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue( typeof(ItemsPresenter), KeyboardNavigationMode.Once); + + VirtualizationModeProperty.Changed + .AddClassHandler(x => x.VirtualizationModeChanged); } /// @@ -113,5 +116,20 @@ namespace Avalonia.Controls.Presenters var maxY = Math.Max(scrollable.Extent.Height - scrollable.Viewport.Height, 0); return new Vector(Clamp(value.X, 0, maxX), Clamp(value.Y, 0, maxY)); } + + private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) + { + _virtualizer?.Dispose(); + _virtualizer = ItemVirtualizer.Create(this); + + if (Items != null && Panel != null) + { + _virtualizer.ItemsChanged( + Items, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); + } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index ef3297b5c3..5351231fec 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -59,6 +59,20 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.True(((ILogicalScrollable)target).IsLogicalScrollEnabled); } + [Fact] + public void Parent_ScrollContentPresenter_Properties_Should_Be_Set() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var scroll = (ScrollContentPresenter)target.Parent; + Assert.Equal(new Size(0, 20), scroll.Extent); + Assert.Equal(new Size(0, 10), scroll.Viewport); + } + [Fact] public void Should_Fill_Panel_With_Containers() { @@ -138,6 +152,27 @@ namespace Avalonia.Controls.UnitTests.Presenters } } + [Fact] + public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Scroll_Properties() + { + var target = CreateTarget(); + + target.ApplyTemplate(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + var scroll = (ScrollContentPresenter)target.Parent; + Assert.Equal(10, target.Panel.Children.Count); + Assert.Equal(new Size(0, 20), scroll.Extent); + Assert.Equal(new Size(0, 10), scroll.Viewport); + + target.VirtualizationMode = ItemVirtualizationMode.None; + + Assert.Equal(20, target.Panel.Children.Count); + Assert.Equal(new Size(0, 200), scroll.Extent); + Assert.Equal(new Size(0, 100), scroll.Viewport); + } + private static ItemsPresenter CreateTarget( ItemVirtualizationMode mode = ItemVirtualizationMode.Simple, Orientation orientation = Orientation.Vertical, From 7e4f667d8907cd0cace397b9812a3b1ca908bf6b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Jun 2016 23:51:14 +0200 Subject: [PATCH 079/158] Fix PixelOverflow during arrange. --- .../VirtualizingStackPanel.cs | 2 +- .../VirtualizingStackPanelTests.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 9b2e1ffe21..0e40011760 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls get { var bounds = Orientation == Orientation.Horizontal ? - Bounds.Width : Bounds.Height; + _availableSpace.Width : _availableSpace.Height; return Math.Max(0, _takenSpace - bounds); } } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index a024fb5ec5..8ed7f9d0bb 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -80,6 +80,30 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(2, target.OverflowCount); } + [Fact] + public void Reports_Correct_Overflow_During_Arrange() + { + var target = (IVirtualizingPanel)new VirtualizingStackPanel(); + var controller = new Mock(); + var called = false; + + target.Children.Add(new Canvas { Width = 50, Height = 50 }); + target.Children.Add(new Canvas { Width = 50, Height = 52 }); + target.Measure(new Size(100, 100)); + + controller.Setup(x => x.UpdateControls()).Callback(() => + { + Assert.Equal(2, target.PixelOverflow); + Assert.Equal(0, target.OverflowCount); + called = true; + }); + + target.Controller = controller.Object; + target.Arrange(new Rect(target.DesiredSize)); + + Assert.True(called); + } + [Fact] public void Reports_PixelOverflow_After_Arrange() { From 32760bbc088f6119f63ff8865665800434b35d9c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 00:09:46 +0200 Subject: [PATCH 080/158] Fix changing virtualization mode in theory. That is; tests pass. In practise in the virtualization test app it still doesn't quite work. --- .../Presenters/ItemVirtualizer.cs | 2 ++ .../Presenters/ItemVirtualizerNone.cs | 4 +++ .../Presenters/ItemVirtualizerSimple.cs | 4 +++ .../Presenters/ItemsPresenter.cs | 8 ----- .../Presenters/ScrollContentPresenter.cs | 3 +- .../ItemsPresenterTests_Virtualization.cs | 31 +++++++++++++++++-- 6 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index c32e225bb0..8bc7bd9121 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -25,6 +25,8 @@ namespace Avalonia.Controls.Presenters public ItemVirtualizer(ItemsPresenter owner) { Owner = owner; + Items = owner.Items; + ItemCount = owner.Items.Count(); } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 7450fdb2b4..959afe0065 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -19,6 +19,10 @@ namespace Avalonia.Controls.Presenters public ItemVirtualizerNone(ItemsPresenter owner) : base(owner) { + if (Items != null && owner.Panel != null) + { + AddContainers(0, Items); + } } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index a6ca5dc18c..f67a46308e 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -23,6 +23,10 @@ namespace Avalonia.Controls.Presenters public ItemVirtualizerSimple(ItemsPresenter owner) : base(owner) { + if (Items != null && VirtualizingPanel != null) + { + CreateAndRemoveContainers(); + } } /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index d06d083220..7298535b15 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -121,14 +121,6 @@ namespace Avalonia.Controls.Presenters { _virtualizer?.Dispose(); _virtualizer = ItemVirtualizer.Create(this); - - if (Items != null && Panel != null) - { - _virtualizer.ItemsChanged( - Items, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); } } diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index c7a302f71d..5e20e8d3c9 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -290,8 +290,9 @@ namespace Avalonia.Controls.Presenters if (!scrollable.IsLogicalScrollEnabled) { Offset = default(Vector); - InvalidateMeasure(); } + + InvalidateMeasure(); } if (scrollable.IsLogicalScrollEnabled) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 5351231fec..d8ebbe50c6 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -153,7 +153,30 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Scroll_Properties() + public void Changing_VirtualizationMode_None_To_Simple_Should_Update_Control() + { + var target = CreateTarget(mode: ItemVirtualizationMode.None); + var scroll = (ScrollContentPresenter)target.Parent; + + target.ApplyTemplate(); + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(20, target.Panel.Children.Count); + Assert.Equal(new Size(10, 200), scroll.Extent); + Assert.Equal(new Size(100, 100), scroll.Viewport); + + target.VirtualizationMode = ItemVirtualizationMode.Simple; + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(10, target.Panel.Children.Count); + Assert.Equal(new Size(0, 20), scroll.Extent); + Assert.Equal(new Size(0, 10), scroll.Viewport); + } + + [Fact] + public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Control() { var target = CreateTarget(); @@ -167,10 +190,12 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Size(0, 10), scroll.Viewport); target.VirtualizationMode = ItemVirtualizationMode.None; + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(20, target.Panel.Children.Count); - Assert.Equal(new Size(0, 200), scroll.Extent); - Assert.Equal(new Size(0, 100), scroll.Viewport); + Assert.Equal(new Size(10, 200), scroll.Extent); + Assert.Equal(new Size(100, 100), scroll.Viewport); } private static ItemsPresenter CreateTarget( From 5467841e3e46e830e555fc1842af667a3e7c658b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 01:08:53 +0200 Subject: [PATCH 081/158] Fixed unit test name. --- ...e.cs => ScrollContentPresenterTests_ILogicalScrollable.cs} | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename tests/Avalonia.Controls.UnitTests/Presenters/{ScrollContentPresenterTests_IScrollable.cs => ScrollContentPresenterTests_ILogicalScrollable.cs} (98%) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs similarity index 98% rename from tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs rename to tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 142df9c6eb..8f3bee3bb9 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -6,13 +6,11 @@ using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Layout; -using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests { - public class ScrollContentPresenterTests_IScrollable + public class ScrollContentPresenterTests_ILogicalScrollable { [Fact] public void Measure_Should_Pass_Unchanged_Bounds_To_IScrollable() From 4c000b9ce4b1fcc12dc2446b7d72ee60e7b2b340 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 01:33:20 +0200 Subject: [PATCH 082/158] Fixed ScrollContentPresenter. Did not react correctly to child changing. --- .../Presenters/ScrollContentPresenter.cs | 18 ++++--- .../Avalonia.Controls.UnitTests.csproj | 2 +- ...ontentPresenterTests_ILogicalScrollable.cs | 50 +++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 5e20e8d3c9..25fe728d49 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -59,6 +59,7 @@ namespace Avalonia.Controls.Presenters static ScrollContentPresenter() { ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true); + ChildProperty.Changed.AddClassHandler(x => x.ChildChanged); AffectsArrange(OffsetProperty); } @@ -258,6 +259,16 @@ namespace Avalonia.Controls.Presenters e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); } + private void ChildChanged(AvaloniaPropertyChangedEventArgs e) + { + UpdateScrollableSubscription((IControl)e.NewValue); + + if (e.OldValue != null) + { + Offset = default(Vector); + } + } + private void UpdateScrollableSubscription(IControl child) { var scrollable = child as ILogicalScrollable; @@ -286,12 +297,7 @@ namespace Avalonia.Controls.Presenters if (logicalScroll != scrollable.IsLogicalScrollEnabled) { UpdateScrollableSubscription(Child); - - if (!scrollable.IsLogicalScrollEnabled) - { - Offset = default(Vector); - } - + Offset = default(Vector); InvalidateMeasure(); } diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index d3a48c02b0..3732f13769 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -109,7 +109,7 @@ - + diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 8f3bee3bb9..5a7e5f9f8b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -236,6 +236,56 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); } + [Fact] + public void Changing_Content_Should_Update_State() + { + var logicalScrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + Viewport = new Size(25, 25), + }; + + var nonLogicalScrollable = new TestScrollable + { + IsLogicalScrollEnabled = false, + }; + + var target = new ScrollContentPresenter + { + Content = logicalScrollable, + }; + + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(logicalScrollable.Extent, target.Extent); + Assert.Equal(logicalScrollable.Offset, target.Offset); + Assert.Equal(logicalScrollable.Viewport, target.Viewport); + Assert.Equal(new Rect(0, 0, 100, 100), logicalScrollable.Bounds); + + target.Content = nonLogicalScrollable; + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Size(150, 150), target.Extent); + Assert.Equal(new Vector(0, 0), target.Offset); + Assert.Equal(new Size(100, 100), target.Viewport); + Assert.Equal(new Rect(0, 0, 150, 150), nonLogicalScrollable.Bounds); + + target.Content = logicalScrollable; + target.UpdateChild(); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(logicalScrollable.Extent, target.Extent); + Assert.Equal(logicalScrollable.Offset, target.Offset); + Assert.Equal(logicalScrollable.Viewport, target.Viewport); + Assert.Equal(new Rect(0, 0, 100, 100), logicalScrollable.Bounds); + } + private class TestScrollable : Control, ILogicalScrollable { private Size _extent; From 8e6976a0da9359f7f9a3d92edf68cf8451a8cfbc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 18:47:13 +0200 Subject: [PATCH 083/158] Tweak measure/arrange algorithm. To ensure that parent controls are measured/arranged before children. --- src/Avalonia.Layout/LayoutManager.cs | 35 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 42ed45b0bb..11119e9b4a 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Logging; using Avalonia.Threading; @@ -13,8 +14,8 @@ namespace Avalonia.Layout /// public class LayoutManager : ILayoutManager { - private readonly Queue _toMeasure = new Queue(); - private readonly Queue _toArrange = new Queue(); + private readonly HashSet _toMeasure = new HashSet(); + private readonly HashSet _toArrange = new HashSet(); private bool _queued; private bool _running; @@ -29,8 +30,8 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toMeasure.Enqueue(control); - _toArrange.Enqueue(control); + _toMeasure.Add(control); + _toArrange.Add(control); QueueLayoutPass(); } @@ -40,7 +41,7 @@ namespace Avalonia.Layout Contract.Requires(control != null); Dispatcher.UIThread.VerifyAccess(); - _toArrange.Enqueue(control); + _toArrange.Add(control); QueueLayoutPass(); } @@ -107,7 +108,7 @@ namespace Avalonia.Layout { while (_toMeasure.Count > 0) { - var next = _toMeasure.Dequeue(); + var next = _toMeasure.First(); Measure(next); } } @@ -116,7 +117,7 @@ namespace Avalonia.Layout { while (_toArrange.Count > 0 && _toMeasure.Count == 0) { - var next = _toArrange.Dequeue(); + var next = _toArrange.First(); Arrange(next); } } @@ -124,29 +125,45 @@ namespace Avalonia.Layout private void Measure(ILayoutable control) { var root = control as ILayoutRoot; + var parent = control.VisualParent as ILayoutable; if (root != null) { root.Measure(root.MaxClientSize); } - else if (control.PreviousMeasure.HasValue) + else if (parent != null) + { + Measure(parent); + } + + if (!control.IsMeasureValid) { control.Measure(control.PreviousMeasure.Value); } + + _toMeasure.Remove(control); } private void Arrange(ILayoutable control) { var root = control as ILayoutRoot; + var parent = control.VisualParent as ILayoutable; if (root != null) { root.Arrange(new Rect(root.DesiredSize)); } - else if (control.PreviousArrange.HasValue) + else if (parent != null) + { + Measure(parent); + } + + if (control.PreviousArrange.HasValue) { control.Arrange(control.PreviousArrange.Value); } + + _toArrange.Remove(control); } private void QueueLayoutPass() From 67647f925fe68035385bb24faf325d57708078d0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 19:27:52 +0200 Subject: [PATCH 084/158] Tidied ScrollContentPresenter logic. --- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 25fe728d49..e0061ed386 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -280,7 +280,7 @@ namespace Avalonia.Controls.Presenters { scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); - if (scrollable?.IsLogicalScrollEnabled == true) + if (scrollable.IsLogicalScrollEnabled == true) { _logicalScrollSubscription = new CompositeDisposable( this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), @@ -300,8 +300,7 @@ namespace Avalonia.Controls.Presenters Offset = default(Vector); InvalidateMeasure(); } - - if (scrollable.IsLogicalScrollEnabled) + else if (scrollable.IsLogicalScrollEnabled) { Viewport = scrollable.Viewport; Extent = scrollable.Extent; From 13642613cf39a752d1200599db0067fd82ca23af Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 19:37:52 +0200 Subject: [PATCH 085/158] Added failing test. --- .../ItemsPresenterTests_Virtualization.cs | 46 ++++++++++++++++--- tests/Avalonia.UnitTests/TestServices.cs | 3 ++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index d8ebbe50c6..26657a0fbb 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -7,6 +7,8 @@ using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters @@ -158,7 +160,6 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = CreateTarget(mode: ItemVirtualizationMode.None); var scroll = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); scroll.Measure(new Size(100, 100)); scroll.Arrange(new Rect(0, 0, 100, 100)); @@ -167,29 +168,60 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Size(100, 100), scroll.Viewport); target.VirtualizationMode = ItemVirtualizationMode.Simple; - scroll.Measure(new Size(100, 100)); - scroll.Arrange(new Rect(0, 0, 100, 100)); + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); Assert.Equal(10, target.Panel.Children.Count); Assert.Equal(new Size(0, 20), scroll.Extent); Assert.Equal(new Size(0, 10), scroll.Viewport); } + [Fact] + public void Changing_VirtualizationMode_None_To_Simple_Should_Add_Correct_Number_Of_Controls() + { + using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + { + var target = CreateTarget(mode: ItemVirtualizationMode.None); + var scroll = (ScrollContentPresenter)target.Parent; + + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); + + // Ensure than an intermediate measure pass doesn't add more controls than it + // should. This can happen if target gets measured with Size.Infinity which + // is what the available size should be when VirtualizationMode == None but not + // what it should after VirtualizationMode is changed to Simple. + target.Panel.Children.CollectionChanged += (s, e) => + { + Assert.InRange(target.Panel.Children.Count, 0, 10); + }; + + target.VirtualizationMode = ItemVirtualizationMode.Simple; + LayoutManager.Instance.ExecuteLayoutPass(); + + Assert.Equal(10, target.Panel.Children.Count); + } + } + [Fact] public void Changing_VirtualizationMode_Simple_To_None_Should_Update_Control() { var target = CreateTarget(); + var scroll = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 100)); - target.Arrange(new Rect(0, 0, 100, 100)); + scroll.Measure(new Size(100, 100)); + scroll.Arrange(new Rect(0, 0, 100, 100)); - var scroll = (ScrollContentPresenter)target.Parent; Assert.Equal(10, target.Panel.Children.Count); Assert.Equal(new Size(0, 20), scroll.Extent); Assert.Equal(new Size(0, 10), scroll.Viewport); target.VirtualizationMode = ItemVirtualizationMode.None; + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Here - unlike changing the other way - we need to do a layout pass on the scroll + // content presenter as non-logical scroll values are only updated on arrange. scroll.Measure(new Size(100, 100)); scroll.Arrange(new Rect(0, 0, 100, 100)); diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 9ea13e07ff..c4a0c98908 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -44,6 +44,9 @@ namespace Avalonia.UnitTests keyboardDevice: () => new KeyboardDevice(), inputManager: new InputManager()); + public static readonly TestServices RealLayoutManager = new TestServices( + layoutManager: new LayoutManager()); + public static readonly TestServices RealStyler = new TestServices( styler: new Styler()); From 8ed3a7bc7cf0da47d632ac3df069f920a1ce6375 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 19:48:20 +0200 Subject: [PATCH 086/158] Fix copy/paste error in LayoutManager. Fixes test previously added. --- src/Avalonia.Layout/LayoutManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs index 11119e9b4a..85daff28b9 100644 --- a/src/Avalonia.Layout/LayoutManager.cs +++ b/src/Avalonia.Layout/LayoutManager.cs @@ -155,7 +155,7 @@ namespace Avalonia.Layout } else if (parent != null) { - Measure(parent); + Arrange(parent); } if (control.PreviousArrange.HasValue) From 37390e8b33fb7c3a293c733252db9ff0bcc391fa Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 19:49:06 +0200 Subject: [PATCH 087/158] Don't create items immediately... ...in virtualized ItemsPresenters. --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index f67a46308e..d25dcdde61 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -23,10 +23,8 @@ namespace Avalonia.Controls.Presenters public ItemVirtualizerSimple(ItemsPresenter owner) : base(owner) { - if (Items != null && VirtualizingPanel != null) - { - CreateAndRemoveContainers(); - } + // Don't need to add children here as UpdateControls should be called by the panel + // measure/arrange. } /// From d0942f84a98f34c2e1de8c5b301df608426d0df5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 20:36:15 +0200 Subject: [PATCH 088/158] FocusNavigationDirection => NavigationDirection And added page up/down. --- .../XamlTestApplicationPcl/TestScrollable.cs | 2 +- src/Avalonia.Controls/Canvas.cs | 2 +- .../Presenters/ItemVirtualizer.cs | 2 +- .../Presenters/ItemVirtualizerSimple.cs | 10 ++-- .../Presenters/ItemsPresenter.cs | 2 +- .../Primitives/ILogicalScrollable.cs | 2 +- src/Avalonia.Controls/StackPanel.cs | 20 +++---- .../VirtualizingStackPanel.cs | 2 +- src/Avalonia.Controls/WrapPanel.cs | 18 +++--- src/Avalonia.Input/Avalonia.Input.csproj | 4 +- .../IKeyboardNavigationHandler.cs | 2 +- src/Avalonia.Input/INavigableContainer.cs | 2 +- .../KeyboardNavigationHandler.cs | 22 +++---- .../Navigation/DirectionalNavigation.cs | 26 ++++----- .../Navigation/TabNavigation.cs | 26 ++++----- ...ionDirection.cs => NavigationDirection.cs} | 14 ++++- ...emsPresenterTests_Virtualization_Simple.cs | 16 ++--- ...ontentPresenterTests_ILogicalScrollable.cs | 2 +- .../VirtualizingStackPanelTests.cs | 4 +- .../KeyboardNavigationTests_Arrows.cs | 44 +++++++------- .../KeyboardNavigationTests_Tab.cs | 58 +++++++++---------- 21 files changed, 145 insertions(+), 135 deletions(-) rename src/Avalonia.Input/{FocusNavigationDirection.cs => NavigationDirection.cs} (78%) diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs index c2bf2f005a..9d4c1d9b29 100644 --- a/samples/XamlTestApplicationPcl/TestScrollable.cs +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -81,7 +81,7 @@ namespace XamlTestApplication throw new NotImplementedException(); } - public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + public IControl GetControlInDirection(NavigationDirection direction, IControl from) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index ebccb7435e..74eebea4bc 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -136,7 +136,7 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) { // TODO: Implement this return null; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 8bc7bd9121..4112c849d2 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -156,7 +156,7 @@ namespace Avalonia.Controls.Presenters /// The movement direction. /// The control from which movement begins. /// The control. - public virtual IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + public virtual IControl GetControlInDirection(NavigationDirection direction, IControl from) { return null; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index d25dcdde61..e9fbd33c72 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -142,7 +142,7 @@ namespace Avalonia.Controls.Presenters InvalidateScroll(); } - public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + public override IControl GetControlInDirection(NavigationDirection direction, IControl from) { var generator = Owner.ItemContainerGenerator; var panel = VirtualizingPanel; @@ -159,10 +159,10 @@ namespace Avalonia.Controls.Presenters { switch (direction) { - case FocusNavigationDirection.Up: + case NavigationDirection.Up: newItemIndex = itemIndex - 1; break; - case FocusNavigationDirection.Down: + case NavigationDirection.Down: newItemIndex = itemIndex + 1; break; } @@ -171,10 +171,10 @@ namespace Avalonia.Controls.Presenters { switch (direction) { - case FocusNavigationDirection.Left: + case NavigationDirection.Left: newItemIndex = itemIndex - 1; break; - case FocusNavigationDirection.Right: + case NavigationDirection.Right: newItemIndex = itemIndex + 1; break; } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 7298535b15..151d8679cf 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -81,7 +81,7 @@ namespace Avalonia.Controls.Presenters } /// - IControl ILogicalScrollable.GetControlInDirection(FocusNavigationDirection direction, IControl from) + IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) { return _virtualizer?.GetControlInDirection(direction, from); } diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index 3fb201affc..6c8f463a96 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -64,6 +64,6 @@ namespace Avalonia.Controls.Primitives /// The movement direction. /// The control from which movement begins. /// The control. - IControl GetControlInDirection(FocusNavigationDirection direction, IControl from); + IControl GetControlInDirection(NavigationDirection direction, IControl from); } } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index c52b738d8b..f5b3c20d5e 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -72,7 +72,7 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) { var fromControl = from as IControl; return (fromControl != null) ? GetControlInDirection(direction, fromControl) : null; @@ -84,35 +84,35 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - protected virtual IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) + protected virtual IInputElement GetControlInDirection(NavigationDirection direction, IControl from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); switch (direction) { - case FocusNavigationDirection.First: + case NavigationDirection.First: index = 0; break; - case FocusNavigationDirection.Last: + case NavigationDirection.Last: index = Children.Count - 1; break; - case FocusNavigationDirection.Next: + case NavigationDirection.Next: ++index; break; - case FocusNavigationDirection.Previous: + case NavigationDirection.Previous: --index; break; - case FocusNavigationDirection.Left: + case NavigationDirection.Left: index = horiz ? index - 1 : -1; break; - case FocusNavigationDirection.Right: + case NavigationDirection.Right: index = horiz ? index + 1 : -1; break; - case FocusNavigationDirection.Up: + case NavigationDirection.Up: index = horiz ? -1 : index - 1; break; - case FocusNavigationDirection.Down: + case NavigationDirection.Down: index = horiz ? -1 : index + 1; break; } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 0e40011760..46a17d601f 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls } } - protected override IInputElement GetControlInDirection(FocusNavigationDirection direction, IControl from) + protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from) { var logicalScrollable = Parent as ILogicalScrollable; var fromControl = from as IControl; diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index d0f1e93c56..9d3a5b6536 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -48,35 +48,35 @@ namespace Avalonia.Controls /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement INavigableContainer.GetControl(FocusNavigationDirection direction, IInputElement from) + IInputElement INavigableContainer.GetControl(NavigationDirection direction, IInputElement from) { var horiz = Orientation == Orientation.Horizontal; int index = Children.IndexOf((IControl)from); switch (direction) { - case FocusNavigationDirection.First: + case NavigationDirection.First: index = 0; break; - case FocusNavigationDirection.Last: + case NavigationDirection.Last: index = Children.Count - 1; break; - case FocusNavigationDirection.Next: + case NavigationDirection.Next: ++index; break; - case FocusNavigationDirection.Previous: + case NavigationDirection.Previous: --index; break; - case FocusNavigationDirection.Left: + case NavigationDirection.Left: index = horiz ? index - 1 : -1; break; - case FocusNavigationDirection.Right: + case NavigationDirection.Right: index = horiz ? index + 1 : -1; break; - case FocusNavigationDirection.Up: + case NavigationDirection.Up: index = horiz ? -1 : index - 1; break; - case FocusNavigationDirection.Down: + case NavigationDirection.Down: index = horiz ? -1 : index + 1; break; } diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index 569e1bb2cc..3722c80def 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -1,4 +1,4 @@ - + @@ -79,7 +79,7 @@ - + diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Input/IKeyboardNavigationHandler.cs index 14e2144da9..db3a3cf114 100644 --- a/src/Avalonia.Input/IKeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/IKeyboardNavigationHandler.cs @@ -25,7 +25,7 @@ namespace Avalonia.Input /// Any input modifiers active at the time of focus. void Move( IInputElement element, - FocusNavigationDirection direction, + NavigationDirection direction, InputModifiers modifiers = InputModifiers.None); } } \ No newline at end of file diff --git a/src/Avalonia.Input/INavigableContainer.cs b/src/Avalonia.Input/INavigableContainer.cs index 48f85819c0..13d734bd0b 100644 --- a/src/Avalonia.Input/INavigableContainer.cs +++ b/src/Avalonia.Input/INavigableContainer.cs @@ -14,6 +14,6 @@ namespace Avalonia.Input /// The movement direction. /// The control from which movement begins. /// The control. - IInputElement GetControl(FocusNavigationDirection direction, IInputElement from); + IInputElement GetControl(NavigationDirection direction, IInputElement from); } } diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 9991f7ec10..562f683079 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -48,11 +48,11 @@ namespace Avalonia.Input /// public static IInputElement GetNext( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); - if (direction == FocusNavigationDirection.Next || direction == FocusNavigationDirection.Previous) + if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous) { return TabNavigation.GetNextInTabOrder(element, direction); } @@ -70,7 +70,7 @@ namespace Avalonia.Input /// Any input modifiers active at the time of focus. public void Move( IInputElement element, - FocusNavigationDirection direction, + NavigationDirection direction, InputModifiers modifiers = InputModifiers.None) { Contract.Requires(element != null); @@ -79,8 +79,8 @@ namespace Avalonia.Input if (next != null) { - var method = direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Previous ? + var method = direction == NavigationDirection.Next || + direction == NavigationDirection.Previous ? NavigationMethod.Tab : NavigationMethod.Directional; FocusManager.Instance.Focus(next, method, modifiers); } @@ -97,25 +97,25 @@ namespace Avalonia.Input if (current != null) { - FocusNavigationDirection? direction = null; + NavigationDirection? direction = null; switch (e.Key) { case Key.Tab: direction = (e.Modifiers & InputModifiers.Shift) == 0 ? - FocusNavigationDirection.Next : FocusNavigationDirection.Previous; + NavigationDirection.Next : NavigationDirection.Previous; break; case Key.Up: - direction = FocusNavigationDirection.Up; + direction = NavigationDirection.Up; break; case Key.Down: - direction = FocusNavigationDirection.Down; + direction = NavigationDirection.Down; break; case Key.Left: - direction = FocusNavigationDirection.Left; + direction = NavigationDirection.Left; break; case Key.Right: - direction = FocusNavigationDirection.Right; + direction = NavigationDirection.Right; break; } diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs index 4d6e706516..efd4539719 100644 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs @@ -24,12 +24,12 @@ namespace Avalonia.Input.Navigation /// public static IInputElement GetNext( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); Contract.Requires( - direction != FocusNavigationDirection.Next && - direction != FocusNavigationDirection.Previous); + direction != NavigationDirection.Next && + direction != NavigationDirection.Previous); var container = element.GetVisualParent(); @@ -63,12 +63,12 @@ namespace Avalonia.Input.Navigation /// /// The direction. /// True if the direction is forward. - private static bool IsForward(FocusNavigationDirection direction) + private static bool IsForward(NavigationDirection direction) { - return direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Last || - direction == FocusNavigationDirection.Right || - direction == FocusNavigationDirection.Down; + return direction == NavigationDirection.Next || + direction == NavigationDirection.Last || + direction == NavigationDirection.Right || + direction == NavigationDirection.Down; } /// @@ -77,7 +77,7 @@ namespace Avalonia.Input.Navigation /// The element. /// The direction to search. /// The element or null if not found.## - private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction) + private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction) { return IsForward(direction) ? GetFocusableDescendents(container).FirstOrDefault() : @@ -121,9 +121,9 @@ namespace Avalonia.Input.Navigation private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { - if (direction == FocusNavigationDirection.Down) + if (direction == NavigationDirection.Down) { var descendent = GetFocusableDescendents(element).FirstOrDefault(); @@ -156,7 +156,7 @@ namespace Avalonia.Input.Navigation element = null; } - if (element != null && direction == FocusNavigationDirection.Up) + if (element != null && direction == NavigationDirection.Up) { var descendent = GetFocusableDescendents(element).LastOrDefault(); @@ -180,7 +180,7 @@ namespace Avalonia.Input.Navigation /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { var parent = container.GetVisualParent(); var isForward = IsForward(direction); diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs index 90502cdbf9..bc3826d90e 100644 --- a/src/Avalonia.Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Input/Navigation/TabNavigation.cs @@ -24,12 +24,12 @@ namespace Avalonia.Input.Navigation /// public static IInputElement GetNextInTabOrder( IInputElement element, - FocusNavigationDirection direction) + NavigationDirection direction) { Contract.Requires(element != null); Contract.Requires( - direction == FocusNavigationDirection.Next || - direction == FocusNavigationDirection.Previous); + direction == NavigationDirection.Next || + direction == NavigationDirection.Previous); var container = element.GetVisualParent(); @@ -63,9 +63,9 @@ namespace Avalonia.Input.Navigation /// The element. /// The direction to search. /// The element or null if not found.## - private static IInputElement GetFocusableDescendent(IInputElement container, FocusNavigationDirection direction) + private static IInputElement GetFocusableDescendent(IInputElement container, NavigationDirection direction) { - return direction == FocusNavigationDirection.Next ? + return direction == NavigationDirection.Next ? GetFocusableDescendents(container).FirstOrDefault() : GetFocusableDescendents(container).LastOrDefault(); } @@ -128,9 +128,9 @@ namespace Avalonia.Input.Navigation private static IInputElement GetNextInContainer( IInputElement element, IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { - if (direction == FocusNavigationDirection.Next) + if (direction == NavigationDirection.Next) { var descendent = GetFocusableDescendents(element).FirstOrDefault(); @@ -165,7 +165,7 @@ namespace Avalonia.Input.Navigation element = null; } - if (element != null && direction == FocusNavigationDirection.Previous) + if (element != null && direction == NavigationDirection.Previous) { var descendent = GetFocusableDescendents(element).LastOrDefault(); @@ -189,14 +189,14 @@ namespace Avalonia.Input.Navigation /// The first element, or null if there are no more elements. private static IInputElement GetFirstInNextContainer( IInputElement container, - FocusNavigationDirection direction) + NavigationDirection direction) { var parent = container.GetVisualParent(); IInputElement next = null; if (parent != null) { - if (direction == FocusNavigationDirection.Previous && parent.CanFocus()) + if (direction == NavigationDirection.Previous && parent.CanFocus()) { return parent; } @@ -204,7 +204,7 @@ namespace Avalonia.Input.Navigation var siblings = parent.GetVisualChildren() .OfType() .Where(FocusExtensions.CanFocusDescendents); - var sibling = direction == FocusNavigationDirection.Next ? + var sibling = direction == NavigationDirection.Next ? siblings.SkipWhile(x => x != container).Skip(1).FirstOrDefault() : siblings.TakeWhile(x => x != container).LastOrDefault(); @@ -216,7 +216,7 @@ namespace Avalonia.Input.Navigation } else { - next = direction == FocusNavigationDirection.Next ? + next = direction == NavigationDirection.Next ? GetFocusableDescendents(sibling).FirstOrDefault() : GetFocusableDescendents(sibling).LastOrDefault(); } @@ -229,7 +229,7 @@ namespace Avalonia.Input.Navigation } else { - next = direction == FocusNavigationDirection.Next ? + next = direction == NavigationDirection.Next ? GetFocusableDescendents(container).FirstOrDefault() : GetFocusableDescendents(container).LastOrDefault(); } diff --git a/src/Avalonia.Input/FocusNavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs similarity index 78% rename from src/Avalonia.Input/FocusNavigationDirection.cs rename to src/Avalonia.Input/NavigationDirection.cs index c8f670a13c..fbaa7e74c7 100644 --- a/src/Avalonia.Input/FocusNavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -4,9 +4,9 @@ namespace Avalonia.Input { /// - /// Describes how focus should be moved. + /// Describes how focus should be moved by directional or tab keys. /// - public enum FocusNavigationDirection + public enum NavigationDirection { /// /// Move the focus to the next control in the tab order. @@ -47,5 +47,15 @@ namespace Avalonia.Input /// Move the focus down. /// Down, + + /// + /// Move the focus up a page. + /// + PageUp, + + /// + /// Move the focus down a page. + /// + PageDown, } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index f70915ad3c..5e7daacf60 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -384,7 +384,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Down, + NavigationDirection.Down, from); Assert.Same(target.Panel.Children[6], result); @@ -401,7 +401,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Down, + NavigationDirection.Down, from); Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); @@ -419,7 +419,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[8]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Down, + NavigationDirection.Down, from); Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); @@ -438,7 +438,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[1]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Up, + NavigationDirection.Up, from); Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); @@ -459,7 +459,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[5]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Right, + NavigationDirection.Right, from); Assert.Same(target.Panel.Children[6], result); @@ -476,7 +476,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[9]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Right, + NavigationDirection.Right, from); Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); @@ -494,7 +494,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[8]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Right, + NavigationDirection.Right, from); Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); @@ -513,7 +513,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var from = target.Panel.Children[1]; var result = ((ILogicalScrollable)target).GetControlInDirection( - FocusNavigationDirection.Left, + NavigationDirection.Left, from); Assert.Equal(new Vector(10, 0), ((ILogicalScrollable)target).Offset); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs index 5a7e5f9f8b..1258a26d6a 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_ILogicalScrollable.cs @@ -353,7 +353,7 @@ namespace Avalonia.Controls.UnitTests return new Size(150, 150); } - public IControl GetControlInDirection(FocusNavigationDirection direction, IControl from) + public IControl GetControlInDirection(NavigationDirection direction, IControl from) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 8ed7f9d0bb..b8cd868252 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -161,9 +161,9 @@ namespace Avalonia.Controls.UnitTests scrollable.Setup(x => x.IsLogicalScrollEnabled).Returns(true); ((ISetLogicalParent)target).SetParent(presenter.Object); - ((INavigableContainer)target).GetControl(FocusNavigationDirection.Next, from); + ((INavigableContainer)target).GetControl(NavigationDirection.Next, from); - scrollable.Verify(x => x.GetControlInDirection(FocusNavigationDirection.Next, from)); + scrollable.Verify(x => x.GetControlInDirection(NavigationDirection.Next, from)); } } } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs index c41162ffed..dddb120f26 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs @@ -44,7 +44,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -83,7 +83,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -113,7 +113,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -158,7 +158,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -177,7 +177,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(top, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Down); Assert.Equal(next, result); } @@ -224,7 +224,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -262,7 +262,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -300,7 +300,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -339,7 +339,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Equal(next, result); } @@ -378,7 +378,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Null(result); } @@ -416,7 +416,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Down); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down); Assert.Null(result); } @@ -455,7 +455,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -494,7 +494,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -525,7 +525,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -570,7 +570,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -615,7 +615,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -635,7 +635,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(top, result); } @@ -674,7 +674,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -713,7 +713,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -752,7 +752,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Equal(next, result); } @@ -790,7 +790,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Null(result); } @@ -813,7 +813,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Up); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Up); Assert.Null(result); } diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index cc77dba1fc..efc9abedbb 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -42,7 +42,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -79,7 +79,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -123,7 +123,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -152,7 +152,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -195,7 +195,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -213,7 +213,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(top, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(top, NavigationDirection.Next); Assert.Equal(next, result); } @@ -256,7 +256,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -294,7 +294,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -332,7 +332,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -370,7 +370,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -408,7 +408,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Null(result); } @@ -446,7 +446,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -486,7 +486,7 @@ namespace Avalonia.Input.UnitTests KeyboardNavigation.SetTabOnceActiveElement(container, next); - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -524,7 +524,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -564,7 +564,7 @@ namespace Avalonia.Input.UnitTests KeyboardNavigation.SetTabOnceActiveElement(container, next); - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Next); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); Assert.Equal(next, result); } @@ -601,7 +601,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -638,7 +638,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -667,7 +667,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -710,7 +710,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -753,7 +753,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -772,7 +772,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(top, result); } @@ -810,7 +810,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -848,7 +848,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -886,7 +886,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -924,7 +924,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Null(result); } @@ -962,7 +962,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -1002,7 +1002,7 @@ namespace Avalonia.Input.UnitTests KeyboardNavigation.SetTabOnceActiveElement(container, next); - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -1040,7 +1040,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Equal(next, result); } @@ -1063,7 +1063,7 @@ namespace Avalonia.Input.UnitTests } }; - var result = KeyboardNavigationHandler.GetNext(current, FocusNavigationDirection.Previous); + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); Assert.Null(result); } From 6d5b3e02c4458ae75df2bf04fb2df9146aa90102 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Jun 2016 20:57:30 +0200 Subject: [PATCH 089/158] Added page up/down support to virt lists. --- .../Presenters/ItemVirtualizerSimple.cs | 27 ++++++++++++++++++- .../KeyboardNavigationHandler.cs | 6 +++++ .../Navigation/DirectionalNavigation.cs | 1 - 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index e9fbd33c72..2b65add3f7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -7,6 +7,7 @@ using System.Collections.Specialized; using System.Linq; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Utilities; namespace Avalonia.Controls.Presenters { @@ -165,6 +166,12 @@ namespace Avalonia.Controls.Presenters case NavigationDirection.Down: newItemIndex = itemIndex + 1; break; + case NavigationDirection.PageUp: + newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); + break; + case NavigationDirection.PageDown: + newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); + break; } } else @@ -177,6 +184,12 @@ namespace Avalonia.Controls.Presenters case NavigationDirection.Right: newItemIndex = itemIndex + 1; break; + case NavigationDirection.PageUp: + newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); + break; + case NavigationDirection.PageDown: + newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); + break; } } @@ -189,7 +202,8 @@ namespace Avalonia.Controls.Presenters if (newItemIndex < firstIndex || newItemIndex > lastIndex) { - OffsetValue += newItemIndex - itemIndex; + var newOffset = OffsetValue + (newItemIndex - itemIndex); + OffsetValue = CoerceOffset(newOffset); InvalidateScroll(); } @@ -398,5 +412,16 @@ namespace Avalonia.Controls.Presenters Owner.ItemContainerGenerator.Dematerialize(FirstIndex + index, count); NextIndex -= count; } + + /// + /// Ensures an offset value is within the value range. + /// + /// The value. + /// The coerced value. + private double CoerceOffset(double value) + { + var max = Math.Max(ExtentValue - ViewportValue, 0); + return MathUtilities.Clamp(value, 0, max); + } } } diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 562f683079..959e478141 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -117,6 +117,12 @@ namespace Avalonia.Input case Key.Right: direction = NavigationDirection.Right; break; + case Key.PageUp: + direction = NavigationDirection.PageUp; + break; + case Key.PageDown: + direction = NavigationDirection.PageDown; + break; } if (direction.HasValue) diff --git a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs index efd4539719..6d4da5d976 100644 --- a/src/Avalonia.Input/Navigation/DirectionalNavigation.cs +++ b/src/Avalonia.Input/Navigation/DirectionalNavigation.cs @@ -35,7 +35,6 @@ namespace Avalonia.Input.Navigation if (container != null) { - var isForward = IsForward(direction); var mode = KeyboardNavigation.GetDirectionalNavigation((InputElement)container); switch (mode) From 5346f20d816206402064b7fa82081b31192a14a0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 15:43:11 +0200 Subject: [PATCH 090/158] Use correct ItemContainerGenerator for menus. --- src/Avalonia.Controls/Menu.cs | 7 +++++++ src/Avalonia.Controls/MenuItem.cs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index d5d70ab1ea..2185fd982b 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Controls.Generators; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; @@ -131,6 +132,12 @@ namespace Avalonia.Controls _subscription.Dispose(); } + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + } + /// /// Called when a key is pressed within the menu. /// diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index e4bb44049d..f73f2c755b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Windows.Input; +using Avalonia.Controls.Generators; using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -204,6 +205,12 @@ namespace Avalonia.Controls IsSelected = true; } + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + } + /// /// Called when a key is pressed in the . /// From 0d32bc420c96a2c620cc0245f72520c0ed1f9509 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 16:27:21 +0200 Subject: [PATCH 091/158] Allow changing orientation in virt sample. --- samples/VirtualizationTest/MainWindow.xaml | 7 +++++++ .../ViewModels/MainWindowViewModel.cs | 10 ++++++++++ .../Presenters/ItemVirtualizerSimple.cs | 5 ++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index 681de0a815..e8baecc28c 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -7,6 +7,8 @@ Gap="4"> + @@ -32,6 +34,11 @@ SelectedItems="{Binding SelectedItems}" SelectionMode="Multiple" VirtualizationMode="{Binding VirtualizationMode}"> + + + + + diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 3e07aa54e9..eb0a7bf01e 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -17,6 +17,7 @@ namespace VirtualizationTest.ViewModels private int _newItemIndex; private IReactiveList _items; private string _prefix = "Item"; + private Orientation _orientation; private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple; public MainWindowViewModel() @@ -53,6 +54,15 @@ namespace VirtualizationTest.ViewModels private set { this.RaiseAndSetIfChanged(ref _items, value); } } + public Orientation Orientation + { + get { return _orientation; } + set { this.RaiseAndSetIfChanged(ref _orientation, value); } + } + + public IEnumerable Orientations => + Enum.GetValues(typeof(Orientation)).Cast(); + public ItemVirtualizationMode VirtualizationMode { get { return _virtualizationMode; } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 2b65add3f7..381080ce4a 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -72,7 +72,10 @@ namespace Avalonia.Controls.Presenters // offset the panel by the height of the first item. var firstIndex = ItemCount - panel.Children.Count; RecycleContainersForMove(firstIndex - FirstIndex); - panel.PixelOffset = panel.Children[0].Bounds.Height; + + panel.PixelOffset = VirtualizingPanel.ScrollDirection == Orientation.Vertical ? + panel.Children[0].Bounds.Height : + panel.Children[0].Bounds.Width; } } } From ad9abd53c12142d585702a020e068d5731e0822e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 18:48:43 +0200 Subject: [PATCH 092/158] Fixed scrolling differently sized items. --- .../Presenters/ItemVirtualizerSimple.cs | 15 ++++++++++++++- src/Avalonia.SceneGraph/Rect.cs | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 381080ce4a..f9b383a928 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -7,6 +7,7 @@ using System.Collections.Specialized; using System.Linq; using Avalonia.Controls.Utils; using Avalonia.Input; +using Avalonia.Layout; using Avalonia.Utilities; namespace Avalonia.Controls.Presenters @@ -210,7 +211,19 @@ namespace Avalonia.Controls.Presenters InvalidateScroll(); } - return generator.ContainerFromIndex(newItemIndex); + var container = generator.ContainerFromIndex(newItemIndex); + + // We need to do a layout here because it's possible that the container we moved to + // is only partially visible due to differing item sizes. If the container is only + // partially visible, scroll again. + LayoutManager.Instance?.ExecuteLayoutPass(); + + if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + { + OffsetValue += newItemIndex > itemIndex ? 1 : -1; + } + + return container; } return null; diff --git a/src/Avalonia.SceneGraph/Rect.cs b/src/Avalonia.SceneGraph/Rect.cs index de4d13890b..10253ec829 100644 --- a/src/Avalonia.SceneGraph/Rect.cs +++ b/src/Avalonia.SceneGraph/Rect.cs @@ -224,14 +224,24 @@ namespace Avalonia } /// - /// Determines whether a points in in the bounds of the rectangle. + /// Determines whether a point in in the bounds of the rectangle. /// /// The point. /// true if the point is in the bounds of the rectangle; otherwise false. public bool Contains(Point p) { - return p.X >= _x && p.X < _x + _width && - p.Y >= _y && p.Y < _y + _height; + return p.X >= _x && p.X <= _x + _width && + p.Y >= _y && p.Y <= _y + _height; + } + + /// + /// Determines whether the rectangle fully contains another rectangle. + /// + /// The rectangle. + /// true if the rectangle is fully contained; otherwise false. + public bool Contains(Rect r) + { + return Contains(r.TopLeft) && Contains(r.BottomRight); } /// From d7a757385a0e8c76ad4223c9204499645629977a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 20:15:53 +0200 Subject: [PATCH 093/158] Fix failing tests. --- .../Presenters/ItemVirtualizerSimple.cs | 15 ++++++++++----- .../ItemsPresenterTests_Virtualization_Simple.cs | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index f9b383a928..e06b1749db 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -212,15 +212,20 @@ namespace Avalonia.Controls.Presenters } var container = generator.ContainerFromIndex(newItemIndex); + var layoutManager = LayoutManager.Instance; // We need to do a layout here because it's possible that the container we moved to // is only partially visible due to differing item sizes. If the container is only - // partially visible, scroll again. - LayoutManager.Instance?.ExecuteLayoutPass(); - - if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + // partially visible, scroll again. Don't do this if there's no layout manager: + // it means we're running a unit test. + if (layoutManager != null) { - OffsetValue += newItemIndex > itemIndex ? 1 : -1; + layoutManager.ExecuteLayoutPass(); + + if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + { + OffsetValue += newItemIndex > itemIndex ? 1 : -1; + } } return container; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 5e7daacf60..2afd3f4bc3 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Presenters From 95eefa3cf1904d86358d81c1a06abd1c73cf29ca Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 22:24:16 +0200 Subject: [PATCH 094/158] Implement AutoScrollToSelectedItem For virtualizing lists. --- samples/VirtualizationTest/MainWindow.xaml | 2 + .../ViewModels/MainWindowViewModel.cs | 16 +++- .../Mixins/SelectableMixin.cs | 17 +--- .../Presenters/IItemsPresenter.cs | 2 + .../Presenters/ItemVirtualizer.cs | 9 +- .../Presenters/ItemVirtualizerSimple.cs | 95 ++++++++++++------- .../Presenters/ItemsPresenter.cs | 7 +- .../Presenters/ItemsPresenterBase.cs | 5 + .../Primitives/SelectingItemsControl.cs | 12 +++ ...emsPresenterTests_Virtualization_Simple.cs | 71 ++++++++------ 10 files changed, 148 insertions(+), 88 deletions(-) diff --git a/samples/VirtualizationTest/MainWindow.xaml b/samples/VirtualizationTest/MainWindow.xaml index e8baecc28c..55bd729fec 100644 --- a/samples/VirtualizationTest/MainWindow.xaml +++ b/samples/VirtualizationTest/MainWindow.xaml @@ -27,6 +27,8 @@ + + Remove()); + + SelectFirstCommand = ReactiveCommand.Create(); + SelectFirstCommand.Subscribe(_ => SelectItem(0)); + + SelectLastCommand = ReactiveCommand.Create(); + SelectLastCommand.Subscribe(_ => SelectItem(Items.Count - 1)); } public string NewItemString @@ -73,10 +79,10 @@ namespace VirtualizationTest.ViewModels Enum.GetValues(typeof(ItemVirtualizationMode)).Cast(); public ReactiveCommand AddItemCommand { get; private set; } - public ReactiveCommand RecreateCommand { get; private set; } - public ReactiveCommand RemoveItemCommand { get; private set; } + public ReactiveCommand SelectFirstCommand { get; private set; } + public ReactiveCommand SelectLastCommand { get; private set; } private void ResizeItems(int count) { @@ -125,5 +131,11 @@ namespace VirtualizationTest.ViewModels .Select(x => new ItemViewModel(x, _prefix)); Items = new ReactiveList(items); } + + private void SelectItem(int index) + { + SelectedItems.Clear(); + SelectedItems.Add(Items[index]); + } } } diff --git a/src/Avalonia.Controls/Mixins/SelectableMixin.cs b/src/Avalonia.Controls/Mixins/SelectableMixin.cs index 0472b604db..2369e9f530 100644 --- a/src/Avalonia.Controls/Mixins/SelectableMixin.cs +++ b/src/Avalonia.Controls/Mixins/SelectableMixin.cs @@ -51,22 +51,7 @@ namespace Avalonia.Controls.Mixins if (sender != null) { - var itemsControl = sender.Parent as SelectingItemsControl; - - if ((bool)x.NewValue) - { - ((IPseudoClasses)sender.Classes).Add(":selected"); - - if (((IVisual)sender).IsAttachedToVisualTree && - itemsControl?.AutoScrollToSelectedItem == true) - { - sender.BringIntoView(); - } - } - else - { - ((IPseudoClasses)sender.Classes).Remove(":selected"); - } + ((IPseudoClasses)sender.Classes).Set(":selected", (bool)x.NewValue); sender.RaiseEvent(new RoutedEventArgs { diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 95df903ed3..42311dc781 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -6,5 +6,7 @@ namespace Avalonia.Controls.Presenters public interface IItemsPresenter : IPresenter { IPanel Panel { get; } + + void ScrollIntoView(object item); } } diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index 4112c849d2..964ce82849 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -175,14 +175,11 @@ namespace Avalonia.Controls.Presenters } /// - /// Called when a request is made to bring an item into view. + /// Scrolls the specified item into view. /// - /// The item to bring into view. - /// The rect on the item to bring into view. - /// True if the request was handled; otherwise false. - public virtual bool BringIntoView(IVisual target, Rect targetRect) + /// The item. + public virtual void ScrollIntoView(object item) { - return false; } /// diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index e06b1749db..55e684015c 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -197,41 +197,18 @@ namespace Avalonia.Controls.Presenters } } - if (newItemIndex >= 0 && newItemIndex < ItemCount) - { - // Get the index of the first and last fully visible items (i.e. excluding any - // partially visible item at the beginning or end). - var firstIndex = panel.PixelOffset == 0 ? FirstIndex : FirstIndex + 1; - var lastIndex = (FirstIndex + ViewportValue) - 1; - - if (newItemIndex < firstIndex || newItemIndex > lastIndex) - { - var newOffset = OffsetValue + (newItemIndex - itemIndex); - OffsetValue = CoerceOffset(newOffset); - InvalidateScroll(); - } - - var container = generator.ContainerFromIndex(newItemIndex); - var layoutManager = LayoutManager.Instance; - - // We need to do a layout here because it's possible that the container we moved to - // is only partially visible due to differing item sizes. If the container is only - // partially visible, scroll again. Don't do this if there's no layout manager: - // it means we're running a unit test. - if (layoutManager != null) - { - layoutManager.ExecuteLayoutPass(); + return ScrollIntoView(newItemIndex); + } - if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) - { - OffsetValue += newItemIndex > itemIndex ? 1 : -1; - } - } + /// + public override void ScrollIntoView(object item) + { + var index = Items.IndexOf(item); - return container; + if (index != -1) + { + ScrollIntoView(index); } - - return null; } /// @@ -434,6 +411,60 @@ namespace Avalonia.Controls.Presenters NextIndex -= count; } + /// + /// Scrolls the item with the specified index into view. + /// + /// The item index. + /// The container that was brought into view. + private IControl ScrollIntoView(int index) + { + var panel = VirtualizingPanel; + var generator = Owner.ItemContainerGenerator; + var newOffset = -1.0; + + if (index >= 0 && index < ItemCount) + { + if (index < FirstIndex) + { + newOffset = index; + } + else if (index >= NextIndex) + { + newOffset = index - Math.Ceiling(ViewportValue - 1); + } + else if (OffsetValue + ViewportValue >= ItemCount) + { + newOffset = OffsetValue - 1; + } + + if (newOffset != -1) + { + OffsetValue = newOffset; + } + + var container = generator.ContainerFromIndex(index); + var layoutManager = LayoutManager.Instance; + + // We need to do a layout here because it's possible that the container we moved to + // is only partially visible due to differing item sizes. If the container is only + // partially visible, scroll again. Don't do this if there's no layout manager: + // it means we're running a unit test. + if (layoutManager != null) + { + layoutManager.ExecuteLayoutPass(); + + if (!new Rect(panel.Bounds.Size).Contains(container.Bounds)) + { + OffsetValue += 1; + } + } + + return container; + } + + return null; + } + /// /// Ensures an offset value is within the value range. /// diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 151d8679cf..4547c8fd45 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls.Presenters /// bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) { - return _virtualizer?.BringIntoView(target, targetRect) ?? false; + return false; } /// @@ -86,6 +86,11 @@ namespace Avalonia.Controls.Presenters return _virtualizer?.GetControlInDirection(direction, from); } + public override void ScrollIntoView(object item) + { + _virtualizer?.ScrollIntoView(item); + } + /// protected override void PanelCreated(IPanel panel) { diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index abebe85080..5a56e52029 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -163,6 +163,11 @@ namespace Avalonia.Controls.Presenters } } + /// + public virtual void ScrollIntoView(object item) + { + } + /// /// Creates the for the control. /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a46aa8d853..27f3407fd9 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -280,6 +280,12 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Scrolls the specified item into view. + /// + /// The item. + public void ScrollIntoView(object item) => Presenter?.ScrollIntoView(item); + /// /// Tries to get the container that was the source of an event. /// @@ -723,6 +729,12 @@ namespace Avalonia.Controls.Primitives { case NotifyCollectionChangedAction.Add: SelectedItemsAdded(e.NewItems.Cast().ToList()); + + if (AutoScrollToSelectedItem) + { + ScrollIntoView(e.NewItems[0]); + } + added = e.NewItems; break; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 2afd3f4bc3..354f93097b 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -412,38 +412,44 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible() { - var target = CreateTarget(); + using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + { + var target = CreateTarget(); + var scroller = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); + scroller.Measure(new Size(100, 95)); + scroller.Arrange(new Rect(0, 0, 100, 95)); - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Down, - from); + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Down, + from); - Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); + Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); + } } [Fact] public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Item_Is_Currently_Shown() { - var target = CreateTarget(); + using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + { + var target = CreateTarget(); + var scroller = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(100, 95)); - target.Arrange(new Rect(0, 0, 100, 95)); - ((ILogicalScrollable)target).Offset = new Vector(0, 11); + scroller.Measure(new Size(100, 95)); + scroller.Arrange(new Rect(0, 0, 100, 95)); + ((ILogicalScrollable)target).Offset = new Vector(0, 11); - var from = target.Panel.Children[1]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Up, - from); + var from = target.Panel.Children[1]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Up, + from); - Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[0], result); + Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[0], result); + } } } @@ -487,19 +493,22 @@ namespace Avalonia.Controls.UnitTests.Presenters [Fact] public void GetControlInDirection_Right_Should_Scroll_If_Partially_Visible() { - var target = CreateTarget(orientation: Orientation.Horizontal); + using (UnitTestApplication.Start(TestServices.RealLayoutManager)) + { + var target = CreateTarget(orientation: Orientation.Horizontal); + var scroller = (ScrollContentPresenter)target.Parent; - target.ApplyTemplate(); - target.Measure(new Size(95, 100)); - target.Arrange(new Rect(0, 0, 95, 100)); + scroller.Measure(new Size(95, 100)); + scroller.Arrange(new Rect(0, 0, 95, 100)); - var from = target.Panel.Children[8]; - var result = ((ILogicalScrollable)target).GetControlInDirection( - NavigationDirection.Right, - from); + var from = target.Panel.Children[8]; + var result = ((ILogicalScrollable)target).GetControlInDirection( + NavigationDirection.Right, + from); - Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); - Assert.Same(target.Panel.Children[8], result); + Assert.Equal(new Vector(1, 0), ((ILogicalScrollable)target).Offset); + Assert.Same(target.Panel.Children[8], result); + } } [Fact] From 86dfadf514967e156c28fab72d19f72362d42be4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 23:12:13 +0200 Subject: [PATCH 095/158] Implement Home and End navigation. --- .../Presenters/ItemVirtualizerSimple.cs | 72 +++++++++++-------- .../KeyboardNavigationHandler.cs | 6 ++ src/Avalonia.Input/NavigationDirection.cs | 10 +++ 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 55e684015c..32e80e0566 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -152,6 +152,7 @@ namespace Avalonia.Controls.Presenters var generator = Owner.ItemContainerGenerator; var panel = VirtualizingPanel; var itemIndex = generator.IndexFromContainer(from); + var vertical = VirtualizingPanel.ScrollDirection == Orientation.Vertical; if (itemIndex == -1) { @@ -160,41 +161,52 @@ namespace Avalonia.Controls.Presenters var newItemIndex = -1; - if (VirtualizingPanel.ScrollDirection == Orientation.Vertical) + switch (direction) { - switch (direction) - { - case NavigationDirection.Up: + case NavigationDirection.Up: + if (vertical) + { newItemIndex = itemIndex - 1; - break; - case NavigationDirection.Down: + } + + break; + case NavigationDirection.Down: + if (vertical) + { newItemIndex = itemIndex + 1; - break; - case NavigationDirection.PageUp: - newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); - break; - case NavigationDirection.PageDown: - newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); - break; - } - } - else - { - switch (direction) - { - case NavigationDirection.Left: + } + + break; + + case NavigationDirection.Left: + if (!vertical) + { newItemIndex = itemIndex - 1; - break; - case NavigationDirection.Right: + } + break; + + case NavigationDirection.Right: + if (!vertical) + { newItemIndex = itemIndex + 1; - break; - case NavigationDirection.PageUp: - newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); - break; - case NavigationDirection.PageDown: - newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); - break; - } + } + break; + + case NavigationDirection.PageUp: + newItemIndex = Math.Max(0, itemIndex - (int)ViewportValue); + break; + + case NavigationDirection.PageDown: + newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); + break; + + case NavigationDirection.Home: + newItemIndex = 0; + break; + + case NavigationDirection.End: + newItemIndex = ItemCount - 1; + break; } return ScrollIntoView(newItemIndex); diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 959e478141..41c15986c7 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -123,6 +123,12 @@ namespace Avalonia.Input case Key.PageDown: direction = NavigationDirection.PageDown; break; + case Key.Home: + direction = NavigationDirection.Home; + break; + case Key.End: + direction = NavigationDirection.End; + break; } if (direction.HasValue) diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index fbaa7e74c7..e04d08c6b2 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -57,5 +57,15 @@ namespace Avalonia.Input /// Move the focus down a page. /// PageDown, + + /// + /// Move the focus to the first item. + /// + Home, + + /// + /// Move the focus to the last item. + /// + End, } } From 22b495b1ac19900f0222caf6b92352503d0cf6ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 23:30:17 +0200 Subject: [PATCH 096/158] Fix directional nav for non-virtualized lists. Except Page Up/Down - these will need some extra code. --- .../Presenters/ItemVirtualizerNone.cs | 15 +++++++++++++++ .../Presenters/ItemVirtualizerSimple.cs | 16 ++++++++-------- src/Avalonia.Controls/StackPanel.cs | 3 +++ src/Avalonia.Input/KeyboardNavigationHandler.cs | 4 ++-- src/Avalonia.Input/NavigationDirection.cs | 10 ---------- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 959afe0065..56bb9299ae 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -110,6 +110,21 @@ namespace Avalonia.Controls.Presenters Owner.InvalidateMeasure(); } + /// + /// Scrolls the specified item into view. + /// + /// The item. + public override void ScrollIntoView(object item) + { + var index = Items.IndexOf(item); + + if (index != -1) + { + var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); + container.BringIntoView(); + } + } + private IList AddContainers(int index, IEnumerable items) { var generator = Owner.ItemContainerGenerator; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 32e80e0566..1dca52f885 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -163,6 +163,14 @@ namespace Avalonia.Controls.Presenters switch (direction) { + case NavigationDirection.First: + newItemIndex = 0; + break; + + case NavigationDirection.Last: + newItemIndex = ItemCount - 1; + break; + case NavigationDirection.Up: if (vertical) { @@ -199,14 +207,6 @@ namespace Avalonia.Controls.Presenters case NavigationDirection.PageDown: newItemIndex = Math.Min(ItemCount - 1, itemIndex + (int)ViewportValue); break; - - case NavigationDirection.Home: - newItemIndex = 0; - break; - - case NavigationDirection.End: - newItemIndex = ItemCount - 1; - break; } return ScrollIntoView(newItemIndex); diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index f5b3c20d5e..26a755e5f1 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -115,6 +115,9 @@ namespace Avalonia.Controls case NavigationDirection.Down: index = horiz ? -1 : index + 1; break; + default: + index = -1; + break; } if (index >= 0 && index < Children.Count) diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index 41c15986c7..57da49fa03 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -124,10 +124,10 @@ namespace Avalonia.Input direction = NavigationDirection.PageDown; break; case Key.Home: - direction = NavigationDirection.Home; + direction = NavigationDirection.First; break; case Key.End: - direction = NavigationDirection.End; + direction = NavigationDirection.Last; break; } diff --git a/src/Avalonia.Input/NavigationDirection.cs b/src/Avalonia.Input/NavigationDirection.cs index e04d08c6b2..fbaa7e74c7 100644 --- a/src/Avalonia.Input/NavigationDirection.cs +++ b/src/Avalonia.Input/NavigationDirection.cs @@ -57,15 +57,5 @@ namespace Avalonia.Input /// Move the focus down a page. /// PageDown, - - /// - /// Move the focus to the first item. - /// - Home, - - /// - /// Move the focus to the last item. - /// - End, } } From 5ce57ad02974ebb41730532ac13e8fd0d59a6774 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 11 Jun 2016 23:53:15 +0200 Subject: [PATCH 097/158] Fix menu separators. --- samples/ControlCatalog/Pages/MenuPage.xaml | 1 + .../Avalonia.Controls.csproj | 1 + .../Generators/MenuItemContainerGenerator.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/MenuItem.cs | 2 +- 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index ca28fadea0..98171f29d6 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -10,6 +10,7 @@ + diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index 47a56fcc82..c181aab1e2 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -51,6 +51,7 @@ + diff --git a/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs new file mode 100644 index 0000000000..c9b3a55aaa --- /dev/null +++ b/src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Controls.Generators +{ + public class MenuItemContainerGenerator : ItemContainerGenerator + { + /// + /// Initializes a new instance of the class. + /// + /// The owner control. + public MenuItemContainerGenerator(IControl owner) + : base(owner, MenuItem.HeaderProperty, null) + { + } + + /// + protected override IControl CreateContainer(object item) + { + var separator = item as Separator; + return separator != null ? separator : base.CreateContainer(item); + } + } +} diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index f73f2c755b..3d15ed99e7 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -208,7 +208,7 @@ namespace Avalonia.Controls /// protected override IItemContainerGenerator CreateItemContainerGenerator() { - return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + return new MenuItemContainerGenerator(this); } /// From f24ab044f78edf98fb9bd69c2b624adbc5da1c1e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 00:26:00 +0200 Subject: [PATCH 098/158] Added failing binding tests. --- .../Data/BindingTests.cs | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index ca65b763f5..c5a1744810 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -1,11 +1,13 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Moq; +using System; +using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Data; +using Moq; using ReactiveUI; using Xunit; @@ -112,6 +114,24 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("baz", source.Foo); } + [Fact] + public void Broken_Binding_Should_Use_Default_Value_Instead_Of_Inherited_Value() + { + var parent = new InheritanceTest() + { + Baz = 9, + DataContext = "data", + }; + + var child = new InheritanceTest(); + var bazBinding = new Binding("Missing"); + parent.Child = child; + + child.Bind(InheritanceTest.BazProperty, bazBinding); + + Assert.Equal(6, child.Baz); + } + [Fact] public void DataContext_Binding_Should_Use_Parent_DataContext() { @@ -159,6 +179,26 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("foo", child.DataContext); } + [Fact] + public void DataContext_Binding_Should_Produce_Correct_Results() + { + var root = new Decorator + { + DataContext = new { Foo = "bar" }, + }; + + var child = new Control(); + var dataContextBinding = new Binding("Foo"); + var values = new List(); + + child.GetObservable(Border.DataContextProperty).Subscribe(x => values.Add(x)); + child.Bind(ContentControl.DataContextProperty, dataContextBinding); + + root.Child = child; + + Assert.Equal(new[] { null, "bar" }, values); + } + [Fact] public void Should_Use_DefaultValueConverter_When_No_Converter_Specified() { @@ -337,5 +377,17 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Bind(BarProperty, this.GetObservable(FooProperty)); } } + + private class InheritanceTest : Decorator + { + public static readonly StyledProperty BazProperty = + AvaloniaProperty.Register("Baz", defaultValue: 6, inherits: true); + + public int Baz + { + get { return GetValue(BazProperty); } + set { SetValue(BazProperty, value); } + } + } } } From bf02ae41a9050a2e2db21dadb9970d9248424f99 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 00:26:45 +0200 Subject: [PATCH 099/158] Revert "Set ContentPresenter.DataContext in UpdateChild." This reverts commit f3c7ea27a1e98dbcd777e27b9bc2a5eb1fffbdff. --- .../Presenters/ContentPresenter.cs | 92 ++++++++++--------- .../ItemsControlTests.cs | 1 - .../Presenters/ContentPresenterTests.cs | 4 +- .../Presenters/ItemsPresenterTests.cs | 1 - 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 54d7e76d73..81ed48f5b1 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -96,6 +96,10 @@ namespace Avalonia.Controls.Presenters /// public ContentPresenter() { + var dataContext = this.GetObservable(ContentProperty) + .Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x); + + Bind(Control.DataContextProperty, dataContext); } /// @@ -217,72 +221,70 @@ namespace Avalonia.Controls.Presenters /// public void UpdateChild() { + var old = Child; var content = Content; - var oldChild = Child; - var newChild = content as IControl; + var result = content as IControl; - if (content != null && newChild == null) + if (result == null) { - // We have content and it isn't a control, so first try to recycle the existing - // child control to display the new data by querying if the template that created - // the child can recycle items and that it also matches the new data. - if (oldChild != null && - _dataTemplate != null && - _dataTemplate.SupportsRecycling && - _dataTemplate.Match(content)) - { - newChild = oldChild; - } - else + DataContext = content; + + if (content != null) { - // We couldn't recycle an existing control so find a data template for the data - // and use it to create a control. - _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; - newChild = _dataTemplate.Build(content); + if (old != null && + _dataTemplate != null && + _dataTemplate.SupportsRecycling && + _dataTemplate.Match(content)) + { + result = old; + } + else + { + _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + result = _dataTemplate.Build(content); - // Try to give the new control its own name scope. - var controlResult = newChild as Control; + var controlResult = result as Control; - if (controlResult != null) - { - NameScope.SetNameScope(controlResult, new NameScope()); + if (controlResult != null) + { + NameScope.SetNameScope(controlResult, new NameScope()); + } } } + else + { + _dataTemplate = null; + } } else { _dataTemplate = null; } - // Remove the old child if we're not recycling it. - if (oldChild != null && newChild != oldChild) + if (result != old) { - VisualChildren.Remove(oldChild); - } + if (old != null) + { + VisualChildren.Remove(old); + } - // Set the DataContext if the data isn't a control. - if (!(content is IControl)) - { - DataContext = content; - } + if (result != null) + { + ((ISetInheritanceParent)result).SetParent(this); - // Update the Child. - if (newChild == null) - { - Child = null; - } - else if (newChild != oldChild) - { - ((ISetInheritanceParent)newChild).SetParent(this); + Child = result; - Child = newChild; + if (result.Parent == null) + { + ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); + } - if (newChild.Parent == null) + VisualChildren.Add(result); + } + else { - ((ISetLogicalParent)newChild).SetParent((ILogical)this.TemplatedParent ?? this); + Child = null; } - - VisualChildren.Add(newChild); } _createdChild = true; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index dcbc71b9a1..ce2dc4ab6c 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -370,7 +370,6 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); var dataContexts = target.Presenter.Panel.Children - .Do(x => (x as ContentPresenter)?.UpdateChild()) .Cast() .Select(x => x.DataContext) .ToList(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index 5c26ba5a5e..cd24631661 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -143,15 +143,13 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() + public void Assigning_NonControl_To_Content_Should_Set_DataContext() { var target = new ContentPresenter { Content = "foo", }; - target.UpdateChild(); - Assert.Equal("foo", target.DataContext); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index da1558439f..9f76767ec1 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -312,7 +312,6 @@ namespace Avalonia.Controls.UnitTests.Presenters var dataContexts = target.Panel.Children .Cast() - .Do(x => x.UpdateChild()) .Select(x => x.DataContext) .ToList(); From ee37ab8df4c5d132e0aad015bfe97dda1c4e1dc2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 15 Jun 2016 19:18:06 +0300 Subject: [PATCH 100/158] Added UsePlatformDetect --- samples/BindingTest/App.xaml.cs | 3 +- samples/ControlCatalog.Desktop/Program.cs | 3 +- samples/TestApplication/Program.cs | 3 +- samples/XamlTestApplication/Program.cs | 3 +- .../Avalonia.Android/AndroidPlatform.cs | 1 - src/Avalonia.Controls/AppBuilder.cs | 46 ++++++++++++++++- src/Avalonia.Controls/Application.cs | 51 ------------------- .../DesignerAssist.cs | 4 +- 8 files changed, 50 insertions(+), 64 deletions(-) diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingTest/App.xaml.cs index fd457f7ba9..7bcaf837a8 100644 --- a/samples/BindingTest/App.xaml.cs +++ b/samples/BindingTest/App.xaml.cs @@ -19,8 +19,7 @@ namespace BindingTest InitializeLogging(); AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index ae8c9f5d8f..03842f78c1 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -17,8 +17,7 @@ namespace ControlCatalog // TODO: Make this work with GTK/Skia/Cairo depending on command-line args // again. AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs index 5e79916a8d..9e331c8634 100644 --- a/samples/TestApplication/Program.cs +++ b/samples/TestApplication/Program.cs @@ -35,8 +35,7 @@ namespace TestApplication var app = new App(); AppBuilder.Configure(app) - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .SetupWithoutStarting(); app.Run(); diff --git a/samples/XamlTestApplication/Program.cs b/samples/XamlTestApplication/Program.cs index 50b3c364d4..6485796ce7 100644 --- a/samples/XamlTestApplication/Program.cs +++ b/samples/XamlTestApplication/Program.cs @@ -21,8 +21,7 @@ namespace XamlTestApplication InitializeLogging(); AppBuilder.Configure() - .UseWin32() - .UseDirect2D1() + .UsePlatformDetect() .Start(); } diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 012e19da50..3192629494 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -38,7 +38,6 @@ namespace Avalonia.Android .Bind().ToConstant(this); SkiaPlatform.Initialize(); - Application.RegisterPlatformCallback(() => { }); _scalingFactor = global::Android.App.Application.Context.Resources.DisplayMetrics.ScaledDensity; diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilder.cs index 86ba8c3786..8b3e0e731a 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reflection; namespace Avalonia.Controls { @@ -98,23 +99,64 @@ namespace Avalonia.Controls /// /// The method to call to initialize the windowing subsystem. /// An instance. - public AppBuilder WithWindowingSubsystem(Action initializer) + public AppBuilder UseWindowingSubsystem(Action initializer) { WindowingSubsystem = initializer; return this; } + /// + /// Specifies a windowing subsystem to use. + /// + /// The dll in which to look for subsystem. + /// An instance. + public AppBuilder UseWindowingSubsystem(string dll) => UseWindowingSubsystem(GetInitializer(dll)); + /// /// Specifies a rendering subsystem to use. /// /// The method to call to initialize the rendering subsystem. /// An instance. - public AppBuilder WithRenderingSubsystem(Action initializer) + public AppBuilder UseRenderingSubsystem(Action initializer) { RenderingSubsystem = initializer; return this; } + /// + /// Specifies a rendering subsystem to use. + /// + /// The dll in which to look for subsystem. + /// An instance. + public AppBuilder UseRenderingSubsystem(string dll) => UseRenderingSubsystem(GetInitializer(dll)); + + static Action GetInitializer(string assemblyName) => () => + { + var assembly = Assembly.Load(new AssemblyName(assemblyName)); + var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform"; + var platformClassFullName = assemblyName + "." + platformClassName; + var platformClass = assembly.GetType(platformClassFullName); + var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]); + init.Invoke(null, null); + }; + + public AppBuilder UsePlatformDetect() + { + var platformId = (int) + ((dynamic) Type.GetType("System.Environment").GetRuntimeProperty("OSVersion").GetValue(null)).Platform; + if (platformId == 4 || platformId == 6) + { + UseRenderingSubsystem("Avalonia.Cairo"); + UseWindowingSubsystem("Avalonia.Gtk"); + } + else + { + UseRenderingSubsystem("Avalonia.Direct2D1"); + UseWindowingSubsystem("Avalonia.Win32"); + } + return this; + } + /// /// Sets up the platform-speciic services for the . /// diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index f73ead5576..1991f49ac0 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -26,16 +26,12 @@ namespace Avalonia /// - A global set of . /// - A . /// - An . - /// - Loads and initializes rendering and windowing subsystems with - /// and . /// - Registers services needed by the rest of Avalonia in the /// method. /// - Tracks the lifetime of the application. /// public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle { - static Action _platformInitializationCallback; - /// /// The application-global data templates. /// @@ -121,11 +117,6 @@ namespace Avalonia /// IStyleHost IStyleHost.StylingParent => null; - public static void RegisterPlatformCallback(Action cb) - { - _platformInitializationCallback = cb; - } - /// /// Initializes the application by loading XAML etc. /// @@ -189,47 +180,5 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(this); } - - /// - /// Initializes the rendering and windowing subsystems according to platform. - /// - /// The value of Environment.OSVersion.Platform. - protected void InitializeSubsystems(int platformID) - { - if (_platformInitializationCallback != null) - { - _platformInitializationCallback(); - } - else if (platformID == 4 || platformID == 6) - { - InitializeSubsystem("Avalonia.Cairo"); - InitializeSubsystem("Avalonia.Gtk"); - } - else - { - InitializeSubsystem("Avalonia.Direct2D1"); - InitializeSubsystem("Avalonia.Win32"); - } - } - - /// - /// Initializes the rendering or windowing subsystem defined by the specified assemblt. - /// - /// The name of the assembly. - protected static void InitializeSubsystem(string assemblyName) - { - var assembly = Assembly.Load(new AssemblyName(assemblyName)); - var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform"; - var platformClassFullName = assemblyName + "." + platformClassName; - var platformClass = assembly.GetType(platformClassFullName); - var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]); - init.Invoke(null, null); - } - - internal static void InitializeWin32Subsystem() - { - InitializeSubsystem("Avalonia.Direct2D1"); - InitializeSubsystem("Avalonia.Win32"); - } } } diff --git a/src/Avalonia.DesignerSupport/DesignerAssist.cs b/src/Avalonia.DesignerSupport/DesignerAssist.cs index 64aa9106c7..7050d17840 100644 --- a/src/Avalonia.DesignerSupport/DesignerAssist.cs +++ b/src/Avalonia.DesignerSupport/DesignerAssist.cs @@ -60,8 +60,8 @@ namespace Avalonia.DesignerSupport } AppBuilder.Configure(app == null ? new DesignerApp() : (Application) Activator.CreateInstance(app.AsType())) - .WithWindowingSubsystem(Application.InitializeWin32Subsystem) - .WithRenderingSubsystem(() => { }) + .UseWindowingSubsystem("Avalonia.Win32") + .UseRenderingSubsystem("Avalonia.Direct2D1") .SetupWithoutStarting(); } From 7018b3c36d33052d7b95b7471789c625fa34c9d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 22:54:14 +0200 Subject: [PATCH 101/158] Removed test. Decided that we shouldn't do this. --- .../Data/BindingTests.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index c5a1744810..1174b4ceb5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -114,24 +114,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("baz", source.Foo); } - [Fact] - public void Broken_Binding_Should_Use_Default_Value_Instead_Of_Inherited_Value() - { - var parent = new InheritanceTest() - { - Baz = 9, - DataContext = "data", - }; - - var child = new InheritanceTest(); - var bazBinding = new Binding("Missing"); - parent.Child = child; - - child.Bind(InheritanceTest.BazProperty, bazBinding); - - Assert.Equal(6, child.Baz); - } - [Fact] public void DataContext_Binding_Should_Use_Parent_DataContext() { From 0b28e10f212e7429593c3485309d2222eb6140e5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 22:56:38 +0200 Subject: [PATCH 102/158] Make broken DataContext bindings produce null. This prevents incorrect DataContexts cascading down to children when the DataContext binding is invalid, e.g. when things are being set up. --- .../Avalonia.Markup.Xaml/Data/Binding.cs | 15 +++++++++- .../MarkupExtensions/BindingExtension.cs | 2 +- .../Avalonia.Markup/Data/ExpressionSubject.cs | 28 ++++++++++++++++--- .../Data/ExpressionSubjectTests.cs | 8 +++--- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index 4b812e12f0..ec60695374 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -20,6 +20,7 @@ namespace Avalonia.Markup.Xaml.Data /// public Binding() { + FallbackValue = AvaloniaProperty.UnsetValue; } /// @@ -27,6 +28,7 @@ namespace Avalonia.Markup.Xaml.Data /// /// The binding path. public Binding(string path) + : this() { Path = path; } @@ -122,12 +124,23 @@ namespace Avalonia.Markup.Xaml.Data throw new NotSupportedException(); } + var fallback = FallbackValue; + + // If we're binding to DataContext and our fallback is UnsetValue then override + // the fallback value to null, as broken bindings to DataContext must reset the + // DataContext in order to not propagate incorrect DataContexts to child controls. + // See Avalonia.Markup.Xaml.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. + if (targetProperty == Control.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) + { + fallback = null; + } + var subject = new ExpressionSubject( observer, targetProperty?.PropertyType ?? typeof(object), + fallback, Converter ?? DefaultValueConverter.Instance, ConverterParameter, - FallbackValue, Priority); return new InstancedBinding(subject, Mode, Priority); diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index a58a8614ee..70d3f7d161 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -36,7 +36,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public IValueConverter Converter { get; set; } public object ConverterParameter { get; set; } public string ElementName { get; set; } - public object FallbackValue { get; set; } + public object FallbackValue { get; set; } = AvaloniaProperty.UnsetValue; public BindingMode Mode { get; set; } public string Path { get; set; } public BindingPriority Priority { get; set; } = BindingPriority.LocalValue; diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs index 05bf818aad..0a3be26c18 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs @@ -41,16 +41,36 @@ namespace Avalonia.Markup.Data /// /// A parameter to pass to . /// + /// The binding priority. + public ExpressionSubject( + ExpressionObserver inner, + Type targetType, + IValueConverter converter, + object converterParameter = null, + BindingPriority priority = BindingPriority.LocalValue) + : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. /// /// The value to use when the binding is unable to produce a value. /// + /// The value converter to use. + /// + /// A parameter to pass to . + /// /// The binding priority. public ExpressionSubject( ExpressionObserver inner, - Type targetType, + Type targetType, + object fallbackValue, IValueConverter converter, object converterParameter = null, - object fallbackValue = null, BindingPriority priority = BindingPriority.LocalValue) { Contract.Requires(inner != null); @@ -117,7 +137,7 @@ namespace Avalonia.Markup.Data _inner.Expression, error.Exception.Message); - if (_fallbackValue != null) + if (_fallbackValue != AvaloniaProperty.UnsetValue) { if (TypeUtilities.TryConvert( type, @@ -162,7 +182,7 @@ namespace Avalonia.Markup.Data ConverterParameter, CultureInfo.CurrentUICulture); - if (_fallbackValue != null && + if (_fallbackValue != AvaloniaProperty.UnsetValue && (converted == AvaloniaProperty.UnsetValue || converted is BindingError)) { diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs index 9ee32149e0..8b763e7fb9 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs @@ -123,8 +123,8 @@ namespace Avalonia.Markup.UnitTests.Data var target = new ExpressionSubject( new ExpressionObserver(data, "DoubleValue"), typeof(string), - DefaultValueConverter.Instance, - fallbackValue: "9.8"); + "9.8", + DefaultValueConverter.Instance); target.OnNext("foo"); @@ -162,7 +162,7 @@ namespace Avalonia.Markup.UnitTests.Data new ExpressionObserver(data, "DoubleValue"), typeof(string), converter.Object, - "foo"); + converterParameter: "foo"); target.Subscribe(_ => { }); @@ -178,7 +178,7 @@ namespace Avalonia.Markup.UnitTests.Data new ExpressionObserver(data, "DoubleValue"), typeof(string), converter.Object, - "foo"); + converterParameter: "foo"); target.OnNext("bar"); From e252a35509b2e20e0fa19522ee1b97d4d90ff7ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 22:57:24 +0200 Subject: [PATCH 103/158] Don't delay bind to DataContext. DataContext bindings are special in that they return null instead of UnsetValue when broken, and it's important that they do that when things are being set up. --- .../Avalonia.Markup.Xaml/Context/PropertyAccessor.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs b/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs index 20d9b07daf..e295292ba0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Context/PropertyAccessor.cs @@ -136,7 +136,14 @@ namespace Avalonia.Markup.Xaml.Context if (control != null) { - DelayedBinding.Add(control, property, binding); + if (property != Control.DataContextProperty) + { + DelayedBinding.Add(control, property, binding); + } + else + { + control.Bind(property, binding); + } } else { From 7b3e25949e4b98a9aadb4f5fd2e97cd702b42776 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 22:58:10 +0200 Subject: [PATCH 104/158] Revert "Revert "Set ContentPresenter.DataContext in UpdateChild."" This reverts commit bf02ae41a9050a2e2db21dadb9970d9248424f99. --- .../Presenters/ContentPresenter.cs | 92 +++++++++---------- .../ItemsControlTests.cs | 1 + .../Presenters/ContentPresenterTests.cs | 4 +- .../Presenters/ItemsPresenterTests.cs | 1 + 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 81ed48f5b1..54d7e76d73 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -96,10 +96,6 @@ namespace Avalonia.Controls.Presenters /// public ContentPresenter() { - var dataContext = this.GetObservable(ContentProperty) - .Select(x => x is IControl ? AvaloniaProperty.UnsetValue : x); - - Bind(Control.DataContextProperty, dataContext); } /// @@ -221,39 +217,36 @@ namespace Avalonia.Controls.Presenters /// public void UpdateChild() { - var old = Child; var content = Content; - var result = content as IControl; + var oldChild = Child; + var newChild = content as IControl; - if (result == null) + if (content != null && newChild == null) { - DataContext = content; - - if (content != null) + // We have content and it isn't a control, so first try to recycle the existing + // child control to display the new data by querying if the template that created + // the child can recycle items and that it also matches the new data. + if (oldChild != null && + _dataTemplate != null && + _dataTemplate.SupportsRecycling && + _dataTemplate.Match(content)) { - if (old != null && - _dataTemplate != null && - _dataTemplate.SupportsRecycling && - _dataTemplate.Match(content)) - { - result = old; - } - else - { - _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; - result = _dataTemplate.Build(content); - - var controlResult = result as Control; - - if (controlResult != null) - { - NameScope.SetNameScope(controlResult, new NameScope()); - } - } + newChild = oldChild; } else { - _dataTemplate = null; + // We couldn't recycle an existing control so find a data template for the data + // and use it to create a control. + _dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; + newChild = _dataTemplate.Build(content); + + // Try to give the new control its own name scope. + var controlResult = newChild as Control; + + if (controlResult != null) + { + NameScope.SetNameScope(controlResult, new NameScope()); + } } } else @@ -261,30 +254,35 @@ namespace Avalonia.Controls.Presenters _dataTemplate = null; } - if (result != old) + // Remove the old child if we're not recycling it. + if (oldChild != null && newChild != oldChild) { - if (old != null) - { - VisualChildren.Remove(old); - } + VisualChildren.Remove(oldChild); + } - if (result != null) - { - ((ISetInheritanceParent)result).SetParent(this); + // Set the DataContext if the data isn't a control. + if (!(content is IControl)) + { + DataContext = content; + } - Child = result; + // Update the Child. + if (newChild == null) + { + Child = null; + } + else if (newChild != oldChild) + { + ((ISetInheritanceParent)newChild).SetParent(this); - if (result.Parent == null) - { - ((ISetLogicalParent)result).SetParent((ILogical)this.TemplatedParent ?? this); - } + Child = newChild; - VisualChildren.Add(result); - } - else + if (newChild.Parent == null) { - Child = null; + ((ISetLogicalParent)newChild).SetParent((ILogical)this.TemplatedParent ?? this); } + + VisualChildren.Add(newChild); } _createdChild = true; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index ce2dc4ab6c..dcbc71b9a1 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -370,6 +370,7 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); var dataContexts = target.Presenter.Panel.Children + .Do(x => (x as ContentPresenter)?.UpdateChild()) .Cast() .Select(x => x.DataContext) .ToList(); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs index cd24631661..5c26ba5a5e 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests.cs @@ -143,13 +143,15 @@ namespace Avalonia.Controls.UnitTests.Presenters } [Fact] - public void Assigning_NonControl_To_Content_Should_Set_DataContext() + public void Assigning_NonControl_To_Content_Should_Set_DataContext_On_UpdateChild() { var target = new ContentPresenter { Content = "foo", }; + target.UpdateChild(); + Assert.Equal("foo", target.DataContext); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 9f76767ec1..da1558439f 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -312,6 +312,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var dataContexts = target.Panel.Children .Cast() + .Do(x => x.UpdateChild()) .Select(x => x.DataContext) .ToList(); From e1275eae56b2bda3af6e2898b198dde5ae7dcf8d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 15 Jun 2016 23:29:16 +0200 Subject: [PATCH 105/158] Fixed .sln file. .sln files, you suck for merging. --- Avalonia.sln | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Avalonia.sln b/Avalonia.sln index 7a9c1f04f5..087e42af65 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -156,7 +156,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DesignerSupport.TestApp", "tests\Avalonia.DesignerSupport.TestApp\Avalonia.DesignerSupport.TestApp.csproj", "{F1381F98-4D24-409A-A6C5-1C5B1E08BB08}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}" +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Aval+onia.RenderTests", "tests\Avalonia.RenderTests\Avalonia.RenderTests.shproj", "{48840EDD-24BF-495D-911E-2EB12AE75D3B}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}" EndProject Global From 69e2c459c4ce70d182357b4a0b540f3cdb7d526b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 Jun 2016 00:23:08 +0200 Subject: [PATCH 106/158] Do a null Items check in ScrollIntoView This can happen when bindings are in the process of updating, or if the client calls SelectingItemsControl.ScrollIntoView themselves. --- .../Presenters/ItemVirtualizerNone.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 56bb9299ae..411f309368 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -116,12 +116,15 @@ namespace Avalonia.Controls.Presenters /// The item. public override void ScrollIntoView(object item) { - var index = Items.IndexOf(item); - - if (index != -1) + if (Items != null) { - var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); - container.BringIntoView(); + var index = Items.IndexOf(item); + + if (index != -1) + { + var container = Owner.ItemContainerGenerator.ContainerFromIndex(index); + container.BringIntoView(); + } } } From 99878ef8bb86bb837b2f1e6485093fb9dad677e1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Jun 2016 01:44:26 +0300 Subject: [PATCH 107/158] Designer proccess fixes --- .../AppHost/AvaloniaAppHost.cs | 87 +------------------ .../Avalonia.Designer.csproj | 7 +- .../AvaloniaDesigner.xaml.cs | 25 +++--- .../Avalonia.Designer/Comm/ProcessHost.cs | 13 +-- .../Avalonia.Designer/Comm/UpdateMetadata.cs | 20 ----- src/Windows/Avalonia.Designer/DemoWindow.xaml | 5 +- .../Avalonia.Designer/DemoWindow.xaml.cs | 21 +++++ .../Metadata/AvaloniaDesignerMetadata.cs | 51 ----------- 8 files changed, 47 insertions(+), 182 deletions(-) delete mode 100644 src/Windows/Avalonia.Designer/Comm/UpdateMetadata.cs delete mode 100644 src/Windows/Avalonia.Designer/Metadata/AvaloniaDesignerMetadata.cs diff --git a/src/Windows/Avalonia.Designer/AppHost/AvaloniaAppHost.cs b/src/Windows/Avalonia.Designer/AppHost/AvaloniaAppHost.cs index 442499c783..b1ad9f0f69 100644 --- a/src/Windows/Avalonia.Designer/AppHost/AvaloniaAppHost.cs +++ b/src/Windows/Avalonia.Designer/AppHost/AvaloniaAppHost.cs @@ -10,7 +10,6 @@ using System.Windows.Forms.Integration; using System.Xml; using Avalonia.Designer.Comm; using Avalonia.Designer.InProcDesigner; -using Avalonia.Designer.Metadata; using Timer = System.Windows.Forms.Timer; using Avalonia.DesignerSupport; @@ -91,88 +90,7 @@ namespace Avalonia.Designer.AppHost } } - AvaloniaDesignerMetadata BuildMetadata(List asms, Type xmlNsAttr) - { - var rv = new AvaloniaDesignerMetadata() - { - - NamespaceAliases = new List(), - Types = new List() - }; - - - foreach (var asm in asms) - { - foreach (dynamic xmlns in asm.GetCustomAttributes().Where(a => a.GetType() == xmlNsAttr)) - { - rv.NamespaceAliases.Add(new MetadataNamespaceAlias - { - Namespace = (string)xmlns.ClrNamespace, - XmlNamespace = (string)xmlns.XmlNamespace - }); - } - - try - { - foreach (var type in asm.GetTypes()) - { - try - { - if (!type.IsPublic || type.IsAbstract) - continue; - var t = new MetadataType() - { - Name = type.Name, - Namespace = type.Namespace, - Properties = new List() - }; - rv.Types.Add(t); - foreach (var prop in type.GetProperties()) - { - if (prop.GetMethod?.IsPublic != true) - continue; - var p = new MetadataProperty() - { - Name = prop.Name, - Type = - prop.PropertyType == typeof (string) || - (prop.PropertyType.IsValueType && - prop.PropertyType.Assembly == typeof (int).Assembly) - ? MetadataPropertyType.BasicType - : prop.PropertyType.IsEnum - ? MetadataPropertyType.Enum - : MetadataPropertyType.MetadataType - - }; - if (p.Type == MetadataPropertyType.Enum) - p.EnumValues = Enum.GetNames(prop.PropertyType); - if (p.Type == MetadataPropertyType.MetadataType) - p.MetadataFullTypeName = prop.PropertyType.Namespace + "." + prop.PropertyType.Name; - t.Properties.Add(p); - } - } - catch - { - // - } - } - } - catch - { - // - } - } - return rv; - } - - void BuildMetadataAndSendMessageAsync(List asms) - { - var xmlNsAttr = LookupType("Avalonia.Metadata.XmlnsDefinitionAttribute"); - new Thread(() => - { - _comm.SendMessage(new UpdateMetadataMessage(BuildMetadata(asms, xmlNsAttr))); - }).Start(); - } + private void DoInit(string targetExe, StringBuilder logger) { @@ -196,9 +114,6 @@ namespace Avalonia.Designer.AppHost logger.AppendLine(e.ToString()); } - log("Looking up Avalonia types"); - BuildMetadataAndSendMessageAsync(asms); - log("Initializing built-in designer"); var dic = new Dictionary(); Api = new DesignerApi(dic) {OnResize = OnResize, OnWindowCreated = OnWindowCreated}; diff --git a/src/Windows/Avalonia.Designer/Avalonia.Designer.csproj b/src/Windows/Avalonia.Designer/Avalonia.Designer.csproj index 5ab3b2e78d..a84cec6e5a 100644 --- a/src/Windows/Avalonia.Designer/Avalonia.Designer.csproj +++ b/src/Windows/Avalonia.Designer/Avalonia.Designer.csproj @@ -1,4 +1,4 @@ - + @@ -82,7 +82,6 @@ - @@ -91,7 +90,6 @@ InProcDesignerView.xaml - AvaloniaDesigner.xaml @@ -126,6 +124,9 @@ + + + "}, + new TextBlock {Text = ""}, + new TextBlock {Text = "before setters in your first Style"} + } + }; + } + else + control = (Control) loaded; + window = control as Window; if (window == null) { - window = new Window() {Content = original}; + window = new Window() {Content = (Control)control}; } if (!window.IsSet(Window.SizeToContentProperty)) @@ -114,7 +139,7 @@ namespace Avalonia.DesignerSupport s_currentWindow?.Close(); s_currentWindow = window; window.Show(); - Design.ApplyDesignerProperties(window, original); + Design.ApplyDesignerProperties(window, control); Api.OnWindowCreated?.Invoke(window.PlatformImpl.Handle.Handle); Api.OnResize?.Invoke(); } From f439c141db354cdc9e70d9533e7d399a589b4f2f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Jun 2016 17:04:23 +0300 Subject: [PATCH 110/158] Don't try to preview App.xaml --- src/Avalonia.DesignerSupport/DesignerAssist.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.DesignerSupport/DesignerAssist.cs b/src/Avalonia.DesignerSupport/DesignerAssist.cs index 3f6a46020f..8787144665 100644 --- a/src/Avalonia.DesignerSupport/DesignerAssist.cs +++ b/src/Avalonia.DesignerSupport/DesignerAssist.cs @@ -115,7 +115,7 @@ namespace Avalonia.DesignerSupport { Children = { - new TextBlock {Text = "Styles can't be edited without Design.PreviewWith. Add"}, + new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"}, new TextBlock {Text = ""}, new TextBlock {Text = " "}, new TextBlock {Text = ""}, @@ -123,6 +123,8 @@ namespace Avalonia.DesignerSupport } }; } + if (loaded is Application) + control = new TextBlock {Text = "Application can't be previewed in design view"}; else control = (Control) loaded; From 259144838cd7f68308a46efae0dca89e74bf431a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Jun 2016 00:49:49 +0200 Subject: [PATCH 111/158] Set item container properties with Style priority. So that they can be overridden by styles. --- .../Generators/ItemContainerGenerator.cs | 14 +++++--- .../Generators/ItemContainerGenerator`1.cs | 5 +-- .../Generators/TreeItemContainerGenerator.cs | 2 +- .../Templates/TreeDataTemplate.cs | 2 +- .../Generators/ItemContainerGeneratorTests.cs | 36 +++++++++++++++++++ 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 45f7dff49c..6bbf757106 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; namespace Avalonia.Controls.Generators { @@ -187,11 +188,16 @@ namespace Avalonia.Controls.Generators if (result == null) { - result = new ContentPresenter + result = new ContentPresenter(); + result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style); + + if (ItemTemplate != null) { - ContentTemplate = ItemTemplate, - Content = item, - }; + result.SetValue( + ContentPresenter.ContentTemplateProperty, + ItemTemplate, + BindingPriority.TemplatedParent); + } } return result; diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 35107a84fc..3aa2181cd4 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -5,6 +5,7 @@ using System; using System.Linq.Expressions; using System.Reflection; using Avalonia.Controls.Templates; +using Avalonia.Data; namespace Avalonia.Controls.Generators { @@ -62,10 +63,10 @@ namespace Avalonia.Controls.Generators if (ContentTemplateProperty != null) { - result.SetValue(ContentTemplateProperty, ItemTemplate); + result.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style); } - result.SetValue(ContentProperty, item); + result.SetValue(ContentProperty, item, BindingPriority.Style); if (!(item is IControl)) { diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index e26a4fb0d6..8a70aa7307 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -78,7 +78,7 @@ namespace Avalonia.Controls.Generators var template = GetTreeDataTemplate(item, ItemTemplate); var result = new T(); - result.SetValue(ContentProperty, template.Build(item)); + result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style); var itemsSelector = template.ItemsSelector(item); diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index a640f00c65..32e0e4f4cb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -43,7 +43,7 @@ namespace Avalonia.Markup.Xaml.Templates if (ItemsSource != null) { var obs = new ExpressionObserver(item, ItemsSource.Path); - return new InstancedBinding(obs); + return new InstancedBinding(obs, BindingPriority.Style); } return null; diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index 5d2269e73c..01b550fb3b 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; +using Avalonia.Data; using Xunit; namespace Avalonia.Controls.UnitTests.Generators @@ -111,6 +113,40 @@ namespace Avalonia.Controls.UnitTests.Generators Assert.Equal(new[] { 0, 1 }, target.Containers.Select(x => x.Index)); } + [Fact] + public void Style_Binding_Should_Be_Able_To_Override_Content() + { + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner); + var container = (ContentPresenter)target.Materialize(0, "foo", null).ContainerControl; + + Assert.Equal("foo", container.Content); + + container.Bind( + ContentPresenter.ContentProperty, + Observable.Never().StartWith("bar"), + BindingPriority.Style); + + Assert.Equal("bar", container.Content); + } + + [Fact] + public void Style_Binding_Should_Be_Able_To_Override_Content_Typed() + { + var owner = new Decorator(); + var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null); + var container = (ListBoxItem)target.Materialize(0, "foo", null).ContainerControl; + + Assert.Equal("foo", container.Content); + + container.Bind( + ContentPresenter.ContentProperty, + Observable.Never().StartWith("bar"), + BindingPriority.Style); + + Assert.Equal("bar", container.Content); + } + private IList Materialize( IItemContainerGenerator generator, int index, From d772017768fd952c38ba9ae2d89ac323739b2c02 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Jun 2016 01:28:32 +0200 Subject: [PATCH 112/158] Added