using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
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
{
///
/// 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));
private static readonly AttachedProperty AppliedItemContainerTheme =
AvaloniaProperty.RegisterAttached("AppliedItemContainerTheme");
///
/// Gets or sets the to use for binding to the display member of each item.
///
[AssignBinding]
[InheritDataTypeFromItems(nameof(ItemsSource))]
public BindingBase? 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 immediately before 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? PreparingContainer;
///
/// 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;
///
/// Gets a default recycle key that can be used when an supports
/// a single container type.
///
protected static object DefaultRecycleKey { get; } = new object();
///
/// 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();
///
/// Scrolls the specified item into view.
///
/// The index of the item.
public void ScrollIntoView(int index) => Presenter?.ScrollIntoView(index);
///
/// Scrolls the specified item into view.
///
/// The item.
public void ScrollIntoView(object item) => ScrollIntoView(ItemsView.IndexOf(item));
///
/// Returns the that owns the specified container control.
///
/// The container.
///
/// The owning or null if the control is not an items container.
///
[Obsolete("Typo, use ItemsControlFromItemContainer instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
[Browsable(false)]
public static ItemsControl? ItemsControlFromItemContaner(Control container) =>
ItemsControlFromItemContainer(container);
///
/// Returns the that owns the specified container control.
///
/// The container.
///
/// The owning or null if the control is not an items container.
///
public static ItemsControl? ItemsControlFromItemContainer(Control container)
{
var c = container.Parent as Control;
while (c is not null)
{
if (c is ItemsControl itemsControl)
{
return itemsControl.IndexFromContainer(container) >= 0 ? itemsControl : null;
}
c = c.Parent as Control;
}
return null;
}
///
/// Creates or a container that can be used to display an item.
///
protected internal virtual Control CreateContainerForItemOverride(object? item, int index, object? recycleKey)
{
return 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)
{
SetIfUnset(hcc, HeaderedContentControl.ContentProperty, item);
if (item is IHeadered headered)
SetIfUnset(hcc, HeaderedContentControl.HeaderProperty, headered.Header);
else if (item is not Visual)
SetIfUnset(hcc, HeaderedContentControl.HeaderProperty, item);
if (itemTemplate is not null)
SetIfUnset(hcc, HeaderedContentControl.HeaderTemplateProperty, itemTemplate);
}
else if (container is ContentControl cc)
{
SetIfUnset(cc, ContentControl.ContentProperty, item);
if (itemTemplate is not null)
SetIfUnset(cc, ContentControl.ContentTemplateProperty, itemTemplate);
}
else if (container is ContentPresenter p)
{
SetIfUnset(p, ContentPresenter.ContentProperty, item);
if (itemTemplate is not null)
SetIfUnset(p, ContentPresenter.ContentTemplateProperty, itemTemplate);
}
else if (container is ItemsControl ic)
{
if (itemTemplate is not null)
SetIfUnset(ic, ItemTemplateProperty, itemTemplate);
if (ItemContainerTheme is { } ict)
SetIfUnset(ic, ItemContainerThemeProperty, ict);
}
// These conditions are separate because HeaderedItemsControl and
// HeaderedSelectingItemsControl also need to run the ItemsControl preparation.
if (container is HeaderedItemsControl hic)
{
SetIfUnset(hic, HeaderedItemsControl.HeaderProperty, item);
SetIfUnset(hic, HeaderedItemsControl.HeaderTemplateProperty, itemTemplate);
hic.PrepareItemContainer(this);
}
else if (container is HeaderedSelectingItemsControl hsic)
{
SetIfUnset(hsic, HeaderedSelectingItemsControl.HeaderProperty, item);
SetIfUnset(hsic, HeaderedSelectingItemsControl.HeaderTemplateProperty, 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)
{
hcc.ClearValue(HeaderedContentControl.ContentProperty);
hcc.ClearValue(HeaderedContentControl.HeaderProperty);
hcc.ClearValue(HeaderedContentControl.HeaderTemplateProperty);
}
else if (container is ContentControl cc)
{
cc.ClearValue(ContentControl.ContentProperty);
cc.ClearValue(ContentControl.ContentTemplateProperty);
}
else if (container is ContentPresenter p)
{
p.ClearValue(ContentPresenter.ContentProperty);
p.ClearValue(ContentPresenter.ContentTemplateProperty);
}
else if (container is ItemsControl ic)
{
ic.ClearValue(ItemTemplateProperty);
ic.ClearValue(ItemContainerThemeProperty);
}
if (container is HeaderedItemsControl hic)
{
hic.ClearValue(HeaderedItemsControl.HeaderProperty);
hic.ClearValue(HeaderedItemsControl.HeaderTemplateProperty);
}
else if (container is HeaderedSelectingItemsControl hsic)
{
hsic.ClearValue(HeaderedSelectingItemsControl.HeaderProperty);
hsic.ClearValue(HeaderedSelectingItemsControl.HeaderTemplateProperty);
}
// 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 can be its own container.
///
/// The item to check.
/// The index of the item.
///
/// When the method returns, contains a key that can be used to locate a previously
/// recycled container of the correct type, or null if the item cannot be recycled.
/// If the item is its own container then by definition it cannot be recycled, so
/// shoud be set to null.
///
///
/// true if the item needs a container; otherwise false if the item can itself be used
/// as a container.
///
protected internal virtual bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
{
return NeedsContainer(item, out recycleKey);
}
///
/// A default implementation of
/// that returns true and sets the recycle key to if the item
/// is not a .
///
/// The container type.
/// The item.
///
/// When the method returns, contains if
/// is not of type ; otherwise null.
///
///
/// true if is of type ; otherwise false.
///
protected bool NeedsContainer(object? item, out object? recycleKey) where T : Control
{
if (item is T)
{
recycleKey = null;
return false;
}
else
{
recycleKey = DefaultRecycleKey;
return true;
}
}
///
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_itemsPresenter = e.NameScope.Find("PART_ItemsPresenter");
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
// If the focus is coming from a child control, set the tab once active element to
// the focused control. This ensures that tabbing back into the control will focus
// the last focused control when TabNavigationMode == Once.
if (e.Source != this && e.Source is IInputElement ie)
KeyboardNavigation.SetTabOnceActiveElement(this, ie);
}
///
/// Handles directional navigation within the .
///
/// The key events.
protected override void OnKeyDown(KeyEventArgs e)
{
if (!e.Handled)
{
var focus = FocusManager.GetFocusManager(this);
var direction = e.Key.ToNavigationDirection();
var container = Presenter?.Panel as INavigableContainer;
if (focus == null ||
container == null ||
focus.GetFocusedElement() == null ||
direction == null ||
direction.Value.IsTab())
{
return;
}
Visual? current = focus.GetFocusedElement() as Visual;
while (current != null)
{
if (current.VisualParent == container && current is IInputElement inputElement)
{
var next = GetNextControl(container, direction.Value, inputElement, WrapFocus);
if (next != null)
{
next.Focus(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, EditorBrowsable(EditorBrowsableState.Never)]
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)
{
PreparingContainer?.Invoke(this, new(container, index));
// If the container has no theme set, or we've already applied our ItemContainerTheme
// (and it hasn't changed since) then we're in control of the container's Theme and may
// need to update it.
if (!container.IsSet(ThemeProperty) || container.GetValue(AppliedItemContainerTheme) == container.Theme)
{
var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme?.TargetType?.IsAssignableFrom(GetStyleKey(container)) == true)
{
// We have an ItemContainerTheme and it matches the container. Set the Theme
// property, and mark the container as having had ItemContainerTheme applied.
container.SetCurrentValue(ThemeProperty, itemContainerTheme);
container.SetValue(AppliedItemContainerTheme, itemContainerTheme);
}
else
{
// Otherwise clear the theme and the AppliedItemContainerTheme property.
container.ClearValue(ThemeProperty);
container.ClearValue(AppliedItemContainerTheme);
}
}
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 SetIfUnset(AvaloniaObject target, StyledProperty property, T value)
{
if (!target.IsSet(property))
target.SetCurrentValue(property, value);
}
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