Browse Source

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.
Steven Kirk 2 years ago
parent
commit
ba7e8a20b5

+ 1 - 1
samples/ControlCatalog/Pages/ComboBoxPage.xaml.cs

@@ -18,7 +18,7 @@ namespace ControlCatalog.Pages
         {
             AvaloniaXamlLoader.Load(this);
             var fontComboBox = this.Get<ComboBox>("fontComboBox");
-            fontComboBox.Items = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
+            fontComboBox.ItemsSource = FontManager.Current.GetInstalledFontFamilyNames().Select(x => new FontFamily(x));
             fontComboBox.SelectedIndex = 0;
         }
     }

+ 4 - 1
src/Avalonia.Controls.ItemsRepeater/Controls/ItemsRepeater.cs

@@ -39,7 +39,10 @@ namespace Avalonia.Controls
         /// Defines the <see cref="Items"/> property.
         /// </summary>
         public static readonly DirectProperty<ItemsRepeater, IEnumerable?> ItemsProperty =
-            ItemsControl.ItemsProperty.AddOwner<ItemsRepeater>(o => o.Items, (o, v) => o.Items = v);
+            AvaloniaProperty.RegisterDirect<ItemsRepeater, IEnumerable?>(
+                nameof(Items),
+                o => o.Items,
+                (o, v) => o.Items = v);
 
         /// <summary>
         /// Defines the <see cref="Layout"/> property.

+ 3 - 1
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@@ -18,7 +18,9 @@ namespace Avalonia.Controls
         /// Defines the <see cref="Items"/> property
         /// </summary>
         public static readonly DirectProperty<MenuFlyout, IEnumerable?> ItemsProperty =
-            ItemsControl.ItemsProperty.AddOwner<MenuFlyout>(x => x.Items,
+            AvaloniaProperty.RegisterDirect<MenuFlyout, IEnumerable?>(
+                nameof(Items),
+                x => x.Items,
                 (x, v) => x.Items = v);
 
         /// <summary>

+ 114 - 0
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<object?> 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,
+        }
+    }
+}

+ 94 - 81
src/Avalonia.Controls/ItemsControl.cs

@@ -34,8 +34,13 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="Items"/> property.
         /// </summary>
-        public static readonly DirectProperty<ItemsControl, IEnumerable?> ItemsProperty =
-            AvaloniaProperty.RegisterDirect<ItemsControl, IEnumerable?>(nameof(Items), o => o.Items, (o, v) => o.Items = v);
+        public static readonly DirectProperty<ItemsControl, IList?> ItemsProperty =
+            AvaloniaProperty.RegisterDirect<ItemsControl, IList?>(
+                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
 
         /// <summary>
         /// Defines the <see cref="ItemContainerTheme"/> property.
@@ -56,23 +61,23 @@ namespace Avalonia.Controls
             AvaloniaProperty.Register<ItemsControl, ITemplate<Panel>>(nameof(ItemsPanel), DefaultPanel);
 
         /// <summary>
-        /// Defines the <see cref="ItemTemplate"/> property.
+        /// Defines the <see cref="ItemsSource"/> property.
         /// </summary>
-        public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
-            AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
+        public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
+            AvaloniaProperty.Register<ItemsControl, IEnumerable?>(nameof(ItemsSource));
 
         /// <summary>
-        /// Defines the <see cref="ItemsView"/> property.
+        /// Defines the <see cref="ItemTemplate"/> property.
         /// </summary>
-        public static readonly DirectProperty<ItemsControl, ItemsSourceView> ItemsViewProperty =
-            AvaloniaProperty.RegisterDirect<ItemsControl, ItemsSourceView>(nameof(ItemsView), o => o.ItemsView);
+        public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
+            AvaloniaProperty.Register<ItemsControl, IDataTemplate?>(nameof(ItemTemplate));
 
         /// <summary>
         /// Defines the <see cref="DisplayMemberBinding" /> property
         /// </summary>
         public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
             AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
-        
+
         /// <summary>
         /// Defines the <see cref="AreHorizontalSnapPointsRegular"/> property.
         /// </summary>
@@ -89,15 +94,14 @@ namespace Avalonia.Controls
         /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
         /// </summary>
         [AssignBinding]
-        [InheritDataTypeFromItems(nameof(Items))]
+        [InheritDataTypeFromItems(nameof(ItemsSource))]
         public IBinding? DisplayMemberBinding
         {
             get => GetValue(DisplayMemberBindingProperty);
             set => SetValue(DisplayMemberBindingProperty, value);
         }
-        
-        private IEnumerable? _items = new AvaloniaList<object>();
-        private ItemsSourceView _itemsView;
+
+        private readonly ItemCollection _items = new();
         private int _itemCount;
         private ItemContainerGenerator? _itemContainerGenerator;
         private EventHandler<ChildIndexChangedEventArgs>? _childIndexChanged;
@@ -110,9 +114,8 @@ namespace Avalonia.Controls
         /// </summary>
         public ItemsControl()
         {
-            _itemsView = ItemsSourceView.GetOrCreate(_items);
-            _itemsView.PostCollectionChanged += ItemsCollectionChanged;
-            UpdatePseudoClasses(0);
+            UpdatePseudoClasses();
+            _items.CollectionChanged += OnItemsViewCollectionChanged;
         }
 
         /// <summary>
@@ -129,10 +132,21 @@ namespace Avalonia.Controls
         /// Gets or sets the items to display.
         /// </summary>
         [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);
+                }
+            }
         }
 
         /// <summary>
