Browse Source

Reimplemented Carousel.

Only `VirtualizingCarouselPanel` currently implemented.
pull/9677/head
Steven Kirk 3 years ago
parent
commit
f0c89a614e
  1. 4
      samples/ControlCatalog/Pages/CarouselPage.xaml
  2. 50
      src/Avalonia.Controls/Carousel.cs
  3. 276
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  4. 351
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  5. 20
      src/Avalonia.Themes.Fluent/Controls/Carousel.xaml
  6. 23
      src/Avalonia.Themes.Simple/Controls/Carousel.xaml
  7. 652
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  8. 732
      tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs
  9. 271
      tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs
  10. 5
      tests/Avalonia.UnitTests/UnitTestSynchronizationContext.cs

4
samples/ControlCatalog/Pages/CarouselPage.xaml

@ -12,7 +12,7 @@
</Button>
<Carousel Name="carousel" Grid.Column="1">
<Carousel.PageTransition>
<PageSlide Duration="0.25" Orientation="Vertical" />
<PageSlide Duration="0.25" Orientation="Horizontal" />
</Carousel.PageTransition>
<Image Source="/Assets/delicate-arch-896885_640.jpg"/>
<Image Source="/Assets/hirsch-899118_640.jpg"/>
@ -35,7 +35,7 @@
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock VerticalAlignment="Center">Orientation</TextBlock>
<ComboBox Name="orientation" SelectedIndex="1" VerticalAlignment="Center">
<ComboBox Name="orientation" SelectedIndex="0" VerticalAlignment="Center">
<ComboBoxItem>Horizontal</ComboBoxItem>
<ComboBoxItem>Vertical</ComboBoxItem>
</ComboBox>

50
src/Avalonia.Controls/Carousel.cs

@ -2,7 +2,6 @@ using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Input;
namespace Avalonia.Controls
{
@ -11,12 +10,6 @@ namespace Avalonia.Controls
/// </summary>
public class Carousel : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="IsVirtualized"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVirtualizedProperty =
AvaloniaProperty.Register<Carousel, bool>(nameof(IsVirtualized), true);
/// <summary>
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
@ -28,7 +21,9 @@ namespace Avalonia.Controls
/// <see cref="Carousel"/>.
/// </summary>
private static readonly ITemplate<Panel> PanelTemplate =
new FuncTemplate<Panel>(() => new Panel());
new FuncTemplate<Panel>(() => new VirtualizingCarouselPanel());
private IScrollable? _scroller;
/// <summary>
/// Initializes static members of the <see cref="Carousel"/> class.
@ -38,18 +33,6 @@ namespace Avalonia.Controls
SelectionModeProperty.OverrideDefaultValue<Carousel>(SelectionMode.AlwaysSelected);
ItemsPanelProperty.OverrideDefaultValue<Carousel>(PanelTemplate);
}
/// <summary>
/// Gets or sets a value indicating whether the items in the carousel are virtualized.
/// </summary>
/// <remarks>
/// When the carousel is virtualized, only the active page is held in memory.
/// </remarks>
public bool IsVirtualized
{
get { return GetValue(IsVirtualizedProperty); }
set { SetValue(IsVirtualizedProperty, value); }
}
/// <summary>
/// Gets or sets the transition to use when moving between pages.
@ -81,5 +64,32 @@ namespace Avalonia.Controls
--SelectedIndex;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
if (_scroller is not null)
_scroller.Offset = new(SelectedIndex, 0);
return result;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_scroller = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SelectedIndexProperty && _scroller is not null)
{
var value = change.GetNewValue<int>();
_scroller.Offset = new(value, 0);
}
}
}
}

