Browse Source

Refactored ItemContainerGenerator.

A bunch of tests still failing, and some code commented out, but outlines the new API shape.
pull/9677/head
Steven Kirk 3 years ago
parent
commit
1101f28dd7
  1. 26
      src/Avalonia.Controls/ComboBox.cs
  2. 5
      src/Avalonia.Controls/ContextMenu.cs
  3. 5
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  4. 114
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  5. 18
      src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs
  6. 49
      src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs
  7. 284
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  8. 102
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  9. 42
      src/Avalonia.Controls/Generators/ItemContainerInfo.cs
  10. 21
      src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs
  11. 105
      src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs
  12. 166
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  13. 164
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  14. 205
      src/Avalonia.Controls/ItemsControl.cs
  15. 9
      src/Avalonia.Controls/ListBox.cs
  16. 13
      src/Avalonia.Controls/MenuBase.cs
  17. 13
      src/Avalonia.Controls/MenuItem.cs
  18. 23
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  19. 57
      src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs
  20. 138
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  21. 8
      src/Avalonia.Controls/Primitives/TabStrip.cs
  22. 13
      src/Avalonia.Controls/TabControl.cs
  23. 440
      src/Avalonia.Controls/TreeView.cs
  24. 35
      src/Avalonia.Controls/TreeViewItem.cs
  25. 101
      src/Avalonia.Controls/VirtualizingPanel.cs
  26. 290
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  27. 12
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  28. 658
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  29. 173
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs
  30. 42
      tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs
  31. 65
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  32. 119
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  33. 2900
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  34. 27
      tests/Avalonia.LeakTests/ControlTests.cs

26
src/Avalonia.Controls/ComboBox.cs

@ -158,15 +158,6 @@ namespace Avalonia.Controls
set { SetValue(VerticalContentAlignmentProperty, value); }
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ComboBoxItem>(
this,
ComboBoxItem.ContentProperty,
ComboBoxItem.ContentTemplateProperty);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
@ -449,14 +440,15 @@ namespace Avalonia.Controls
private void SelectFocusedItem()
{
foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)
{
if (dropdownItem.ContainerControl.IsFocused)
{
SelectedIndex = dropdownItem.Index;
break;
}
}
throw new NotImplementedException();
////foreach (ItemContainerInfo dropdownItem in ItemContainerGenerator.Containers)
////{
//// if (dropdownItem.ContainerControl.IsFocused)
//// {
//// SelectedIndex = dropdownItem.Index;
//// break;
//// }
////}
}
private void SelectNext()

5
src/Avalonia.Controls/ContextMenu.cs

@ -315,11 +315,6 @@ namespace Avalonia.Controls
remove => _popupHostChangedHandler -= value;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
private void Open(Control control, Control placementTarget, bool requestedByPointer)
{
if (IsOpen)

5
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@ -35,11 +35,6 @@ namespace Avalonia.Controls
throw new NotSupportedException("Use MenuFlyout.ShowAt(Control) instead");
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);

114
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -1,114 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// </summary>
public interface IItemContainerGenerator
{
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets or sets the theme to be applied to the items in the control.
/// </summary>
ControlTheme? ItemContainerTheme { get; set; }
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// </summary>
IDataTemplate? ItemTemplate { get; set; }
/// <summary>
/// Gets or sets the binding to use to bind to the member of an item used for displaying
/// </summary>
IBinding? DisplayMemberBinding { get; set; }
/// <summary>
/// Gets the ContainerType, or null if its an untyped ContainerGenerator.
/// </summary>
Type? ContainerType { get; }
/// <summary>
/// Signaled whenever new containers are materialized.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <summary>
/// Event raised whenever containers are recycled.
/// </summary>
event EventHandler<ItemContainerEventArgs>? Recycled;
/// <summary>
/// Creates a container control for an item.
/// </summary>
/// <param name="index">
/// The index of the item of data in the control's items.
/// </param>
/// <param name="item">The item.</param>
/// <returns>The created controls.</returns>
ItemContainerInfo Materialize(int index, object item);
/// <summary>
/// Removes a set of created containers.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count);
/// <summary>
/// Inserts space for newly inserted containers in the index.
/// </summary>
/// <param name="index">The index at which space should be inserted.</param>
/// <param name="count">The number of blank spaces to create.</param>
void InsertSpace(int index, int count);
/// <summary>
/// Removes a set of created containers and updates the index of later containers to fill
/// the gap.
/// </summary>
/// <param name="startingIndex">
/// The index of the first item in the control's items.
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
bool TryRecycle(int oldIndex, int newIndex, object item);
/// <summary>
/// Clears all created containers and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IEnumerable<ItemContainerInfo> Clear();
/// <summary>
/// Gets the container control representing the item with the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The container, or null if no container created.</returns>
Control? ContainerFromIndex(int index);
/// <summary>
/// Gets the index of the specified container control.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The index of the container, or -1 if not found.</returns>
int IndexFromContainer(Control? container);
}
}

18
src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs

@ -1,18 +0,0 @@
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for tree items and maintains a list of created containers.
/// </summary>
public interface ITreeItemContainerGenerator : IItemContainerGenerator
{
/// <summary>
/// Gets the container index for the tree.
/// </summary>
TreeContainerIndex? Index { get; }
/// <summary>
/// Updates the index based on the parent <see cref="TreeView"/>.
/// </summary>
void UpdateIndex();
}
}

49
src/Avalonia.Controls/Generators/ItemContainerEventArgs.cs

@ -1,49 +0,0 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Provides details for the <see cref="IItemContainerGenerator.Materialized"/>
/// and <see cref="IItemContainerGenerator.Dematerialized"/> events.
/// </summary>
public class ItemContainerEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="container">The container.</param>
public ItemContainerEventArgs(ItemContainerInfo container)
{
StartingIndex = container.Index;
Containers = new[] { container };
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="containers">The containers.</param>
/// <remarks>
/// TODO: Do we really need to pass in StartingIndex here? The ItemContainerInfo objects
/// have an index, and what happens if the contains passed in aren't sequential?
/// </remarks>
public ItemContainerEventArgs(
int startingIndex,
IList<ItemContainerInfo> containers)
{
StartingIndex = startingIndex;
Containers = containers;
}
/// <summary>
/// Gets the containers.
/// </summary>
public IList<ItemContainerInfo> Containers { get; }
/// <summary>
/// Gets the index of the first container in the source items.
/// </summary>
public int StartingIndex { get; }
}
}

284
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -1,262 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Styling;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// Generates containers for an <see cref="ItemsControl"/>.
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
/// <remarks>
/// Although this class is similar to that found in WPF/UWP, in Avalonia this class only
/// concerns itself with generating and clearing item containers; it does not maintain a
/// record of the currently realized containers, that responsibility is delegated to the
/// items panel.
/// </remarks>
public class ItemContainerGenerator
{
private SortedDictionary<int, ItemContainerInfo> _containers = new SortedDictionary<int, ItemContainerInfo>();
private ItemsControl _owner;
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public ItemContainerGenerator(Control owner)
{
Owner = owner ?? throw new ArgumentNullException(nameof(owner));
}
/// <inheritdoc/>
public IEnumerable<ItemContainerInfo> Containers => _containers.Values;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Materialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs>? Recycled;
internal ItemContainerGenerator(ItemsControl owner) => _owner = owner;
/// <summary>
/// Gets or sets the theme to be applied to the items in the control.
/// Creates a new container control.
/// </summary>
public ControlTheme? ItemContainerTheme { get; set; }
/// <returns>The newly created container control.</returns>
/// <remarks>
/// Before calling this method, <see cref="IsItemItsOwnContainer(Control)"/> should be
/// called to determine whether the item itself should be used as a container. After
/// calling this method, <see cref="PrepareItemContainer(Control, object, int)"/> should
/// be called to prepare the container to display the specified item.
/// </remarks>
public Control CreateContainer() => _owner.CreateContainerOverride();
/// <summary>
/// Gets or sets the data template used to display the items in the control.
/// Determines whether the specified item is (or is eligible to be) its own container.
/// </summary>
public IDataTemplate? ItemTemplate { get; set; }
/// <inheritdoc />
public IBinding? DisplayMemberBinding { get; set; }
/// <param name="container">The item.</param>
/// <returns>true if the item is its own container, otherwise false.</returns>
/// <remarks>
/// Whereas in WPF/UWP, non-control items can be their own container, in Avalonia only
/// control items may be; the caller is responsible for checking if each item is a control
/// and calling this method before creating a new container.
/// </remarks>
public bool IsItemItsOwnContainer(Control container) => _owner.IsItemItsOwnContainerOverride(container);
/// <summary>
/// Gets the owner control.
/// Prepares the specified element as the container for the corresponding item.
/// </summary>
public Control Owner { get; }
/// <inheritdoc/>
public virtual Type? ContainerType => null;
/// <inheritdoc/>
public ItemContainerInfo Materialize(int index, object item)
{
var container = new ItemContainerInfo(CreateContainer(item)!, item, index);
_containers.Add(container.Index, container);
Materialized?.Invoke(this, new ItemContainerEventArgs(container));
return container;
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
var result = new List<ItemContainerInfo>();
for (int i = startingIndex; i < startingIndex + count; ++i)
{
result.Add(_containers[i]);
_containers.Remove(i);
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result;
}
/// <inheritdoc/>
public virtual void InsertSpace(int index, int count)
{
if (count > 0)
{
var toMove = _containers.Where(x => x.Key >= index)
.OrderByDescending(x => x.Key)
.ToArray();
foreach (var i in toMove)
{
_containers.Remove(i.Key);
i.Value.Index += count;
_containers.Add(i.Value.Index, i.Value);
}
}
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
var result = new List<ItemContainerInfo>();
if (count > 0)
{
for (var i = startingIndex; i < startingIndex + count; ++i)
{
if (_containers.TryGetValue(i, out var found))
{
result.Add(found);
}
_containers.Remove(i);
}
var toMove = _containers.Where(x => x.Key >= startingIndex)
.OrderBy(x => x.Key).ToArray();
foreach (var i in toMove)
{
_containers.Remove(i.Key);
i.Value.Index -= count;
_containers.Add(i.Value.Index, i.Value);
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
if (toMove.Length > 0)
{
var containers = toMove.Select(x => x.Value).ToArray();
Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers));
}
}
return result;
}
/// <inheritdoc/>
public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false;
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Clear()
{
var result = Containers.ToArray();
_containers.Clear();
if (result.Length > 0)
{
Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result));
}
return result;
}
/// <inheritdoc/>
public Control? ContainerFromIndex(int index)
{
ItemContainerInfo? result;
_containers.TryGetValue(index, out result);
return result?.ContainerControl;
}
/// <inheritdoc/>
public int IndexFromContainer(Control? container)
{
foreach (var i in _containers)
{
if (i.Value.ContainerControl == container)
{
return i.Key;
}
}
return -1;
}
/// <param name="container">The element that's used to display the specified item.</param>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
/// <remarks>
/// If <see cref="IsItemItsOwnContainer(Control)"/> is true for an item, then this method
/// only needs to be called a single time, otherwise this method should be called after the
/// container is created, and each subsequent time the container is recycled to display a
/// new item.
/// </remarks>
public void PrepareItemContainer(Control container, object? item, int index) =>
_owner.PrepareItemContainer(container, item, index);
/// <summary>
/// Creates the container for an item.
/// Undoes the effects of the <see cref="PrepareItemContainer(Control, object, int)"/> method.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The created container control.</returns>
protected virtual Control? CreateContainer(object item)
{
var result = item as Control;
if (result == null)
{
result = new ContentPresenter();
if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentPresenter.ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Style);
}
if (ItemTemplate != null)
{
result.SetValue(
ContentPresenter.ContentTemplateProperty,
ItemTemplate,
BindingPriority.Style);
}
}
/// <param name="container">The element that's used to display the specified item.</param>
public void ClearItemContainer(Control container) => _owner.ClearContainerForItemOverride(container);
if (ItemContainerTheme != null)
{
result.SetValue(
StyledElement.ThemeProperty,
ItemContainerTheme,
BindingPriority.Template);
}
return result;
}
/// <summary>
/// Moves a container.
/// </summary>
/// <param name="oldIndex">The old index.</param>
/// <param name="newIndex">The new index.</param>
/// <param name="item">The new item.</param>
/// <returns>The container info.</returns>
protected ItemContainerInfo MoveContainer(int oldIndex, int newIndex, object item)
{
var container = _containers[oldIndex];
container.Index = newIndex;
container.Item = item;
_containers.Remove(oldIndex);
_containers.Add(newIndex, container);
return container;
}
/// <summary>
/// Gets all containers with an index that fall within a range.
/// </summary>
/// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns>
protected IEnumerable<ItemContainerInfo> GetContainerRange(int index, int count)
{
return _containers.Where(x => x.Key >= index && x.Key < index + count).Select(x => x.Value);
}
/// <summary>
/// Raises the <see cref="Recycled"/> event.
/// </summary>
/// <param name="e">The event args.</param>
protected void RaiseRecycled(ItemContainerEventArgs e)
{
Recycled?.Invoke(this, e);
}
public Control? ContainerFromIndex(int index) => _owner.ContainerFromIndex(index);
public int IndexFromContainer(Control container) => _owner.IndexFromContainer(container);
}
}

