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/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)
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;
diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs
index b027da6d0c..bf22f0a08a 100644
--- a/src/Avalonia.Controls/ItemsControl.cs
+++ b/src/Avalonia.Controls/ItemsControl.cs
@@ -359,6 +359,12 @@ namespace Avalonia.Controls
UpdateItemCount();
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
+
+ if (Presenter != null)
+ {
+ Presenter.Items = newValue;
+ }
+
SubscribeToItems(newValue);
}
@@ -370,6 +376,8 @@ namespace Avalonia.Controls
/// The event args.
protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
+ UpdateItemCount();
+
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@@ -381,7 +389,7 @@ namespace Avalonia.Controls
break;
}
- UpdateItemCount();
+ Presenter?.ItemsChanged(e);
var collection = sender as ICollection;
PseudoClasses.Set(":empty", collection == null || collection.Count == 0);
diff --git a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
index 42311dc781..c4acf1ebef 100644
--- a/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/IItemsPresenter.cs
@@ -1,12 +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.Collections;
+using System.Collections.Specialized;
+
namespace Avalonia.Controls.Presenters
{
public interface IItemsPresenter : IPresenter
{
+ IEnumerable Items { get; set; }
+
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..b6ae567123 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;
@@ -1088,9 +1097,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;
+ }
}
}
}
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.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));
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
index b2839360ee..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()
{
@@ -522,6 +545,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