276
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -1,276 +0,0 @@
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Data;
namespace Avalonia.Controls.Presenters
{
/// <summary>
/// Displays pages inside an <see cref="ItemsControl"/>.
/// </summary>
public class CarouselPresenter : ItemsPresenter
{
/// <summary>
/// Defines the <see cref="IsVirtualized"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVirtualizedProperty =
Carousel.IsVirtualizedProperty.AddOwner<CarouselPresenter>();
/// <summary>
/// Defines the <see cref="SelectedIndex"/> property.
/// </summary>
public static readonly DirectProperty<CarouselPresenter, int> SelectedIndexProperty =
SelectingItemsControl.SelectedIndexProperty.AddOwner<CarouselPresenter>(
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v);
/// <summary>
/// Defines the <see cref="PageTransition"/> property.
/// </summary>
public static readonly StyledProperty<IPageTransition?> PageTransitionProperty =
Carousel.PageTransitionProperty.AddOwner<CarouselPresenter>();
private int _selectedIndex = -1;
private Task? _currentTransition;
private int _queuedTransitionIndex = -1;
/// <summary>
/// Initializes static members of the <see cref="CarouselPresenter"/> class.
/// </summary>
static CarouselPresenter()
{
////IsVirtualizedProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.IsVirtualizedChanged(e));
////SelectedIndexProperty.Changed.AddClassHandler<CarouselPresenter>((x, e) => x.SelectedIndexChanged(e));
}
/// <summary>
/// Gets or sets a value indicating whether the items in the carousel are virtualized.
/// </summary>
/// <remarks>
/// When the carousel is virtualized, only the active page is held in memory.
/// </remarks>
public bool IsVirtualized
{
get { return GetValue(IsVirtualizedProperty); }
set { SetValue(IsVirtualizedProperty, value); }
}
/// <summary>
/// Gets or sets the index of the selected page.
/// </summary>
public int SelectedIndex
{
get
{
return _selectedIndex;
}
set
{
////var old = SelectedIndex;
////var effective = (value >= 0 && value < Items?.Cast<object>().Count()) ? value : -1;
////if (old != effective)
////{
//// _selectedIndex = effective;
//// RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue);
////}
}
}
/// <summary>
/// Gets or sets a transition to use when switching pages.
/// </summary>
public IPageTransition? PageTransition
{
get { return GetValue(PageTransitionProperty); }
set { SetValue(PageTransitionProperty, value); }
}
/// <inheritdoc/>
////protected override void ItemsChanged(NotifyCollectionChangedEventArgs e)
////{
//// if (!IsVirtualized)
//// {
//// base.ItemsChanged(e);
//// if (Items == null || SelectedIndex >= Items.Count())
//// {
//// SelectedIndex = Items.Count() - 1;
//// }
//// foreach (var c in ItemContainerGenerator.Containers)
//// {
//// c.ContainerControl.IsVisible = c.Index == SelectedIndex;
//// }
//// }
//// else if (SelectedIndex != -1 && Panel != null)
//// {
//// switch (e.Action)
//// {
//// case NotifyCollectionChangedAction.Add:
//// if (e.NewStartingIndex > SelectedIndex)
//// {
//// return;
//// }
//// break;
//// case NotifyCollectionChangedAction.Remove:
//// if (e.OldStartingIndex > SelectedIndex)
//// {
//// return;
//// }
//// break;
//// case NotifyCollectionChangedAction.Replace:
//// if (e.OldStartingIndex > SelectedIndex ||
//// e.OldStartingIndex + e.OldItems!.Count - 1 < SelectedIndex)
//// {
//// return;
//// }
//// break;
//// case NotifyCollectionChangedAction.Move:
//// if (e.OldStartingIndex > SelectedIndex &&
//// e.NewStartingIndex > SelectedIndex)
//// {
//// return;
//// }
//// break;
//// }
//// if (Items == null || SelectedIndex >= Items.Count())
//// {
//// SelectedIndex = Items.Count() - 1;
//// }
//// Panel.Children.Clear();
//// ItemContainerGenerator.Clear();
//// if (SelectedIndex != -1)
//// {
//// GetOrCreateContainer(SelectedIndex);
//// }
//// }
////}
////protected override void PanelCreated(Panel panel)
////{
//// ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
////}
/////// <summary>
/////// Moves to the selected page, animating if a <see cref="PageTransition"/> is set.
/////// </summary>
/////// <param name="fromIndex">The index of the old page.</param>
/////// <param name="toIndex">The index of the new page.</param>
/////// <returns>A task tracking the animation.</returns>
////private async Task MoveToPage(int fromIndex, int toIndex)
////{
//// if (fromIndex != toIndex)
//// {
//// var generator = ItemContainerGenerator;
//// Control? from = null;
//// Control? to = null;
//// if (fromIndex != -1)
//// {
//// from = generator.ContainerFromIndex(fromIndex);
//// }
//// if (toIndex != -1)
//// {
//// to = GetOrCreateContainer(toIndex);
//// }
//// if (PageTransition != null && (from != null || to != null))
//// {
//// await PageTransition.Start((Visual?)from, (Visual?)to, fromIndex < toIndex, default);
//// }
//// else if (to != null)
//// {
//// to.IsVisible = true;
//// }
//// if (from != null)
//// {
//// if (IsVirtualized)
//// {
//// Panel!.Children.Remove(from);
//// generator.Dematerialize(fromIndex, 1);
//// }
//// else
//// {
//// from.IsVisible = false;
//// }
//// }
//// }
////}
////private Control? GetOrCreateContainer(int index)
////{
//// var container = ItemContainerGenerator.ContainerFromIndex(index);
//// if (container == null && IsVirtualized)
//// {
//// var item = Items!.Cast<object>().ElementAt(index);
//// var materialized = ItemContainerGenerator.Materialize(index, item);
//// Panel!.Children.Add(materialized.ContainerControl);
//// container = materialized.ContainerControl;
//// }
//// return container;
////}
/////// <summary>
/////// Called when the <see cref="IsVirtualized"/> property changes.
/////// </summary>
/////// <param name="e">The event args.</param>
////private void IsVirtualizedChanged(AvaloniaPropertyChangedEventArgs e)
////{
//// if (Panel != null)
//// {
//// ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
//// }
////}
/////// <summary>
/////// Called when the <see cref="SelectedIndex"/> property changes.
/////// </summary>
/////// <param name="e">The event args.</param>
////private async void SelectedIndexChanged(AvaloniaPropertyChangedEventArgs e)
////{
//// if (Panel != null)
//// {
//// if (_currentTransition == null)
//// {
//// int fromIndex = (int)e.OldValue!;
//// int toIndex = (int)e.NewValue!;
//// for (;;)
//// {
//// _currentTransition = MoveToPage(fromIndex, toIndex);
//// await _currentTransition;
//// if (_queuedTransitionIndex != -1)
//// {
//// fromIndex = toIndex;
//// toIndex = _queuedTransitionIndex;
//// _queuedTransitionIndex = -1;
//// }
//// else
//// {
//// _currentTransition = null;
//// break;
//// }
//// }
//// }
//// else
//// {
//// _queuedTransitionIndex = (int)e.NewValue!;
//// }
//// }
////}
}
}