102
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -1,102 +0,0 @@
using System;
using Avalonia.Data;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for items and maintains a list of created containers.
/// </summary>
/// <typeparam name="T">The type of the container.</typeparam>
public class ItemContainerGenerator<T> : ItemContainerGenerator where T : Control, new()
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
/// <param name="contentProperty">The container's Content property.</param>
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
public ItemContainerGenerator(
Control owner,
AvaloniaProperty contentProperty,
AvaloniaProperty? contentTemplateProperty)
: base(owner)
{
ContentProperty = contentProperty ?? throw new ArgumentNullException(nameof(contentProperty));
ContentTemplateProperty = contentTemplateProperty;
}
/// <inheritdoc/>
public override Type ContainerType => typeof(T);
/// <summary>
/// Gets the container's Content property.
/// </summary>
protected AvaloniaProperty ContentProperty { get; }
/// <summary>
/// Gets the container's ContentTemplate property.
/// </summary>
protected AvaloniaProperty? ContentTemplateProperty { get; }
/// <inheritdoc/>
protected override Control? CreateContainer(object item)
{
var container = item as T;
if (container is null)
{
container = new T();
if (ContentTemplateProperty != null)
{
container.SetValue(ContentTemplateProperty, ItemTemplate, BindingPriority.Style);
}
if (DisplayMemberBinding is not null)
{
container.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
container.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
container.SetValue(ContentProperty, item, BindingPriority.Style);
}
if (!(item is Control))
{
container.DataContext = item;
}
}
if (ItemContainerTheme != null)
{
container.SetValue(StyledElement.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
}
return container;
}
/// <inheritdoc/>
public override bool TryRecycle(int oldIndex, int newIndex, object item)
{
var container = ContainerFromIndex(oldIndex);
if (container == null)
{
throw new IndexOutOfRangeException("Could not recycle container: not materialized.");
}
container.SetValue(ContentProperty, item);
if (!(item is Control))
{
container.DataContext = item;
}
var info = MoveContainer(oldIndex, newIndex, item);
RaiseRecycled(new ItemContainerEventArgs(info));
return true;
}
}
}

42
src/Avalonia.Controls/Generators/ItemContainerInfo.cs

@ -1,42 +0,0 @@
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Holds information about an item container generated by an
/// <see cref="IItemContainerGenerator"/>.
/// </summary>
public class ItemContainerInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerInfo"/> class.
/// </summary>
/// <param name="container">The container control.</param>
/// <param name="item">The item that the container represents.</param>
/// <param name="index">
/// The index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </param>
public ItemContainerInfo(Control container, object item, int index)
{
ContainerControl = container;
Item = item;
Index = index;
}
/// <summary>
/// Gets the container control.
/// </summary>
/// <remarks>
/// This will be null if <see cref="Item"/> is null.
/// </remarks>
public Control ContainerControl { get; }
/// <summary>
/// Gets the item that the container represents.
/// </summary>
public object Item { get; internal set; }
/// <summary>
/// Gets the index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </summary>
public int Index { get; set; }
}
}

21
src/Avalonia.Controls/Generators/MenuItemContainerGenerator.cs

@ -1,21 +0,0 @@
namespace Avalonia.Controls.Generators
{
public class MenuItemContainerGenerator : ItemContainerGenerator<MenuItem>
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
public MenuItemContainerGenerator(Control owner)
: base(owner, MenuItem.HeaderProperty, null)
{
}
/// <inheritdoc/>
protected override Control? CreateContainer(object item)
{
var separator = item as Separator;
return separator != null ? separator : base.CreateContainer(item);
}
}
}

105
src/Avalonia.Controls/Generators/TabItemContainerGenerator.cs

@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Generators
{
public class TabItemContainerGenerator : ItemContainerGenerator<TabItem>
{
public TabItemContainerGenerator(TabControl owner)
: base(owner, ContentControl.ContentProperty, ContentControl.ContentTemplateProperty)
{
Owner = owner;
}
public new TabControl Owner { get; }
protected override Control CreateContainer(object item)
{
var tabItem = (TabItem)base.CreateContainer(item)!;
tabItem.Bind(TabItem.TabStripPlacementProperty, new OwnerBinding<Dock>(
tabItem,
TabControl.TabStripPlacementProperty));
if (tabItem.HeaderTemplate == null)
{
tabItem.Bind(TabItem.HeaderTemplateProperty, new OwnerBinding<IDataTemplate?>(
tabItem,
TabControl.ItemTemplateProperty));
}
if (Owner.HeaderDisplayMemberBinding is not null)
{
tabItem.Bind(HeaderedContentControl.HeaderProperty, Owner.HeaderDisplayMemberBinding,
BindingPriority.Style);
}
if (tabItem.Header == null)
{
if (item is IHeadered headered)
{
tabItem.Header = headered.Header;
}
else
{
if (!(tabItem.DataContext is Control))
{
tabItem.Header = tabItem.DataContext;
}
}
}
if (!(tabItem.Content is Control))
{
tabItem.Bind(TabItem.ContentTemplateProperty, new OwnerBinding<IDataTemplate?>(
tabItem,
TabControl.ContentTemplateProperty));
}
return tabItem;
}
private class OwnerBinding<T> : SingleSubscriberObservableBase<T>
{
private readonly TabItem _item;
private readonly StyledProperty<T> _ownerProperty;
private IDisposable? _ownerSubscription;
private IDisposable? _propertySubscription;
public OwnerBinding(TabItem item, StyledProperty<T> ownerProperty)
{
_item = item;
_ownerProperty = ownerProperty;
}
protected override void Subscribed()
{
_ownerSubscription = ControlLocator.Track(_item, 0, typeof(TabControl)).Subscribe(OwnerChanged);
}
protected override void Unsubscribed()
{
_ownerSubscription?.Dispose();
_ownerSubscription = null;
}
private void OwnerChanged(ILogical? c)
{
_propertySubscription?.Dispose();
_propertySubscription = null;
if (c is TabControl tabControl)
{
_propertySubscription = tabControl.GetObservable(_ownerProperty)
.Subscribe(x => PublishNext(x));
}
}
}
}
}

166
src/Avalonia.Controls/Generators/TreeContainerIndex.cs

@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Maintains an index of all item containers currently materialized by a <see cref="TreeView"/>.
/// </summary>
/// <remarks>
/// Each <see cref="TreeViewItem"/> has its own <see cref="TreeItemContainerGenerator{T}"/>
/// that maintains the list of its direct children, but they also share an instance of this
/// class in their <see cref="TreeItemContainerGenerator{T}.Index"/> property which tracks
/// the containers materialized for the entire tree.
/// </remarks>
public class TreeContainerIndex
{
private readonly Dictionary<object, HashSet<Control>> _itemToContainerSet = new Dictionary<object, HashSet<Control>>();
private readonly Dictionary<object, Control> _itemToContainer = new Dictionary<object, Control>();
private readonly Dictionary<Control, object> _containerToItem = new Dictionary<Control, object>();
/// <summary>
/// Signaled whenever new containers are materialized.
/// </summary>
public event EventHandler<ItemContainerEventArgs>? Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
public event EventHandler<ItemContainerEventArgs>? Dematerialized;
/// <summary>
/// Gets the currently materialized containers.
/// </summary>
public IEnumerable<Control> Containers => _containerToItem.Keys;
/// <summary>
/// Gets the items of currently materialized containers.
/// </summary>
public IEnumerable<object> Items => _containerToItem.Values;
/// <summary>
/// Adds an entry to the index.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="container">The item container.</param>
public void Add(object item, Control container)
{
_itemToContainer[item] = container;
if (_itemToContainerSet.TryGetValue(item, out var set))
{
set.Add(container);
}
else
{
_itemToContainerSet.Add(item, new HashSet<Control> { container });
}
_containerToItem.Add(container, item);
Materialized?.Invoke(
this,
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
/// <summary>
/// Removes a container from private collections.
/// </summary>
/// <param name="container">The item container.</param>
/// <param name="item">The DataContext object</param>
private void RemoveContainer(Control container, object item)
{
if (_itemToContainerSet.TryGetValue(item, out var set))
{
set.Remove(container);
if (set.Count == 0)
{
_itemToContainerSet.Remove(item);
_itemToContainer.Remove(item);
}
else
{
_itemToContainer[item] = set.First();
}
}
}
/// <summary>
/// Removes a container from the index.
/// </summary>
/// <param name="container">The item container.</param>
public void Remove(Control container)
{
var item = _containerToItem[container];
_containerToItem.Remove(container);
RemoveContainer(container, item);
Dematerialized?.Invoke(
this,
new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0)));
}
/// <summary>
/// Removes a set of containers from the index.
/// </summary>
/// <param name="startingIndex">The index of the first item.</param>
/// <param name="containers">The item containers.</param>
public void Remove(int startingIndex, IEnumerable<ItemContainerInfo> containers)
{
foreach (var container in containers)
{
var item = _containerToItem[container.ContainerControl];
_containerToItem.Remove(container.ContainerControl);
RemoveContainer(container.ContainerControl, item);
}
Dematerialized?.Invoke(
this,
new ItemContainerEventArgs(startingIndex, containers.ToList()));
}
/// <summary>
/// Gets the container for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The container, or null of not found.</returns>
public Control? ContainerFromItem(object item)
{
if (item != null)
{
_itemToContainer.TryGetValue(item, out var result);
if (result == null)
{
_itemToContainerSet.TryGetValue(item, out var set);
if (set?.Count > 0)
{
return set.FirstOrDefault();
}
}
return result;
}
return null;
}
/// <summary>
/// Gets the item for a container.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The item, or null of not found.</returns>
public object? ItemFromContainer(Control? container)
{
if (container != null)
{
_containerToItem.TryGetValue(container, out var result);
if (result != null)
{
_itemToContainer[result] = container;
}
return result;
}
return null;
}
}
}