@@ -140,17 +154,24 @@ namespace Avalonia.Controls
         /// </summary>
         public ControlTheme? ItemContainerTheme
         {
-            get => GetValue(ItemContainerThemeProperty); 
+            get => GetValue(ItemContainerThemeProperty);
             set => SetValue(ItemContainerThemeProperty, value);
         }
 
         /// <summary>
-        /// Gets the number of items in <see cref="Items"/>.
+        /// Gets the number of items being displayed by the <see cref="ItemsControl"/>.
         /// </summary>
         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);
+                }
+            }
         }
 
         /// <summary>
@@ -162,13 +183,44 @@ namespace Avalonia.Controls
             set => SetValue(ItemsPanelProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets a collection used to generate the content of the <see cref="ItemsControl"/>.
+        /// </summary>
+        /// <remarks>
+        /// Since Avalonia 11, <see cref="ItemsControl"/> has both an <see cref="Items"/> property
+        /// and an <see cref="ItemsSource"/> property. The properties have the following differences:
+        /// 
+        /// <list type="bullet">
+        /// <item><see cref="Items"/> is initialized with an empty collection and is a direct property,
+        /// meaning that it cannot be styled </item>
+        /// <item><see cref="ItemsSource"/> 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.</item>
+        /// </list>
+        /// 
+        /// 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:
+        /// 
+        /// <list type="bullet">
+        /// <item>You should use the <see cref="Items"/> property when you're assigning a collection of
+        /// item containers directly, for example adding a collection of <see cref="ListBoxItem"/>s
+        /// directly to a <see cref="ListBox"/>.</item>
+        /// <item>You should use the <see cref="ItemsSource"/> property when you're assigning or
+        /// binding a collection of models which will be transformed by a data template.</item>
+        /// </list>
+        /// </remarks>
+        public IEnumerable? ItemsSource
+        {
+            get => GetValue(ItemsSourceProperty);
+            set => SetValue(ItemsSourceProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the data template used to display the items in the control.
         /// </summary>
-        [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
         /// </summary>
         public Panel? ItemsPanelRoot => Presenter?.Panel;
 
-        /// <summary>
-        /// Gets a standardized view over <see cref="Items"/>.
-        /// </summary>
-        /// <remarks>
-        /// The <see cref="Items"/> property may be an enumerable which does not implement
-        /// <see cref="IList"/> 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.
-        /// </remarks>
-        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
         /// </summary>
         public bool AreHorizontalSnapPointsRegular
         {
-            get => GetValue(AreHorizontalSnapPointsRegularProperty); 
+            get => GetValue(AreHorizontalSnapPointsRegularProperty);
             set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
         }
 
@@ -271,7 +298,7 @@ namespace Avalonia.Controls
         /// </summary>
         public bool AreVerticalSnapPointsRegular
         {
-            get => GetValue(AreVerticalSnapPointsRegularProperty); 
+            get => GetValue(AreVerticalSnapPointsRegularProperty);
             set => SetValue(AreVerticalSnapPointsRegularProperty, value);
         }
 
@@ -295,7 +322,7 @@ namespace Avalonia.Controls
         /// </returns>
         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;
         }
 
         /// <summary>
@@ -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<IEnumerable?>());
-                ItemCount = ItemsView.Count;
-            }
-            else if (change.Property == ItemCountProperty)
+            if (change.Property == ItemContainerThemeProperty && _itemContainerGenerator is not null)
             {
-                UpdatePseudoClasses(change.GetNewValue<int>());
-                _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<IEnumerable?>());
             }
             else if (change.Property == ItemTemplateProperty)
             {
@@ -524,14 +545,12 @@ namespace Avalonia.Controls
 
         /// <summary>
         /// Called when the <see cref="INotifyCollectionChanged.CollectionChanged"/> event is
-        /// raised on <see cref="Items"/>.
+        /// raised on <see cref="ItemsView"/>.
         /// </summary>
         /// <param name="sender">The event sender.</param>
         /// <param name="e">The event args.</param>
-        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;
         }
 
         /// <summary>