351
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -0,0 +1,351 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
namespace Avalonia.Controls
{
/// <summary>
/// A panel used by <see cref="Carousel"/> to display the current item.
/// </summary>
public class VirtualizingCarouselPanel : VirtualizingPanel, ILogicalScrollable
{
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingCarouselPanel, Control, bool>("ItemIsOwnContainer");
private Size _extent;
private Vector _offset;
private Size _viewport;
private Stack<Control>? _recyclePool;
private Control? _realized;
private int _realizedIndex = -1;
private Control? _transitionFrom;
private int _transitionFromIndex = -1;
private CancellationTokenSource? _transition;
private EventHandler? _scrollInvalidated;
bool ILogicalScrollable.CanHorizontallyScroll { get; set; }
bool ILogicalScrollable.CanVerticallyScroll { get; set; }
bool ILogicalScrollable.IsLogicalScrollEnabled => true;
Size ILogicalScrollable.ScrollSize => new(1, 1);
Size ILogicalScrollable.PageScrollSize => new(1, 1);
Size IScrollable.Extent => Extent;
Size IScrollable.Viewport => Viewport;
Vector IScrollable.Offset
{
get => _offset;
set
{
if ((int)_offset.X != value.X)
InvalidateMeasure();
_offset = value;
}
}
private Size Extent
{
get => _extent;
set
{
if (_extent != value)
{
_extent = value;
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
}
}
}
private Size Viewport
{
get => _viewport;
set
{
if (_viewport != value)
{
_viewport = value;
_scrollInvalidated?.Invoke(this, EventArgs.Empty);
}
}
}
event EventHandler? ILogicalScrollable.ScrollInvalidated
{
add => _scrollInvalidated += value;
remove => _scrollInvalidated -= value;
}
bool ILogicalScrollable.BringIntoView(Control target, Rect targetRect)
{
throw new NotImplementedException();
}
Control? ILogicalScrollable.GetControlInDirection(NavigationDirection direction, Control? from)
{
throw new NotImplementedException();
}
void ILogicalScrollable.RaiseScrollInvalidated(EventArgs e) => _scrollInvalidated?.Invoke(this, e);
protected override Size MeasureOverride(Size availableSize)
{
var items = ItemsControl?.Items as IList ?? Array.Empty<object?>();
var index = (int)_offset.X;
if (index != _realizedIndex)
{
if (_realized is not null)
{
var cancelTransition = _transition is not null;
// Cancel any already running transition, and recycle the element we're transitioning from.
if (cancelTransition)
{
_transition!.Cancel();
_transition = null;
if (_transitionFrom is not null)
RecycleElement(_transitionFrom);
_transitionFrom = null;
_transitionFromIndex = -1;
}
if (cancelTransition || GetTransition() is null)
{
// If don't have a transition or we've just canceled a transition then recycle the element
// we're moving from.
RecycleElement(_realized);
}
else
{
// We have a transition to do: record the current element as the element we're transitioning
// from and we'll start the transition in the arrange pass.
_transitionFrom = _realized;
_transitionFromIndex = _realizedIndex;
}
_realized = null;
_realizedIndex = -1;
}
// Get or create an element for the new item.
if (index >= 0 && index < items.Count)
{
_realized = GetOrCreateElement(items, index);
_realizedIndex = index;
}
}
if (_realized is null)
{
Extent = Viewport = new(0, 0);
_transitionFrom = null;
_transitionFromIndex = -1;
return default;
}
_realized.Measure(availableSize);
Extent = new(items.Count, 1);
Viewport = new(1, 1);
return _realized.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var result = base.ArrangeOverride(finalSize);
if (_transition is null &&
_transitionFrom is not null &&
_realized is { } to &&
GetTransition() is { } transition)
{
_transition = new CancellationTokenSource();
transition.Start(_transitionFrom, to, _realizedIndex > _transitionFromIndex, _transition.Token)
.ContinueWith(TransitionFinished, TaskScheduler.FromCurrentSynchronizationContext());
}
return result;
}
protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap) => null;
protected internal override Control? ContainerFromIndex(int index)
{
return index == _realizedIndex ? _realized : null;
}
protected internal override IEnumerable<Control>? GetRealizedContainers()
{
return _realized is not null ? new[] { _realized } : null;
}
protected internal override int IndexFromContainer(Control container)
{
return container == _realized ? _realizedIndex : -1;
}
protected internal override Control? ScrollIntoView(int index)
{
return null;
}
protected override void OnItemsChanged(IList items, NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(items, e);
void Add(int index, int count)
{
if (index <= _realizedIndex)
_realizedIndex += count;
}
void Remove(int index, int count)
{
var end = index + (count - 1);
if (_realized is not null && index <= _realizedIndex && end >= _realizedIndex)
{
RecycleElement(_realized);
_realized = null;
_realizedIndex = -1;
}
else if (index < _realizedIndex)
{
_realizedIndex -= count;
}
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Add(e.NewStartingIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Remove:
Remove(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
Remove(e.OldStartingIndex, e.OldItems!.Count);
Add(e.NewStartingIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Reset:
if (_realized is not null)
{
RecycleElement(_realized);
_realized = null;
_realizedIndex = -1;
}
break;
}
InvalidateMeasure();
}
private Control GetOrCreateElement(IList items, int index)
{
return GetRealizedElement(index) ??
GetItemIsOwnContainer(items, index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
}
private Control? GetRealizedElement(int index)
{
return _realizedIndex == index ? _realized : null;
}
private Control? GetItemIsOwnContainer(IList items, int index)
{
Debug.Assert(ItemsControl is not null);
if (items[index] is Control controlItem)
{
var generator = ItemsControl!.ItemContainerGenerator;
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
AddInternalChild(controlItem);
generator.PrepareItemContainer(controlItem, controlItem, index);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
return controlItem;
}
}
return null;
}
private Control? GetRecycledElement(IList items, int index)
{
Debug.Assert(ItemsControl is not null);
var generator = ItemsControl!.ItemContainerGenerator;
var item = items[index];
if (_recyclePool?.Count > 0)
{
var recycled = _recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
return recycled;
}
return null;
}
private Control CreateElement(IList items, int index)
{
Debug.Assert(ItemsControl is not null);
var generator = ItemsControl!.ItemContainerGenerator;
var item = items[index];
var container = generator.CreateContainer();
AddInternalChild(container);
generator.PrepareItemContainer(container, item, index);
return container;
}
private void RecycleElement(Control element)
{
Debug.Assert(ItemsControl is not null);
if (element.IsSet(ItemIsOwnContainerProperty))
{
element.IsVisible = false;
}
else
{
ItemsControl!.ItemContainerGenerator.ClearItemContainer(element);
_recyclePool ??= new();
_recyclePool.Push(element);
element.IsVisible = false;
}
}
private IPageTransition? GetTransition() => (ItemsControl as Carousel)?.PageTransition;
private void TransitionFinished(Task task)
{
if (task.IsCanceled)
return;
if (_transitionFrom is not null)
RecycleElement(_transitionFrom);
_transition = null;
_transitionFrom = null;
_transitionFromIndex = -1;
}
}
}

20
src/Avalonia.Themes.Fluent/Controls/Carousel.xaml

@ -3,16 +3,16 @@
<ControlTheme x:Key="{x:Type Carousel}" TargetType="Carousel">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<CarouselPresenter IsVirtualized="{TemplateBinding IsVirtualized}"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"
SelectedIndex="{TemplateBinding SelectedIndex}"
PageTransition="{TemplateBinding PageTransition}"/>
</Border>
<ScrollViewer Name="PART_ScrollViewer"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</ControlTemplate>
</Setter>
</ControlTheme>

23
src/Avalonia.Themes.Simple/Controls/Carousel.xaml

@ -1,19 +1,18 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type Carousel}"
TargetType="Carousel">
<ControlTheme x:Key="{x:Type Carousel}" TargetType="Carousel">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<CarouselPresenter Margin="{TemplateBinding Padding}"
IsVirtualized="{TemplateBinding IsVirtualized}"
ItemsPanel="{TemplateBinding ItemsPanel}"
PageTransition="{TemplateBinding PageTransition}"
SelectedIndex="{TemplateBinding SelectedIndex}" />
</Border>
<ScrollViewer Name="PART_ScrollViewer"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</ControlTemplate>
</Setter>
</ControlTheme>

652
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@ -1,341 +1,321 @@
////using System.Collections.ObjectModel;
////using System.Linq;
////using System.Reactive.Subjects;
////using Avalonia.Controls.Presenters;
////using Avalonia.Controls.Templates;
////using Avalonia.Data;
////using Avalonia.LogicalTree;
////using Avalonia.UnitTests;
////using Avalonia.VisualTree;
////using Xunit;
////namespace Avalonia.Controls.UnitTests
////{
//// public class CarouselTests
//// {
//// [Fact]
//// public void First_Item_Should_Be_Selected_By_Default()
//// {
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = new[]
//// {
//// "Foo",
//// "Bar"
//// }
//// };
//// target.ApplyTemplate();
//// Assert.Equal(0, target.SelectedIndex);
//// Assert.Equal("Foo", target.SelectedItem);
//// }
//// [Fact]
//// public void LogicalChild_Should_Be_Selected_Item()
//// {
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = new[]
//// {
//// "Foo",
//// "Bar"
//// }
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// Assert.Single(target.GetLogicalChildren());
//// var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
//// Assert.Equal("Foo", child.Text);
//// }
//// [Fact]
//// public void Should_Remove_NonCurrent_Page_When_IsVirtualized_True()
//// {
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = new[] { "foo", "bar" },
//// IsVirtualized = true,
//// SelectedIndex = 0,
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// Assert.Single(target.ItemContainerGenerator.Containers);
//// target.SelectedIndex = 1;
//// Assert.Single(target.ItemContainerGenerator.Containers);
//// }
//// [Fact]
//// public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "Foo",
//// "Bar",
//// "FooBar"
//// };
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = items,
//// IsVirtualized = false
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// Assert.Equal(3, target.GetLogicalChildren().Count());
//// var child = GetContainerTextBlock(target.GetLogicalChildren().First());
//// Assert.Equal("Foo", child.Text);
//// var newItems = items.ToList();
//// newItems.RemoveAt(0);
//// target.Items = newItems;
//// child = GetContainerTextBlock(target.GetLogicalChildren().First());
//// Assert.Equal("Bar", child.Text);
//// }
//// [Fact]
//// public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes_And_Virtualized()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "Foo",
//// "Bar",
//// "FooBar"
//// };
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = items,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// Assert.Single(target.GetLogicalChildren());
//// var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
//// Assert.Equal("Foo", child.Text);
//// var newItems = items.ToList();
//// newItems.RemoveAt(0);
//// target.Items = newItems;
//// child = GetContainerTextBlock(target.GetLogicalChildren().Single());
//// Assert.Equal("Bar", child.Text);
//// }
//// [Fact]
//// public void Selected_Item_Changes_To_First_Item_When_Item_Added()
//// {
//// var items = new ObservableCollection<string>();
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = items,
//// IsVirtualized = false
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class CarouselTests
{
[Fact]
public void First_Item_Should_Be_Selected_By_Default()
{
using var app = Start();
var target = new Carousel
{
Template = CarouselTemplate(),
Items = new[]
{
"Foo",
"Bar"
}
};
Prepare(target);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("Foo", target.SelectedItem);
}
[Fact]
public void LogicalChild_Should_Be_Selected_Item()
{
using var app = Start();
var target = new Carousel
{
Template = CarouselTemplate(),
Items = new[]
{
"Foo",
"Bar"
}
};
Prepare(target);
Assert.Single(target.GetRealizedContainers());
var child = GetContainerTextBlock(target.GetRealizedContainers().Single());
Assert.Equal("Foo", child.Text);
}
[Fact]
public void Selected_Item_Changes_To_First_Item_When_Items_Property_Changes()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
{
Template = CarouselTemplate(),
Items = items,
};
Prepare(target);
Assert.Single(target.GetRealizedContainers());
var child = GetContainerTextBlock(target.GetRealizedContainers().Single());
Assert.Equal("Foo", child.Text);
var newItems = items.ToList();
newItems.RemoveAt(0);
Layout(target);
target.Items = newItems;
Layout(target);
child = GetContainerTextBlock(target.GetRealizedContainers().Single());
Assert.Equal("Bar", child.Text);
}
[Fact]
public void Selected_Item_Changes_To_First_Item_When_Item_Added()
{
using var app = Start();
var items = new ObservableCollection<string>();
var target = new Carousel
{
Template = CarouselTemplate(),
Items = items,
};
Prepare(target);
Assert.Equal(-1, target.SelectedIndex);
Assert.Empty(target.GetRealizedContainers());
items.Add("Foo");
Layout(target);
Assert.Equal(0, target.SelectedIndex);
Assert.Single(target.GetRealizedContainers());
}
[Fact]
public void Selected_Index_Changes_To_None_When_Items_Assigned_Null()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
{
Template = CarouselTemplate(),
Items = items,
};
Prepare(target);
Assert.Equal(1, target.GetRealizedContainers().Count());
var child = GetContainerTextBlock(target.GetRealizedContainers().First());
Assert.Equal("Foo", child.Text);
target.Items = null;
Layout(target);
var numChildren = target.GetRealizedContainers().Count();
//// Assert.Equal(-1, target.SelectedIndex);
//// Assert.Empty(target.GetLogicalChildren());
Assert.Equal(-1, target.SelectedIndex);
}
//// items.Add("Foo");
[Fact]
public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
{
Template = CarouselTemplate(),
Items = items,
SelectedIndex = 2
};
//// Assert.Equal(0, target.SelectedIndex);
//// Assert.Single(target.GetLogicalChildren());
//// }
Prepare(target);
Assert.Equal("FooBar", target.SelectedItem);
//// [Fact]
//// public void Selected_Index_Changes_To_None_When_Items_Assigned_Null()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "Foo",
//// "Bar",
//// "FooBar"
//// };
var child = GetContainerTextBlock(target.GetRealizedContainers().LastOrDefault());
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = items,
//// IsVirtualized = false
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// Assert.Equal(3, target.GetLogicalChildren().Count());
//// var child = GetContainerTextBlock(target.GetLogicalChildren().First());
//// Assert.Equal("Foo", child.Text);
//// target.Items = null;
//// var numChildren = target.GetLogicalChildren().Count();
//// Assert.Equal(0, numChildren);
//// Assert.Equal(-1, target.SelectedIndex);
//// }
//// [Fact]
//// public void Selected_Index_Is_Maintained_Carousel_Created_With_Non_Zero_SelectedIndex()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "Foo",
//// "Bar",
//// "FooBar"
//// };
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = items,
//// IsVirtualized = false,
//// SelectedIndex = 2
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// Assert.Equal("FooBar", target.SelectedItem);
//// var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault());
//// Assert.IsType<TextBlock>(child);
//// Assert.Equal("FooBar", ((TextBlock)child).Text);
//// }
//// [Fact]
//// public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "Foo",
//// "Bar",
//// "FooBar"
//// };
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = items,
//// IsVirtualized = false
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// Assert.Equal(3, target.GetLogicalChildren().Count());
//// var child = GetContainerTextBlock(target.GetLogicalChildren().First());
//// Assert.Equal("Foo", child.Text);
//// items.RemoveAt(0);
//// child = GetContainerTextBlock(target.GetLogicalChildren().First());
//// Assert.IsType<TextBlock>(child);
//// Assert.Equal("Bar", ((TextBlock)child).Text);
//// }
//// [Fact]
//// public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "Foo",
//// "Bar",
//// "FooBar"
//// };
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = items,
//// IsVirtualized = false
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// target.SelectedIndex = 1;
//// items.RemoveAt(1);
//// Assert.Equal(0, target.SelectedIndex);
//// Assert.Equal("Foo", target.SelectedItem);
//// }
//// private Control CreateTemplate(Carousel control, INameScope scope)
//// {
//// return new CarouselPresenter
//// {
//// Name = "PART_ItemsPresenter",
//// [~CarouselPresenter.IsVirtualizedProperty] = control[~Carousel.IsVirtualizedProperty],
//// [~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty],
//// [~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty],
//// [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],
//// }.RegisterInNameScope(scope);
//// }
//// private static TextBlock GetContainerTextBlock(object control)
//// {
//// var contentPresenter = Assert.IsType<ContentPresenter>(control);
//// contentPresenter.UpdateChild();
//// return Assert.IsType<TextBlock>(contentPresenter.Child);
//// }
//// [Fact]
//// public void SelectedItem_Validation()
//// {
//// using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
//// {
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// IsVirtualized = false
//// };
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
//// var exception = new System.InvalidCastException("failed validation");
//// var textObservable =
//// new BehaviorSubject<BindingNotification>(new BindingNotification(exception,
//// BindingErrorType.DataValidationError));
//// target.Bind(ComboBox.SelectedItemProperty, textObservable);
//// Assert.True(DataValidationErrors.GetHasErrors(target));
//// Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
//// }
//// }
//// }
////}
Assert.Equal("FooBar", child.Text);
}
[Fact]
public void Selected_Item_Changes_To_Next_First_Item_When_Item_Removed_From_Beggining_Of_List()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
{
Template = CarouselTemplate(),
Items = items,
};
Prepare(target);
var child = GetContainerTextBlock(target.GetRealizedContainers().First());
Assert.Equal("Foo", child.Text);
items.RemoveAt(0);
Layout(target);
child = GetContainerTextBlock(target.GetRealizedContainers().First());
Assert.IsType<TextBlock>(child);
Assert.Equal("Bar", child.Text);
}
[Fact]
public void Selected_Item_Changes_To_First_Item_If_SelectedItem_Is_Removed_From_Middle()
{
using var app = Start();
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
};
var target = new Carousel
{
Template = CarouselTemplate(),
Items = items,
};
Prepare(target);
target.SelectedIndex = 1;
items.RemoveAt(1);
Assert.Equal(0, target.SelectedIndex);
Assert.Equal("Foo", target.SelectedItem);
}
[Fact]
public void SelectedItem_Validation()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new Carousel
{
Template = CarouselTemplate(),
};
Prepare(target);
var exception = new System.InvalidCastException("failed validation");
var textObservable =
new BehaviorSubject<BindingNotification>(new BindingNotification(exception,
BindingErrorType.DataValidationError));
target.Bind(ComboBox.SelectedItemProperty, textObservable);
Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
}
}
private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
private static void Prepare(Carousel target)
{
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
}
private static void Layout(Carousel target)
{
((ILayoutRoot)target.GetVisualRoot()).LayoutManager.ExecuteLayoutPass();
}
private static IControlTemplate CarouselTemplate()
{
return new FuncControlTemplate((c, ns) =>
new ScrollViewer
{
Name = "PART_ScrollViewer",
Template = ScrollViewerTemplate(),
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsPanelProperty] = c[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(ns)
}.RegisterInNameScope(ns));
}
private static FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
new Panel
{
Children =
{
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
[~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty],
[~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty],
}.RegisterInNameScope(scope),
}
});
}
private static TextBlock GetContainerTextBlock(object control)
{
var contentPresenter = Assert.IsType<ContentPresenter>(control);
return Assert.IsType<TextBlock>(contentPresenter.Child);
}
}
}