164
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -1,164 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Generators
{
/// <summary>
/// Creates containers for tree items and maintains a list of created containers.
/// </summary>
/// <typeparam name="T">The type of the container.</typeparam>
public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
where T : Control, new()
{
private TreeView? _treeView;
/// <summary>
/// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
/// </summary>
/// <param name="owner">The owner control.</param>
/// <param name="contentProperty">The container's Content property.</param>
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
/// <param name="itemsProperty">The container's Items property.</param>
/// <param name="isExpandedProperty">The container's IsExpanded property.</param>
public TreeItemContainerGenerator(
Control owner,
AvaloniaProperty contentProperty,
AvaloniaProperty contentTemplateProperty,
AvaloniaProperty itemsProperty,
AvaloniaProperty isExpandedProperty)
: base(owner, contentProperty, contentTemplateProperty)
{
ItemsProperty = itemsProperty ?? throw new ArgumentNullException(nameof(itemsProperty));
IsExpandedProperty = isExpandedProperty ?? throw new ArgumentNullException(nameof(isExpandedProperty));
UpdateIndex();
}
/// <summary>
/// Gets the container index for the tree.
/// </summary>
public TreeContainerIndex? Index { get; private set; }
/// <summary>
/// Gets the item container's Items property.
/// </summary>
protected AvaloniaProperty ItemsProperty { get; }
/// <summary>
/// Gets the item container's IsExpanded property.
/// </summary>
protected AvaloniaProperty IsExpandedProperty { get; }
/// <inheritdoc/>
protected override Control? CreateContainer(object? item)
{
var container = item as T;
if (item == null)
{
return null;
}
else if (container != null)
{
Index?.Add(item, container);
return container;
}
else
{
var template = GetTreeDataTemplate(item, ItemTemplate);
var result = new T();
if (ItemContainerTheme != null)
{
result.SetValue(Control.ThemeProperty, ItemContainerTheme, BindingPriority.Style);
}
if (DisplayMemberBinding is not null)
{
result.SetValue(StyledElement.DataContextProperty, item, BindingPriority.Style);
result.Bind(ContentProperty, DisplayMemberBinding, BindingPriority.Style);
}
else
{
result.SetValue(ContentProperty, template.Build(item), BindingPriority.Style);
}
var itemsSelector = template.ItemsSelector(item);
if (itemsSelector != null)
{
BindingOperations.Apply(result, ItemsProperty, itemsSelector, null);
}
if (!(item is Control))
{
result.DataContext = item;
}
Index?.Add(item, result);
return result;
}
}
public override IEnumerable<ItemContainerInfo> Clear()
{
var items = base.Clear();
Index?.Remove(0, items);
return items;
}
public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count);
}
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
public void UpdateIndex()
{
if (Owner is TreeView treeViewOwner && Index == null)
{
Index = new TreeContainerIndex();
_treeView = treeViewOwner;
}
else
{
var treeView = Owner.GetSelfAndLogicalAncestors().OfType<TreeView>().FirstOrDefault();
if (treeView != _treeView)
{
Clear();
Index = treeView?.ItemContainerGenerator?.Index;
_treeView = treeView;
}
}
}
class WrapperTreeDataTemplate : ITreeDataTemplate
{
private readonly IDataTemplate _inner;
public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner;
public Control? Build(object? param) => _inner.Build(param);
public bool Match(object? data) => _inner.Match(data);
public InstancedBinding? ItemsSelector(object item) => null;
}
private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate? primary)
{
var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default;
var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template);
return treeTemplate;
}
}
}

205
src/Avalonia.Controls/ItemsControl.cs

@ -3,8 +3,8 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Automation.Peers;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
@ -15,7 +15,6 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
using Avalonia.Styling;
namespace Avalonia.Controls
@ -81,7 +80,7 @@ namespace Avalonia.Controls
private IEnumerable? _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator? _itemContainerGenerator;
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
/// <summary>
@ -103,27 +102,9 @@ namespace Avalonia.Controls
}
/// <summary>
/// Gets the <see cref="IItemContainerGenerator"/> for the control.
/// Gets the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
public IItemContainerGenerator ItemContainerGenerator
{
get
{
if (_itemContainerGenerator == null)
{
_itemContainerGenerator = CreateItemContainerGenerator();
_itemContainerGenerator.ItemContainerTheme = ItemContainerTheme;
_itemContainerGenerator.ItemTemplate = ItemTemplate;
_itemContainerGenerator.DisplayMemberBinding = DisplayMemberBinding;
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
_itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e);
}
return _itemContainerGenerator;
}
}
public ItemContainerGenerator ItemContainerGenerator => _itemContainerGenerator ??= new(this);
/// <summary>
/// Gets or sets the items to display.
@ -174,11 +155,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the items presenter control.
/// </summary>
public ItemsPresenter? Presenter
{
get;
protected set;
}
public ItemsPresenter? Presenter { get; private set; }
private protected bool WrapFocus { get; set; }
@ -188,6 +165,52 @@ namespace Avalonia.Controls
remove => _childIndexChanged -= value;
}
/// <summary>
/// Returns the container for the item at the specified index.
/// </summary>
/// <param name="index">The index of the item to retrieve.</param>
/// <returns>
/// The container for the item at the specified index within the item collection, if the
/// item has a container; otherwise, null.
/// </returns>
public Control? ContainerFromIndex(int index) => Presenter?.ContainerFromIndex(index);
/// <summary>
/// Returns the container corresponding to the specified item.
/// </summary>
/// <param name="item">The item to retrieve the container for.</param>
/// <returns>
/// A container that corresponds to the specified item, if the item has a container and
/// exists in the collection; otherwise, null.
/// </returns>
public Control? ContainerFromItem(object item)
{
throw new NotImplementedException();
}
/// <summary>
/// Returns the index to the item that has the specified, generated container.
/// </summary>
/// <param name="container">The generated container to retrieve the item index for.</param>
/// <returns>
/// The index to the item that corresponds to the specified generated container, or -1 if
/// <paramref name="container"/> is not found.
/// </returns>
public int IndexFromContainer(Control container) => Presenter?.IndexFromContainer(container) ?? -1;
/// <summary>
/// Returns the item that corresponds to the specified, generated container.
/// </summary>
/// <param name="container">The control that corresponds to the item to be returned.</param>
/// <returns>
/// The contained item, or the container if it does not contain an item.
/// </returns>
public object? ItemFromContainer(Control container)
{
// TODO: Should this throw or return null of container isn't a container?
throw new NotImplementedException();
}
/// <inheritdoc/>
void IItemsPresenterHost.RegisterItemsPresenter(ItemsPresenter presenter)
{
@ -197,7 +220,7 @@ namespace Avalonia.Controls
}
Presenter = presenter;
ItemContainerGenerator?.Clear();
////ItemContainerGenerator?.Clear();
if (Presenter is IChildIndexProvider innerProvider)
{
@ -237,6 +260,47 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Creates or a container that can be used to display an item.
/// </summary>
protected internal virtual Control CreateContainerOverride() => new ContentPresenter();
/// <summary>
/// Prepares the specified element to display the specified item.
/// </summary>
/// <param name="container">The element that's used to display the specified item.</param>
/// <param name="item">The item to display.</param>
/// <param name="index">The index of the item to display.</param>
protected internal virtual void PrepareContainerForItemOverride(Control container, object? item, int index)
{
if (container == item)
return;
if (container is ContentControl cc)
{
cc.SetValue(ContentControl.ContentProperty, item, BindingPriority.Template);
if (ItemTemplate is { } it)
cc.SetValue(ContentControl.ContentTemplateProperty, it, BindingPriority.Template);
}
else if (container is ContentPresenter p)
{
p.SetValue(ContentPresenter.ContentProperty, item, BindingPriority.Template);
if (ItemTemplate is { } it)
p.SetValue(ContentPresenter.ContentTemplateProperty, it, BindingPriority.Template);
}
if (ItemContainerTheme is not null)
container.SetValue(ThemeProperty, ItemContainerTheme, BindingPriority.Template);
}
/// <summary>
/// Undoes the effects of the <see cref="PrepareContainerForItemOverride(Control, object?, int)"/> method.
/// </summary>
/// <param name="container">The container element.</param>
protected internal virtual void ClearContainerForItemOverride(Control container)
{
}
/// <summary>
/// Gets the index of an item in a collection.
/// </summary>
@ -273,60 +337,11 @@ namespace Avalonia.Controls
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// Determines whether the specified item is (or is eligible to be) its own container.
/// </summary>
/// <returns>
/// An <see cref="IItemContainerGenerator"/>.
/// </returns>
protected virtual IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator(this);
}
/// <summary>
/// Called when new containers are materialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersMaterialized(ItemContainerEventArgs e)
{
foreach (var container in e.Containers)
{
// If the item is its own container, then it will be added to the logical tree when
// it was added to the Items collection.
if (container.ContainerControl != null && container.ContainerControl != container.Item)
{
LogicalChildren.Add(container.ContainerControl);
}
}
}
/// <summary>
/// Called when containers are dematerialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersDematerialized(ItemContainerEventArgs e)
{
foreach (var container in e.Containers)
{
// If the item is its own container, then it will be removed from the logical tree
// when it is removed from the Items collection.
if (container.ContainerControl != container.Item)
{
LogicalChildren.Remove(container.ContainerControl);
}
}
}
/// <summary>
/// Called when containers are recycled for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
{
}
/// <param name="item">The item to check.</param>
/// <returns>true if the item is (or is eligible to be) its own container; otherwise, false.</returns>
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
/// <summary>
/// Handles directional navigation within the <see cref="ItemsControl"/>.
@ -387,7 +402,8 @@ namespace Avalonia.Controls
}
else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
_itemContainerGenerator.ItemContainerTheme = change.GetNewValue<ControlTheme?>();
throw new NotImplementedException();
////_itemContainerGenerator.ItemContainerTheme = change.GetNewValue<ControlTheme?>();
}
}
@ -433,6 +449,27 @@ namespace Avalonia.Controls
}
}
internal void AddLogicalChild(Control c) => LogicalChildren.Add(c);
internal void RemoveLogicalChild(Control c) => LogicalChildren.Remove(c);
internal void ClearLogicalChildren() => LogicalChildren.Clear();
internal void PrepareItemContainer(Control container, object? item, int index)
{
var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme is not null &&
!container.IsSet(ThemeProperty) &&
((IStyleable)container).StyleKey == itemContainerTheme.TargetType)
{
container.Theme = itemContainerTheme;
}
if (item is not Control)
container.DataContext = item;
PrepareContainerForItemOverride(container, item, index);
}
/// <summary>
/// Given a collection of items, adds those that are controls to the logical children.
/// </summary>
@ -501,7 +538,7 @@ namespace Avalonia.Controls
{
if (_itemContainerGenerator != null)
{
_itemContainerGenerator.ItemTemplate = (IDataTemplate?)e.NewValue;
////_itemContainerGenerator.ItemTemplate = (IDataTemplate?)e.NewValue;
// TODO: Rebuild the item containers.
}
}

9
src/Avalonia.Controls/ListBox.cs

@ -103,14 +103,7 @@ namespace Avalonia.Controls
/// </summary>
public void UnselectAll() => Selection.Clear();
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<ListBoxItem>(
this,
ListBoxItem.ContentProperty,
ListBoxItem.ContentTemplateProperty);
}
protected internal override Control CreateContainerOverride() => new ListBoxItem();
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)

13
src/Avalonia.Controls/MenuBase.cs

@ -99,9 +99,10 @@ namespace Avalonia.Controls
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
throw new NotImplementedException();
////return ItemContainerGenerator.Containers
//// .Select(x => x.ContainerControl)
//// .OfType<IMenuItem>();
}
}
@ -141,12 +142,6 @@ namespace Avalonia.Controls
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{

13
src/Avalonia.Controls/MenuItem.cs

@ -332,9 +332,10 @@ namespace Avalonia.Controls
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType<IMenuItem>();
throw new NotImplementedException();
////return ItemContainerGenerator.Containers
//// .Select(x => x.ContainerControl)
//// .OfType<IMenuItem>();
}
}
@ -357,12 +358,6 @@ namespace Avalonia.Controls
/// <inheritdoc/>
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);

