From ba7e8a20b5cd7e67f536dfe782e10972eda761ae Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Mar 2023 17:10:05 +0100 Subject: [PATCH 01/16] Added ItemsControl.ItemsSource. `ItemsControl` now works more like WPF, in that there are separate `Items` and `ItemsSource` properties. For backwards compatibility `Items` can still be set, though the setter is deprecated. `Items` needed to be changed from `IEnumerable` to `IList` though. --- .../ControlCatalog/Pages/ComboBoxPage.xaml.cs | 2 +- .../Controls/ItemsRepeater.cs | 5 +- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 4 +- src/Avalonia.Controls/ItemCollection.cs | 114 ++++++++++++ src/Avalonia.Controls/ItemsControl.cs | 175 ++++++++++-------- src/Avalonia.Controls/ItemsSourceView.cs | 155 +++++++++------- .../Presenters/PanelContainerGenerator.cs | 15 -- .../Primitives/SelectingItemsControl.cs | 44 ++--- .../Selection/SelectionModel.cs | 4 +- .../SelectingItemsControlSelectionAdapter.cs | 4 +- src/Avalonia.Controls/VirtualizingPanel.cs | 20 +- .../ComboBoxTests.cs | 2 +- .../ItemsControlTests.cs | 60 +++--- .../ItemsSourceViewTests.cs | 39 ++++ .../Presenters/ItemsPresenterTests.cs | 2 +- .../TabControlTests.cs | 72 ++++--- .../TreeViewTests.cs | 2 +- .../VirtualizingCarouselPanelTests.cs | 2 +- .../VirtualizingStackPanelTests.cs | 13 +- .../CompiledBindingExtensionTests.cs | 5 +- 20 files changed, 452 insertions(+), 287 deletions(-) create mode 100644 src/Avalonia.Controls/ItemCollection.cs diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d624c9a07..54251417b3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); + fontComboBox.ItemsSource = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x)); fontComboBox.SelectedIndex = 0; } } diff --git a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs index 3d3d01e06e..499904deac 100644 --- a/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs +++ b/src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs @@ -39,7 +39,10 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + AvaloniaProperty.RegisterDirect( + nameof(Items), + o => o.Items, + (o, v) => o.Items = v); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index b028a8f007..8211d1baf2 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -18,7 +18,9 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(x => x.Items, + AvaloniaProperty.RegisterDirect( + nameof(Items), + x => x.Items, (x, v) => x.Items = v); /// diff --git a/src/Avalonia.Controls/ItemCollection.cs b/src/Avalonia.Controls/ItemCollection.cs new file mode 100644 index 0000000000..120aef41dc --- /dev/null +++ b/src/Avalonia.Controls/ItemCollection.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Collections; + +namespace Avalonia.Controls +{ + public class ItemCollection : ItemsSourceView, IList + { +// Suppress "Avoid zero-length array allocations": This is a sentinel value and must be unique. +#pragma warning disable CA1825 + private static readonly object?[] s_uninitialized = new object?[0]; +#pragma warning restore CA1825 + + private Mode _mode; + + internal ItemCollection() + : base(s_uninitialized) + { + } + + public new object? this[int index] + { + get => base[index]; + set => WritableSource[index] = value; + } + + public bool IsReadOnly => _mode == Mode.ItemsSource; + + internal event EventHandler? SourceChanged; + + public int Add(object? value) => WritableSource.Add(value); + public void Clear() => WritableSource.Clear(); + public void Insert(int index, object? value) => WritableSource.Insert(index, value); + public void RemoveAt(int index) => WritableSource.RemoveAt(index); + + public bool Remove(object? value) + { + var c = Count; + WritableSource.Remove(value); + return Count < c; + } + + int IList.Add(object? value) => Add(value); + void IList.Clear() => Clear(); + void IList.Insert(int index, object? value) => Insert(index, value); + void IList.RemoveAt(int index) => RemoveAt(index); + + private IList WritableSource + { + get + { + if (IsReadOnly) + ThrowIsItemsSource(); + if (Source == s_uninitialized) + SetSource(CreateDefaultCollection()); + return Source; + } + } + + internal IList? GetItemsPropertyValue() + { + if (_mode == Mode.ObsoleteItemsSetter) + return Source == s_uninitialized ? null : Source; + return this; + } + + internal void SetItems(IList? items) + { + _mode = Mode.ObsoleteItemsSetter; + SetSource(items ?? s_uninitialized); + } + + internal void SetItemsSource(IEnumerable? value) + { + _mode = value is not null ? Mode.ItemsSource : Mode.Items; + SetSource(value ?? CreateDefaultCollection()); + } + + private new void SetSource(IEnumerable source) + { + var oldSource = Source; + + base.SetSource(source); + + if (oldSource.Count > 0) + RaiseCollectionChanged(new(NotifyCollectionChangedAction.Remove, oldSource, 0)); + if (Source.Count > 0) + RaiseCollectionChanged(new(NotifyCollectionChangedAction.Add, Source, 0)); + SourceChanged?.Invoke(this, EventArgs.Empty); + } + + private static AvaloniaList CreateDefaultCollection() + { + return new() { ResetBehavior = ResetBehavior.Remove }; + } + + [DoesNotReturn] + private static void ThrowIsItemsSource() + { + throw new InvalidOperationException( + "Operation is not valid while ItemsSource is in use." + + "Access and modify elements with ItemsControl.ItemsSource instead."); + } + + private enum Mode + { + Items, + ItemsSource, + ObsoleteItemsSetter, + } + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 9483f98881..56ba9c7183 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -34,8 +34,13 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly DirectProperty ItemsProperty = - AvaloniaProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect( + nameof(Items), + o => o.Items, +#pragma warning disable CS0618 // Type or member is obsolete + (o, v) => o.Items = v); +#pragma warning restore CS0618 // Type or member is obsolete /// /// Defines the property. @@ -56,23 +61,23 @@ namespace Avalonia.Controls AvaloniaProperty.Register>(nameof(ItemsPanel), DefaultPanel); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty ItemTemplateProperty = - AvaloniaProperty.Register(nameof(ItemTemplate)); + public static readonly StyledProperty ItemsSourceProperty = + AvaloniaProperty.Register(nameof(ItemsSource)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly DirectProperty ItemsViewProperty = - AvaloniaProperty.RegisterDirect(nameof(ItemsView), o => o.ItemsView); + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register(nameof(ItemTemplate)); /// /// Defines the property /// public static readonly StyledProperty DisplayMemberBindingProperty = AvaloniaProperty.Register(nameof(DisplayMemberBinding)); - + /// /// Defines the property. /// @@ -89,15 +94,14 @@ namespace Avalonia.Controls /// Gets or sets the to use for binding to the display member of each item. /// [AssignBinding] - [InheritDataTypeFromItems(nameof(Items))] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IBinding? DisplayMemberBinding { get => GetValue(DisplayMemberBindingProperty); set => SetValue(DisplayMemberBindingProperty, value); } - - private IEnumerable? _items = new AvaloniaList(); - private ItemsSourceView _itemsView; + + private readonly ItemCollection _items = new(); private int _itemCount; private ItemContainerGenerator? _itemContainerGenerator; private EventHandler? _childIndexChanged; @@ -110,9 +114,8 @@ namespace Avalonia.Controls /// public ItemsControl() { - _itemsView = ItemsSourceView.GetOrCreate(_items); - _itemsView.PostCollectionChanged += ItemsCollectionChanged; - UpdatePseudoClasses(0); + UpdatePseudoClasses(); + _items.CollectionChanged += OnItemsViewCollectionChanged; } /// @@ -129,10 +132,21 @@ namespace Avalonia.Controls /// Gets or sets the items to display. /// [Content] - public IEnumerable? Items + public IList? Items { - get => _items; - set => SetAndRaise(ItemsProperty, ref _items, value); + get => _items.GetItemsPropertyValue(); + + [Obsolete("Use ItemsSource to set or bind items.")] + set + { + var oldItems = _items.GetItemsPropertyValue(); + + if (value != oldItems) + { + _items.SetItems(value); + RaisePropertyChanged(ItemsProperty, oldItems, value); + } + } } /// @@ -140,17 +154,24 @@ namespace Avalonia.Controls /// public ControlTheme? ItemContainerTheme { - get => GetValue(ItemContainerThemeProperty); + get => GetValue(ItemContainerThemeProperty); set => SetValue(ItemContainerThemeProperty, value); } /// - /// Gets the number of items in . + /// Gets the number of items being displayed by the . /// public int ItemCount { get => _itemCount; - private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); + private set + { + if (SetAndRaise(ItemCountProperty, ref _itemCount, value)) + { + UpdatePseudoClasses(); + _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); + } + } } /// @@ -162,13 +183,44 @@ namespace Avalonia.Controls set => SetValue(ItemsPanelProperty, value); } + /// + /// Gets or sets a collection used to generate the content of the . + /// + /// + /// Since Avalonia 11, has both an property + /// and an property. The properties have the following differences: + /// + /// + /// is initialized with an empty collection and is a direct property, + /// meaning that it cannot be styled + /// is by default null, and is a styled property. This property + /// is marked as the content property and will be used for items added via inline XAML. + /// + /// + /// In Avalonia 11 the two properties can be used almost interchangeably but this will change + /// in a later version. In order to be ready for this change, follow the following guidance: + /// + /// + /// You should use the property when you're assigning a collection of + /// item containers directly, for example adding a collection of s + /// directly to a . + /// You should use the property when you're assigning or + /// binding a collection of models which will be transformed by a data template. + /// + /// + 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(Items))] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IDataTemplate? ItemTemplate { - get => GetValue(ItemTemplateProperty); + get => GetValue(ItemTemplateProperty); set => SetValue(ItemTemplateProperty, value); } @@ -182,32 +234,7 @@ namespace Avalonia.Controls /// public Panel? ItemsPanelRoot => Presenter?.Panel; - /// - /// Gets a standardized view over . - /// - /// - /// The property may be an enumerable which does not implement - /// or may be null. This view can be used to provide a standardized - /// view of the current items regardless of the type of the concrete collection, and - /// without having to deal with null values. - /// - public ItemsSourceView ItemsView - { - get => _itemsView; - private set - { - if (ReferenceEquals(_itemsView, value)) - return; - - var oldValue = _itemsView; - RemoveControlItemsFromLogicalChildren(_itemsView); - _itemsView.PostCollectionChanged -= ItemsCollectionChanged; - _itemsView = value; - _itemsView.PostCollectionChanged += ItemsCollectionChanged; - AddControlItemsToLogicalChildren(_itemsView); - RaisePropertyChanged(ItemsViewProperty, oldValue, _itemsView); - } - } + public ItemCollection ItemsView => _items; private protected bool WrapFocus { get; set; } @@ -262,7 +289,7 @@ namespace Avalonia.Controls /// public bool AreHorizontalSnapPointsRegular { - get => GetValue(AreHorizontalSnapPointsRegularProperty); + get => GetValue(AreHorizontalSnapPointsRegularProperty); set => SetValue(AreHorizontalSnapPointsRegularProperty, value); } @@ -271,7 +298,7 @@ namespace Avalonia.Controls /// public bool AreVerticalSnapPointsRegular { - get => GetValue(AreVerticalSnapPointsRegularProperty); + get => GetValue(AreVerticalSnapPointsRegularProperty); set => SetValue(AreVerticalSnapPointsRegularProperty, value); } @@ -295,7 +322,7 @@ namespace Avalonia.Controls /// public Control? ContainerFromItem(object item) { - var index = ItemsView.IndexOf(item); + var index = _items.IndexOf(item); return index >= 0 ? ContainerFromIndex(index) : null; } @@ -319,7 +346,7 @@ namespace Avalonia.Controls public object? ItemFromContainer(Control container) { var index = IndexFromContainer(container); - return index >= 0 && index < ItemsView.Count ? ItemsView[index] : null; + return index >= 0 && index < _items.Count ? _items[index] : null; } /// @@ -389,7 +416,7 @@ namespace Avalonia.Controls if (itemTemplate is ITreeDataTemplate treeTemplate) { if (item is not null && treeTemplate.ItemsSelector(item) is { } itemsBinding) - BindingOperations.Apply(hic, ItemsProperty, itemsBinding, null); + BindingOperations.Apply(hic, ItemsSourceProperty, itemsBinding, null); } } } @@ -485,19 +512,13 @@ namespace Avalonia.Controls { base.OnPropertyChanged(change); - if (change.Property == ItemsProperty) - { - ItemsView = ItemsSourceView.GetOrCreate(change.GetNewValue()); - ItemCount = ItemsView.Count; - } - else if (change.Property == ItemCountProperty) + if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) { - UpdatePseudoClasses(change.GetNewValue()); - _childIndexChanged?.Invoke(this, ChildIndexChangedEventArgs.TotalCountChanged); + RefreshContainers(); } - else if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null) + else if (change.Property == ItemsSourceProperty) { - RefreshContainers(); + _items.SetItemsSource(change.GetNewValue()); } else if (change.Property == ItemTemplateProperty) { @@ -524,14 +545,12 @@ namespace Avalonia.Controls /// /// Called when the event is - /// raised on . + /// raised on . /// /// The event sender. /// The event args. - protected virtual void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - ItemCount = _itemsView.Count; - switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -542,6 +561,8 @@ namespace Avalonia.Controls RemoveControlItemsFromLogicalChildren(e.OldItems); break; } + + ItemCount = ItemsView.Count; } /// @@ -585,7 +606,7 @@ namespace Avalonia.Controls { var itemContainerTheme = ItemContainerTheme; - if (itemContainerTheme is not null && + if (itemContainerTheme is not null && !container.IsSet(ThemeProperty) && ((IStyleable)container).StyleKey == itemContainerTheme.TargetType) { @@ -616,10 +637,6 @@ namespace Avalonia.Controls ClearContainerForItemOverride(container); } - /// - /// Given a collection of items, adds those that are controls to the logical children. - /// - /// The items. private void AddControlItemsToLogicalChildren(IEnumerable? items) { if (items is null) @@ -640,10 +657,6 @@ namespace Avalonia.Controls LogicalChildren.AddRange(toAdd); } - /// - /// Given a collection of items, removes those that are controls to from logical children. - /// - /// The items. private void RemoveControlItemsFromLogicalChildren(IEnumerable? items) { if (items is null) @@ -681,10 +694,10 @@ namespace Avalonia.Controls return _displayMemberItemTemplate; } - private void UpdatePseudoClasses(int itemCount) + private void UpdatePseudoClasses() { - PseudoClasses.Set(":empty", itemCount == 0); - PseudoClasses.Set(":singleitem", itemCount == 1); + PseudoClasses.Set(":empty", ItemCount == 0); + PseudoClasses.Set(":singleitem", ItemCount == 1); } protected static IInputElement? GetNextControl( diff --git a/src/Avalonia.Controls/ItemsSourceView.cs b/src/Avalonia.Controls/ItemsSourceView.cs index 416b909219..614b70d0ba 100644 --- a/src/Avalonia.Controls/ItemsSourceView.cs +++ b/src/Avalonia.Controls/ItemsSourceView.cs @@ -7,6 +7,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Controls.Utils; @@ -17,15 +18,16 @@ namespace Avalonia.Controls /// and an items control. /// public class ItemsSourceView : IReadOnlyList, + IList, INotifyCollectionChanged, ICollectionChangedListener { /// - /// Gets an empty + /// Gets an empty /// - public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); + public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty()); - private readonly IList _inner; + private IList _source; private NotifyCollectionChangedEventHandler? _collectionChanged; private NotifyCollectionChangedEventHandler? _preCollectionChanged; private NotifyCollectionChangedEventHandler? _postCollectionChanged; @@ -35,30 +37,17 @@ namespace Avalonia.Controls /// Initializes a new instance of the ItemsSourceView class for the specified data source. /// /// The data source. - private protected ItemsSourceView(IEnumerable source) - { - _inner = source switch - { - ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), - IList list => list, - INotifyCollectionChanged => throw new ArgumentException( - "Collection implements INotifyCollectionChanged but not IList.", - nameof(source)), - IEnumerable iObj => new List(iObj), - null => throw new ArgumentNullException(nameof(source)), - _ => new List(source.Cast()) - }; - } + private protected ItemsSourceView(IEnumerable source) => SetSource(source); /// /// Gets the number of items in the collection. /// - public int Count => Inner.Count; + public int Count => Source.Count; /// - /// Gets the inner collection. + /// Gets the source collection. /// - public IList Inner => _inner; + public IList Source => _source; /// /// Retrieves the item at the specified index. @@ -67,12 +56,20 @@ namespace Avalonia.Controls /// The item. public object? this[int index] => GetAt(index); + bool IList.IsFixedSize => false; + bool IList.IsReadOnly => true; + bool ICollection.IsSynchronized => false; + object ICollection.SyncRoot => this; + + object? IList.this[int index] + { + get => GetAt(index); + set => ThrowReadOnly(); + } + /// - /// Gets a value that indicates whether the items source can provide a unique key for each item. - /// - /// /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. - /// + /// internal bool HasKeyIndexMapping => false; /// @@ -131,39 +128,14 @@ namespace Avalonia.Controls } } - private void AddListenerIfNecessary() - { - if (!_listening) - { - if (_inner is INotifyCollectionChanged incc) - CollectionChangedEventManager.Instance.AddListener(incc, this); - _listening = true; - } - } - - private void RemoveListenerIfNecessary() - { - if (_listening && _collectionChanged is null && _postCollectionChanged is null) - { - if (_inner is INotifyCollectionChanged incc) - CollectionChangedEventManager.Instance.RemoveListener(incc, this); - _listening = false; - } - } - /// /// Retrieves the item at the specified index. /// /// The index. /// The item. - public object? GetAt(int index) => Inner[index]; - - /// - /// Determines the index of a specific item in the collection. - /// - /// The object to locate in the collection. - /// The index of value if found in the list; otherwise, -1. - public int IndexOf(object? item) => Inner.IndexOf(item); + public object? GetAt(int index) => Source[index]; + public bool Contains(object? item) => Source.Contains(item); + public int IndexOf(object? item) => Source.IndexOf(item); /// /// Gets or creates an for the specified enumerable. @@ -201,7 +173,8 @@ namespace Avalonia.Controls { return items switch { - ItemsSourceView isv => isv, + ItemsSourceView isvt => isvt, + ItemsSourceView isv => new ItemsSourceView(isv.Source), null => ItemsSourceView.Empty, _ => new ItemsSourceView(items) }; @@ -236,7 +209,7 @@ namespace Avalonia.Controls yield return o; } - var inner = Inner; + var inner = Source; return inner switch { @@ -245,7 +218,7 @@ namespace Avalonia.Controls }; } - IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator(); void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) { @@ -262,15 +235,69 @@ namespace Avalonia.Controls _postCollectionChanged?.Invoke(this, e); } + int IList.Add(object? value) => ThrowReadOnly(); + void IList.Clear() => ThrowReadOnly(); + void IList.Insert(int index, object? value) => ThrowReadOnly(); + void IList.Remove(object? value) => ThrowReadOnly(); + void IList.RemoveAt(int index) => ThrowReadOnly(); + void ICollection.CopyTo(Array array, int index) => Source.CopyTo(array, index); + /// - /// Retrieves the index of the item that has the specified unique identifier (key). + /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage. /// - /// The index. - /// The key - /// - /// TODO: Not yet implemented in Avalonia. - /// internal string KeyFromIndex(int index) => throw new NotImplementedException(); + + private protected void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e) + { + _preCollectionChanged?.Invoke(this, e); + _collectionChanged?.Invoke(this, e); + _postCollectionChanged?.Invoke(this, e); + } + + [MemberNotNull(nameof(_source))] + private protected void SetSource(IEnumerable source) + { + if (_listening && _source is INotifyCollectionChanged inccOld) + CollectionChangedEventManager.Instance.RemoveListener(inccOld, this); + + _source = source switch + { + ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), + IList list => list, + INotifyCollectionChanged => throw new ArgumentException( + "Collection implements INotifyCollectionChanged but not IList.", + nameof(source)), + IEnumerable iObj => new List(iObj), + null => throw new ArgumentNullException(nameof(source)), + _ => new List(source.Cast()) + }; + + if (_listening && _source is INotifyCollectionChanged inccNew) + CollectionChangedEventManager.Instance.AddListener(inccNew, this); + } + + private void AddListenerIfNecessary() + { + if (!_listening) + { + if (_source is INotifyCollectionChanged incc) + CollectionChangedEventManager.Instance.AddListener(incc, this); + _listening = true; + } + } + + private void RemoveListenerIfNecessary() + { + if (_listening && _collectionChanged is null && _postCollectionChanged is null) + { + if (_source is INotifyCollectionChanged incc) + CollectionChangedEventManager.Instance.RemoveListener(incc, this); + _listening = false; + } + } + + [DoesNotReturn] + private static int ThrowReadOnly() => throw new NotSupportedException("Collection is read-only."); } public sealed class ItemsSourceView : ItemsSourceView, IReadOnlyList @@ -306,7 +333,7 @@ namespace Avalonia.Controls /// /// The index. /// The item. - public new T GetAt(int index) => (T)Inner[index]!; + public new T GetAt(int index) => (T)Source[index]!; public new IEnumerator GetEnumerator() { @@ -316,7 +343,7 @@ namespace Avalonia.Controls yield return (T)o; } - var inner = Inner; + var inner = Source; return inner switch { @@ -325,6 +352,6 @@ namespace Avalonia.Controls }; } - IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Source.GetEnumerator(); } } diff --git a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs index a2df8c3e5f..1cf0202772 100644 --- a/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs +++ b/src/Avalonia.Controls/Presenters/PanelContainerGenerator.cs @@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters Debug.Assert(presenter.Panel is not null or VirtualizingPanel); _presenter = presenter; - _presenter.ItemsControl.PropertyChanged += OnItemsControlPropertyChanged; _presenter.ItemsControl.ItemsView.PostCollectionChanged += OnItemsChanged; OnItemsChanged(null, CollectionUtils.ResetEventArgs); @@ -32,9 +31,7 @@ namespace Avalonia.Controls.Presenters { if (_presenter.ItemsControl is { } itemsControl) { - itemsControl.PropertyChanged -= OnItemsControlPropertyChanged; itemsControl.ItemsView.PostCollectionChanged -= OnItemsChanged; - ClearItemsControlLogicalChildren(); } @@ -43,18 +40,6 @@ namespace Avalonia.Controls.Presenters internal void Refresh() => OnItemsChanged(null, CollectionUtils.ResetEventArgs); - private void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == ItemsControl.ItemsProperty) - { - if (e.OldValue is INotifyCollectionChanged inccOld) - inccOld.CollectionChanged -= OnItemsChanged; - OnItemsChanged(null, CollectionUtils.ResetEventArgs); - if (e.NewValue is INotifyCollectionChanged inccNew) - inccNew.CollectionChanged += OnItemsChanged; - } - } - private void OnItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (_presenter.Panel is null || _presenter.ItemsControl is null) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2ee32b0dda..7716e32e26 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -145,6 +145,11 @@ namespace Avalonia.Controls.Primitives private BindingHelper? _bindingHelper; private bool _isSelectionChangeActive; + public SelectingItemsControl() + { + ItemsView.SourceChanged += OnItemsViewSourceChanged; + } + /// /// Initializes static members of the class. /// @@ -229,7 +234,7 @@ namespace Avalonia.Controls.Primitives /// property /// [AssignBinding] - [InheritDataTypeFromItems(nameof(Items))] + [InheritDataTypeFromItems(nameof(ItemsSource))] public IBinding? SelectedValueBinding { get => GetValue(SelectedValueBindingProperty); @@ -322,7 +327,7 @@ namespace Avalonia.Controls.Primitives } else if (_selection != value) { - if (value.Source != null && value.Source != Items) + if (value.Source != null && value.Source != ItemsView.Source) { throw new ArgumentException( "The supplied ISelectionModel already has an assigned Source but this " + @@ -434,10 +439,9 @@ namespace Avalonia.Controls.Primitives return null; } - /// - protected override void ItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + private protected override void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - base.ItemsCollectionChanged(sender!, e); + base.OnItemsViewCollectionChanged(sender!, e); if (AlwaysSelected && SelectedIndex == -1 && ItemCount > 0) { @@ -547,7 +551,7 @@ namespace Avalonia.Controls.Primitives if (_selection is object) { - _selection.Source = Items; + _selection.Source = ItemsView.Source; } } @@ -635,16 +639,6 @@ namespace Avalonia.Controls.Primitives { AutoScrollToSelectedItemIfNecessary(); } - if (change.Property == ItemsProperty && _updateState is null && _selection is object) - { - var newValue = change.GetNewValue(); - _selection.Source = newValue; - - if (newValue is null) - { - _selection.Clear(); - } - } else if (change.Property == SelectionModeProperty && _selection is object) { var newValue = change.GetNewValue(); @@ -880,6 +874,12 @@ namespace Avalonia.Controls.Primitives return false; } + private void OnItemsViewSourceChanged(object? sender, EventArgs e) + { + if (_selection is not null && _updateState is null) + _selection.Source = ItemsView.Source; + } + /// /// Called when is raised on /// . @@ -968,7 +968,7 @@ namespace Avalonia.Controls.Primitives /// The event args. private void OnSelectionModelLostSelection(object? sender, EventArgs e) { - if (AlwaysSelected && Items is object) + if (AlwaysSelected && ItemsView.Count > 0) { SelectedIndex = 0; } @@ -998,14 +998,14 @@ namespace Avalonia.Controls.Primitives } } - private object FindItemWithValue(object? value) + private object? FindItemWithValue(object? value) { if (ItemCount == 0 || value is null) { return AvaloniaProperty.UnsetValue; } - var items = Items; + var items = ItemsView; var binding = SelectedValueBinding; if (binding is null) @@ -1169,7 +1169,7 @@ namespace Avalonia.Controls.Primitives { if (_updateState is null) { - model.Source = Items; + model.Source = ItemsView.Source; } model.PropertyChanged += OnSelectionModelPropertyChanged; @@ -1236,9 +1236,9 @@ namespace Avalonia.Controls.Primitives SelectedItems = state.SelectedItems.Value; } - Selection.Source = Items; + Selection.Source = ItemsView.Source; - if (Items is null) + if (ItemsView.Count == 0) { Selection.Clear(); } diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index d4c2b32974..68bad598d0 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -30,9 +30,9 @@ namespace Avalonia.Controls.Selection Source = source; } - public new IEnumerable? Source + public new IEnumerable? Source { - get => base.Source as IEnumerable; + get => base.Source; set => SetSource(value); } diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 3c1b1262ae..5f528e2c72 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -144,13 +144,13 @@ namespace Avalonia.Controls.Utils { get { - return SelectorControl?.Items; + return SelectorControl?.ItemsSource; } set { if (SelectorControl != null) { - SelectorControl.Items = value; + SelectorControl.ItemsSource = value; } } } diff --git a/src/Avalonia.Controls/VirtualizingPanel.cs b/src/Avalonia.Controls/VirtualizingPanel.cs index 7780843eb5..a95d4f1ffa 100644 --- a/src/Avalonia.Controls/VirtualizingPanel.cs +++ b/src/Avalonia.Controls/VirtualizingPanel.cs @@ -34,7 +34,8 @@ namespace Avalonia.Controls /// /// Gets the items to display. /// - protected IReadOnlyList Items => ItemsControl?.ItemsView ?? ItemsSourceView.Empty; + protected IReadOnlyList Items => (IReadOnlyList?)ItemsControl?.ItemsView ?? + Array.Empty(); /// /// Gets the that the panel is displaying items for. @@ -192,17 +193,13 @@ namespace Avalonia.Controls throw new InvalidOperationException("The VirtualizingPanel is already attached to an ItemsControl"); ItemsControl = itemsControl; - ItemsControl.PropertyChanged += OnItemsControlPropertyChanged; ItemsControl.ItemsView.PostCollectionChanged += OnItemsControlItemsChanged; } internal void Detach() { var itemsControl = EnsureItemsControl(); - - itemsControl.PropertyChanged -= OnItemsControlPropertyChanged; itemsControl.ItemsView.PostCollectionChanged -= OnItemsControlItemsChanged; - ItemsControl = null; Children.Clear(); } @@ -216,20 +213,9 @@ namespace Avalonia.Controls return ItemsControl; } - private protected virtual void OnItemsControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == ItemsControl.ItemsViewProperty) - { - var (oldValue, newValue) = e.GetOldAndNewValue(); - oldValue.PostCollectionChanged -= OnItemsControlItemsChanged; - Refresh(); - newValue.PostCollectionChanged += OnItemsControlItemsChanged; - } - } - private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - OnItemsChanged(_itemsControl?.ItemsView ?? ItemsSourceView.Empty, e); + OnItemsChanged(Items, e); } [DoesNotReturn] diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index e206d809d3..9e18285315 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -257,7 +257,7 @@ namespace Avalonia.Controls.UnitTests var target = new ComboBox { Template = GetTemplate(), - Items = items.Select(x => new ComboBoxItem { Content = x }) + Items = items.Select(x => new ComboBoxItem { Content = x }).ToList(), }; target.ApplyTemplate(); diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 3aaf62f0bf..e7db6e3e67 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -16,6 +16,19 @@ namespace Avalonia.Controls.UnitTests { public class ItemsControlTests { + [Fact] + public void Setting_ItemsSource_Should_Populate_Items() + { + var target = new ItemsControl + { + Template = GetTemplate(), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + ItemsSource = new[] { "foo", "bar" }, + }; + + Assert.Equal(target.ItemsSource, target.Items); + } + [Fact] public void Should_Use_ItemTemplate_To_Create_Control() { @@ -153,7 +166,7 @@ namespace Avalonia.Controls.UnitTests var child = new Control(); target.Template = GetTemplate(); - target.Items = new[] { child }; + target.Items.Add(child); Assert.Equal(child.Parent, target); Assert.Equal(child.GetLogicalParent(), target); @@ -206,11 +219,13 @@ namespace Avalonia.Controls.UnitTests { var target = new ItemsControl(); var child = new Control(); - var items = new AvaloniaList(child); target.Template = GetTemplate(); - target.Items = items; - items.RemoveAt(0); + target.Items.Add(child); + + Assert.Single(target.GetLogicalChildren()); + + target.Items.RemoveAt(0); Assert.Null(child.Parent); Assert.Null(child.GetLogicalParent()); @@ -224,8 +239,11 @@ namespace Avalonia.Controls.UnitTests var child = new Control(); target.Template = GetTemplate(); - target.Items = new[] { child }; - target.Items = null; + target.Items.Add(child); + + Assert.Single(target.GetLogicalChildren()); + + target.Items.Clear(); Assert.Null(child.Parent); Assert.Null(((ILogical)child).LogicalParent); @@ -253,7 +271,7 @@ namespace Avalonia.Controls.UnitTests var child = new Control(); target.Template = GetTemplate(); - target.Items = new[] { child }; + target.Items.Add(child); // Should appear both before and after applying template. Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren()); @@ -299,7 +317,7 @@ namespace Avalonia.Controls.UnitTests [Fact] - public void Setting_Items_Should_Fire_LogicalChildren_CollectionChanged() + public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged() { var target = new ItemsControl(); var child = new Control(); @@ -311,7 +329,7 @@ namespace Avalonia.Controls.UnitTests ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = e.Action == NotifyCollectionChangedAction.Add; - target.Items = new[] { child }; + target.Items.Add(child); Assert.True(called); } @@ -324,7 +342,7 @@ namespace Avalonia.Controls.UnitTests var called = false; target.Template = GetTemplate(); - target.Items = new[] { child }; + target.Items.Add(child); target.ApplyTemplate(); ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => @@ -343,7 +361,7 @@ namespace Avalonia.Controls.UnitTests var called = false; target.Template = GetTemplate(); - target.Items = new[] { child }; + target.Items.Add(child); target.ApplyTemplate(); ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; @@ -353,26 +371,6 @@ namespace Avalonia.Controls.UnitTests Assert.True(called); } - [Fact] - public void Adding_Items_Should_Fire_LogicalChildren_CollectionChanged() - { - var target = new ItemsControl(); - var items = new AvaloniaList { "Foo" }; - var called = false; - - target.Template = GetTemplate(); - target.Items = items; - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => - called = e.Action == NotifyCollectionChangedAction.Add; - - items.Add("Bar"); - - Assert.True(called); - } - [Fact] public void Removing_Items_Should_Fire_LogicalChildren_CollectionChanged() { diff --git a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs index df842b21a7..faa143bb8e 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs @@ -38,6 +38,35 @@ namespace Avalonia.Controls.UnitTests Assert.Throws(() => ItemsSourceView.GetOrCreate(source)); } + [Fact] + public void Reassigning_Source_Unsubscribes_From_Previous_Source() + { + var source = new AvaloniaList(); + var target = new ReassignableItemsSourceView(source); + var debug = (INotifyCollectionChangedDebug)source; + + target.CollectionChanged += (s, e) => { }; + + Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length); + + target.SetSource(new string[0]); + + Assert.Null(debug.GetCollectionChangedSubscribers()); + } + + [Fact] + public void Reassigning_Source_Subscribes_To_New_Source() + { + var source = new AvaloniaList(); + var target = new ReassignableItemsSourceView(new string[0]); + var debug = (INotifyCollectionChangedDebug)source; + + target.CollectionChanged += (s, e) => { }; + target.SetSource(source); + + Assert.Equal(1, debug.GetCollectionChangedSubscribers().Length); + } + private class InvalidCollection : INotifyCollectionChanged, IEnumerable { public event NotifyCollectionChangedEventHandler CollectionChanged { add { } remove { } } @@ -52,5 +81,15 @@ namespace Avalonia.Controls.UnitTests yield break; } } + + private class ReassignableItemsSourceView : ItemsSourceView + { + public ReassignableItemsSourceView(IEnumerable source) + : base(source) + { + } + + public new void SetSource(IEnumerable source) => base.SetSource(source); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 71f803fab7..c1ec66b2e9 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -139,7 +139,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var itemsControl = new ItemsControl { - Items = items, + ItemsSource = items, Template = new FuncControlTemplate((_, _) => result) }; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 0f72b2101a..e40ca44ba6 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -71,27 +71,25 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Logical_Children_Should_Be_TabItems() { - var items = new[] - { - new TabItem - { - Content = "foo" - }, - new TabItem - { - Content = "bar" - }, - }; - var target = new TabControl { Template = TabControlTemplate(), - Items = items, + Items = + { + new TabItem + { + Content = "foo" + }, + new TabItem + { + Content = "bar" + }, + } }; - Assert.Equal(items, target.GetLogicalChildren()); + Assert.Equal(target.Items.Cast(), target.GetLogicalChildren()); target.ApplyTemplate(); - Assert.Equal(items, target.GetLogicalChildren()); + Assert.Equal(target.Items.Cast(), target.GetLogicalChildren()); } [Fact] @@ -207,26 +205,8 @@ namespace Avalonia.Controls.UnitTests [Fact] public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate() { - var collection = new[] - { - new TabItem - { - Name = "first", - Content = "foo", - }, - new TabItem - { - Name = "second", - Content = "bar", - }, - new TabItem - { - Name = "3rd", - Content = "barf", - }, - }; - var template = new FuncControlTemplate((x, __) => new Decorator()); + TabControl target; var root = new TestRoot { Styles = @@ -239,13 +219,31 @@ namespace Avalonia.Controls.UnitTests } } }, - Child = new TabControl + Child = (target = new TabControl { Template = TabControlTemplate(), - Items = collection, - } + Items = + { + new TabItem + { + Name = "first", + Content = "foo", + }, + new TabItem + { + Name = "second", + Content = "bar", + }, + new TabItem + { + Name = "3rd", + Content = "barf", + }, + }, + }) }; + var collection = target.Items.Cast().ToList(); Assert.Same(collection[0].Template, template); Assert.Same(collection[1].Template, template); Assert.Same(collection[2].Template, template); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 2ca716fa8f..c397b0efb8 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1807,7 +1807,7 @@ namespace Avalonia.Controls.UnitTests return (TreeViewItem)c; } - private IList CreateTestTreeData() + private AvaloniaList CreateTestTreeData() { return new AvaloniaList { diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs index ea6b9367cf..721e8bde68 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingCarouselPanelTests.cs @@ -218,7 +218,7 @@ namespace Avalonia.Controls.UnitTests { var carousel = new Carousel { - Items = items, + ItemsSource = items, Template = CarouselTemplate(), PageTransition = transition, }; diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index ba8e7242a1..c3dafb02db 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -9,15 +9,12 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Layout; -using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; -#nullable enable - namespace Avalonia.Controls.UnitTests { public class VirtualizingStackPanelTests @@ -99,7 +96,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var (target, _, itemsControl) = CreateTarget(); - var items = (IList)itemsControl.Items!; + var items = (IList)itemsControl.ItemsSource!; Assert.Equal(10, target.GetRealizedElements().Count); @@ -131,7 +128,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var (target, _, itemsControl) = CreateTarget(); - var items = (IList)itemsControl.Items!; + var items = (IList)itemsControl.ItemsSource!; Assert.Equal(10, target.GetRealizedElements().Count); @@ -161,7 +158,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var (target, _, itemsControl) = CreateTarget(); - var items = (ObservableCollection)itemsControl.Items!; + var items = (ObservableCollection)itemsControl.ItemsSource!; Assert.Equal(10, target.GetRealizedElements().Count); @@ -190,7 +187,7 @@ namespace Avalonia.Controls.UnitTests { using var app = App(); var (target, _, itemsControl) = CreateTarget(); - var items = (ObservableCollection)itemsControl.Items!; + var items = (ObservableCollection)itemsControl.ItemsSource!; Assert.Equal(10, target.GetRealizedElements().Count); @@ -473,7 +470,7 @@ namespace Avalonia.Controls.UnitTests var itemsControl = new ItemsControl { - Items = items, + ItemsSource = items, Template = new FuncControlTemplate((_, _) => scroll), ItemsPanel = new FuncTemplate(() => target), }; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 27634b457b..c37f779df9 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1991,7 +1991,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public class DataGridLikeControl : Control { public static readonly DirectProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); + AvaloniaProperty.RegisterDirect( + nameof(Items), + x => x.Items, + (x, v) => x.Items = v); private IEnumerable _items; public IEnumerable Items From 57c997bed79a73ffa4bdb6d10a04c24077aff744 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Mar 2023 19:36:38 +0100 Subject: [PATCH 02/16] Support multiple `InheritDataTypeFromItems`. To allow binding both `ItemsControl.Items` and `ItemsSource` we need to support multiple `InheritDataTypeFromItems` attributes. --- .../InheritDataTypeFromItemsAttribute.cs | 2 +- src/Avalonia.Controls/ItemsControl.cs | 2 + .../Primitives/SelectingItemsControl.cs | 1 + ...valoniaXamlIlDataContextTypeTransformer.cs | 43 +++++++++++-------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs index fac8cd8737..e9bd6ab89f 100644 --- a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs @@ -9,7 +9,7 @@ namespace Avalonia.Metadata; /// A typical usage example is a ListBox control, where is defined on the ItemTemplate property, /// allowing the template to inherit the data type from the Items collection binding. /// -[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] public sealed class InheritDataTypeFromItemsAttribute : Attribute { /// diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 56ba9c7183..4914a833de 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -95,6 +95,7 @@ namespace Avalonia.Controls /// [AssignBinding] [InheritDataTypeFromItems(nameof(ItemsSource))] + [InheritDataTypeFromItems(nameof(Items))] public IBinding? DisplayMemberBinding { get => GetValue(DisplayMemberBindingProperty); @@ -218,6 +219,7 @@ namespace Avalonia.Controls /// Gets or sets the data template used to display the items in the control. /// [InheritDataTypeFromItems(nameof(ItemsSource))] + [InheritDataTypeFromItems(nameof(Items))] public IDataTemplate? ItemTemplate { get => GetValue(ItemTemplateProperty); diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 7716e32e26..a31472fab4 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -235,6 +235,7 @@ namespace Avalonia.Controls.Primitives /// [AssignBinding] [InheritDataTypeFromItems(nameof(ItemsSource))] + [InheritDataTypeFromItems(nameof(Items))] public IBinding? SelectedValueBinding { get => GetValue(SelectedValueBindingProperty); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index a24d4eb6e9..681d2a38d4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -73,27 +73,32 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers // Infer data type from collection binding on a control that displays items. var property = context.ParentNodes().OfType().FirstOrDefault(); var attributeType = context.GetAvaloniaTypes().InheritDataTypeFromItemsAttribute; - var attribute = property?.Property?.GetClrProperty().CustomAttributes - .FirstOrDefault(a => a.Type == attributeType); - - if (attribute is not null) + var attributes = property?.Property?.GetClrProperty().CustomAttributes + .Where(a => a.Type == attributeType).ToList(); + + if (attributes?.Count > 0) { - var propertyName = (string)attribute.Parameters.First(); - XamlAstConstructableObjectNode parentObject; - if (attribute.Properties.TryGetValue("AncestorType", out var type) - && type is IXamlType xamlType) - { - parentObject = context.ParentNodes().OfType() - .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName); - } - else + foreach (var attribute in attributes) { - parentObject = context.ParentNodes().OfType().FirstOrDefault(); - } - - if (parentObject != null) - { - inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName); + var propertyName = (string)attribute.Parameters.First(); + XamlAstConstructableObjectNode parentObject; + if (attribute.Properties.TryGetValue("AncestorType", out var type) + && type is IXamlType xamlType) + { + parentObject = context.ParentNodes().OfType() + .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName); + } + else + { + parentObject = context.ParentNodes().OfType().FirstOrDefault(); + } + + if (parentObject != null) + { + inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName); + if (inferredDataContextTypeNode != null) + break; + } } } From 0bbb66eeebe26975233f8ec74b8b615d2e0b5979 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 11:01:09 +0100 Subject: [PATCH 03/16] Fix failing selection tests. --- .../Primitives/SelectingItemsControl.cs | 16 ++++++----- .../Selection/InternalSelectionModel.cs | 27 ++++++++++++++++++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a31472fab4..cb4efb344f 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1232,16 +1232,18 @@ namespace Avalonia.Controls.Primitives Selection = state.Selection.Value; } - if (state.SelectedItems.HasValue) + if (_selection is InternalSelectionModel s) { - SelectedItems = state.SelectedItems.Value; + s.Update(ItemsView.Source, state.SelectedItems); } - - Selection.Source = ItemsView.Source; - - if (ItemsView.Count == 0) + else { - Selection.Clear(); + if (state.SelectedItems.HasValue) + { + SelectedItems = state.SelectedItems.Value; + } + + Selection.Source = ItemsView.Source; } if (state.SelectedValue.HasValue) diff --git a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs index d0e6144f59..c8ad9bd88b 100644 --- a/src/Avalonia.Controls/Selection/InternalSelectionModel.cs +++ b/src/Avalonia.Controls/Selection/InternalSelectionModel.cs @@ -5,6 +5,7 @@ using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Collections; +using Avalonia.Data; namespace Avalonia.Controls.Selection { @@ -13,6 +14,7 @@ namespace Avalonia.Controls.Selection private IList? _writableSelectedItems; private int _ignoreModelChanges; private bool _ignoreSelectedItemsChanges; + private bool _skipSyncFromSelectedItems; private bool _isResetting; public InternalSelectionModel() @@ -60,6 +62,29 @@ namespace Avalonia.Controls.Selection } } + internal void Update(IEnumerable? source, Optional selectedItems) + { + var previousSource = Source; + var previousWritableSelectedItems = _writableSelectedItems; + + try + { + _skipSyncFromSelectedItems = true; + SetSource(source); + if (selectedItems.HasValue) + WritableSelectedItems = selectedItems.Value; + } + finally + { + _skipSyncFromSelectedItems = false; + } + + // We skipped the sync from WritableSelectedItems before; do it now that both + // the source and WritableSelectedItems are updated. + if (previousSource != Source || previousWritableSelectedItems != _writableSelectedItems) + SyncFromSelectedItems(); + } + private protected override void SetSource(IEnumerable? value) { if (Source == value) @@ -121,7 +146,7 @@ namespace Avalonia.Controls.Selection private void SyncFromSelectedItems() { - if (Source is null || _writableSelectedItems is null) + if (_skipSyncFromSelectedItems || Source is null || _writableSelectedItems is null) { return; } From d2bc8a09c94450a2c1d9414d7bc285891a99bf41 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 14:35:21 +0100 Subject: [PATCH 04/16] Update ncrunch config. --- .ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject | 5 +++++ .ncrunch/Generators.Sandbox.v3.ncrunchproject | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject create mode 100644 .ncrunch/Generators.Sandbox.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject b/.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Avalonia.Generators.Tests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/Generators.Sandbox.v3.ncrunchproject b/.ncrunch/Generators.Sandbox.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Generators.Sandbox.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From b917cc5a2e6c4c76d2855d4f85894b6b4e1e3dd5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:06:23 +0100 Subject: [PATCH 05/16] Added some XML docs. --- src/Avalonia.Controls/ItemCollection.cs | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/Avalonia.Controls/ItemCollection.cs b/src/Avalonia.Controls/ItemCollection.cs index 120aef41dc..4daea44d5e 100644 --- a/src/Avalonia.Controls/ItemCollection.cs +++ b/src/Avalonia.Controls/ItemCollection.cs @@ -6,6 +6,9 @@ using Avalonia.Collections; namespace Avalonia.Controls { + /// + /// Holds the list of items that constitute the content of an . + /// public class ItemCollection : ItemsSourceView, IList { // Suppress "Avoid zero-length array allocations": This is a sentinel value and must be unique. @@ -30,11 +33,55 @@ namespace Avalonia.Controls internal event EventHandler? SourceChanged; + /// + /// Adds an item to the . + /// + /// The item to add to the collection. + /// + /// The position into which the new element was inserted, or -1 to indicate that + /// the item was not inserted into the collection. + /// + /// + /// The collection is in ItemsSource mode. + /// public int Add(object? value) => WritableSource.Add(value); + + /// + /// Clears the collection and releases the references on all items currently in the + /// collection. + /// + /// + /// The collection is in ItemsSource mode. + /// public void Clear() => WritableSource.Clear(); + + /// + /// Inserts an element into the collection at the specified index. + /// + /// The zero-based index at which to insert the item. + /// The item to insert. + /// + /// The collection is in ItemsSource mode. + /// public void Insert(int index, object? value) => WritableSource.Insert(index, value); + + /// + /// Removes the item at the specified index of the collection or view. + /// + /// The zero-based index of the item to remove. + /// + /// The collection is in ItemsSource mode. + /// public void RemoveAt(int index) => WritableSource.RemoveAt(index); + /// + /// Removes the specified item reference from the collection or view. + /// + /// The object to remove. + /// True if the item was removed; otherwise false. + /// + /// The collection is in ItemsSource mode. + /// public bool Remove(object? value) { var c = Count; From 254d58da6ad2e053e7f62a2b1fdb6d1d32e36475 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:06:46 +0100 Subject: [PATCH 06/16] Change type of ItemsView property. It's a view so makes sense to expose the read-only view interface. --- src/Avalonia.Controls/ItemsControl.cs | 5 ++++- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index bba551b477..c0b9ded2ee 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -236,7 +236,10 @@ namespace Avalonia.Controls /// public Panel? ItemsPanelRoot => Presenter?.Panel; - public ItemCollection ItemsView => _items; + /// + /// Gets a read-only view of the items in the . + /// + public ItemsSourceView ItemsView => _items; private protected bool WrapFocus { get; set; } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index cb4efb344f..b89a75787f 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -147,7 +147,7 @@ namespace Avalonia.Controls.Primitives public SelectingItemsControl() { - ItemsView.SourceChanged += OnItemsViewSourceChanged; + ((ItemCollection)ItemsView).SourceChanged += OnItemsViewSourceChanged; } /// From 2c56019d77688aa05ec69f0518fbfeec519e8b7b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:13:50 +0100 Subject: [PATCH 07/16] Update ItemsControl XML docs. --- src/Avalonia.Controls/ItemsControl.cs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index c0b9ded2ee..b476df5fda 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -132,6 +132,29 @@ namespace Avalonia.Controls /// /// Gets or sets the items to display. /// + /// + /// Since Avalonia 11, has both an property + /// and an property. The properties have the following differences: + /// + /// + /// is initialized with an empty collection and is a direct property, + /// meaning that it cannot be styled + /// is by default null, and is a styled property. This property + /// is marked as the content property and will be used for items added via inline XAML. + /// + /// + /// In Avalonia 11 the two properties can be used almost interchangeably but this will change + /// in a later version. In order to be ready for this change, follow the following guidance: + /// + /// + /// You should use the property when you're assigning a collection of + /// item containers directly, for example adding a collection of s + /// directly to a . Add the containers to the pre-existing list, do not + /// reassign the property via the setter or with a binding. + /// You should use the property when you're assigning or + /// binding a collection of models which will be transformed by a data template. + /// + /// [Content] public IList? Items { @@ -204,7 +227,8 @@ namespace Avalonia.Controls /// /// You should use the property when you're assigning a collection of /// item containers directly, for example adding a collection of s - /// directly to a . + /// directly to a . Add the containers to the pre-existing list, do not + /// reassign the property via the setter or with a binding. /// You should use the property when you're assigning or /// binding a collection of models which will be transformed by a data template. /// From 9856711494f14cbf7d59cbd3f9ddc1dda7112cfc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:27:04 +0100 Subject: [PATCH 08/16] Don't modify logical tree in ItemsSource mode. --- src/Avalonia.Controls/ItemsControl.cs | 17 +++++----- .../ItemsControlTests.cs | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index b476df5fda..ee2899e50c 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -573,15 +573,18 @@ namespace Avalonia.Controls /// The event args. private protected virtual void OnItemsViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - switch (e.Action) + if (!_items.IsReadOnly) { - case NotifyCollectionChangedAction.Add: - AddControlItemsToLogicalChildren(e.NewItems); - break; + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + AddControlItemsToLogicalChildren(e.NewItems); + break; - case NotifyCollectionChangedAction.Remove: - RemoveControlItemsFromLogicalChildren(e.OldItems); - break; + case NotifyCollectionChangedAction.Remove: + RemoveControlItemsFromLogicalChildren(e.OldItems); + break; + } } ItemCount = ItemsView.Count; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 9b28ca11f0..12fc0a82ed 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -249,6 +249,37 @@ namespace Avalonia.Controls.UnitTests Assert.Null(((ILogical)child).LogicalParent); } + [Fact] + public void Assigning_ItemsSource_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate() + { + var target = new ItemsControl(); + var child = new Control(); + var called = false; + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; + + var list = new AvaloniaList(new[] { child }); + target.ItemsSource = list; + + Assert.False(called); + } + + [Fact] + public void Changing_ItemsSource_Should_Not_Fire_LogicalChildren_CollectionChanged_Before_ApplyTemplate() + { + var target = new ItemsControl(); + var child = new Control(); + var called = false; + + ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; + + var list = new AvaloniaList(); + target.ItemsSource = list; + list.Add(child); + + Assert.False(called); + } + [Fact] public void Clearing_Items_Should_Clear_Child_Controls_Parent() { From 183fed8985287500d3687f5980a5c01f4a2822bc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:34:50 +0100 Subject: [PATCH 09/16] Add more tests. One failing. --- .../ItemsControlTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 12fc0a82ed..83b82eb1d4 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -26,9 +26,36 @@ namespace Avalonia.Controls.UnitTests ItemsSource = new[] { "foo", "bar" }, }; + Assert.NotSame(target.ItemsSource, target.Items); Assert.Equal(target.ItemsSource, target.Items); } + [Fact] + public void Cannot_Set_ItemsSource_With_Items_Present() + { + var target = new ItemsControl + { + Template = GetTemplate(), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + Items = { "foo", "bar" }, + }; + + Assert.Throws(() => target.ItemsSource = new[] { "baz" }); + } + + [Fact] + public void Cannot_Modify_Items_When_ItemsSource_Set() + { + var target = new ItemsControl + { + Template = GetTemplate(), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), + ItemsSource = Array.Empty(), + }; + + Assert.Throws(() => target.Items.Add("foo")); + } + [Fact] public void Should_Use_ItemTemplate_To_Create_Control() { From ddacc112acfc482bb35498bb63a0ce38db797ca0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 15:35:22 +0100 Subject: [PATCH 10/16] Throw if setting ItemsSource when Items present. --- src/Avalonia.Controls/ItemCollection.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Controls/ItemCollection.cs b/src/Avalonia.Controls/ItemCollection.cs index 4daea44d5e..c9265558f0 100644 --- a/src/Avalonia.Controls/ItemCollection.cs +++ b/src/Avalonia.Controls/ItemCollection.cs @@ -121,6 +121,10 @@ namespace Avalonia.Controls internal void SetItemsSource(IEnumerable? value) { + if (_mode != Mode.ItemsSource && Count > 0) + throw new InvalidOperationException( + "Items collection must be empty before using ItemsSource."); + _mode = value is not null ? Mode.ItemsSource : Mode.Items; SetSource(value ?? CreateDefaultCollection()); } From b9171f32f2dcde2eff9a8d966aa85c388326b437 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 16:06:03 +0100 Subject: [PATCH 11/16] Fix compile error. --- samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs index 6d759597b5..47f97e63a3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs @@ -18,7 +18,7 @@ namespace ControlCatalog.Pages { AvaloniaXamlLoader.Load(this); var fontComboBox = this.Get("fontComboBox"); - fontComboBox.Items = FontManager.Current.SystemFonts; + fontComboBox.ItemsSource = FontManager.Current.SystemFonts; fontComboBox.SelectedIndex = 0; } } From f8eceb4af9f78d9a9da582885593fc957ef8fffe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Mar 2023 16:18:36 +0100 Subject: [PATCH 12/16] Update usages of ItemsControl Items/ItemsSource. - Use `ItemsSource` when appropriate - When `Items` is appropriate, don't use the setter --- samples/ControlCatalog/MainView.xaml | 2 +- .../Pages/CompositionPage.axaml.cs | 2 +- .../Pages/ContextFlyoutPage.xaml | 2 +- .../ControlCatalog/Pages/ContextMenuPage.xaml | 4 +- samples/ControlCatalog/Pages/CursorPage.xaml | 2 +- .../ControlCatalog/Pages/DialogsPage.xaml.cs | 12 +- samples/ControlCatalog/Pages/ListBoxPage.xaml | 2 +- samples/ControlCatalog/Pages/MenuPage.xaml | 6 +- .../Pages/NativeEmbedPage.xaml.cs | 8 +- .../Pages/NumericUpDownPage.xaml | 6 +- .../Pages/RefreshContainerPage.axaml | 2 +- .../ControlCatalog/Pages/ScrollSnapPage.xaml | 4 +- .../Pages/ScrollViewerPage.xaml | 4 +- .../ControlCatalog/Pages/TabStripPage.xaml | 2 +- .../ControlCatalog/Pages/ThemePage.axaml.cs | 2 +- .../TransitioningContentControlPage.axaml | 2 +- .../ControlCatalog/Pages/TreeViewPage.xaml | 2 +- samples/IntegrationTestApp/MainWindow.axaml | 2 +- .../Themes/Fluent/ColorPicker.xaml | 2 +- .../Themes/Fluent/ColorView.xaml | 2 +- .../Themes/Simple/ColorPicker.xaml | 2 +- .../Themes/Simple/ColorView.xaml | 2 +- src/Avalonia.Controls/ItemsControl.cs | 1 + .../Diagnostics/Views/ConsoleView.xaml | 2 +- .../Diagnostics/Views/ControlDetailsView.xaml | 6 +- .../Diagnostics/Views/EventsPageView.xaml | 6 +- .../Views/PropertyValueEditorView.cs | 2 +- .../Diagnostics/Views/TreePageView.xaml | 2 +- .../Controls/DataValidationErrors.xaml | 4 +- .../Controls/ManagedFileChooser.xaml | 6 +- .../Controls/NativeMenuBar.xaml | 4 +- .../Controls/DataValidationErrors.xaml | 2 +- .../Controls/ManagedFileChooser.xaml | 6 +- .../Controls/NativeMenuBar.xaml | 4 +- src/Windows/Avalonia.Win32/TrayIconImpl.cs | 2 +- .../Win32NativeToManagedMenuExporter.cs | 2 +- .../CarouselTests.cs | 20 +- .../ComboBoxTests.cs | 88 ++++--- .../ItemsControlTests.cs | 113 +++------ .../ListBoxTests.cs | 43 ++-- .../ListBoxTests_Multiple.cs | 6 +- .../ListBoxTests_Single.cs | 24 +- .../MenuItemTests.cs | 10 +- .../Presenters/ItemsPresenterTests.cs | 2 +- .../Primitives/PopupTests.cs | 4 +- .../Primitives/SelectingItemsControlTests.cs | 166 ++++++------- .../SelectingItemsControlTests_AutoSelect.cs | 12 +- .../SelectingItemsControlTests_Multiple.cs | 231 +++++++++--------- ...electingItemsControlTests_SelectedValue.cs | 26 +- .../Primitives/TabStripTests.cs | 94 ++++--- .../TabControlTests.cs | 154 ++++++------ .../TreeViewTests.cs | 116 +++++---- .../Utils/HotKeyManagerTests.cs | 4 +- .../VirtualizingStackPanelTests.cs | 2 +- tests/Avalonia.LeakTests/ControlTests.cs | 14 +- .../Data/MultiBindingTests.cs | 8 +- .../CompiledBindingExtensionTests.cs | 8 +- .../Xaml/BasicTests.cs | 8 +- .../Xaml/DataTemplateTests.cs | 2 +- .../Xaml/StyleTests.cs | 2 +- .../Xaml/XamlIlTests.cs | 2 +- .../AutoDataTemplateBindingHookTest.cs | 2 +- 62 files changed, 607 insertions(+), 677 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 3681298a72..9f06525821 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -241,7 +241,7 @@ diff --git a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs index 8b12a2d663..0d3061f361 100644 --- a/samples/ControlCatalog/Pages/CompositionPage.axaml.cs +++ b/samples/ControlCatalog/Pages/CompositionPage.axaml.cs @@ -32,7 +32,7 @@ public partial class CompositionPage : UserControl protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - this.Get("Items").Items = CreateColorItems(); + this.Get("Items").ItemsSource = CreateColorItems(); } diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml index 6ef6a202b6..cb294442d2 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml @@ -61,7 +61,7 @@ diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 962f0308f7..06eba52605 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -51,13 +51,13 @@ - + diff --git a/samples/ControlCatalog/Pages/CursorPage.xaml b/samples/ControlCatalog/Pages/CursorPage.xaml index 30bad06d72..66f2b8b2e3 100644 --- a/samples/ControlCatalog/Pages/CursorPage.xaml +++ b/samples/ControlCatalog/Pages/CursorPage.xaml @@ -8,7 +8,7 @@ Defines a cursor (mouse pointer) - + @@ -68,7 +68,7 @@ - + - + - + @@ -173,7 +173,7 @@ @@ -220,7 +220,7 @@ + ItemsSource="{Binding $parent[TopLevel].(NativeMenu.Menu).Items}">