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
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;
}
}
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.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 df973debc7..ba5489ac25 100644
--- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs
+++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs
@@ -19,7 +19,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..c9265558f0
--- /dev/null
+++ b/src/Avalonia.Controls/ItemCollection.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Diagnostics.CodeAnalysis;
+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.
+#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;
+
+ ///
+ /// 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;
+ 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)
+ {
+ 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());
+ }
+
+ 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
public ItemsControl()
{
- _itemsView = ItemsSourceView.GetOrCreate(_items);
- _itemsView.PostCollectionChanged += ItemsCollectionChanged;
- UpdatePseudoClasses(0);
+ UpdatePseudoClasses();
+ _items.CollectionChanged += OnItemsViewCollectionChanged;
}
///
@@ -128,11 +132,45 @@ 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 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 +178,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 +207,46 @@ 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 . 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.
+ ///
+ ///
+ public IEnumerable? ItemsSource
+ {
+ get => GetValue(ItemsSourceProperty);
+ set => SetValue(ItemsSourceProperty, value);
+ }
+
///
/// Gets or sets the data template used to display the items in the control.
///
+ [InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate
{
- get => GetValue(ItemTemplateProperty);
+ get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
@@ -183,31 +261,9 @@ namespace Avalonia.Controls
public Panel? ItemsPanelRoot => Presenter?.Panel;
///
- /// Gets a standardized view over .
+ /// Gets a read-only view of the items in the .
///
- ///
- /// 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 ItemsSourceView ItemsView => _items;
private protected bool WrapFocus { get; set; }
@@ -262,7 +318,7 @@ namespace Avalonia.Controls
///
public bool AreHorizontalSnapPointsRegular
{
- get => GetValue(AreHorizontalSnapPointsRegularProperty);
+ get => GetValue(AreHorizontalSnapPointsRegularProperty);
set => SetValue(AreHorizontalSnapPointsRegularProperty, value);
}
@@ -271,7 +327,7 @@ namespace Avalonia.Controls
///
public bool AreVerticalSnapPointsRegular
{
- get => GetValue(AreVerticalSnapPointsRegularProperty);
+ get => GetValue(AreVerticalSnapPointsRegularProperty);
set => SetValue(AreVerticalSnapPointsRegularProperty, value);
}
@@ -295,7 +351,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 +375,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;
}
///
@@ -478,19 +534,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)
{
@@ -517,24 +567,27 @@ 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)
+ 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;
}
///
@@ -578,7 +631,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)
{
@@ -609,10 +662,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)
@@ -633,10 +682,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)
@@ -674,10 +719,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..b89a75787f 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()
+ {
+ ((ItemCollection)ItemsView).SourceChanged += OnItemsViewSourceChanged;
+ }
+
///
/// Initializes static members of the class.
///
@@ -229,6 +234,7 @@ namespace Avalonia.Controls.Primitives
/// property
///
[AssignBinding]
+ [InheritDataTypeFromItems(nameof(ItemsSource))]
[InheritDataTypeFromItems(nameof(Items))]
public IBinding? SelectedValueBinding
{
@@ -322,7 +328,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 +440,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 +552,7 @@ namespace Avalonia.Controls.Primitives
if (_selection is object)
{
- _selection.Source = Items;
+ _selection.Source = ItemsView.Source;
}
}
@@ -635,16 +640,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 +875,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 +969,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 +999,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 +1170,7 @@ namespace Avalonia.Controls.Primitives
{
if (_updateState is null)
{
- model.Source = Items;
+ model.Source = ItemsView.Source;
}
model.PropertyChanged += OnSelectionModelPropertyChanged;
@@ -1231,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 = Items;
-
- if (Items is null)
+ 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;
}
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/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;
+ }
}
}
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 7b5aa83b46..83b82eb1d4 100644
--- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
@@ -16,6 +16,46 @@ 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.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()
{
@@ -153,7 +193,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 +246,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,13 +266,47 @@ 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);
}
+ [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()
{
@@ -253,7 +329,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 +375,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 +387,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 +400,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 +419,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 +429,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 573ce5834d..2987813d07 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 eee6e49617..5101886a25 100644
--- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
@@ -1832,7 +1832,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