23
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -1,3 +1,4 @@
using System;
using System.Diagnostics;
namespace Avalonia.Controls.Presenters
@ -13,7 +14,7 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<ITemplate<Panel>> ItemsPanelProperty =
ItemsControl.ItemsPanelProperty.AddOwner<ItemsPresenter>();
private ItemsPresenterContainerGenerator? _generator;
private PanelContainerGenerator? _generator;
/// <summary>
/// Gets or sets a template which creates the <see cref="Panel"/> used to display the items.
@ -52,7 +53,10 @@ namespace Avalonia.Controls.Presenters
internal void ScrollIntoView(int index)
{
if (Panel is VirtualizingPanel v)
v.ScrollIntoView(index);
else if (index >= 0 && index < Panel?.Children.Count)
Panel.Children[index].BringIntoView();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -97,5 +101,20 @@ namespace Avalonia.Controls.Presenters
_generator?.Dispose();
_generator = new(this);
}
internal Control? ContainerFromIndex(int index)
{
if (Panel is VirtualizingPanel v)
return v.ContainerFromIndex(index);
return index >= 0 && index < Panel?.Children.Count ? Panel.Children[index] : null;
}
internal int IndexFromContainer(Control container)
{
if (Panel is VirtualizingPanel v)
return v.IndexFromContainer(container);
return Panel?.Children.IndexOf(container) ?? -1;
}
}
}

57
src/Avalonia.Controls/Presenters/ItemsPresenterContainerGenerator.cs → src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs

@ -9,11 +9,11 @@ namespace Avalonia.Controls.Presenters
/// <summary>
/// Generates containers for <see cref="ItemsPresenter"/>s that have non-virtualizing panels.
/// </summary>
internal class ItemsPresenterContainerGenerator : IDisposable
internal class PanelContainerGenerator : IDisposable
{
private readonly ItemsPresenter _presenter;
public ItemsPresenterContainerGenerator(ItemsPresenter presenter)
public PanelContainerGenerator(ItemsPresenter presenter)
{
Debug.Assert(presenter.ItemsControl is not null);
Debug.Assert(presenter.Panel is not null or VirtualizingPanel);
@ -29,12 +29,17 @@ namespace Avalonia.Controls.Presenters
public void Dispose()
{
_presenter.ItemsControl!.PropertyChanged -= OnItemsControlPropertyChanged;
if (_presenter.ItemsControl is { } itemsControl)
{
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
if (_presenter.ItemsControl.Items is INotifyCollectionChanged incc)
incc.CollectionChanged -= OnItemsChanged;
if (itemsControl.Items is INotifyCollectionChanged incc)
incc.CollectionChanged -= OnItemsChanged;
_presenter.Panel!.Children.Clear();
itemsControl.ClearLogicalChildren();
}
_presenter.Panel?.Children.Clear();
}
private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
@ -51,58 +56,68 @@ namespace Avalonia.Controls.Presenters
private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (_presenter.ItemsControl?.Items is null || _presenter.Panel is null)
if (_presenter.Panel is null || _presenter.ItemsControl is null)
return;
var generator = _presenter.ItemsControl.ItemContainerGenerator;
var itemsControl = _presenter.ItemsControl;
var panel = _presenter.Panel;
void Add(int index, IEnumerable items)
{
var i = index;
foreach (var item in items)
{
var c = generator.Materialize(i, item);
panel.Children.Insert(i++, c.ContainerControl);
}
panel.Children.Insert(i++, CreateContainer(itemsControl, item, i));
}
void Remove(int index, int count)
{
for (var i = 0; i < count; ++i)
{
itemsControl.RemoveLogicalChild(panel.Children[i + index]);
panel.Children.RemoveAt(i + index);
}
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
generator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Remove:
generator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count);
Remove(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Replace:
generator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count);
Remove(e.OldStartingIndex, e.OldItems!.Count);
generator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Move:
generator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count);
Remove(e.OldStartingIndex, e.OldItems!.Count);
generator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count);
Add(e.NewStartingIndex, e.NewItems!);
break;
case NotifyCollectionChangedAction.Reset:
generator.Clear();
itemsControl.ClearLogicalChildren();
panel.Children.Clear();
if (_presenter.ItemsControl.Items is { } items)
if (_presenter.ItemsControl?.Items is { } items)
Add(0, items);
break;
}
}
private static Control CreateContainer(ItemsControl itemsControl, object? item, int index)
{
var generator = itemsControl.ItemContainerGenerator;
if (item is Control c && generator.IsItemItsOwnContainer(c))
{
return c;
}
else
{
c = generator.CreateContainer();
itemsControl.AddLogicalChild(c);
generator.PrepareItemContainer(c, item, index);
return c;
}
}
}
}

