// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.LogicalTree; using Avalonia.Metadata; namespace Avalonia.Controls { /// /// Displays a collection of items. /// public class ItemsControl : TemplatedControl, IItemsPresenterHost { /// /// The default value for the property. /// [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Needs to be before or a NullReferenceException is thrown.")] 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 StyledProperty> ItemsPanelProperty = AvaloniaProperty.Register>(nameof(ItemsPanel), DefaultPanel); /// /// Defines the property. /// public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); /// /// Defines the property. /// public static readonly StyledProperty MemberSelectorProperty = AvaloniaProperty.Register(nameof(MemberSelector)); private IEnumerable _items = new AvaloniaList(); private IItemContainerGenerator _itemContainerGenerator; /// /// Initializes static members of the class. /// static ItemsControl() { ItemsProperty.Changed.AddClassHandler(x => x.ItemsChanged); } /// /// Initializes a new instance of the class. /// public ItemsControl() { PseudoClasses.Add(":empty"); SubscribeToItems(_items); ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// /// Gets the for the control. /// public IItemContainerGenerator ItemContainerGenerator { get { if (_itemContainerGenerator == null) { _itemContainerGenerator = CreateItemContainerGenerator(); if (_itemContainerGenerator != null) { _itemContainerGenerator.ItemTemplate = ItemTemplate; _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); } } return _itemContainerGenerator; } } /// /// Gets or sets the items to display. /// [Content] public IEnumerable Items { get { return _items; } set { SetAndRaise(ItemsProperty, ref _items, 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); } } /// /// Selects a member from to use as the list item. /// public IMemberSelector MemberSelector { get { return GetValue(MemberSelectorProperty); } set { SetValue(MemberSelectorProperty, value); } } /// /// Gets the items presenter control. /// public IItemsPresenter Presenter { get; protected set; } /// void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter) { Presenter = presenter; } /// /// Gets the item at the specified index in a collection. /// /// The collection. /// The index. /// The index of the item or -1 if the item was not found. protected static object ElementAt(IEnumerable items, int index) { var typedItems = items?.Cast(); if (index != -1 && typedItems != null && index < typedItems.Count()) { return typedItems.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 or null. /// /// /// Certain controls such as don't actually create item /// containers; however they want it to be ItemsControls so that they have an Items /// property etc. In this case, a derived class can override this method to return null /// in order to disable the creation of item containers. /// 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) { var toAdd = new List(); 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) { toAdd.Add(container.ContainerControl); } } LogicalChildren.AddRange(toAdd); } /// /// Called when containers are dematerialized for the by its /// . /// /// The details of the containers. protected virtual void OnContainersDematerialized(ItemContainerEventArgs e) { var toRemove = new List(); 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) { toRemove.Add(container.ContainerControl); } } LogicalChildren.RemoveAll(toRemove); } /// protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e) { base.OnTemplateChanged(e); if (e.NewValue == null) { ItemContainerGenerator?.Clear(); } } /// /// Caled when the property changes. /// /// The event args. protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e) { var incc = e.OldValue as INotifyCollectionChanged; if (incc != null) { incc.CollectionChanged -= ItemsCollectionChanged; } var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; RemoveControlItemsFromLogicalChildren(oldValue); AddControlItemsToLogicalChildren(newValue); SubscribeToItems(newValue); } /// /// Called when the event is /// raised on . /// /// The event sender. /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: AddControlItemsToLogicalChildren(e.NewItems); break; case NotifyCollectionChangedAction.Remove: RemoveControlItemsFromLogicalChildren(e.OldItems); break; } var collection = sender as ICollection; PseudoClasses.Set(":empty", collection.Count == 0); } /// /// 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) { PseudoClasses.Set(":empty", items == null || items.Count() == 0); var incc = items as INotifyCollectionChanged; if (incc != null) { incc.CollectionChanged += ItemsCollectionChanged; } } /// /// 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. } } } }