using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls
{
///
/// Displays a collection of items.
///
[PseudoClasses(":empty", ":singleitem")]
public class ItemsControl : TemplatedControl, IChildIndexProvider, IScrollSnapPointsInfo
{
///
/// The default value for the property.
///
private static readonly FuncTemplate DefaultPanel =
new(() => new StackPanel());
///
/// Defines the property.
///
public static readonly StyledProperty ItemContainerThemeProperty =
AvaloniaProperty.Register(nameof(ItemContainerTheme));
///
/// Defines the property.
///
public static readonly DirectProperty ItemCountProperty =
AvaloniaProperty.RegisterDirect(nameof(ItemCount), o => o.ItemCount);
///
/// Defines the property.
///
public static readonly StyledProperty> ItemsPanelProperty =
AvaloniaProperty.Register>(nameof(ItemsPanel), DefaultPanel);
///
/// Defines the property.
///
public static readonly StyledProperty ItemsSourceProperty =
AvaloniaProperty.Register(nameof(ItemsSource));
///
/// Defines the property.
///
public static readonly StyledProperty ItemTemplateProperty =
AvaloniaProperty.Register(nameof(ItemTemplate));
///
/// Defines the property
///
public static readonly StyledProperty DisplayMemberBindingProperty =
AvaloniaProperty.Register(nameof(DisplayMemberBinding));
///
/// Defines the property.
///
public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty =
AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular));
///
/// Defines the property.
///
public static readonly StyledProperty AreVerticalSnapPointsRegularProperty =
AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular));
///
/// Gets or sets the to use for binding to the display member of each item.
///
[AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IBinding? DisplayMemberBinding
{
get => GetValue(DisplayMemberBindingProperty);
set => SetValue(DisplayMemberBindingProperty, value);
}
private readonly ItemCollection _items = new();
private int _itemCount;
private ItemContainerGenerator? _itemContainerGenerator;
private EventHandler? _childIndexChanged;
private IDataTemplate? _displayMemberItemTemplate;
private ItemsPresenter? _itemsPresenter;
///
/// Initializes a new instance of the class.
///
public ItemsControl()
{
UpdatePseudoClasses();
_items.CollectionChanged += OnItemsViewCollectionChanged;
}
///
/// Gets the for the control.
///
public ItemContainerGenerator ItemContainerGenerator
{
#pragma warning disable CS0612 // Type or member is obsolete
get => _itemContainerGenerator ??= CreateItemContainerGenerator();
#pragma warning restore CS0612 // Type or member is obsolete
}
///
/// Gets the items to display.
///
///
/// You use either the or the property to
/// specify the collection that should be used to generate the content of your
/// . When the property is set, the
/// collection is made read-only and fixed-size.
///
/// When is in use, setting the
/// property to null removes the collection and restores usage to ,
/// which will be an empty .
///
[Content]
public ItemCollection Items => _items;
///
/// Gets or sets the that is applied to the container element generated for each item.
///
public ControlTheme? ItemContainerTheme
{
get => GetValue(ItemContainerThemeProperty);
set => SetValue(ItemContainerThemeProperty, value);
}
///
/// Gets the number of items being displayed by the .
///
public int ItemCount
{
get => _itemCount;
private set
{
if (SetAndRaise(ItemCountProperty, ref _itemCount, value))
{
UpdatePseudoClasses();
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged);
}
}
}
///
/// Gets or sets the panel used to display the items.
///
public ITemplate ItemsPanel
{
get => GetValue(ItemsPanelProperty);
set => SetValue(ItemsPanelProperty, value);
}
///
/// Gets or sets a collection used to generate the content of the .
///
///
/// A common scenario is to use an such as a
/// to display a data collection, or to bind an
/// to a collection object. To bind an
/// to a collection object, use the property.
///
/// When the property is set, the collection
/// is made read-only and fixed-size.
///
/// When is in use, setting the property to null removes the
/// collection and restores usage to , which will be an empty
/// .
///
public IEnumerable? ItemsSource
{
get => GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
///
/// Gets or sets the data template used to display the items in the control.
///
[InheritDataTypeFromItems(nameof(ItemsSource))]
public IDataTemplate? ItemTemplate
{
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
///
/// Gets the items presenter control.
///
public ItemsPresenter? Presenter { get; private set; }
///
/// Gets the specified by .
///
public Panel? ItemsPanelRoot => Presenter?.Panel;
///
/// Gets a read-only view of the items in the .
///
public ItemsSourceView ItemsView => _items;
private protected bool WrapFocus { get; set; }
event EventHandler? IChildIndexProvider.ChildIndexChanged
{
add => _childIndexChanged += value;
remove => _childIndexChanged -= value;
}
///
/// Occurs each time a container is prepared for use.
///
///
/// The prepared element might be newly created or an existing container that is being re-
/// used.
///
public event EventHandler? ContainerPrepared;
///
/// Occurs for each realized container when the index for the item it represents has changed.
///
///
/// This event is raised for each realized container where the index for the item it
/// represents has changed. For example, when another item is added or removed in the data
/// source, the index for items that come after in the ordering will be impacted.
///
public event EventHandler? ContainerIndexChanged;
///
/// Occurs each time a container is cleared.
///
///
/// This event is raised immediately each time an container is cleared, such as when it
/// falls outside the range of realized items or the corresponding item is removed.
///
public event EventHandler? ContainerClearing;
///
public event EventHandler HorizontalSnapPointsChanged
{
add
{
if (_itemsPresenter != null)
{
_itemsPresenter.HorizontalSnapPointsChanged += value;
}
}
remove
{
if (_itemsPresenter != null)
{
_itemsPresenter.HorizontalSnapPointsChanged -= value;
}
}
}
///
public event EventHandler VerticalSnapPointsChanged
{
add
{
if (_itemsPresenter != null)
{
_itemsPresenter.VerticalSnapPointsChanged += value;
}
}
remove
{
if (_itemsPresenter != null)
{
_itemsPresenter.VerticalSnapPointsChanged -= value;
}
}
}
///
/// Gets or sets whether the horizontal snap points for the are equidistant from each other.
///
public bool AreHorizontalSnapPointsRegular
{
get => GetValue(AreHorizontalSnapPointsRegularProperty);
set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
///
/// Gets or sets whether the vertical snap points for the are equidistant from each other.
///
public bool AreVerticalSnapPointsRegular
{
get => GetValue(AreVerticalSnapPointsRegularProperty);
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
///
/// Returns the container for the item at the specified index.
///
/// The index of the item to retrieve.
///
/// The container for the item at the specified index within the item collection, if the
/// item has a container; otherwise, null.
///
public Control? ContainerFromIndex(int index) => Presenter?.ContainerFromIndex(index);
///
/// Returns the container corresponding to the specified item.
///
/// The item to retrieve the container for.
///
/// A container that corresponds to the specified item, if the item has a container and
/// exists in the collection; otherwise, null.
///
public Control? ContainerFromItem(object item)
{
var index = _items.IndexOf(item);
return index >= 0 ? ContainerFromIndex(index) : null;
}
///
/// Returns the index to the item that has the specified, generated container.
///
/// The generated container to retrieve the item index for.
///
/// The index to the item that corresponds to the specified generated container, or -1 if
/// is not found.
///
public int IndexFromContainer(Control container) => Presenter?.IndexFromContainer(container) ?? -1;
///
/// Returns the item that corresponds to the specified, generated container.
///
/// The control that corresponds to the item to be returned.
///
/// The contained item, or the container if it does not contain an item.
///
public object? ItemFromContainer(Control container)
{
var index = IndexFromContainer(container);
return index >= 0 && index < _items.Count ? _items[index] : null;
}
///
/// Gets the currently realized containers.
///
public IEnumerable GetRealizedContainers() => Presenter?.GetRealizedContainers() ?? Array.Empty();
///
/// Creates or a container that can be used to display an item.
///
protected internal virtual Control CreateContainerForItemOverride() => new ContentPresenter();
///
/// Prepares the specified element to display the specified item.
///
/// The element that's used to display the specified item.
/// The item to display.
/// The index of the item to display.
protected internal virtual void PrepareContainerForItemOverride(Control container, object? item, int index)
{
if (container == item)
return;
var itemTemplate = GetEffectiveItemTemplate();
if (container is HeaderedContentControl hcc)
{
hcc.Content = item;
if (item is IHeadered headered)
hcc.Header = headered.Header;
else if (item is not Visual)
hcc.Header = item;
if (itemTemplate is not null)
hcc.HeaderTemplate = itemTemplate;
}
else if (container is ContentControl cc)
{
cc.Content = item;
if (itemTemplate is not null)
cc.ContentTemplate = itemTemplate;
}
else if (container is ContentPresenter p)
{
p.Content = item;
if (itemTemplate is not null)
p.ContentTemplate = itemTemplate;
}
else if (container is ItemsControl ic)
{
if (itemTemplate is not null)
ic.ItemTemplate = itemTemplate;
if (ItemContainerTheme is { } ict && !ict.IsSet(ItemContainerThemeProperty))
ic.ItemContainerTheme = ict;
}
// These conditions are separate because HeaderedItemsControl and
// HeaderedSelectingItemsControl also need to run the ItemsControl preparation.
if (container is HeaderedItemsControl hic)
{
hic.Header = item;
hic.HeaderTemplate = itemTemplate;
hic.PrepareItemContainer(this);
}
else if (container is HeaderedSelectingItemsControl hsic)
{
hsic.Header = item;
hsic.HeaderTemplate = itemTemplate;
hsic.PrepareItemContainer(this);
}
}
///
/// Called when a container has been fully prepared to display an item.
///
/// The container control.
/// The item being displayed.
/// The index of the item being displayed.
///
/// This method will be called when a container has been fully prepared and added to the
/// logical and visual trees, but may be called before a layout pass has completed. It is
/// called immediately before the event is raised.
///
protected internal virtual void ContainerForItemPreparedOverride(Control container, object? item, int index)
{
}
///
/// Called when the index for a container changes due to an insertion or removal in the
/// items collection.
///
/// The container whose index changed.
/// The old index.
/// The new index.
protected virtual void ContainerIndexChangedOverride(Control container, int oldIndex, int newIndex)
{
}
///
/// Undoes the effects of the method.
///
/// The container element.
protected internal virtual void ClearContainerForItemOverride(Control container)
{
if (container is HeaderedContentControl hcc)
{
if (hcc.Content is Control)
hcc.Content = null;
if (hcc.Header is Control)
hcc.Header = null;
}
else if (container is ContentControl cc)
{
if (cc.Content is Control)
cc.Content = null;
}
else if (container is ContentPresenter p)
{
if (p.Content is Control)
p.Content = null;
}
else if (container is HeaderedItemsControl hic)
{
if (hic.Header is Control)
hic.Header = null;
}
else if (container is HeaderedSelectingItemsControl hsic)
{
if (hsic.Header is Control)
hsic.Header = null;
}
// Feels like we should be clearing the HeaderedItemsControl.Items binding here, but looking at
// the WPF source it seems that this isn't done there.
}
///
/// Determines whether the specified item is (or is eligible to be) its own container.
///
/// The item to check.
/// true if the item is (or is eligible to be) its own container; otherwise, false.
protected internal virtual bool IsItemItsOwnContainerOverride(Control item) => true;
///
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_itemsPresenter = e.NameScope.Find("PART_ItemsPresenter");
}
///
/// Handles directional navigation within the .
///
/// The key events.
protected override void OnKeyDown(KeyEventArgs e)
{
if (!e.Handled)
{
var focus = FocusManager.Instance;
var direction = e.Key.ToNavigationDirection();
var container = Presenter?.Panel as INavigableContainer;
if (container == null ||
focus?.Current == null ||
direction == null ||
direction.Value.IsTab())
{
return;
}
Visual? current = focus.Current as Visual;
while (current != null)
{
if (current.VisualParent == container && current is IInputElement inputElement)
{
var next = GetNextControl(container, direction.Value, inputElement, WrapFocus);
if (next != null)
{
focus.Focus(next, NavigationMethod.Directional, e.KeyModifiers);
e.Handled = true;
}
break;
}
current = current.VisualParent;
}
}
base.OnKeyDown(e);
}
///
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ItemsControlAutomationPeer(this);
}
///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
{
RefreshContainers();
}
else if (change.Property == ItemsSourceProperty)
{
_items.SetItemsSource(change.GetNewValue());
}
else if (change.Property == ItemTemplateProperty)
{
if (change.NewValue is not null && DisplayMemberBinding is not null)
throw new InvalidOperationException("Cannot set both DisplayMemberBinding and ItemTemplate.");
RefreshContainers();
}
else if (change.Property == DisplayMemberBindingProperty)
{
if (change.NewValue is not null && ItemTemplate is not null)
throw new InvalidOperationException("Cannot set both DisplayMemberBinding and ItemTemplate.");
_displayMemberItemTemplate = null;
RefreshContainers();
}
}
///
/// Refreshes the containers displayed by the control.
///
///
/// Causes all containers to be unrealized and re-realized.
///
protected void RefreshContainers() => Presenter?.Refresh();
///
/// Called when the event is
/// raised on .
///
/// The event sender.
/// The event args.
private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (!_items.IsReadOnly)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddControlItemsToLogicalChildren(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
}
ItemCount = ItemsView.Count;
}
///
/// Creates the
///
///
/// This method is only present for backwards compatibility with 0.10.x in order for
/// TreeView to be able to create a . Can be
/// removed in 12.0.
///
[Obsolete]
private protected virtual ItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator(this);
}
internal void AddLogicalChild(Control c)
{
if (!LogicalChildren.Contains(c))
LogicalChildren.Add(c);
}
internal void RemoveLogicalChild(Control c) => LogicalChildren.Remove(c);
///
/// Called by to register with the .
///
/// The items presenter.
///
/// ItemsPresenters can be within nested templates or in popups and so are not necessarily
/// created immediately when the ItemsControl control's template is instantiated. Instead
/// they register themselves using this method.
///
internal void RegisterItemsPresenter(ItemsPresenter presenter)
{
Presenter = presenter;
_childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.ChildIndexesReset);
}
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);
}
internal void ItemContainerPrepared(Control container, object? item, int index)
{
ContainerForItemPreparedOverride(container, item, index);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, index));
ContainerPrepared?.Invoke(this, new(container, index));
}
internal void ItemContainerIndexChanged(Control container, int oldIndex, int newIndex)
{
ContainerIndexChangedOverride(container, oldIndex, newIndex);
_childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(container, newIndex));
ContainerIndexChanged?.Invoke(this, new(container, oldIndex, newIndex));
}
internal void ClearItemContainer(Control container)
{
ClearContainerForItemOverride(container);
ContainerClearing?.Invoke(this, new(container));
}
private void AddControlItemsToLogicalChildren(IEnumerable? items)
{
if (items is null)
return;
List? toAdd = null;
foreach (var i in items)
{
if (i is Control control && !LogicalChildren.Contains(control))
{
toAdd ??= new();
toAdd.Add(control);
}
}
if (toAdd is not null)
LogicalChildren.AddRange(toAdd);
}
private void RemoveControlItemsFromLogicalChildren(IEnumerable? items)
{
if (items is null)
return;
List? toRemove = null;
foreach (var i in items)
{
if (i is Control control)
{
toRemove ??= new();
toRemove.Add(control);
}
}
if (toRemove is not null)
LogicalChildren.RemoveAll(toRemove);
}
private IDataTemplate? GetEffectiveItemTemplate()
{
if (ItemTemplate is { } itemTemplate)
return itemTemplate;
if (_displayMemberItemTemplate is null && DisplayMemberBinding is { } binding)
{
_displayMemberItemTemplate = new FuncDataTemplate