138
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Xml.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Selection;
using Avalonia.Data;
@ -391,7 +392,7 @@ namespace Avalonia.Controls.Primitives
for (var current = eventSource as Visual; current != null; current = current.VisualParent)
{
if (current is Control control && control.Parent == this &&
ItemContainerGenerator?.IndexFromContainer(control) != -1)
IndexFromContainer(control) != -1)
{
return control;
}
@ -432,54 +433,34 @@ namespace Avalonia.Controls.Primitives
}
}
/// <inheritdoc/>
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.OnContainersMaterialized(e);
base.PrepareContainerForItemOverride(element, item, index);
foreach (var container in e.Containers)
if ((element as ISelectable)?.IsSelected == true)
{
if ((container.ContainerControl as ISelectable)?.IsSelected == true)
{
Selection.Select(container.Index);
MarkContainerSelected(container.ContainerControl, true);
}
else
{
var selected = Selection.IsSelected(container.Index);
MarkContainerSelected(container.ContainerControl, selected);
}
Selection.Select(index);
MarkContainerSelected(element, true);
}
}
/// <inheritdoc/>
protected override void OnContainersDematerialized(ItemContainerEventArgs e)
{
base.OnContainersDematerialized(e);
if (Presenter?.Panel is InputElement panel)
else
{
foreach (var container in e.Containers)
{
if (KeyboardNavigation.GetTabOnceActiveElement(panel) == container.ContainerControl)
{
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
break;
}
}
var selected = Selection.IsSelected(index);
MarkContainerSelected(element, selected);
}
}
protected override void OnContainersRecycled(ItemContainerEventArgs e)
protected internal override void ClearContainerForItemOverride(Control element)
{
foreach (var i in e.Containers)
base.ClearContainerForItemOverride(element);
if (Presenter?.Panel is InputElement panel &&
KeyboardNavigation.GetTabOnceActiveElement(panel) == element)
{
if (i.ContainerControl != null && i.Item != null)
{
bool selected = Selection.IsSelected(i.Index);
MarkContainerSelected(i.ContainerControl, selected);
}
KeyboardNavigation.SetTabOnceActiveElement(panel, null);
}
if (element is ISelectable selectable)
MarkContainerSelected(element, false);
}
/// <inheritdoc/>
@ -526,44 +507,45 @@ namespace Avalonia.Controls.Primitives
protected override void OnTextInput(TextInputEventArgs e)
{
if (!e.Handled)
{
if (!IsTextSearchEnabled)
return;
StopTextSearchTimer();
_textSearchTerm += e.Text;
bool Match(ItemContainerInfo info)
{
if (info.ContainerControl is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty))
{
var searchText = ao.GetValue(TextSearch.TextProperty);
if (searchText?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true)
{
return true;
}
}
return info.ContainerControl is IContentControl control &&
control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;
}
throw new NotImplementedException();
////if (!e.Handled)
////{
//// if (!IsTextSearchEnabled)
//// return;
//// StopTextSearchTimer();
//// _textSearchTerm += e.Text;
//// bool Match(ItemContainerInfo info)
//// {
//// if (info.ContainerControl is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty))
//// {
//// var searchText = ao.GetValue(TextSearch.TextProperty);
//// if (searchText?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true)
//// {
//// return true;
//// }
//// }
//// return info.ContainerControl is IContentControl control &&
//// control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;
//// }
var info = ItemContainerGenerator?.Containers.FirstOrDefault(Match);
//// var info = ItemContainerGenerator?.Containers.FirstOrDefault(Match);
if (info != null)
{
SelectedIndex = info.Index;
}
//// if (info != null)
//// {
//// SelectedIndex = info.Index;
//// }
StartTextSearchTimer();
//// StartTextSearchTimer();
e.Handled = true;
}
//// e.Handled = true;
////}
base.OnTextInput(e);
////base.OnTextInput(e);
}
protected override void OnKeyDown(KeyEventArgs e)
@ -634,7 +616,7 @@ namespace Avalonia.Controls.Primitives
/// <returns>True if the selection was moved; otherwise false.</returns>
protected bool MoveSelection(NavigationDirection direction, bool wrap)
{
var from = SelectedIndex != -1 ? ItemContainerGenerator?.ContainerFromIndex(SelectedIndex) : null;
var from = SelectedIndex != -1 ? ContainerFromIndex(SelectedIndex) : null;
return MoveSelection(from, direction, wrap);
}
@ -650,7 +632,7 @@ namespace Avalonia.Controls.Primitives
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is Control next)
{
var index = ItemContainerGenerator?.IndexFromContainer(next) ?? -1;
var index = IndexFromContainer(next);
if (index != -1)
{
@ -733,7 +715,7 @@ namespace Avalonia.Controls.Primitives
if (Presenter?.Panel != null)
{
var container = ItemContainerGenerator?.ContainerFromIndex(index);
var container = ContainerFromIndex(index);
KeyboardNavigation.SetTabOnceActiveElement(
(InputElement)Presenter.Panel,
container);
@ -757,7 +739,7 @@ namespace Avalonia.Controls.Primitives
bool rightButton = false,
bool fromFocus = false)
{
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
var index = IndexFromContainer(container);
if (index != -1)
{
@ -842,7 +824,7 @@ namespace Avalonia.Controls.Primitives
{
void Mark(int index, bool selected)
{
var container = ItemContainerGenerator?.ContainerFromIndex(index);
var container = ContainerFromIndex(index);
if (container != null)
{
@ -909,7 +891,7 @@ namespace Avalonia.Controls.Primitives
e.Source is Control control &&
e.Source is ISelectable selectable &&
control.Parent == this &&
ItemContainerGenerator?.IndexFromContainer(control) != -1)
IndexFromContainer(control) != -1)
{
UpdateSelection(control, selectable.IsSelected);
}
@ -961,7 +943,7 @@ namespace Avalonia.Controls.Primitives
{
MarkContainerSelected(
container,
Selection.IsSelected(ItemContainerGenerator.IndexFromContainer(container)));
Selection.IsSelected(IndexFromContainer(container)));
}
}
}

8
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -18,14 +18,6 @@ namespace Avalonia.Controls.Primitives
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator<TabStripItem>(
this,
ContentControl.ContentProperty,
ContentControl.ContentTemplateProperty);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{

13
src/Avalonia.Controls/TabControl.cs

@ -165,15 +165,15 @@ namespace Avalonia.Controls
return RegisterContentPresenter(presenter);
}
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
protected internal override void PrepareContainerForItemOverride(Control element, object? item, int index)
{
base.OnContainersMaterialized(e);
base.PrepareContainerForItemOverride(element, item, index);
UpdateSelectedContent();
}
protected override void OnContainersRecycled(ItemContainerEventArgs e)
protected internal override void ClearContainerForItemOverride(Control element)
{
base.OnContainersRecycled(e);
base.ClearContainerForItemOverride(element);
UpdateSelectedContent();
}
@ -207,11 +207,6 @@ namespace Avalonia.Controls
return false;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new TabItemContainerGenerator(this);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");

440
src/Avalonia.Controls/TreeView.cs

@ -75,12 +75,6 @@ namespace Avalonia.Controls
remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value);
}
/// <summary>
/// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
/// </summary>
public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <summary>
/// Gets or sets a value indicating whether to automatically scroll to newly selected items.
/// </summary>
@ -188,7 +182,8 @@ namespace Avalonia.Controls
/// </remarks>
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index!.Items);
throw new NotImplementedException();
////SynchronizeItems(SelectedItems, ItemContainerGenerator.Index!.Items);
}
/// <summary>
@ -242,7 +237,7 @@ namespace Avalonia.Controls
if (AutoScrollToSelectedItem)
{
var container = ItemContainerGenerator.Index!.ContainerFromItem(e.NewItems![0]!);
var container = ContainerFromItem(e.NewItems![0]!);
container?.BringIntoView();
}
@ -282,9 +277,10 @@ namespace Avalonia.Controls
break;
case NotifyCollectionChangedAction.Reset:
foreach (Control container in ItemContainerGenerator.Index!.Containers)
foreach (var child in LogicalChildren)
{
MarkContainerSelected(container, false);
if (child is Control container && IndexFromContainer(container) != -1)
MarkContainerSelected(container, false);
}
if (SelectedItems.Count > 0)
@ -337,7 +333,7 @@ namespace Avalonia.Controls
private void MarkItemSelected(object item, bool selected)
{
var container = ItemContainerGenerator.Index!.ContainerFromItem(item)!;
var container = ContainerFromItem(item)!;
MarkContainerSelected(container, selected);
}
@ -378,8 +374,8 @@ namespace Avalonia.Controls
if (!this.IsVisualAncestorOf((Visual)element))
{
var result = _selectedItem != null ?
ItemContainerGenerator.Index!.ContainerFromItem(_selectedItem) :
ItemContainerGenerator.ContainerFromIndex(0);
ContainerFromItem(_selectedItem) :
ContainerFromIndex(0);
return (result != null, result); // SelectedItem may not be in the treeview.
}
@ -390,27 +386,6 @@ namespace Avalonia.Controls
return (false, null);
}
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
var result = CreateTreeItemContainerGenerator();
result.Index!.Materialized += ContainerMaterialized;
return result;
}
protected virtual ITreeItemContainerGenerator CreateTreeItemContainerGenerator() =>
CreateTreeItemContainerGenerator<TreeViewItem>();
protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator<TVItem>() where TVItem: TreeViewItem, new()
{
return new TreeItemContainerGenerator<TVItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty);
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
@ -466,53 +441,54 @@ namespace Avalonia.Controls
NavigationDirection direction,
bool intoChildren)
{
IItemContainerGenerator? parentGenerator = GetParentContainerGenerator(from);
if (parentGenerator == null)
{
return null;
}
var index = from is not null ? parentGenerator.IndexFromContainer(from) : -1;
var parent = from?.Parent as ItemsControl;
TreeViewItem? result = null;
switch (direction)
{
case NavigationDirection.Up:
if (index > 0)
{
var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1)!;
result = previous.IsExpanded && previous.ItemCount > 0 ?
(TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1)! :
previous;
}
else
{
result = from?.Parent as TreeViewItem;
}
break;
case NavigationDirection.Down:
case NavigationDirection.Right:
if (from?.IsExpanded == true && intoChildren && from.ItemCount > 0)
{
result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0)!;
}
else if (index < parent?.ItemCount - 1)
{
result = (TreeViewItem)parentGenerator.ContainerFromIndex(index + 1)!;
}
else if (parent is TreeViewItem parentItem)
{
return GetContainerInDirection(parentItem, direction, false);
}
break;
}
return result;
throw new NotImplementedException();
////IItemContainerGenerator? parentGenerator = GetParentContainerGenerator(from);
////if (parentGenerator == null)
////{
//// return null;
////}
////var index = from is not null ? parentGenerator.IndexFromContainer(from) : -1;
////var parent = from?.Parent as ItemsControl;
////TreeViewItem? result = null;
////switch (direction)
////{
//// case NavigationDirection.Up:
//// if (index > 0)
//// {
//// var previous = (TreeViewItem)parentGenerator.ContainerFromIndex(index - 1)!;
//// result = previous.IsExpanded && previous.ItemCount > 0 ?
//// (TreeViewItem)previous.ItemContainerGenerator.ContainerFromIndex(previous.ItemCount - 1)! :
//// previous;
//// }
//// else
//// {
//// result = from?.Parent as TreeViewItem;
//// }
//// break;
//// case NavigationDirection.Down:
//// case NavigationDirection.Right:
//// if (from?.IsExpanded == true && intoChildren && from.ItemCount > 0)
//// {
//// result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0)!;
//// }
//// else if (index < parent?.ItemCount - 1)
//// {
//// result = (TreeViewItem)parentGenerator.ContainerFromIndex(index + 1)!;
//// }
//// else if (parent is TreeViewItem parentItem)
//// {
//// return GetContainerInDirection(parentItem, direction, false);
//// }
//// break;
////}
////return result;
}
/// <inheritdoc/>
@ -551,80 +527,63 @@ namespace Avalonia.Controls
bool toggleModifier = false,
bool rightButton = false)
{
var item = ItemContainerGenerator.Index!.ItemFromContainer(container);
if (item == null)
{
return;
}
Control? selectedContainer = null;
if (SelectedItem != null)
{
selectedContainer = ItemContainerGenerator.Index!.ContainerFromItem(SelectedItem);
}
var mode = SelectionMode;
var toggle = toggleModifier || mode.HasAllFlags(SelectionMode.Toggle);
var multi = mode.HasAllFlags(SelectionMode.Multiple);
var range = multi && rangeModifier && selectedContainer != null;
if (rightButton)
{
if (!SelectedItems.Contains(item))
{
SelectSingleItem(item);
}
}
else if (!toggle && !range)
{
SelectSingleItem(item);
}
else if (multi && range)
{
SynchronizeItems(
SelectedItems,
GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
}
else
{
var i = SelectedItems.IndexOf(item);
if (i != -1)
{
SelectedItems.Remove(item);
}
else
{
if (multi)
{
SelectedItems.Add(item);
}
else
{
SelectedItem = item;
}
}
}
}
private static IItemContainerGenerator? GetParentContainerGenerator(TreeViewItem? item)
{
if (item == null)
{
return null;
}
switch (item.Parent)
{
case TreeView treeView:
return treeView.ItemContainerGenerator;
case TreeViewItem treeViewItem:
return treeViewItem.ItemContainerGenerator;
default:
return null;
}
throw new NotImplementedException();
////var item = ItemContainerGenerator.Index!.ItemFromContainer(container);
////if (item == null)
////{
//// return;
////}
////Control? selectedContainer = null;
////if (SelectedItem != null)
////{
//// selectedContainer = ItemContainerGenerator.Index!.ContainerFromItem(SelectedItem);
////}
////var mode = SelectionMode;
////var toggle = toggleModifier || mode.HasAllFlags(SelectionMode.Toggle);
////var multi = mode.HasAllFlags(SelectionMode.Multiple);
////var range = multi && rangeModifier && selectedContainer != null;
////if (rightButton)
////{
//// if (!SelectedItems.Contains(item))
//// {
//// SelectSingleItem(item);
//// }
////}
////else if (!toggle && !range)
////{
//// SelectSingleItem(item);
////}
////else if (multi && range)
////{
//// SynchronizeItems(
//// SelectedItems,
//// GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
////}
////else
////{
//// var i = SelectedItems.IndexOf(item);
//// if (i != -1)
//// {
//// SelectedItems.Remove(item);
//// }
//// else
//// {
//// if (multi)
//// {
//// SelectedItems.Add(item);
//// }
//// else
//// {
//// SelectedItem = item;
//// }
//// }
////}
}
/// <summary>
@ -639,23 +598,24 @@ namespace Avalonia.Controls
return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB);
}
private static TreeViewItem? FindInContainers(ITreeItemContainerGenerator containerGenerator,
private static TreeViewItem? FindInContainers(ItemContainerGenerator containerGenerator,
TreeViewItem nodeA,
TreeViewItem nodeB)
{
IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
throw new NotImplementedException();
////IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
foreach (ItemContainerInfo container in containers)
{
TreeViewItem? node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB);
////foreach (ItemContainerInfo container in containers)
////{
//// TreeViewItem? node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB);
if (node != null)
{
return node;
}
}
//// if (node != null)
//// {
//// return node;
//// }
////}
return null;
////return null;
}
private static TreeViewItem? FindFirstNode(TreeViewItem? node, TreeViewItem nodeA, TreeViewItem nodeB)
@ -683,59 +643,60 @@ namespace Avalonia.Controls
/// <param name="to">To container.</param>
private List<object> GetItemsInRange(TreeViewItem? from, TreeViewItem? to)
{
var items = new List<object>();
throw new NotImplementedException();
////var items = new List<object>();
if (from == null || to == null)
{
return items;
}
////if (from == null || to == null)
////{
//// return items;
////}
TreeViewItem? firstItem = FindFirstNode(this, from, to);
////TreeViewItem? firstItem = FindFirstNode(this, from, to);
if (firstItem == null)
{
return items;
}
////if (firstItem == null)
////{
//// return items;
////}
bool wasReversed = false;
////bool wasReversed = false;
if (firstItem == to)
{
var temp = from;
////if (firstItem == to)
////{
//// var temp = from;
from = to;
to = temp;
//// from = to;
//// to = temp;
wasReversed = true;
}
//// wasReversed = true;
////}
TreeViewItem? node = from;
////TreeViewItem? node = from;
while (node != to)
{
var item = ItemContainerGenerator.Index!.ItemFromContainer(node);
////while (node != to)
////{
//// var item = ItemContainerGenerator.Index!.ItemFromContainer(node);
if (item != null)
{
items.Add(item);
}
//// if (item != null)
//// {
//// items.Add(item);
//// }
node = GetContainerInDirection(node, NavigationDirection.Down, true);
}
//// node = GetContainerInDirection(node, NavigationDirection.Down, true);
////}
var toItem = ItemContainerGenerator.Index!.ItemFromContainer(to);
////var toItem = ItemContainerGenerator.Index!.ItemFromContainer(to);
if (toItem != null)
{
items.Add(toItem);
}
////if (toItem != null)
////{
//// items.Add(toItem);
////}
if (wasReversed)
{
items.Reverse();
}
////if (wasReversed)
////{
//// items.Reverse();
////}
return items;
////return items;
}
/// <summary>
@ -776,19 +737,20 @@ namespace Avalonia.Controls
/// <returns>The container or null if the event did not originate in a container.</returns>
protected TreeViewItem? GetContainerFromEventSource(object eventSource)
{
var item = ((Visual)eventSource).GetSelfAndVisualAncestors()
.OfType<TreeViewItem>()
.FirstOrDefault();
throw new NotImplementedException();
////var item = ((Visual)eventSource).GetSelfAndVisualAncestors()
//// .OfType<TreeViewItem>()
//// .FirstOrDefault();
if (item != null)
{
if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index)
{
return item;
}
}
////if (item != null)
////{
//// if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index)
//// {
//// return item;
//// }
////}
return null;
////return null;
}
/// <summary>
@ -796,30 +758,30 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void ContainerMaterialized(object? sender, ItemContainerEventArgs e)
{
var selectedItem = SelectedItem;
if (selectedItem == null)
{
return;
}
foreach (var container in e.Containers)
{
if (container.Item == selectedItem)
{
((TreeViewItem)container.ContainerControl).IsSelected = true;
if (AutoScrollToSelectedItem)
{
Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView);
}
break;
}
}
}
////private void ContainerMaterialized(object? sender, ItemContainerEventArgs e)
////{
//// var selectedItem = SelectedItem;
//// if (selectedItem == null)
//// {
//// return;
//// }
//// foreach (var container in e.Containers)
//// {
//// if (container.Item == selectedItem)
//// {
//// ((TreeViewItem)container.ContainerControl).IsSelected = true;
//// if (AutoScrollToSelectedItem)
//// {
//// Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView);
//// }
//// break;
//// }
//// }
////}
/// <summary>
/// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.