732
tests/Avalonia.Controls.UnitTests/Presenters/CarouselPresenterTests.cs

@ -1,732 +0,0 @@
////using System.Linq;
////using Moq;
////using Avalonia.Controls.Generators;
////using Avalonia.Controls.Presenters;
////using Avalonia.Controls.Templates;
////using Xunit;
////using System.Collections.ObjectModel;
////using System.Collections;
////namespace Avalonia.Controls.UnitTests.Presenters
////{
//// public class CarouselPresenterTests
//// {
//// [Fact]
//// public void Should_Register_With_Host_When_TemplatedParent_Set()
//// {
//// var host = new Carousel();
//// var target = new CarouselPresenter();
//// Assert.Null(host.Presenter);
//// target.SetValue(Control.TemplatedParentProperty, host);
//// Assert.Same(target, host.Presenter);
//// }
//// [Fact]
//// public void ApplyTemplate_Should_Create_Panel()
//// {
//// var target = new CarouselPresenter
//// {
//// ItemsPanel = new FuncTemplate<Panel>(() => new Panel()),
//// };
//// target.ApplyTemplate();
//// Assert.IsType<Panel>(target.Panel);
//// }
//// [Fact]
//// public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl()
//// {
//// var parent = new TestItemsControl();
//// var target = new CarouselPresenter
//// {
//// [StyledElement.TemplatedParentProperty] = parent,
//// };
//// Assert.IsType<ItemContainerGenerator<TestItem>>(target.ItemContainerGenerator);
//// }
//// public class Virtualized
//// {
//// [Fact]
//// public void Should_Initially_Materialize_Selected_Container()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// SelectedIndex = 0,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Initially_Materialize_Nothing_If_No_Selected_Container()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// Assert.Empty(target.Panel.Children);
//// Assert.Empty(target.ItemContainerGenerator.Containers);
//// }
//// [Fact]
//// public void Switching_To_Virtualized_Should_Reset_Containers()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// SelectedIndex = 0,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// target.IsVirtualized = true;
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Changing_SelectedIndex_Should_Show_Page()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// SelectedIndex = 0,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// AssertSingle(target);
//// target.SelectedIndex = 1;
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Remove_NonCurrent_Page()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// IsVirtualized = true,
//// SelectedIndex = 0,
//// };
//// target.ApplyTemplate();
//// AssertSingle(target);
//// target.SelectedIndex = 1;
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Handle_Inserting_Item_At_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// items.Insert(1, "item1a");
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Handle_Inserting_Item_Before_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 2,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// items.Insert(1, "item1a");
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Do_Nothing_When_Inserting_Item_After_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// var child = AssertSingle(target);
//// items.Insert(2, "after");
//// Assert.Same(child, AssertSingle(target));
//// }
//// [Fact]
//// public void Should_Handle_Removing_Item_At_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(1);
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Handle_Removing_Item_Before_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(0);
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Do_Nothing_When_Removing_Item_After_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// var child = AssertSingle(target);
//// items.RemoveAt(2);
//// Assert.Same(child, AssertSingle(target));
//// }
//// [Fact]
//// public void Should_Handle_Removing_SelectedItem_When_Its_Last()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 2,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(2);
//// Assert.Equal(1, target.SelectedIndex);
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Handle_Removing_Last_Item()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 0,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(0);
//// Assert.Empty(target.Panel.Children);
//// Assert.Empty(target.ItemContainerGenerator.Containers);
//// }
//// [Fact]
//// public void Should_Handle_Replacing_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// items[1] = "replaced";
//// AssertSingle(target);
//// }
//// [Fact]
//// public void Should_Do_Nothing_When_Replacing_Non_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// var child = AssertSingle(target);
//// items[0] = "replaced";
//// Assert.Same(child, AssertSingle(target));
//// }
//// [Fact]
//// public void Should_Handle_Moving_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// items.Move(1, 0);
//// AssertSingle(target);
//// }
//// private static Control AssertSingle(CarouselPresenter target)
//// {
//// var items = (IList)target.Items;
//// var index = target.SelectedIndex;
//// var content = items[index];
//// var child = Assert.Single(target.Panel.Children);
//// var presenter = Assert.IsType<ContentPresenter>(child);
//// var container = Assert.Single(target.ItemContainerGenerator.Containers);
//// var visible = Assert.Single(target.Panel.Children.Where(x => x.IsVisible));
//// Assert.Same(child, container.ContainerControl);
//// Assert.Same(child, visible);
//// Assert.Equal(content, presenter.Content);
//// Assert.Equal(content, container.Item);
//// Assert.Equal(index, container.Index);
//// return child;
//// }
//// }
//// public class NonVirtualized
//// {
//// [Fact]
//// public void Should_Initially_Materialize_All_Containers()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Initially_Show_Selected_Item()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// SelectedIndex = 1,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// AssertAll(target);
//// }
//// [Fact]
//// public void Switching_To_Non_Virtualized_Should_Reset_Containers()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// SelectedIndex = 0,
//// IsVirtualized = true,
//// };
//// target.ApplyTemplate();
//// target.IsVirtualized = false;
//// AssertAll(target);
//// }
//// [Fact]
//// public void Changing_SelectedIndex_Should_Show_Page()
//// {
//// var target = new CarouselPresenter
//// {
//// Items = new[] { "foo", "bar" },
//// SelectedIndex = 0,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// AssertAll(target);
//// target.SelectedIndex = 1;
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Handle_Inserting_Item_At_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.Insert(1, "item1a");
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Handle_Inserting_Item_Before_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 2,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.Insert(1, "item1a");
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Do_Handle_Inserting_Item_After_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.Insert(2, "after");
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Handle_Removing_Item_At_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(1);
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Handle_Removing_Item_Before_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(0);
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Handle_Removing_Item_After_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(2);
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Handle_Removing_SelectedItem_When_Its_Last()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 2,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(2);
//// Assert.Equal(1, target.SelectedIndex);
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Handle_Removing_Last_Item()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 0,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.RemoveAt(0);
//// Assert.Empty(target.Panel.Children);
//// Assert.Empty(target.ItemContainerGenerator.Containers);
//// }
//// [Fact]
//// public void Should_Handle_Replacing_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items[1] = "replaced";
//// AssertAll(target);
//// }
//// [Fact]
//// public void Should_Handle_Moving_SelectedItem()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "item0",
//// "item1",
//// "item2",
//// };
//// var target = new CarouselPresenter
//// {
//// Items = items,
//// SelectedIndex = 1,
//// IsVirtualized = false,
//// };
//// target.ApplyTemplate();
//// items.Move(1, 0);
//// AssertAll(target);
//// }
//// private static void AssertAll(CarouselPresenter target)
//// {
//// var items = (IList)target.Items;
//// Assert.Equal(items?.Count ?? 0, target.Panel.Children.Count);
//// Assert.Equal(items?.Count ?? 0, target.ItemContainerGenerator.Containers.Count());
//// for (var i = 0; i < items?.Count; ++i)
//// {
//// var content = items[i];
//// var child = target.Panel.Children[i];
//// var presenter = Assert.IsType<ContentPresenter>(child);
//// var container = target.ItemContainerGenerator.ContainerFromIndex(i);
//// Assert.Same(child, container);
//// Assert.Equal(i == target.SelectedIndex, child.IsVisible);
//// Assert.Equal(content, presenter.Content);
//// Assert.Equal(i, target.ItemContainerGenerator.IndexFromContainer(container));
//// }
//// }
//// }
//// private class TestItem : ContentControl
//// {
//// }
//// private class TestItemsControl : ItemsControl
//// {
//// protected override IItemContainerGenerator CreateItemContainerGenerator()
//// {
//// return new ItemContainerGenerator<TestItem>(this, TestItem.ContentProperty, null);
//// }
//// }
//// }
////}