@@ -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);
         }
 
-        /// <summary>
-        /// Given a collection of items, adds those that are controls to the logical children.
-        /// </summary>
-        /// <param name="items">The items.</param>
         private void AddControlItemsToLogicalChildren(IEnumerable? items)
         {
             if (items is null)
@@ -640,10 +657,6 @@ namespace Avalonia.Controls
                 LogicalChildren.AddRange(toAdd);
         }
 
-        /// <summary>
-        /// Given a collection of items, removes those that are controls to from logical children.
-        /// </summary>
-        /// <param name="items">The items.</param>
         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(

+ 91 - 64
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.
     /// </summary>
     public class ItemsSourceView : IReadOnlyList<object?>,
+        IList,
         INotifyCollectionChanged,
         ICollectionChangedListener
     {
         /// <summary>
-        ///  Gets an empty <see cref="ItemsSourceView"/>
+        /// Gets an empty <see cref="ItemsSourceView"/>
         /// </summary>
-        public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
+        public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object?>());
 
-        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.
         /// </summary>
         /// <param name="source">The data source.</param>
-        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<object> iObj => new List<object>(iObj),
-                null => throw new ArgumentNullException(nameof(source)),
-                _ => new List<object>(source.Cast<object>())
-            };
-        }
+        private protected ItemsSourceView(IEnumerable source) => SetSource(source);
 
         /// <summary>
         /// Gets the number of items in the collection.
         /// </summary>
-        public int Count => Inner.Count;
+        public int Count => Source.Count;
 
         /// <summary>
-        /// Gets the inner collection.
+        /// Gets the source collection.
         /// </summary>
-        public IList Inner => _inner;
+        public IList Source => _source;
 
         /// <summary>
         /// Retrieves the item at the specified index.
@@ -67,12 +56,20 @@ namespace Avalonia.Controls
         /// <returns>The item.</returns>
         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();
+        }
+
         /// <summary>
-        /// Gets a value that indicates whether the items source can provide a unique key for each item.
-        /// </summary>
-        /// <remarks>
         /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
-        /// </remarks>
+        /// </summary>
         internal bool HasKeyIndexMapping => false;
 
         /// <summary>
@@ -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;
-            }
-        }
-
         /// <summary>
         /// Retrieves the item at the specified index.
         /// </summary>
         /// <param name="index">The index.</param>
         /// <returns>The item.</returns>
-        public object? GetAt(int index) => Inner[index];
-
-        /// <summary>
-        /// Determines the index of a specific item in the collection.
-        /// </summary>
-        /// <param name="item">The object to locate in the collection.</param>
-        /// <returns>The index of value if found in the list; otherwise, -1.</returns>
-        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);
 
         /// <summary>
         /// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
@@ -201,7 +173,8 @@ namespace Avalonia.Controls
         {
             return items switch
             {
-                ItemsSourceView<T> isv => isv,
+                ItemsSourceView<T> isvt => isvt,
+                ItemsSourceView isv => new ItemsSourceView<T>(isv.Source),
                 null => ItemsSourceView<T>.Empty,
                 _ => new ItemsSourceView<T>(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);
+
         /// <summary>
-        /// Retrieves the index of the item that has the specified unique identifier (key).
+        /// Not implemented in Avalonia, preserved here for ItemsRepeater's usage.
         /// </summary>
-        /// <param name="index">The index.</param>
-        /// <returns>The key</returns>
-        /// <remarks>
-        /// TODO: Not yet implemented in Avalonia.
-        /// </remarks>
         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<object> iObj => new List<object>(iObj),
+                null => throw new ArgumentNullException(nameof(source)),
+                _ => new List<object>(source.Cast<object>())
+            };
+
+            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<T> : ItemsSourceView, IReadOnlyList<T>
@@ -306,7 +333,7 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="index">The index.</param>
         /// <returns>The item.</returns>
-        public new T GetAt(int index) => (T)Inner[index]!;
+        public new T GetAt(int index) => (T)Source[index]!;
 
         public new IEnumerator<T> 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();
     }
 }

+ 0 - 15
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)

+ 22 - 22
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;
+        }
+
         /// <summary>
         /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
         /// </summary>