35
src/Avalonia.Controls/TreeViewItem.cs

@ -58,7 +58,6 @@ namespace Avalonia.Controls
PressedMixin.Attach<TreeViewItem>();
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);
ParentProperty.Changed.AddClassHandler<TreeViewItem>((o, e) => o.OnParentChanged(e));
RequestBringIntoViewEvent.AddClassHandler<TreeViewItem>((x, e) => x.OnRequestBringIntoView(e));
}
@ -89,27 +88,6 @@ namespace Avalonia.Controls
private set { SetAndRaise(LevelProperty, ref _level, value); }
}
/// <summary>
/// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
/// </summary>
public new ITreeItemContainerGenerator ItemContainerGenerator =>
(ITreeItemContainerGenerator)base.ItemContainerGenerator;
/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator() => CreateTreeItemContainerGenerator<TreeViewItem>();
/// <inheritdoc/>
protected ITreeItemContainerGenerator CreateTreeItemContainerGenerator<TVItem>()
where TVItem: TreeViewItem, new()
{
return new TreeItemContainerGenerator<TVItem>(
this,
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty);
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
@ -118,7 +96,6 @@ namespace Avalonia.Controls
_treeView = this.GetLogicalAncestors().OfType<TreeView>().FirstOrDefault();
Level = CalculateDistanceFromLogicalParent<TreeView>(this) - 1;
ItemContainerGenerator.UpdateIndex();
if (ItemTemplate == null && _treeView?.ItemTemplate != null)
{
@ -134,7 +111,6 @@ namespace Avalonia.Controls
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
ItemContainerGenerator.UpdateIndex();
}
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)
@ -219,16 +195,5 @@ namespace Avalonia.Controls
return logical != null ? result : @default;
}
private void OnParentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null)
{
// If we're not attached to the logical tree, then OnDetachedFromLogicalTree isn't going to be
// called when the item is removed. This results in the item not being removed from the index,
// causing #3551. In this case, update the index when Parent is changed to null.
ItemContainerGenerator.UpdateIndex();
}
}
}
}

101
src/Avalonia.Controls/VirtualizingPanel.cs

@ -1,5 +1,7 @@
using System.Collections;
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls
@ -25,6 +27,32 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Scrolls the specified item into view.
/// </summary>
/// <param name="index">The index of the item.</param>
protected internal abstract void ScrollIntoView(int index);
/// <summary>
/// Returns the container for the item at the specified index.
/// </summary>
/// <param name="index">The index of the item to retrieve.</param>
/// <returns>
/// The container for the item at the specified index within the item collection, if the
/// item is realized; otherwise, null.
/// </returns>
protected internal abstract Control? ContainerFromIndex(int index);
/// <summary>
/// Returns the index to the item that has the specified realized container.
/// </summary>
/// <param name="container">The generated container to retrieve the item index for.</param>
/// <returns>
/// The index to the item that corresponds to the specified realized container, or -1 if
/// <paramref name="container"/> is not found.
/// </returns>
protected internal abstract int IndexFromContainer(Control container);
/// <summary>
/// Called when the <see cref="ItemsControl"/> that owns the panel changes.
/// </summary>
@ -51,8 +79,59 @@ namespace Avalonia.Controls
{
}
/// <summary>
/// Adds the specified <see cref="Control"/> to the <see cref="Panel.Children"/> collection
/// of a <see cref="VirtualizingPanel"/> element.
/// </summary>
/// <param name="control">The control to add to the collection.</param>
protected void AddInternalChild(Control control)
{
var itemsControl = EnsureItemsControl();
itemsControl.AddLogicalChild(control);
Children.Add(control);
}
/// <summary>
/// Adds the specified <see cref="Control"/> to the <see cref="Panel.Children"/> collection
/// of a <see cref="VirtualizingPanel"/> element at the specified index position.
/// </summary>
/// <param name="index">
/// The index position within the collection at which the child element is inserted.
/// </param>
/// <param name="control">The control to add to the collection.</param>
protected void InsertInternalChild(int index, Control control)
{
var itemsControl = EnsureItemsControl();
itemsControl.AddLogicalChild(control);
Children.Insert(index, control);
}
/// <summary>
/// Removes child elements from the <see cref="Panel.Children"/> collection.
/// </summary>
/// <param name="index">
/// The beginning index position within the collection at which the first child element is
/// removed.
/// </param>
/// <param name="count">The number of child elements to remove.</param>
protected void RemoveInternalChildRange(int index, int count)
{
var itemsControl = EnsureItemsControl();
for (var i = 0; i < count; ++i)
{
var c = Children[i];
itemsControl.RemoveLogicalChild(c);
}
Children.RemoveRange(index, count);
}
internal void Attach(ItemsControl itemsControl)
{
if (ItemsControl is not null)
throw new InvalidOperationException("The VirtualizingPanel is already attached to an ItemsControl");
ItemsControl = itemsControl;
ItemsControl.PropertyChanged += OnItemsControlPropertyChanged;
@ -62,18 +141,24 @@ namespace Avalonia.Controls
internal void Detach()
{
if (ItemsControl is null)
return;
var itemsControl = EnsureItemsControl();
ItemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
itemsControl.PropertyChanged -= OnItemsControlPropertyChanged;
if (ItemsControl.Items is INotifyCollectionChanged incc)
if (itemsControl.Items is INotifyCollectionChanged incc)
incc.CollectionChanged -= OnItemsControlItemsChanged;
ItemsControl = null;
Children.Clear();
}
private ItemsControl EnsureItemsControl()
{
if (ItemsControl is null)
ThrowNotAttached();
return ItemsControl;
}
private protected virtual void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ItemsControl.ItemsProperty)
@ -91,5 +176,11 @@ namespace Avalonia.Controls
if (_itemsControl?.Items is IList items)
OnItemsChanged(items, e);
}
[DoesNotReturn]
private static void ThrowNotAttached()
{
throw new InvalidOperationException("The VirtualizingPanel does not belong to an ItemsControl.");
}
}
}

