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((_, _) => new TextBlock { [!TextBlock.TextProperty] = binding, }); } return _displayMemberItemTemplate; } private void UpdatePseudoClasses() { PseudoClasses.Set(":empty", ItemCount == 0); PseudoClasses.Set(":singleitem", ItemCount == 1); } protected static IInputElement? GetNextControl( INavigableContainer container, NavigationDirection direction, IInputElement? from, bool wrap) { var current = from; for (;;) { var result = container.GetControl(direction, current, wrap); if (result is null) { return null; } if (result.Focusable && result.IsEffectivelyEnabled && result.IsEffectivelyVisible) { return result; } current = result; if (current == from) { return null; } switch (direction) { //We did not find an enabled first item. Move downwards until we find one. case NavigationDirection.First: direction = NavigationDirection.Down; from = result; break; //We did not find an enabled last item. Move upwards until we find one. case NavigationDirection.Last: direction = NavigationDirection.Up; from = result; break; } } } int IChildIndexProvider.GetChildIndex(ILogical child) { return child is Control container ? IndexFromContainer(container) : -1; } bool IChildIndexProvider.TryGetTotalCount(out int count) { count = ItemsView.Count; return true; } } }