using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; 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.Controls.Utils; using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Controls { /// /// Displays a collection of items. /// [PseudoClasses(":empty", ":singleitem")] public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider { /// /// The default value for the property. /// private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new StackPanel()); /// /// Defines the property. /// public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); /// /// 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 ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); private IEnumerable? _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; /// /// Initializes static members of the class. /// static ItemsControl() { ItemsProperty.Changed.AddClassHandler((x, e) => x.ItemsChanged(e)); ItemTemplateProperty.Changed.AddClassHandler((x, e) => x.ItemTemplateChanged(e)); } /// /// Initializes a new instance of the class. /// public ItemsControl() { UpdatePseudoClasses(0); SubscribeToItems(_items); } /// /// Gets the for the control. /// public IItemContainerGenerator ItemContainerGenerator { get { if (_itemContainerGenerator == null) { _itemContainerGenerator = CreateItemContainerGenerator(); _itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); _itemContainerGenerator.Recycled += (_, e) => OnContainersRecycled(e); } return _itemContainerGenerator; } } /// /// Gets or sets the items to display. /// [Content] public IEnumerable? Items { get { return _items; } set { SetAndRaise(ItemsProperty, ref _items, value); } } /// /// Gets the number of items in . /// public int ItemCount { get => _itemCount; private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); } /// /// Gets or sets the panel used to display the items. /// public ITemplate ItemsPanel { get { return GetValue(ItemsPanelProperty); } set { SetValue(ItemsPanelProperty, value); } } /// /// Gets or sets the data template used to display the items in the control. /// public IDataTemplate? ItemTemplate { get { return GetValue(ItemTemplateProperty); } set { SetValue(ItemTemplateProperty, value); } } /// /// Gets the items presenter control. /// public IItemsPresenter? Presenter { get; protected set; } private protected bool WrapFocus { get; set; } event EventHandler? IChildIndexProvider.ChildIndexChanged { add => _childIndexChanged += value; remove => _childIndexChanged -= value; } /// void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) { if (Presenter is IChildIndexProvider oldInnerProvider) { oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged; } Presenter = presenter; ItemContainerGenerator?.Clear(); if (Presenter is IChildIndexProvider innerProvider) { innerProvider.ChildIndexChanged += PresenterChildIndexChanged; _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.Empty); } } void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { } void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { } void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { ItemsCollectionChanged(sender, e); } /// /// Gets the item at the specified index in a collection. /// /// The collection. /// The index. /// The item at the given index or null if the index is out of bounds. protected static object? ElementAt(IEnumerable? items, int index) { if (index != -1 && index < items.Count()) { return items!.ElementAt(index) ?? null; } else { return null; } } /// /// Gets the index of an item in a collection. /// /// The collection. /// The item. /// The index of the item or -1 if the item was not found. protected static int IndexOf(IEnumerable? items, object item) { if (items != null && item != null) { var list = items as IList; if (list != null) { return list.IndexOf(item); } else { int index = 0; foreach (var i in items) { if (Equals(i, item)) { return index; } ++index; } } } return -1; } /// /// Creates the for the control. /// /// /// An . /// protected virtual IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); } /// /// Called when new containers are materialized for the by its /// . /// /// The details of the containers. 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); } } } /// /// Called when containers are dematerialized for the by its /// . /// /// The details of the containers. 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); } } } /// /// Called when containers are recycled for the by its /// . /// /// The details of the containers. protected virtual void OnContainersRecycled(ItemContainerEventArgs e) { } /// /// 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; } IVisual? current = focus.Current; 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 == ItemCountProperty) { UpdatePseudoClasses(change.GetNewValue()); } } /// /// Called when the property changes. /// /// The event args. protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e) { var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; if (oldValue is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.RemoveListener(incc, this); } UpdateItemCount(); RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); if (Presenter != null) { Presenter.Items = newValue; } SubscribeToItems(newValue); } /// /// Called when the event is /// raised on . /// /// The event sender. /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { UpdateItemCount(); switch (e.Action) { case NotifyCollectionChangedAction.Add: AddControlItemsToLogicalChildren(e.NewItems); break; case NotifyCollectionChangedAction.Remove: RemoveControlItemsFromLogicalChildren(e.OldItems); break; } Presenter?.ItemsChanged(e); } /// /// Given a collection of items, adds those that are controls to the logical children. /// /// The items. private void AddControlItemsToLogicalChildren(IEnumerable? items) { var toAdd = new List(); if (items != null) { foreach (var i in items) { var control = i as IControl; if (control != null && !LogicalChildren.Contains(control)) { toAdd.Add(control); } } } LogicalChildren.AddRange(toAdd); } /// /// Given a collection of items, removes those that are controls to from logical children. /// /// The items. private void RemoveControlItemsFromLogicalChildren(IEnumerable? items) { var toRemove = new List(); if (items != null) { foreach (var i in items) { var control = i as IControl; if (control != null) { toRemove.Add(control); } } } LogicalChildren.RemoveAll(toRemove); } /// /// Subscribes to an collection. /// /// The items collection. private void SubscribeToItems(IEnumerable? items) { if (items is INotifyCollectionChanged incc) { CollectionChangedEventManager.Instance.AddListener(incc, this); } } /// /// Called when the changes. /// /// The event args. private void ItemTemplateChanged(AvaloniaPropertyChangedEventArgs e) { if (_itemContainerGenerator != null) { _itemContainerGenerator.ItemTemplate = (IDataTemplate?)e.NewValue; // TODO: Rebuild the item containers. } } private void UpdateItemCount() { if (Items == null) { ItemCount = 0; } else if (Items is IList list) { ItemCount = list.Count; } else { ItemCount = Items.Count(); } } private void UpdatePseudoClasses(int itemCount) { 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; } } } private void PresenterChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) { _childIndexChanged?.Invoke(this, e); } int IChildIndexProvider.GetChildIndex(ILogical child) { return Presenter is IChildIndexProvider innerProvider ? innerProvider.GetChildIndex(child) : -1; } bool IChildIndexProvider.TryGetTotalCount(out int count) { if (Presenter is IChildIndexProvider presenter && presenter.TryGetTotalCount(out count)) { return true; } count = ItemCount; return true; } } }