290
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Xml.Linq;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Utilities;
@ -18,16 +19,21 @@ namespace Avalonia.Controls
public static readonly StyledProperty<Orientation> OrientationProperty =
StackLayout.OrientationProperty.AddOwner<VirtualizingStackPanel>();
private static readonly AttachedProperty<bool> ItemIsOwnContainerProperty =
AvaloniaProperty.RegisterAttached<VirtualizingStackPanel, Control, bool>("ItemIsOwnContainer");
private static readonly Rect s_invalidViewport = new(double.PositiveInfinity, double.PositiveInfinity, 0, 0);
private readonly Action<Control> _recycleElement;
private readonly Action<Control, int> _updateElementIndex;
private int _anchorIndex = -1;
private Control? _anchorElement;
private bool _isInLayout;
private bool _isWaitingForViewportUpdate;
private double _lastEstimatedElementSizeU = 25;
private RealizedElementList? _measureElements;
private RealizedElementList? _realizedElements;
private Rect _viewport = s_invalidViewport;
private Stack<Control>? _recyclePool;
public VirtualizingStackPanel()
{
@ -54,73 +60,92 @@ namespace Avalonia.Controls
if (!IsEffectivelyVisible)
return default;
var items = ItemsControl?.Items as IList;
_isInLayout = true;
if (items is null || items.Count == 0)
try
{
Children.Clear();
return default;
}
var items = ItemsControl?.Items as IList;
if (items is null || items.Count == 0)
{
RemoveInternalChildRange(0, Children.Count);
return default;
}
var orientation = Orientation;
var orientation = Orientation;
_realizedElements ??= new();
_measureElements ??= new();
_realizedElements ??= new();
_measureElements ??= new();
// If we're bringing an item into view, ignore any layout passes until we receive a new
// effective viewport.
if (_isWaitingForViewportUpdate)
{
var sizeV = orientation == Orientation.Horizontal ? DesiredSize.Height : DesiredSize.Width;
return CalculateDesiredSize(orientation, items, sizeV);
}
// If we're bringing an item into view, ignore any layout passes until we receive a new
// effective viewport.
if (_isWaitingForViewportUpdate)
{
var sizeV = orientation == Orientation.Horizontal ? DesiredSize.Height : DesiredSize.Width;
return CalculateDesiredSize(orientation, items, sizeV);
}
// We handle horizontal and vertical layouts here so X and Y are abstracted to:
// - Horizontal layouts: U = horizontal, V = vertical
// - Vertical layouts: U = vertical, V = horizontal
var viewport = CalculateMeasureViewport(items);
// We handle horizontal and vertical layouts here so X and Y are abstracted to:
// - Horizontal layouts: U = horizontal, V = vertical
// - Vertical layouts: U = vertical, V = horizontal
var viewport = CalculateMeasureViewport(items);
// Recycle elements outside of the expected range.
_realizedElements.RecycleElementsBefore(viewport.firstIndex, _recycleElement);
_realizedElements.RecycleElementsAfter(viewport.estimatedLastIndex, _recycleElement);
// Recycle elements outside of the expected range.
_realizedElements.RecycleElementsBefore(viewport.firstIndex, _recycleElement);
_realizedElements.RecycleElementsAfter(viewport.estimatedLastIndex, _recycleElement);
// Do the measure, creating/recycling elements as necessary to fill the viewport. Don't
// write to _realizedElements yet, only _measureElements.
GenerateElements(availableSize, ref viewport);
// Do the measure, creating/recycling elements as necessary to fill the viewport. Don't
// write to _realizedElements yet, only _measureElements.
GenerateElements(availableSize, ref viewport);
// Now we know what definitely fits, recycle anything left over.
_realizedElements.RecycleElementsAfter(_measureElements.LastModelIndex, _recycleElement);
// Now we know what definitely fits, recycle anything left over.
_realizedElements.RecycleElementsAfter(_measureElements.LastModelIndex, _recycleElement);
// And swap the measureElements and realizedElements collection.
(_measureElements, _realizedElements) = (_realizedElements, _measureElements);
_measureElements.ResetForReuse();
// And swap the measureElements and realizedElements collection.
(_measureElements, _realizedElements) = (_realizedElements, _measureElements);
_measureElements.ResetForReuse();
return CalculateDesiredSize(orientation, items, viewport.measuredV);
return CalculateDesiredSize(orientation, items, viewport.measuredV);
}
finally
{
_isInLayout = false;
}
}
protected override Size ArrangeOverride(Size finalSize)
{
Debug.Assert(_realizedElements is not null);
if (_realizedElements is null)
return default;
var orientation = Orientation;
var u = _realizedElements!.StartU;
_isInLayout = true;
for (var i = 0; i < _realizedElements.Count; ++i)
try
{
var e = _realizedElements.Elements[i];
var orientation = Orientation;
var u = _realizedElements!.StartU;
if (e is object)
for (var i = 0; i < _realizedElements.Count; ++i)
{
var sizeU = _realizedElements.SizeU[i];
var rect = orientation == Orientation.Horizontal ?
new Rect(u, 0, sizeU, finalSize.Height) :
new Rect(0, u, finalSize.Width, sizeU);
e.Arrange(rect);
u += orientation == Orientation.Horizontal ? rect.Width : rect.Height;
var e = _realizedElements.Elements[i];
if (e is object)
{
var sizeU = _realizedElements.SizeU[i];
var rect = orientation == Orientation.Horizontal ?
new Rect(u, 0, sizeU, finalSize.Height) :
new Rect(0, u, finalSize.Width, sizeU);
e.Arrange(rect);
u += orientation == Orientation.Horizontal ? rect.Width : rect.Height;
}
}
}
return finalSize;
return finalSize;
}
finally
{
_isInLayout = false;
}
}
protected override void OnItemsChanged(IList items, NotifyCollectionChangedEventArgs e)
@ -132,20 +157,70 @@ namespace Avalonia.Controls
{
case NotifyCollectionChangedAction.Add:
_realizedElements.ItemsInserted(e.NewStartingIndex, e.NewItems!.Count, _updateElementIndex);
ItemsControl!.ItemContainerGenerator.InsertSpace(e.NewStartingIndex, e.NewItems!.Count);
break;
case NotifyCollectionChangedAction.Remove:
_realizedElements.ItemsRemoved(e.OldStartingIndex, e.OldItems!.Count, _updateElementIndex, _recycleElement);
ItemsControl!.ItemContainerGenerator.RemoveRange(e.OldStartingIndex, e.OldItems!.Count);
break;
case NotifyCollectionChangedAction.Reset:
////RecycleAllElements();
_realizedElements.RecycleAllElements(_recycleElement);
break;
}
InvalidateMeasure();
}
protected internal override Control? ContainerFromIndex(int index) => _realizedElements?.GetElement(index);
protected internal override int IndexFromContainer(Control container) => _realizedElements?.GetIndex(container) ?? -1;
protected internal override void ScrollIntoView(int index)
{
var items = ItemsControl?.Items as IList;
if (_isInLayout || items is null || index < 0 || index >= items.Count)
return;
if (GetRealizedElement(index) is Control element)
{
element.BringIntoView();
}
else if (this.GetVisualRoot() is ILayoutRoot root)
{
// Create and measure the element to be brought into view. Store it in a field so that
// it can be re-used in the layout pass.
_anchorElement = GetOrCreateElement(items, index);
_anchorElement.Measure(Size.Infinity);
_anchorIndex = index;
// Get the expected position of the elment and put it in place.
var anchorU = GetOrEstimateElementPosition(index);
var rect = Orientation == Orientation.Horizontal ?
new Rect(anchorU, 0, _anchorElement.DesiredSize.Width, _anchorElement.DesiredSize.Height) :
new Rect(0, anchorU, _anchorElement.DesiredSize.Width, _anchorElement.DesiredSize.Height);
_anchorElement.Arrange(rect);
// If the item being brought into view was added since the last layout pass then
// our bounds won't be updated, so any containing scroll viewers will not have an
// updated extent. Do a layout pass to ensure that the containing scroll viewers
// will be able to scroll the new item into view.
if (!Bounds.Contains(rect) && !_viewport.Contains(rect))
{
_isWaitingForViewportUpdate = true;
root.LayoutManager.ExecuteLayoutPass();
_isWaitingForViewportUpdate = false;
}
// Try to bring the item into view and do a layout pass.
_anchorElement.BringIntoView();
_isWaitingForViewportUpdate = !_viewport.Contains(rect);
root.LayoutManager.ExecuteLayoutPass();
_isWaitingForViewportUpdate = false;
_anchorElement = null;
_anchorIndex = -1;
}
}
internal IReadOnlyList<Control?> GetRealizedElements()
{
return _realizedElements?.Elements ?? Array.Empty<Control>();
@ -206,7 +281,10 @@ namespace Avalonia.Controls
private double EstimateElementSizeU()
{
var count = _realizedElements!.Count;
if (_realizedElements is null)
return _lastEstimatedElementSizeU;
var count = _realizedElements.Count;
var divisor = 0.0;
var total = 0.0;
@ -280,36 +358,89 @@ namespace Avalonia.Controls
private Control GetOrCreateElement(IList items, int index)
{
var e = GetRealizedElement(index) ?? GetRecycledOrCreateElement(items, index);
var e = GetRealizedElement(index) ??
GetRecycledElement(items, index) ??
CreateElement(items, index);
InvalidateHack(e);
return e;
}
private Control? GetRealizedElement(int index)
{
Debug.Assert(_realizedElements is not null);
if (_anchorIndex == index)
return _anchorElement;
return _realizedElements!.GetElement(index);
return _realizedElements?.GetElement(index);
}
private Control GetRecycledOrCreateElement(IList items, int index)
private Control? GetRecycledElement(IList items, int index)
{
Debug.Assert(ItemsControl is not null);
var c = ItemsControl!.ItemContainerGenerator.Materialize(index, items[index]!).ContainerControl;
Children.Add(c);
return c;
var generator = ItemsControl!.ItemContainerGenerator;
var item = items[index];
if (item is Control controlItem)
{
if (controlItem.IsSet(ItemIsOwnContainerProperty))
{
controlItem.IsVisible = true;
return controlItem;
}
else if (generator.IsItemItsOwnContainer(controlItem))
{
generator.PrepareItemContainer(controlItem, item, index);
controlItem.SetValue(ItemIsOwnContainerProperty, true);
AddInternalChild(controlItem);
return controlItem;
}
}
if (_recyclePool?.Count > 0)
{
var recycled = _recyclePool.Pop();
recycled.IsVisible = true;
generator.PrepareItemContainer(recycled, item, index);
return recycled;
}
return null;
}
private void RecycleElement(Control element)
private Control CreateElement(IList items, int index)
{
Debug.Assert(ItemsControl is not null);
var index = ItemsControl.ItemContainerGenerator!.IndexFromContainer(element);
ItemsControl!.ItemContainerGenerator.Dematerialize(index, 1);
Children.Remove(element);
var generator = ItemsControl!.ItemContainerGenerator;
var item = items[index];
var container = generator.CreateContainer();
AddInternalChild(container);
generator.PrepareItemContainer(container, item, index);
return container;
}
private double GetOrEstimateElementPosition(int index)
{
var estimatedElementSize = EstimateElementSizeU();
return index * estimatedElementSize;
}
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 void UpdateElementIndex(Control element, int index)
@ -478,6 +609,43 @@ namespace Avalonia.Controls
return (-1, 0);
}
/// <summary>
/// Gets the position of an element on the primary axis, if realized.
/// </summary>
/// <param name="modelIndex">The index in the source collection of the element.</param>
/// <param name="position">
/// When the method exits, contains the element's position on the primary axis, if
/// the element is realized.
/// </param>
/// <returns>
/// True if the requested element was found, otherwise false.
/// </returns>
public bool TryGetElementU(int modelIndex, out double position)
{
if (_sizes is null || modelIndex < FirstModelIndex || modelIndex > LastModelIndex)
{
position = double.NaN;
return false;
}
var index = modelIndex - FirstModelIndex;
position = StartU;
for (var i = 0; i < index; ++i)
{
position += _sizes[i];
}
return true;
}
/// <summary>
/// Gets the model index of the specified element.
/// </summary>
/// <param name="element">The element.</param>
/// <returns>The model index or -1 if the element is not present in the collection.</returns>
public int GetIndex(Control element) => _elements?.IndexOf(element) is int index && index >= 0 ? index : -1;
/// <summary>
/// Updates the elements in response to items being inserted into the source collection.
/// </summary>

12
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -21,7 +21,7 @@ namespace Avalonia.Diagnostics.Views
{
InitializeComponent();
_tree = this.GetControl<TreeView>("tree");
_tree.ItemContainerGenerator.Index!.Materialized += TreeViewItemMaterialized;
////_tree.ItemContainerGenerator.Index!.Materialized += TreeViewItemMaterialized;
_adorner = new Panel
{
@ -103,11 +103,11 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e)
{
var item = (TreeViewItem)e.Containers[0].ContainerControl;
item.TemplateApplied += TreeViewItemTemplateApplied;
}
////private void TreeViewItemMaterialized(object? sender, ItemContainerEventArgs e)
////{
//// var item = (TreeViewItem)e.Containers[0].ContainerControl;
//// item.TemplateApplied += TreeViewItemTemplateApplied;
////}
private void TreeViewItemTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{

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

@ -1,341 +1,341 @@
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.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();
Assert.Equal(-1, target.SelectedIndex);
Assert.Empty(target.GetLogicalChildren());
//// Assert.Equal(-1, target.SelectedIndex);
//// Assert.Empty(target.GetLogicalChildren());
items.Add("Foo");
//// items.Add("Foo");
Assert.Equal(0, target.SelectedIndex);
Assert.Single(target.GetLogicalChildren());
}
//// Assert.Equal(0, target.SelectedIndex);
//// Assert.Single(target.GetLogicalChildren());
//// }
[Fact]
public void Selected_Index_Changes_To_None_When_Items_Assigned_Null()
{
var items = new ObservableCollection<string>
{
"Foo",
"Bar",
"FooBar"
};
//// [Fact]
//// public void Selected_Index_Changes_To_None_When_Items_Assigned_Null()
//// {
//// var items = new ObservableCollection<string>
//// {
//// "Foo",
//// "Bar",
//// "FooBar"
//// };
var target = new Carousel
{
Template = new FuncControlTemplate<Carousel>(CreateTemplate),
Items = items,
IsVirtualized = false
};
//// var target = new Carousel
//// {
//// Template = new FuncControlTemplate<Carousel>(CreateTemplate),
//// Items = items,
//// IsVirtualized = false
//// };
target.ApplyTemplate();
((Control)target.Presenter).ApplyTemplate();
//// target.ApplyTemplate();
//// ((Control)target.Presenter).ApplyTemplate();
Assert.Equal(3, target.GetLogicalChildren().Count());
//// Assert.Equal(3, target.GetLogicalChildren().Count());
var child = GetContainerTextBlock(target.GetLogicalChildren().First());
//// var child = GetContainerTextBlock(target.GetLogicalChildren().First());
Assert.Equal("Foo", child.Text);
//// Assert.Equal("Foo", child.Text);
target.Items = null;
//// target.Items = null;
var numChildren = target.GetLogicalChildren().Count();
//// 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(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 }));
//// }
//// }
//// }
////}

173
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs

@ -1,173 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Data;
using Xunit;
namespace Avalonia.Controls.UnitTests.Generators
{
public class ItemContainerGeneratorTests
{
[Fact]
public void Materialize_Should_Create_Containers()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = Materialize(target, 0, items);
var result = containers
.Select(x => x.ContainerControl)
.OfType<ContentPresenter>()
.Select(x => x.Content)
.ToList();
Assert.Equal(items, result);
}
[Fact]
public void ContainerFromIndex_Should_Return_Materialized_Containers()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = Materialize(target, 0, items);
Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0));
Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1));
Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2));
}
[Fact]
public void IndexFromContainer_Should_Return_Index()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = Materialize(target, 0, items);
Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl));
Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl));
Assert.Equal(2, target.IndexFromContainer(containers[2].ContainerControl));
}
[Fact]
public void Dematerialize_Should_Remove_Container()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = Materialize(target, 0, items);
target.Dematerialize(1, 1);
Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0));
Assert.Null(target.ContainerFromIndex(1));
Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2));
}
[Fact]
public void Dematerialize_Should_Return_Removed_Containers()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = Materialize(target, 0, items);
var expected = target.Containers.Take(2).ToList();
var result = target.Dematerialize(0, 2);
Assert.Equal(expected, result);
}
[Fact]
public void InsertSpace_Should_Alter_Successive_Container_Indexes()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = Materialize(target, 0, items);
target.InsertSpace(1, 3);
Assert.Equal(3, target.Containers.Count());
Assert.Equal(new[] { 0, 4, 5 }, target.Containers.Select(x => x.Index));
}
[Fact]
public void RemoveRange_Should_Alter_Successive_Container_Indexes()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var containers = Materialize(target, 0, items);
var removed = target.RemoveRange(1, 1).Single();
Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0));
Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(1));
Assert.Equal(containers[1], removed);
Assert.Equal(new[] { 0, 1 }, target.Containers.Select(x => x.Index));
}
[Fact]
public void Style_Binding_Should_Be_Able_To_Override_Content()
{
var owner = new Decorator();
var target = new ItemContainerGenerator(owner);
var container = (ContentPresenter)target.Materialize(0, "foo").ContainerControl;
Assert.Equal("foo", container.Content);
container.Bind(
ContentPresenter.ContentProperty,
Observable.Never<object>().StartWith("bar"),
BindingPriority.Style);
Assert.Equal("bar", container.Content);
}
[Fact]
public void Style_Binding_Should_Be_Able_To_Override_Content_Typed()
{
var owner = new Decorator();
var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var container = (ListBoxItem)target.Materialize(0, "foo").ContainerControl;
Assert.Equal("foo", container.Content);
container.Bind(
ContentPresenter.ContentProperty,
Observable.Never<object>().StartWith("bar"),
BindingPriority.Style);
Assert.Equal("bar", container.Content);
}
[Fact]
public void Materialize_Should_Create_Containers_When_Item_Is_Null()
{
var owner = new Decorator();
var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var container = (ListBoxItem)target.Materialize(0, null).ContainerControl;
Assert.True(container != null, "The containers is not materialized.");
}
private static IList<ItemContainerInfo> Materialize(
IItemContainerGenerator generator,
int index,
string[] items)
{
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var container = generator.Materialize(index++, item);
result.Add(container);
}
return result;
}
}
}