271
tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs

@ -0,0 +1,271 @@
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
#nullable enable
namespace Avalonia.Controls.UnitTests
{
public class VirtualizingCarouselPanelTests
{
[Fact]
public void Initial_Item_Is_Displayed()
{
using var app = Start();
var items = new[] { "foo", "bar" };
var (target, _) = CreateTarget(items);
Assert.Single(target.Children);
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
Assert.Equal("foo", container.Content);
}
[Fact]
public void Displays_Next_Item()
{
using var app = Start();
var items = new[] { "foo", "bar" };
var (target, carousel) = CreateTarget(items);
carousel.SelectedIndex = 1;
Layout(target);
Assert.Single(target.Children);
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
Assert.Equal("bar", container.Content);
}
[Fact]
public void Handles_Inserted_Item()
{
using var app = Start();
var items = new ObservableCollection<string> { "foo", "bar" };
var (target, carousel) = CreateTarget(items);
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
items.Insert(0, "baz");
Layout(target);
Assert.Single(target.Children);
Assert.Same(container, target.Children[0]);
Assert.Equal("foo", container.Content);
}
[Fact]
public void Handles_Removed_Item()
{
using var app = Start();
var items = new ObservableCollection<string> { "foo", "bar" };
var (target, carousel) = CreateTarget(items);
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
items.RemoveAt(0);
Layout(target);
Assert.Single(target.Children);
Assert.Same(container, target.Children[0]);
Assert.Equal("bar", container.Content);
}
[Fact]
public void Handles_Replaced_Item()
{
using var app = Start();
var items = new ObservableCollection<string> { "foo", "bar" };
var (target, carousel) = CreateTarget(items);
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
items[0] = "baz";
Layout(target);
Assert.Single(target.Children);
Assert.Same(container, target.Children[0]);
Assert.Equal("baz", container.Content);
}
[Fact]
public void Handles_Moved_Item()
{
using var app = Start();
var items = new ObservableCollection<string> { "foo", "bar" };
var (target, carousel) = CreateTarget(items);
var container = Assert.IsType<ContentPresenter>(target.Children[0]);
items.Move(0, 1);
Layout(target);
Assert.Single(target.Children);
Assert.Same(container, target.Children[0]);
Assert.Equal("bar", container.Content);
}
public class Transitions
{
[Fact]
public void Initial_Item_Does_Not_Start_Transition()
{
using var app = Start();
var items = new Control[] { new Button(), new Canvas() };
var transition = new Mock<IPageTransition>();
var (target, _) = CreateTarget(items, transition.Object);
transition.Verify(x => x.Start(
It.IsAny<Visual>(),
It.IsAny<Visual>(),
It.IsAny<bool>(),
It.IsAny<CancellationToken>()),
Times.Never);
}
[Fact]
public void Changing_SelectedIndex_Starts_Transition()
{
using var app = Start();
var items = new Control[] { new Button(), new Canvas() };
var transition = new Mock<IPageTransition>();
var (target, carousel) = CreateTarget(items, transition.Object);
carousel.SelectedIndex = 1;
Layout(target);
transition.Verify(x => x.Start(
items[0],
items[1],
true,
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public void TransitionFrom_Control_Is_Recycled_When_Transition_Completes()
{
using var app = Start();
using var sync = UnitTestSynchronizationContext.Begin();
var items = new Control[] { new Button(), new Canvas() };
var transition = new Mock<IPageTransition>();
var (target, carousel) = CreateTarget(items, transition.Object);
var transitionTask = new TaskCompletionSource();
transition.Setup(x => x.Start(
items[0],
items[1],
true,
It.IsAny<CancellationToken>()))
.Returns(() => transitionTask.Task);
carousel.SelectedIndex = 1;
Layout(target);
Assert.Equal(items, target.Children);
Assert.All(items, x => Assert.True(x.IsVisible));
transitionTask.SetResult();
sync.ExecutePostedCallbacks();
Assert.Equal(items, target.Children);
Assert.False(items[0].IsVisible);
Assert.True(items[1].IsVisible);
}
[Fact]
public void Existing_Transition_Is_Canceled_If_Interrupted()
{
using var app = Start();
using var sync = UnitTestSynchronizationContext.Begin();
var items = new Control[] { new Button(), new Canvas() };
var transition = new Mock<IPageTransition>();
var (target, carousel) = CreateTarget(items, transition.Object);
var transitionTask = new TaskCompletionSource();
CancellationToken? cancelationToken = null;
transition.Setup(x => x.Start(
items[0],
items[1],
true,
It.IsAny<CancellationToken>()))
.Callback<Visual, Visual, bool, CancellationToken>((_, _, _, c) => cancelationToken = c)
.Returns(() => transitionTask.Task);
carousel.SelectedIndex = 1;
Layout(target);
Assert.NotNull(cancelationToken);
Assert.False(cancelationToken!.Value.IsCancellationRequested);
carousel.SelectedIndex = 0;
Layout(target);
Assert.True(cancelationToken!.Value.IsCancellationRequested);
}
}
private static IDisposable Start() => UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
private static (VirtualizingCarouselPanel, Carousel) CreateTarget(
IEnumerable items,
IPageTransition? transition = null)
{
var carousel = new Carousel
{
Items = items,
Template = CarouselTemplate(),
PageTransition = transition,
};
var root = new TestRoot(carousel);
root.LayoutManager.ExecuteInitialLayoutPass();
return ((VirtualizingCarouselPanel)carousel.Presenter!.Panel!, carousel);
}
private static IControlTemplate CarouselTemplate()
{
return new FuncControlTemplate((c, ns) =>
new ScrollViewer
{
Name = "PART_ScrollViewer",
Template = ScrollViewerTemplate(),
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsPanelProperty] = c[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(ns)
}.RegisterInNameScope(ns));
}
private static FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
new Panel
{
Children =
{
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ScrollContentPresenter.ContentProperty] = parent.GetObservable(ScrollViewer.ContentProperty).ToBinding(),
[~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty],
[~ScrollContentPresenter.CanHorizontallyScrollProperty] = parent[~ScrollViewer.CanHorizontallyScrollProperty],
[~ScrollContentPresenter.CanVerticallyScrollProperty] = parent[~ScrollViewer.CanVerticallyScrollProperty],
}.RegisterInNameScope(scope),
}
});
}
private static void Layout(Control c) => ((ILayoutRoot)c.GetVisualRoot()!).LayoutManager.ExecuteLayoutPass();
}
}

5
tests/Avalonia.Base.UnitTests/Data/UnitTestSynchronizationContext.cs → tests/Avalonia.UnitTests/UnitTestSynchronizationContext.cs

@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Threading;
namespace Avalonia.Base.UnitTests.Data
namespace Avalonia.UnitTests
{
internal sealed class UnitTestSynchronizationContext : SynchronizationContext
public sealed class UnitTestSynchronizationContext : SynchronizationContext
{
readonly List<Tuple<SendOrPostCallback, object>> _postedCallbacks =
new List<Tuple<SendOrPostCallback, object>>();
Loading…
Cancel
Save