From 389d58f550cc5f5d4ba8e8883bd96921b0dddf23 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 20 Oct 2019 14:23:28 +0300 Subject: [PATCH 01/13] issue #3089 add failing tests --- .../Primitives/SelectingItemsControlTests.cs | 128 +++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..ce333f66d6 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public class SelectingItemsControlTests { private MouseTestHelper _helper = new MouseTestHelper(); - + [Fact] public void SelectedIndex_Should_Initially_Be_Minus_1() { @@ -168,6 +168,130 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("B", listBox.SelectedItem); } + [Fact] + public void Setting_SelectedIndex_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 1 + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + [Fact] + public void Setting_SelectedIndex_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 2 + }; + + listBox.BeginInit(); + + listBox.SelectedIndex = 1; + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + [Fact] + public void Setting_SelectedItem_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single, + Items = new[] { "foo", "bar", "baz" }, + SelectedItem = "bar" + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + + + [Fact] + public void Setting_SelectedItems_Before_Initialize_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Multiple, + Items = new[] { "foo", "bar", "baz" }, + }; + + var selected = new[] { "foo", "bar" }; + + foreach (var v in selected) + { + listBox.SelectedItems.Add(v); + } + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(selected, listBox.SelectedItems); + } + + [Fact] + public void Setting_SelectedItems_During_Initialize_Should_Take_Priority_Over_Previous_Value() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Multiple, + Items = new[] { "foo", "bar", "baz" }, + }; + + var selected = new[] { "foo", "bar" }; + + foreach (var v in new[] { "bar", "baz" }) + { + listBox.SelectedItems.Add(v); + } + + listBox.BeginInit(); + + listBox.SelectedItems = new AvaloniaList(selected); + + listBox.EndInit(); + + Assert.Equal(selected, listBox.SelectedItems); + } + + [Fact] + public void Setting_SelectedIndex_Before_Initialize_With_AlwaysSelected_Should_Retain() + { + var listBox = new ListBox + { + SelectionMode = SelectionMode.Single | SelectionMode.AlwaysSelected, + + Items = new[] { "foo", "bar", "baz" }, + SelectedIndex = 1 + }; + + listBox.BeginInit(); + + listBox.EndInit(); + + Assert.Equal(1, listBox.SelectedIndex); + Assert.Equal("bar", listBox.SelectedItem); + } + [Fact] public void Setting_SelectedIndex_Before_ApplyTemplate_Should_Set_Item_IsSelected_True() { @@ -849,7 +973,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new ListBox { Template = Template(), - Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz"}, + Items = new[] { "Foo", "Bar", "Baz", "Foo", "Bar", "Baz" }, }; target.ApplyTemplate(); From 2e70be023b4a5236d080be36b11572866acf0271 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sun, 20 Oct 2019 14:36:23 +0300 Subject: [PATCH 02/13] fixed #3089 now set selecteditem/s before initialization should work properly --- .../Primitives/SelectingItemsControl.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..4b33b18475 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1088,9 +1088,15 @@ namespace Avalonia.Controls.Primitives } else { - SelectedIndex = _updateSelectedIndex != int.MinValue ? - _updateSelectedIndex : - AlwaysSelected ? 0 : -1; + if (_updateSelectedIndex != int.MinValue) + { + SelectedIndex = _updateSelectedIndex; + } + + if (AlwaysSelected && SelectedIndex == -1) + { + SelectedIndex = 0; + } } } } From d8ee7531aba3bb77dd07c60012ec117d6c287c5c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 17:29:18 +0300 Subject: [PATCH 03/13] Properly raise PointerCaptureLost on capture transfer --- src/Avalonia.Input/Pointer.cs | 2 +- .../Avalonia.Input.UnitTests/PointerTests.cs | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Input.UnitTests/PointerTests.cs diff --git a/src/Avalonia.Input/Pointer.cs b/src/Avalonia.Input/Pointer.cs index 80d803abb1..819d231b31 100644 --- a/src/Avalonia.Input/Pointer.cs +++ b/src/Avalonia.Input/Pointer.cs @@ -37,7 +37,7 @@ namespace Avalonia.Input { if (Captured != null) Captured.DetachedFromVisualTree -= OnCaptureDetached; - var oldCapture = control; + var oldCapture = Captured; Captured = control; PlatformCapture(control); if (oldCapture != null) diff --git a/tests/Avalonia.Input.UnitTests/PointerTests.cs b/tests/Avalonia.Input.UnitTests/PointerTests.cs new file mode 100644 index 0000000000..e639726dbd --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/PointerTests.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class PointerTests + { + [Fact] + public void On_Capture_Transfer_PointerCaptureLost_Should_Propagate_Up_To_The_Common_Parent() + { + Border initialParent, initialCapture, newParent, newCapture; + var el = new StackPanel + { + Children = + { + (initialParent = new Border { Child = initialCapture = new Border() }), + (newParent = new Border { Child = newCapture = new Border() }) + } + }; + var receivers = new List(); + var root = new TestRoot(el); + foreach (InputElement d in root.GetSelfAndVisualDescendants()) + d.PointerCaptureLost += (s, e) => receivers.Add(s); + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + + pointer.Capture(initialCapture); + pointer.Capture(newCapture); + Assert.True(receivers.SequenceEqual(new[] { initialCapture, initialParent })); + + receivers.Clear(); + pointer.Capture(null); + Assert.True(receivers.SequenceEqual(new object[] { newCapture, newParent, el, root })); + } + } +} From 66b3da044c7a8af634e14bdc4e15beb0071b5eb7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 25 Oct 2019 18:20:01 +0300 Subject: [PATCH 04/13] Fixed MouseTestHelper --- tests/Avalonia.UnitTests/MouseTestHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.UnitTests/MouseTestHelper.cs b/tests/Avalonia.UnitTests/MouseTestHelper.cs index 48c4d73471..f6454a9cd2 100644 --- a/tests/Avalonia.UnitTests/MouseTestHelper.cs +++ b/tests/Avalonia.UnitTests/MouseTestHelper.cs @@ -84,9 +84,9 @@ namespace Avalonia.UnitTests ); if (ButtonCount(props) == 0) { - _pointer.Capture(null); target.RaiseEvent(new PointerReleasedEventArgs(source, _pointer, (IVisual)target, position, Timestamp(), props, GetModifiers(modifiers), _pressedButton)); + _pointer.Capture(null); } else Move(target, source, position); From 58b79a7724f1d492758a60bc7723abbc65b3be3b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 18:47:18 +0100 Subject: [PATCH 05/13] Validate min/max in MathUtilities.Clamp. --- src/Avalonia.Base/Utilities/MathUtilities.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 41b57b6e70..027028480c 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -159,6 +159,11 @@ namespace Avalonia.Utilities /// The clamped value. public static int Clamp(int val, int min, int max) { + if (min > max) + { + throw new ArgumentException($"{min} cannot be greater than {max}."); + } + if (val < min) { return min; From 0a8915b1cc9ffb2868a77e287527cb492dc272d6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 19:55:23 +0100 Subject: [PATCH 06/13] Added failing test for #3148 --- .../Primitives/SelectingItemsControlTests.cs | 61 +++++++++++++++++++ tests/Avalonia.UnitTests/TestRoot.cs | 11 ++++ 2 files changed, 72 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 17f0e609a5..e5bbde2eba 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.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; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -14,6 +15,7 @@ using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Data; +using Avalonia.Styling; using Avalonia.UnitTests; using Moq; using Xunit; @@ -980,6 +982,45 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(raised); } + [Fact] + public void AutoScrollToSelectedItem_On_Reset_Works() + { + // Issue #3148 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new ResettingCollection(100); + + var target = new ListBox + { + Items = items, + ItemTemplate = new FuncDataTemplate((x, _) => + new TextBlock + { + Text = x, + Width = 100, + Height = 10 + }), + AutoScrollToSelectedItem = true, + VirtualizationMode = ItemVirtualizationMode.Simple, + }; + + var root = new TestRoot(true, target); + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + Assert.True(target.Presenter.Panel.Children.Count > 0); + Assert.True(target.Presenter.Panel.Children.Count < 100); + + target.SelectedItem = "Item99"; + + // #3148 triggered here. + items.Reset(new[] { "Item99" }); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(1, target.Presenter.Panel.Children.Count); + } + } + [Fact] public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() { @@ -1028,6 +1069,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + [~ItemsPresenter.VirtualizationModeProperty] = control[~ListBox.VirtualizationModeProperty], }.RegisterInNameScope(scope)); } @@ -1072,5 +1114,24 @@ namespace Avalonia.Controls.UnitTests.Primitives return base.MoveSelection(direction, wrap); } } + + private class ResettingCollection : List, INotifyCollectionChanged + { + public ResettingCollection(int itemCount) + { + AddRange(Enumerable.Range(0, itemCount).Select(x => $"Item{x}")); + } + + public void Reset(IEnumerable items) + { + Clear(); + AddRange(items); + CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } } } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 969d7bc821..56d7f028f2 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -24,8 +24,19 @@ namespace Avalonia.UnitTests } public TestRoot(IControl child) + : this(false, child) + { + Child = child; + } + + public TestRoot(bool useGlobalStyles, IControl child) : this() { + if (useGlobalStyles) + { + StylingParent = UnitTestApplication.Current; + } + Child = child; } From 4f41a70455516f9ee73c87212ea7913a038deb97 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 28 Oct 2019 21:45:06 +0100 Subject: [PATCH 07/13] Make sure ItemsPresenter is updated before selection change. Having a separate subscription to `Items.CollectionChanged` in both `ItemsControl` and `ItemsPresenter` meant that the `ItemsPresenter` sometimes doesn't update itself in time for a selection change, resulting in #3148. Make `ItemsControl` trigger `ItemsPresenter.ItemsChanged` rather than both controls listening to the event separately. --- src/Avalonia.Controls/ItemsControl.cs | 6 ++++-- .../Presenters/IItemsPresenter.cs | 4 ++++ .../Presenters/ItemsPresenterBase.cs | 15 +++++++++++++-- .../Primitives/SelectingItemsControl.cs | 17 +++++++++++++---- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b027da6d0c..b93346792d 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -356,6 +356,7 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; + Presenter?.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); @@ -370,6 +371,9 @@ namespace Avalonia.Controls /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + UpdateItemCount(); + Presenter?.ItemsChanged(e); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -381,8 +385,6 @@ namespace Avalonia.Controls break; } - UpdateItemCount(); - var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 42311dc781..21a03402a0 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +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 System.Collections.Specialized; + namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { IPanel Panel { get; } + void ItemsChanged(NotifyCollectionChangedEventArgs e); + void ScrollIntoView(object item); } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 0f0cdc37cf..ef1f277162 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls.Presenters _itemsSubscription?.Dispose(); _itemsSubscription = null; - if (_createdPanel && value is INotifyCollectionChanged incc) + if (!IsHosted && _createdPanel && value is INotifyCollectionChanged incc) { _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } @@ -130,6 +130,8 @@ namespace Avalonia.Controls.Presenters private set; } + protected bool IsHosted => TemplatedParent is IItemsPresenterHost; + /// public override sealed void ApplyTemplate() { @@ -144,6 +146,15 @@ namespace Avalonia.Controls.Presenters { } + /// + void IItemsPresenter.ItemsChanged(NotifyCollectionChangedEventArgs e) + { + if (Panel != null) + { + ItemsChanged(e); + } + } + /// /// Creates the for the control. /// @@ -215,7 +226,7 @@ namespace Avalonia.Controls.Presenters _createdPanel = true; - if (_itemsSubscription == null && Items is INotifyCollectionChanged incc) + if (!IsHosted && _itemsSubscription == null && Items is INotifyCollectionChanged incc) { _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7fddee1012..abad53f0d6 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -302,13 +302,24 @@ namespace Avalonia.Controls.Primitives /// protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { - base.ItemsCollectionChanged(sender, e); - if (_updateCount > 0) { + base.ItemsCollectionChanged(sender, e); return; } + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); + break; + case NotifyCollectionChangedAction.Remove: + _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); + break; + } + + base.ItemsCollectionChanged(sender, e); + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -318,14 +329,12 @@ namespace Avalonia.Controls.Primitives } else { - _selection.ItemsInserted(e.NewStartingIndex, e.NewItems.Count); UpdateSelectedItem(_selection.First(), false); } break; case NotifyCollectionChangedAction.Remove: - _selection.ItemsRemoved(e.OldStartingIndex, e.OldItems.Count); UpdateSelectedItem(_selection.First(), false); ResetSelectedItems(); break; From 96619e44408aea5319a80b40327d67cee94c4615 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 14:49:19 +0100 Subject: [PATCH 08/13] Added failing test. `ItemsPresenter.Items` isn't correctly in sync with `ItemsControl.Items` when assigning new `Items`. --- .../ItemsControlTests.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index b2839360ee..c338d29e96 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -522,6 +522,36 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Presenter_Items_Should_Be_In_Sync() + { + var target = new ItemsControl + { + Template = GetTemplate(), + Items = new object[] + { + new Button(), + new Button(), + }, + }; + + var root = new TestRoot { Child = target }; + var otherPanel = new StackPanel(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + target.ItemContainerGenerator.Materialized += (s, e) => + { + Assert.IsType(e.Containers[0].Item); + }; + + target.Items = new[] + { + new Canvas() + }; + } + private class Item { public Item(string value) From 496fae20e55995017faf8f6f5672c507dc45fd15 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 14:59:47 +0100 Subject: [PATCH 09/13] Ensure Items are synchronised with Presenter. Ensure that the `ItemsControl`'s presenter is notified of an `Items` change at the appropriate time. --- src/Avalonia.Controls/ItemsControl.cs | 7 ++++++- src/Avalonia.Controls/Presenters/IItemsPresenter.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b93346792d..1203792559 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -356,10 +356,15 @@ namespace Avalonia.Controls var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; - Presenter?.ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); + + if (Presenter != null) + { + Presenter.Items = newValue; + } + SubscribeToItems(newValue); } diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs index 21a03402a0..c4acf1ebef 100644 --- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs @@ -1,12 +1,15 @@ // 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.Specialized; namespace Avalonia.Controls.Presenters { public interface IItemsPresenter : IPresenter { + IEnumerable Items { get; set; } + IPanel Panel { get; } void ItemsChanged(NotifyCollectionChangedEventArgs e); From 9107c0e96eea42756f7d6ea26013979986e7adfe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Oct 2019 23:01:18 +0100 Subject: [PATCH 10/13] Added another failing test. Adapted from failing test described by @MarchingCube in PR feedback: https://github.com/AvaloniaUI/Avalonia/pull/3177#issuecomment-547515972 --- .../ItemsControlTests.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index c338d29e96..227d783874 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -12,6 +12,7 @@ using Xunit; using System.Collections.ObjectModel; using Avalonia.UnitTests; using Avalonia.Input; +using System.Collections.Generic; namespace Avalonia.Controls.UnitTests { @@ -104,6 +105,28 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { child }, target.GetLogicalChildren()); } + [Fact] + public void Added_Container_Should_Have_LogicalParent_Set_To_ItemsControl() + { + var item = new Border(); + var items = new ObservableCollection(); + + var target = new ItemsControl + { + Template = GetTemplate(), + Items = items, + }; + + var root = new TestRoot(true, target); + + root.Measure(new Size(100, 100)); + root.Arrange(new Rect(0, 0, 100, 100)); + + items.Add(item); + + Assert.Equal(target, item.Parent); + } + [Fact] public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate() { From 68b655b5177175fc1b168b2130064992575c1fb6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Oct 2019 07:32:38 +0100 Subject: [PATCH 11/13] Add to logical children before notifying presenter. This makes sure that newly added control items have the correct logical parent. --- src/Avalonia.Controls/ItemsControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1203792559..bf22f0a08a 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -377,7 +377,6 @@ namespace Avalonia.Controls protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { UpdateItemCount(); - Presenter?.ItemsChanged(e); switch (e.Action) { @@ -390,6 +389,8 @@ namespace Avalonia.Controls break; } + Presenter?.ItemsChanged(e); + var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); PseudoClasses.Set(":singleitem", collection != null && collection.Count == 1); From e1df65c94b4b829b87403e472da0d8931ce5cddd Mon Sep 17 00:00:00 2001 From: mstr2 Date: Fri, 1 Nov 2019 18:36:25 +0100 Subject: [PATCH 12/13] Failing unit test for #3197 --- .../Properties/AssemblyInfo.cs | 1 + .../TransitionsTests.cs | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Animation/Properties/AssemblyInfo.cs b/src/Avalonia.Animation/Properties/AssemblyInfo.cs index eb38a66a84..8523b9537d 100644 --- a/src/Avalonia.Animation/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Animation/Properties/AssemblyInfo.cs @@ -10,3 +10,4 @@ using System.Runtime.CompilerServices; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")] [assembly: InternalsVisibleTo("Avalonia.LeakTests")] +[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")] diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index f1b4b0d071..22f3b4f501 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -1,14 +1,7 @@ using System; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Animation; using Avalonia.Controls; -using Avalonia.Styling; using Avalonia.UnitTests; -using Avalonia.Data; using Xunit; -using Avalonia.Animation.Easings; namespace Avalonia.Animation.UnitTests { @@ -69,5 +62,26 @@ namespace Avalonia.Animation.UnitTests Assert.Equal(0, border.Opacity); } } + + [Fact] + public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick() + { + var clock = new MockGlobalClock(); + + using (UnitTestApplication.Start(new TestServices(globalClock: clock))) + { + int i = 0; + var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue => + { + switch (i++) + { + case 0: Assert.Equal(0, nextValue); break; + case 1: Assert.Equal(1d, nextValue); break; + } + }); + + clock.Pulse(TimeSpan.FromMilliseconds(10)); + } + } } } From 533c17a1816b8eb7b8cc38670634e3f9690d264a Mon Sep 17 00:00:00 2001 From: mstr2 Date: Fri, 1 Nov 2019 18:37:18 +0100 Subject: [PATCH 13/13] TransitionInstance with zero duration is now completed on first tick --- src/Avalonia.Animation/TransitionInstance.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/TransitionInstance.cs b/src/Avalonia.Animation/TransitionInstance.cs index 10ea6bf523..a69ad50a4b 100644 --- a/src/Avalonia.Animation/TransitionInstance.cs +++ b/src/Avalonia.Animation/TransitionInstance.cs @@ -28,7 +28,7 @@ namespace Avalonia.Animation private void TimerTick(TimeSpan t) { - var interpVal = (double)t.Ticks / _duration.Ticks; + var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks; // Clamp interpolation value. if (interpVal >= 1d | interpVal < 0d)