42
tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs

@ -1,42 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Generators;
using Xunit;
namespace Avalonia.Controls.UnitTests.Generators
{
public class ItemContainerGeneratorTypedTests
{
[Fact]
public void Materialize_Should_Create_Containers()
{
var items = new[] { "foo", "bar", "baz" };
var owner = new Decorator();
var target = new ItemContainerGenerator<ListBoxItem>(owner, ListBoxItem.ContentProperty, null);
var containers = Materialize(target, 0, items);
var result = containers
.Select(x => x.ContainerControl)
.OfType<ListBoxItem>()
.Select(x => x.Content)
.ToList();
Assert.Equal(items, result);
}
private static IList<ItemContainerInfo> Materialize(
IItemContainerGenerator generator,
int index,
string[] items)
{
var result = new List<ItemContainerInfo>();
foreach (var item in items)
{
var container = generator.Materialize(index++, item);
result.Add(container);
}
return result;
}
}
}

65
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
@ -367,12 +368,13 @@ namespace Avalonia.Controls.UnitTests
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Assert.Equal(2, target.ItemContainerGenerator.Containers.Count());
var panel = target.Presenter.Panel;
Assert.Equal(2, panel.Children.Count());
target.Template = GetTemplate();
target.ApplyTemplate();
Assert.Empty(target.ItemContainerGenerator.Containers);
Assert.Empty(panel.Children);
}
[Fact]
@ -535,26 +537,6 @@ namespace Avalonia.Controls.UnitTests
Assert.DoesNotContain(":singleitem", target.Classes);
}
[Fact]
public void Setting_Presenter_Explicitly_Should_Set_Item_Parent()
{
var target = new TestItemsControl();
var child = new Control();
var presenter = new ItemsPresenter
{
[StyledElement.TemplatedParentProperty] = target,
};
presenter.ApplyTemplate();
target.Presenter = presenter;
target.Items = new[] { child };
target.ApplyTemplate();
Assert.Equal(target, child.Parent);
Assert.Equal(target, ((ILogical)child).LogicalParent);
}
[Fact]
public void DataContexts_Should_Be_Correctly_Set()
{
@ -683,36 +665,6 @@ 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<Canvas>(e.Containers[0].Item);
};
target.Items = new[]
{
new Canvas()
};
}
[Fact]
public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw()
{
@ -780,14 +732,5 @@ namespace Avalonia.Controls.UnitTests
};
});
}
private class TestItemsControl : ItemsControl
{
public new ItemsPresenter Presenter
{
get { return base.Presenter; }
set { base.Presenter = value; }
}
}
}
}

119
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -8,6 +8,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.UnitTests;
@ -218,6 +219,7 @@ namespace Avalonia.Controls.UnitTests
// Scroll down a page.
target.Scroll.Offset = new Vector(0, 10);
Layout(target);
// Make sure recycled item isn't now selected.
Assert.False(((ListBoxItem)target.Presenter.Panel.Children[0]).IsSelected);
@ -239,8 +241,8 @@ namespace Avalonia.Controls.UnitTests
Prepare(target);
Assert.Equal(new Size(20, 20), target.Scroll.Extent);
Assert.Equal(new Size(100, 10), target.Scroll.Viewport);
Assert.Equal(new Size(100, 200), target.Scroll.Extent);
Assert.Equal(new Size(100, 100), target.Scroll.Viewport);
}
}
@ -263,11 +265,13 @@ namespace Avalonia.Controls.UnitTests
items.Clear();
items.AddRange(Enumerable.Range(0, 11).Select(x => $"Item {x}"));
Layout(target);
items.Remove("Item 2");
Layout(target);
Assert.Equal(
items,
target.Presenter.Panel.Children.Cast<ListBoxItem>().Select(x => (string)x.Content));
var actual = target.Presenter.Panel.Children.Cast<ListBoxItem>().Select(x => (string)x.Content).ToList();
Assert.Equal(items.OrderBy(x => x), actual.OrderBy(x => x));
}
}
@ -328,44 +332,6 @@ namespace Avalonia.Controls.UnitTests
_mouse.Click(listBox, item, mouseButton);
}
[Fact]
public void ListBox_After_Scroll_IndexOutOfRangeException_Shouldnt_Be_Thrown()
{
throw new NotImplementedException();
////using (UnitTestApplication.Start(TestServices.StyledWindow))
////{
//// var items = Enumerable.Range(0, 11).Select(x => $"{x}").ToArray();
//// var target = new ListBox
//// {
//// Template = ListBoxTemplate(),
//// Items = items,
//// ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 11 })
//// };
//// Prepare(target);
//// var panel = target.Presenter.Panel as IVirtualizingPanel;
//// var listBoxItems = panel.Children.OfType<ListBoxItem>();
//// //virtualization should have created exactly 10 items
//// Assert.Equal(10, listBoxItems.Count());
//// Assert.Equal("0", listBoxItems.First().DataContext);
//// Assert.Equal("9", listBoxItems.Last().DataContext);
//// //instead pixeloffset > 0 there could be pretty complex sequence for repro
//// //it involves add/remove/scroll to end multiple actions
//// //which i can't find so far :(, but this is the simplest way to add it to unit test
//// panel.PixelOffset = 1;
//// //here scroll to end -> IndexOutOfRangeException is thrown
//// target.Scroll.Offset = new Vector(0, 2);
//// Assert.True(true);
////}
}
[Fact]
public void LayoutManager_Should_Measure_Arrange_All()
{
@ -464,11 +430,11 @@ namespace Avalonia.Controls.UnitTests
items.Remove("1");
lm.ExecuteLayoutPass();
Assert.Equal("30", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 1).DataContext);
Assert.Equal("29", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 2).DataContext);
Assert.Equal("28", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 3).DataContext);
Assert.Equal("27", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 4).DataContext);
Assert.Equal("26", target.ItemContainerGenerator.ContainerFromIndex(items.Count - 5).DataContext);
Assert.Equal("30", target.ContainerFromIndex(items.Count - 1).DataContext);
Assert.Equal("29", target.ContainerFromIndex(items.Count - 2).DataContext);
Assert.Equal("28", target.ContainerFromIndex(items.Count - 3).DataContext);
Assert.Equal("27", target.ContainerFromIndex(items.Count - 4).DataContext);
Assert.Equal("26", target.ContainerFromIndex(items.Count - 5).DataContext);
}
}
@ -525,7 +491,7 @@ namespace Avalonia.Controls.UnitTests
var target = new ListBox()
{
VerticalAlignment = Layout.VerticalAlignment.Top,
VerticalAlignment = VerticalAlignment.Top,
AutoScrollToSelectedItem = true,
Width = 50,
ItemTemplate = new FuncDataTemplate<object>((c, _) => new Border() { Height = 10 }),
@ -627,6 +593,8 @@ namespace Avalonia.Controls.UnitTests
[~~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),
new ScrollBar
{
@ -640,45 +608,28 @@ namespace Avalonia.Controls.UnitTests
private static void Prepare(ListBox target)
{
// The ListBox needs to be part of a rooted visual tree.
var root = new TestRoot();
root.Child = target;
// Apply the template to the ListBox itself.
target.ApplyTemplate();
target.Width = target.Height = 100;
// Then to its inner ScrollViewer.
var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single();
scrollViewer.ApplyTemplate();
// Then make the ScrollViewer create its child.
((ContentPresenter)scrollViewer.Presenter).UpdateChild();
// Now the ItemsPresenter should be reigstered, so apply its template.
target.Presenter.ApplyTemplate();
// Because ListBox items are virtualized we need to do a layout to make them appear.
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
// Now set and apply the item templates.
foreach (ListBoxItem item in target.Presenter.Panel.Children)
var root = new TestRoot(target)
{
item.Template = ListBoxItemTemplate();
item.ApplyTemplate();
item.Presenter.ApplyTemplate();
((ContentPresenter)item.Presenter).UpdateChild();
}
Resources =
{
{
typeof(ListBoxItem),
new ControlTheme(typeof(ListBoxItem))
{
Setters = { new Setter(ListBoxItem.TemplateProperty, ListBoxItemTemplate()) }
}
}
}
};
// The items were created before the template was applied, so now we need to go back
// and re-arrange everything.
foreach (Control i in target.GetSelfAndVisualDescendants())
{
i.InvalidateMeasure();
}
root.LayoutManager.ExecuteInitialLayoutPass();
}
target.Measure(new Size(100, 100));
target.Arrange(new Rect(0, 0, 100, 100));
private static void Layout(Control c)
{
((ILayoutRoot)c.GetVisualRoot()).LayoutManager.ExecuteLayoutPass();
}
private class Item

2900
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

File diff suppressed because it is too large

27
tests/Avalonia.LeakTests/ControlTests.cs

@ -361,7 +361,8 @@ namespace Avalonia.LeakTests
// Do a layout and make sure that TreeViewItems get realized.
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.Single(target.ItemContainerGenerator.Containers);
throw new NotImplementedException();
////Assert.Single(target.ItemContainerGenerator.Containers);
// Clear the content and ensure the TreeView is removed.
window.Content = null;
@ -756,20 +757,22 @@ namespace Avalonia.LeakTests
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass();
void AssertInitialItemState()
{
var item0 = (ListBoxItem)lb.ItemContainerGenerator.Containers.First().ContainerControl;
var canvas0 = (Canvas)item0.Presenter.Child;
Assert.Equal("foo", canvas0.Tag);
}
throw new NotImplementedException();
////void AssertInitialItemState()
////{
//// var item0 = (ListBoxItem)lb.ItemContainerGenerator.Containers.First().ContainerControl;
//// var canvas0 = (Canvas)item0.Presenter.Child;
//// Assert.Equal("foo", canvas0.Tag);
////}
Assert.Equal(10, lb.ItemContainerGenerator.Containers.Count());
AssertInitialItemState();
////Assert.Equal(10, lb.ItemContainerGenerator.Containers.Count());
////AssertInitialItemState();
items.Clear();
window.LayoutManager.ExecuteLayoutPass();
////items.Clear();
////window.LayoutManager.ExecuteLayoutPass();
Assert.Empty(lb.ItemContainerGenerator.Containers);
////Assert.Empty(lb.ItemContainerGenerator.Containers);
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);

Loading…
Cancel
Save