@@ -229,7 +234,7 @@ namespace Avalonia.Controls.Primitives
         /// <see cref="SelectedValue"/> property
         /// </summary>
         [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;
         }
 
-        /// <inheritdoc />
-        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<IEnumerable?>();
-                _selection.Source = newValue;
-
-                if (newValue is null)
-                {
-                    _selection.Clear();
-                }
-            }
             else if (change.Property == SelectionModeProperty && _selection is object)
             {
                 var newValue = change.GetNewValue<SelectionMode>();
@@ -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;
+        }
+
         /// <summary>
         /// Called when <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised on
         /// <see cref="Selection"/>.
@@ -968,7 +968,7 @@ namespace Avalonia.Controls.Primitives
         /// <param name="e">The event args.</param>
         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();
                 }

+ 2 - 2
src/Avalonia.Controls/Selection/SelectionModel.cs

@@ -30,9 +30,9 @@ namespace Avalonia.Controls.Selection
             Source = source;
         }
 
-        public new IEnumerable<T>? Source
+        public new IEnumerable? Source
         {
-            get => base.Source as IEnumerable<T>;
+            get => base.Source;
             set => SetSource(value);
         }
 

+ 2 - 2
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;
                 }
             }
         }

+ 3 - 17
src/Avalonia.Controls/VirtualizingPanel.cs

@@ -34,7 +34,8 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets the items to display.
         /// </summary>
-        protected IReadOnlyList<object?> Items => ItemsControl?.ItemsView ?? ItemsSourceView.Empty;
+        protected IReadOnlyList<object?> Items => (IReadOnlyList<object?>?)ItemsControl?.ItemsView ?? 
+            Array.Empty<object?>();
 
         /// <summary>
         /// Gets the <see cref="ItemsControl"/> 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<ItemsSourceView>();
-                oldValue.PostCollectionChanged -= OnItemsControlItemsChanged;
-                Refresh();
-                newValue.PostCollectionChanged += OnItemsControlItemsChanged;
-            }
-        }
-
         private void OnItemsControlItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
         {
-            OnItemsChanged(_itemsControl?.ItemsView ?? ItemsSourceView.Empty, e);
+            OnItemsChanged(Items, e);
         }
 
         [DoesNotReturn]

+ 1 - 1
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();

+ 29 - 31
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<string>((_, __) => 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<Control>(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<string> { "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()
         {

+ 39 - 0
tests/Avalonia.Controls.UnitTests/ItemsSourceViewTests.cs

@@ -38,6 +38,35 @@ namespace Avalonia.Controls.UnitTests
             Assert.Throws<ArgumentException>(() => ItemsSourceView.GetOrCreate(source));
         }
 
+        [Fact]
+        public void Reassigning_Source_Unsubscribes_From_Previous_Source()
+        {
+            var source = new AvaloniaList<string>();
+            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<string>();
+            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<string>
         {
             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);
+        }
     }
 }

+ 1 - 1
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<ItemsControl>((_, _) => result)
             };
 

+ 35 - 37
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<ILogical>(), target.GetLogicalChildren());
             target.ApplyTemplate();
-            Assert.Equal(items, target.GetLogicalChildren());
+            Assert.Equal(target.Items.Cast<ILogical>(), 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<TabItem>((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<TabItem>().ToList();
             Assert.Same(collection[0].Template, template);
             Assert.Same(collection[1].Template, template);
             Assert.Same(collection[2].Template, template);

+ 1 - 1
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -1807,7 +1807,7 @@ namespace Avalonia.Controls.UnitTests
             return (TreeViewItem)c;
         }
 
-        private IList<Node> CreateTestTreeData()
+        private AvaloniaList<Node> CreateTestTreeData()
         {
             return new AvaloniaList<Node>
             {

+ 1 - 1
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,
             };

+ 5 - 8
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<string>)itemsControl.Items!;
+            var items = (ObservableCollection<string>)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<string>)itemsControl.Items!;
+            var items = (ObservableCollection<string>)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<ItemsControl>((_, _) => scroll),
                 ItemsPanel = new FuncTemplate<Panel>(() => target),
             };

+ 4 - 1
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<DataGridLikeControl, IEnumerable?> ItemsProperty =
-            ItemsControl.ItemsProperty.AddOwner<DataGridLikeControl>(o => o.Items, (o, v) => o.Items = v);
+            AvaloniaProperty.RegisterDirect<DataGridLikeControl, IEnumerable?>(
+                nameof(Items),
+                x => x.Items,
+                (x, v) => x.Items = v);
 
         private IEnumerable _items;
         public IEnumerable Items