From 2ff637883de92ec405d8dbbddbee1eb417c36357 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Fri, 30 Jan 2026 19:08:31 +0500 Subject: [PATCH 1/5] Add support IEnumerable data sources --- src/ColumnFilterHandler.cs | 3 +- src/Extensions/CollectionExtensions.cs | 83 ++++- src/Extensions/ObjectExtensions.cs | 6 +- src/ItemsSource/CollectionView.Properties.cs | 5 +- src/ItemsSource/CollectionView.cs | 15 +- src/TableView.Properties.cs | 7 +- src/TableView.cs | 16 +- tests/CollectionExtensionsTests.cs | 363 +++++++++++++++++++ 8 files changed, 476 insertions(+), 22 deletions(-) diff --git a/src/ColumnFilterHandler.cs b/src/ColumnFilterHandler.cs index 8dea03f6..9c6e94cd 100644 --- a/src/ColumnFilterHandler.cs +++ b/src/ColumnFilterHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -40,7 +41,7 @@ public virtual IList GetFilterItems(TableViewColumn column, })); } - collectionView.Source = column.TableView.ItemsSource; + collectionView.Source = (column.TableView.ItemsSource as IEnumerable) ?? Enumerable.Empty(); var items = _tableView.ShowFilterItemsCount ? GetFilterItemsWithCount(column, searchText, collectionView) : diff --git a/src/Extensions/CollectionExtensions.cs b/src/Extensions/CollectionExtensions.cs index 051e924b..8e66ab5e 100644 --- a/src/Extensions/CollectionExtensions.cs +++ b/src/Extensions/CollectionExtensions.cs @@ -1,4 +1,6 @@ -using System; +using Microsoft.UI.Xaml.Data; +using System; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -38,4 +40,83 @@ public static void RemoveWhere(this ICollection collection, Predicate p collection.Remove(item); } } + + /// + /// Gets the index of the first occurrence of an item in the enumerable. + /// + /// The enumerable to search. + /// The item to find. + /// The index of the item, or -1 if not found. + public static int IndexOf(this IEnumerable enumerable, object? item) + { + if (enumerable is ICollection collection) + return collection.IndexOf(item); + + if (enumerable is ICollectionView collectionView) + return collectionView.IndexOf(item); + + + var index = 0; + foreach (var element in enumerable) + { + if (Equals(element, item)) + return index; + + index++; + } + + return -1; + } + + /// + /// Gets a value indicating whether the enumerable is read-only. + /// + /// The enumerable to check. + /// + public static bool IsReadOnly(this IEnumerable enumerable) + { + if (enumerable is IList list) + return list.IsReadOnly; + + if (enumerable is ICollectionView collectionView) + return collectionView.IsReadOnly; + + return true; + } + + public static void Add(this IEnumerable enumerable, object? item) + { + if (enumerable is ICollection collection) + collection.Add(item); + + if (enumerable is ICollectionView collectionView) + collectionView.Add(item); + } + + public static void Insert(this IEnumerable enumerable, int index, object? item) + { + if (enumerable is ICollection collection) + collection.Insert(index, item); + + if (enumerable is ICollectionView collectionView) + collectionView.Insert(index, item); + } + + public static void Remove(this IEnumerable enumerable, object? item) + { + if (enumerable is ICollection collection) + collection.Remove(item); + + if (enumerable is ICollectionView collectionView) + collectionView.Remove(item); + } + + public static void Clear(this IEnumerable enumerable) + { + if (enumerable is ICollection collection) + collection.Clear(); + + if (enumerable is ICollectionView collectionView) + collectionView.Clear(); + } } diff --git a/src/Extensions/ObjectExtensions.cs b/src/Extensions/ObjectExtensions.cs index 72857e47..1cd519b8 100644 --- a/src/Extensions/ObjectExtensions.cs +++ b/src/Extensions/ObjectExtensions.cs @@ -400,6 +400,7 @@ private static bool TryCreateGenericIndexerExpression(Type type, ParameterExpres var listType = list.GetType(); Type? itemType = null; var isICustomTypeProvider = false; + var isWinRTObject = false; // If it's a generic enumerable, get the generic type. @@ -410,11 +411,14 @@ private static bool TryCreateGenericIndexerExpression(Type type, ParameterExpres if (itemType != null) { isICustomTypeProvider = typeof(ICustomTypeProvider).IsAssignableFrom(itemType); +#if WINDOWS + isWinRTObject = typeof(WinRT.IInspectable).IsAssignableFrom(itemType); +#endif } // Bare IEnumerables mean that result type will be object. In that case, try to get something more interesting. // Or, if the itemType implements ICustomTypeProvider, try to retrieve the custom type from one of the object instances. - if (itemType == null || itemType == typeof(object) || isICustomTypeProvider) + if (itemType == null || itemType == typeof(object) || isICustomTypeProvider || isWinRTObject) { // No type was located yet. Does the list have anything in it? Type? firstItemType = null; diff --git a/src/ItemsSource/CollectionView.Properties.cs b/src/ItemsSource/CollectionView.Properties.cs index 799cc76a..1b6160a2 100644 --- a/src/ItemsSource/CollectionView.Properties.cs +++ b/src/ItemsSource/CollectionView.Properties.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Windows.Foundation.Collections; +using WinUI.TableView.Extensions; namespace WinUI.TableView; @@ -11,7 +12,7 @@ partial class CollectionView /// /// Gets or sets the source collection. /// - public IList Source + public IEnumerable Source { get => _source; set @@ -103,7 +104,7 @@ public object? CurrentItem /// /// Gets a value indicating whether the collection is read-only. /// - public bool IsReadOnly => _source == null || _source.IsReadOnly; + public bool IsReadOnly => _source == null || _source.IsReadOnly(); /// /// Gets or sets a value indicating whether live shaping is enabled. diff --git a/src/ItemsSource/CollectionView.cs b/src/ItemsSource/CollectionView.cs index 3d97def2..6a2ac95e 100644 --- a/src/ItemsSource/CollectionView.cs +++ b/src/ItemsSource/CollectionView.cs @@ -8,6 +8,7 @@ using System.Linq; using Windows.Foundation; using Windows.Foundation.Collections; +using WinUI.TableView.Extensions; using WinUI.TableView.Helpers; namespace WinUI.TableView; @@ -17,7 +18,7 @@ namespace WinUI.TableView; /// internal partial class CollectionView : ICollectionView, ISupportIncrementalLoading, INotifyPropertyChanged, IComparer { - private IList _source = default!; + private IEnumerable _source = new List(); private bool _allowLiveShaping; private readonly List _view = []; private readonly ObservableCollection _filterDescriptions = []; @@ -29,7 +30,7 @@ internal partial class CollectionView : ICollectionView, ISupportIncrementalLoad /// /// The source collection. /// Indicates whether live shaping is enabled. - public CollectionView(IList? source = null, bool liveShapingEnabled = true) + public CollectionView(IEnumerable? source = null, bool liveShapingEnabled = true) { _filterDescriptions.CollectionChanged += OnFilterDescriptionsCollectionChanged; _sortDescriptions.CollectionChanged += OnSortDescriptionsCollectionChanged; @@ -242,7 +243,7 @@ private void HandleSourceChanged() /// private void HandleFilterChanged() { - if (FilterDescriptions.Any()) + if (FilterDescriptions.Count > 0) { for (var index = 0; index < _view.Count; index++) { @@ -259,19 +260,21 @@ private void HandleFilterChanged() var viewHash = new HashSet(_view); var viewIndex = 0; - for (var index = 0; index < _source.Count; index++) + var i = 0; + foreach (var item in _source) { - var item = _source[index]!; if (viewHash.Contains(item)) { viewIndex++; continue; } - if (HandleItemAdded(index, item, viewIndex)) + if (HandleItemAdded(i, item, viewIndex)) { viewIndex++; } + + i++; } } diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs index 2f98d607..6dbf992d 100644 --- a/src/TableView.Properties.cs +++ b/src/TableView.Properties.cs @@ -4,7 +4,6 @@ using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Media; using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -19,7 +18,7 @@ public partial class TableView /// /// Identifies the ItemsSource dependency property. /// - public static readonly new DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(IList), typeof(TableView), new PropertyMetadata(null, OnItemsSourceChanged)); + public static readonly new DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(object), typeof(TableView), new PropertyMetadata(null, OnItemsSourceChanged)); /// /// Identifies the SelectionMode dependency property. @@ -437,9 +436,9 @@ public double RowMinHeight /// /// Gets or sets an object source used to generate the content of the TableView. /// - public new IList? ItemsSource + public new object? ItemsSource { - get => (IList?)GetValue(ItemsSourceProperty); + get => GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } diff --git a/src/TableView.cs b/src/TableView.cs index 66f66910..a07cd7ea 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -573,7 +573,9 @@ private string GetHeadersContent(char separator, int minColumn, int maxColumn) /// private void GenerateColumns() { - var dataType = ItemsSource?.GetItemType(); + if (ItemsSource is not IEnumerable source) return; + + var dataType = source?.GetItemType(); if (dataType is null || dataType.IsPrimitive()) { var columnArgs = GenerateColumn(dataType, null, "", dataType?.IsInheritedFromIComparable() is true); @@ -666,15 +668,15 @@ private static TableViewBoundColumn GetTableViewColumnFromType(string? propertyN private void ItemsSourceChanged(DependencyPropertyChangedEventArgs e) { using var defer = _collectionView.DeferRefresh(); - _collectionView.Source = null!; + _collectionView.Source = null!; - if (e.NewValue is IList source) - { - EnsureAutoColumns(); + if (e.NewValue is IEnumerable source) + { + EnsureAutoColumns(); - _collectionView.Source = source; - } + _collectionView.Source = source; } + } /// /// Ensures that columns are automatically generated based on the current state of the control. diff --git a/tests/CollectionExtensionsTests.cs b/tests/CollectionExtensionsTests.cs index 6147ff67..74a8d349 100644 --- a/tests/CollectionExtensionsTests.cs +++ b/tests/CollectionExtensionsTests.cs @@ -1,6 +1,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using WinUI.TableView.Extensions; namespace WinUI.TableView.Tests; @@ -57,4 +59,365 @@ public void RemoveWhere_EmptyCollection_DoesNothing() list.RemoveWhere(x => true); CollectionAssert.AreEqual(new int[0], list); } + + #region IndexOf Tests + + [TestMethod] + public void IndexOf_IList_FindsItemAtCorrectIndex() + { + var list = new List { "a", "b", "c" }; + Assert.AreEqual(1, list.IndexOf("b")); + } + + [TestMethod] + public void IndexOf_IList_ReturnsNegativeOneWhenNotFound() + { + var list = new List { "a", "b", "c" }; + Assert.AreEqual(-1, list.IndexOf("d")); + } + + [TestMethod] + public void IndexOf_IList_FindsNullItem() + { + var list = new List { "a", null, "c" }; + Assert.AreEqual(1, list.IndexOf(null)); + } + + [TestMethod] + public void IndexOf_IList_ReturnsFirstOccurrence() + { + var list = new List { 1, 2, 3, 2, 4 }; + Assert.AreEqual(1, list.IndexOf(2)); + } + + [TestMethod] + public void IndexOf_IList_EmptyList_ReturnsNegativeOne() + { + var list = new List(); + Assert.AreEqual(-1, list.IndexOf("a")); + } + + [TestMethod] + public void IndexOf_ArrayList_FindsItemAtCorrectIndex() + { + var list = new ArrayList { "a", "b", "c" }; + Assert.AreEqual(1, ((IEnumerable)list).IndexOf("b")); + } + + [TestMethod] + public void IndexOf_GenericEnumerable_FindsItemAtCorrectIndex() + { + var enumerable = Enumerable.Range(0, 5).Select(x => x * 2); + Assert.AreEqual(2, enumerable.IndexOf(4)); + } + + [TestMethod] + public void IndexOf_GenericEnumerable_ReturnsNegativeOneWhenNotFound() + { + var enumerable = Enumerable.Range(0, 5).Select(x => x * 2); + Assert.AreEqual(-1, enumerable.IndexOf(5)); + } + + [TestMethod] + public void IndexOf_GenericEnumerable_EmptyEnumerable_ReturnsNegativeOne() + { + var enumerable = Enumerable.Empty(); + Assert.AreEqual(-1, enumerable.IndexOf(1)); + } + + [TestMethod] + public void IndexOf_GenericEnumerable_WithNull_FindsNull() + { + var enumerable = new[] { "a", null, "c" }.AsEnumerable(); + Assert.AreEqual(1, enumerable.IndexOf(null)); + } + + #endregion + + #region IsReadOnly Tests + + [TestMethod] + public void IsReadOnly_MutableList_ReturnsFalse() + { + var list = new List { 1, 2, 3 }; + Assert.IsFalse(list.IsReadOnly()); + } + + [TestMethod] + public void IsReadOnly_ReadOnlyCollection_ReturnsTrue() + { + var list = new List { 1, 2, 3 }; + var readOnly = new ReadOnlyCollection(list); + Assert.IsTrue(readOnly.IsReadOnly()); + } + + [TestMethod] + public void IsReadOnly_Array_ReturnsFalse() + { + var array = new[] { 1, 2, 3 }; + Assert.IsFalse(array.IsReadOnly()); + } + + [TestMethod] + public void IsReadOnly_ArrayList_ReturnsFalse() + { + var list = new ArrayList { 1, 2, 3 }; + Assert.IsFalse(list.IsReadOnly()); + } + + [TestMethod] + public void IsReadOnly_GenericEnumerable_ReturnsTrue() + { + var enumerable = Enumerable.Range(1, 3); + Assert.IsTrue(enumerable.IsReadOnly()); + } + + [TestMethod] + public void IsReadOnly_Collection_ReturnsFalse() + { + var collection = new Collection { "a", "b", "c" }; + Assert.IsFalse(collection.IsReadOnly()); + } + + #endregion + + #region Add Tests + + [TestMethod] + public void Add_IList_AddsItem() + { + var list = new List { "a", "b" }; + ((IEnumerable)list).Add("c"); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); + } + + [TestMethod] + public void Add_IList_AddsNullItem() + { + var list = new List { "a", "b" }; + ((IEnumerable)list).Add(null); + CollectionAssert.AreEqual(new[] { "a", "b", null }, list); + } + + [TestMethod] + public void Add_ArrayList_AddsItem() + { + var list = new ArrayList { "a", "b" }; + ((IEnumerable)list).Add("c"); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); + } + + [TestMethod] + public void Add_EmptyList_AddsItem() + { + var list = new List(); + ((IEnumerable)list).Add(1); + CollectionAssert.AreEqual(new[] { 1 }, list); + } + + [TestMethod] + public void Add_Collection_AddsItem() + { + var collection = new Collection { 1, 2 }; + ((IEnumerable)collection).Add(3); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, collection); + } + + [TestMethod] + public void Add_GenericEnumerable_DoesNothing() + { + // This tests that calling Add on a non-IList/ICollectionView enumerable doesn't throw + var enumerable = Enumerable.Range(1, 3); + enumerable.Add(4); // Should not throw + } + + #endregion + + #region Insert Tests + + [TestMethod] + public void Insert_IList_InsertsItemAtIndex() + { + var list = new List { "a", "c" }; + ((IEnumerable)list).Insert(1, "b"); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); + } + + [TestMethod] + public void Insert_IList_InsertsAtBeginning() + { + var list = new List { 2, 3 }; + ((IEnumerable)list).Insert(0, 1); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, list); + } + + [TestMethod] + public void Insert_IList_InsertsAtEnd() + { + var list = new List { 1, 2 }; + ((IEnumerable)list).Insert(2, 3); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, list); + } + + [TestMethod] + public void Insert_IList_InsertsNull() + { + var list = new List { "a", "b" }; + ((IEnumerable)list).Insert(1, null); + CollectionAssert.AreEqual(new[] { "a", null, "b" }, list); + } + + [TestMethod] + public void Insert_ArrayList_InsertsItemAtIndex() + { + var list = new ArrayList { "a", "c" }; + ((IEnumerable)list).Insert(1, "b"); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); + } + + [TestMethod] + public void Insert_EmptyList_InsertsAtZero() + { + var list = new List(); + ((IEnumerable)list).Insert(0, 1); + CollectionAssert.AreEqual(new[] { 1 }, list); + } + + [TestMethod] + public void Insert_Collection_InsertsItemAtIndex() + { + var collection = new Collection { 1, 3 }; + ((IEnumerable)collection).Insert(1, 2); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, collection); + } + + [TestMethod] + public void Insert_GenericEnumerable_DoesNothing() + { + // This tests that calling Insert on a non-IList/ICollectionView enumerable doesn't throw + var enumerable = Enumerable.Range(1, 3); + enumerable.Insert(0, 4); // Should not throw + } + + #endregion + + #region Remove Tests + + [TestMethod] + public void Remove_IList_RemovesItem() + { + var list = new List { "a", "b", "c" }; + ((IEnumerable)list).Remove("b"); + CollectionAssert.AreEqual(new[] { "a", "c" }, list); + } + + [TestMethod] + public void Remove_IList_RemovesFirstOccurrence() + { + var list = new List { 1, 2, 3, 2, 4 }; + ((IEnumerable)list).Remove(2); + CollectionAssert.AreEqual(new[] { 1, 3, 2, 4 }, list); + } + + [TestMethod] + public void Remove_IList_RemovesNull() + { + var list = new List { "a", null, "c" }; + ((IEnumerable)list).Remove(null); + CollectionAssert.AreEqual(new[] { "a", "c" }, list); + } + + [TestMethod] + public void Remove_IList_NonExistentItem_DoesNothing() + { + var list = new List { "a", "b", "c" }; + ((IEnumerable)list).Remove("d"); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); + } + + [TestMethod] + public void Remove_ArrayList_RemovesItem() + { + var list = new ArrayList { "a", "b", "c" }; + ((IEnumerable)list).Remove("b"); + CollectionAssert.AreEqual(new[] { "a", "c" }, list); + } + + [TestMethod] + public void Remove_Collection_RemovesItem() + { + var collection = new Collection { 1, 2, 3 }; + ((IEnumerable)collection).Remove(2); + CollectionAssert.AreEqual(new[] { 1, 3 }, collection); + } + + [TestMethod] + public void Remove_EmptyList_DoesNothing() + { + var list = new List(); + ((IEnumerable)list).Remove(1); + CollectionAssert.AreEqual(new int[0], list); + } + + [TestMethod] + public void Remove_GenericEnumerable_DoesNothing() + { + // This tests that calling Remove on a non-IList/ICollectionView enumerable doesn't throw + var enumerable = Enumerable.Range(1, 3); + enumerable.Remove(2); // Should not throw + } + + #endregion + + #region Clear Tests + + [TestMethod] + public void Clear_IList_RemovesAllItems() + { + var list = new List { 1, 2, 3, 4, 5 }; + ((IEnumerable)list).Clear(); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public void Clear_ArrayList_RemovesAllItems() + { + var list = new ArrayList { "a", "b", "c" }; + ((IEnumerable)list).Clear(); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public void Clear_Collection_RemovesAllItems() + { + var collection = new Collection { "a", "b", "c" }; + ((IEnumerable)collection).Clear(); + Assert.AreEqual(0, collection.Count); + } + + [TestMethod] + public void Clear_EmptyList_DoesNothing() + { + var list = new List(); + ((IEnumerable)list).Clear(); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public void Clear_ListWithNulls_RemovesAllItems() + { + var list = new List { "a", null, "c", null }; + ((IEnumerable)list).Clear(); + Assert.AreEqual(0, list.Count); + } + + [TestMethod] + public void Clear_GenericEnumerable_DoesNothing() + { + // This tests that calling Clear on a non-IList/ICollectionView enumerable doesn't throw + var enumerable = Enumerable.Range(1, 3); + enumerable.Clear(); // Should not throw + } + + #endregion } From d6bb2ff2724936d5c9fd558cfdb3483d6e92e1bd Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Sat, 31 Jan 2026 15:47:03 +0500 Subject: [PATCH 2/5] add ICollectionView changes handling --- src/ItemsSource/CollectionView.Properties.cs | 13 +-- src/ItemsSource/CollectionView.cs | 105 ++++++++++++++++++- 2 files changed, 108 insertions(+), 10 deletions(-) diff --git a/src/ItemsSource/CollectionView.Properties.cs b/src/ItemsSource/CollectionView.Properties.cs index 1b6160a2..6c022501 100644 --- a/src/ItemsSource/CollectionView.Properties.cs +++ b/src/ItemsSource/CollectionView.Properties.cs @@ -1,7 +1,6 @@ using Microsoft.UI.Xaml.Data; using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; using Windows.Foundation.Collections; using WinUI.TableView.Extensions; @@ -19,17 +18,15 @@ public IEnumerable Source { if (_source == value) return; - if (_source is not null) DetachPropertyChangedHandlers(_source); + DetachCollectionChangedHandlers(_source); + DetachPropertyChangedHandlers(_source); _source = value; - AttachPropertyChangedHandlers(_source); - _collectionChangedListener?.Detach(); + AttachCollectionChangedHandlers(_source); + AttachPropertyChangedHandlers(_source); - if (_source is INotifyCollectionChanged sourceNcc) - { - _collectionChangedListener = new(this, sourceNcc, OnSourceCollectionChanged); - } + CreateItemsCopy(_source); HandleSourceChanged(); OnPropertyChanged(); diff --git a/src/ItemsSource/CollectionView.cs b/src/ItemsSource/CollectionView.cs index 6a2ac95e..18e42342 100644 --- a/src/ItemsSource/CollectionView.cs +++ b/src/ItemsSource/CollectionView.cs @@ -9,7 +9,6 @@ using Windows.Foundation; using Windows.Foundation.Collections; using WinUI.TableView.Extensions; -using WinUI.TableView.Helpers; namespace WinUI.TableView; @@ -19,11 +18,11 @@ namespace WinUI.TableView; internal partial class CollectionView : ICollectionView, ISupportIncrementalLoading, INotifyPropertyChanged, IComparer { private IEnumerable _source = new List(); + private object[] _itemsCopy = []; // In case the source is ICollection, keep a copy of the items to keep track of removed items. private bool _allowLiveShaping; private readonly List _view = []; private readonly ObservableCollection _filterDescriptions = []; private readonly ObservableCollection _sortDescriptions = []; - private CollectionChangedListener? _collectionChangedListener; /// /// Initializes a new instance of the class. @@ -65,6 +64,36 @@ private void OnSortDescriptionsCollectionChanged(object? sender, NotifyCollectio HandleSortChanged(); } + /// + /// Attaches collection changed handlers to the source collection. + /// + private void AttachCollectionChangedHandlers(IEnumerable source) + { + if (source is INotifyCollectionChanged sourceNcc) + { + sourceNcc.CollectionChanged += OnSourceCollectionChanged; + } + else if (source is ICollectionView sourceCV) + { + sourceCV.VectorChanged += OnSourceVectorChanged; + } + } + + /// + /// Detaches collection changed handlers from the source collection. + /// + private void DetachCollectionChangedHandlers(IEnumerable source) + { + if (source is INotifyCollectionChanged sourceNcc) + { + sourceNcc.CollectionChanged -= OnSourceCollectionChanged; + } + else if (source is ICollectionView sourceCV) + { + sourceCV.VectorChanged -= OnSourceVectorChanged; + } + } + /// /// Attaches property changed handlers to the items in the collection. /// @@ -93,6 +122,75 @@ private void DetachPropertyChangedHandlers(IEnumerable? items) } } + /// + /// Handles changes to the source vector. + /// + private void OnSourceVectorChanged(IObservableVector sender, IVectorChangedEventArgs args) + { + var index = (int)args.Index; + + switch (args.CollectionChange) + { + case CollectionChange.ItemInserted: + if (_deferCounter <= 0) + { + if (index < Count) + { + var item = sender[index]; + AttachPropertyChangedHandlers(new object[] { item }); + HandleItemAdded(index, item); + } + else + { + HandleSourceChanged(); + } + } + + break; + case CollectionChange.ItemRemoved: + if (_deferCounter <= 0) + { + if (index < _itemsCopy.Length) + { + var item = _itemsCopy[index]; + DetachPropertyChangedHandlers(new object[] { item }); + HandleItemRemoved(index, item); + } + else + { + HandleSourceChanged(); + } + } + + break; + case CollectionChange.ItemChanged: + case CollectionChange.Reset: + if (_deferCounter <= 0) + { + HandleSourceChanged(); + } + + DetachPropertyChangedHandlers(_itemsCopy); + AttachPropertyChangedHandlers(_source); + + break; + } + + CreateItemsCopy(_source); + } + + /// + /// Creates a copy of the items from the collection if it implements ICollectionView. + /// + private void CreateItemsCopy(IEnumerable source) + { + if (source is ICollectionView collectionView) + { + _itemsCopy = new object[collectionView.Count]; + collectionView.CopyTo(_itemsCopy, 0); + } + } + /// /// Handles changes to the source collection. /// @@ -138,6 +236,9 @@ private void OnSourceCollectionChanged(object? arg1, NotifyCollectionChangedEven HandleSourceChanged(); } + DetachPropertyChangedHandlers(e.OldItems); + AttachPropertyChangedHandlers(_source); + break; } } From 67eed6d59217f4b9fb655ecedab7379a3400d14d Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Sat, 31 Jan 2026 23:37:00 +0500 Subject: [PATCH 3/5] Update VSTest setup action version --- .github/workflows/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index a3881736..b70fbf87 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -36,7 +36,7 @@ jobs: /p:Version=0.0.${{ github.run_number }}-dev - name: Setup VSTest - uses: darenm/Setup-VSTest@v1 + uses: darenm/Setup-VSTest@v1.3 - name: Build Tests run: | From 9a46adc101aa0da56b22185720a0b5eab6f56cf0 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Tue, 3 Feb 2026 17:26:28 +0500 Subject: [PATCH 4/5] Update CollectionExtensions to use IList instead of ICollection --- src/Extensions/CollectionExtensions.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Extensions/CollectionExtensions.cs b/src/Extensions/CollectionExtensions.cs index 8e66ab5e..890fa2e0 100644 --- a/src/Extensions/CollectionExtensions.cs +++ b/src/Extensions/CollectionExtensions.cs @@ -49,8 +49,8 @@ public static void RemoveWhere(this ICollection collection, Predicate p /// The index of the item, or -1 if not found. public static int IndexOf(this IEnumerable enumerable, object? item) { - if (enumerable is ICollection collection) - return collection.IndexOf(item); + if (enumerable is IList list) + return list.IndexOf(item); if (enumerable is ICollectionView collectionView) return collectionView.IndexOf(item); @@ -86,8 +86,8 @@ public static bool IsReadOnly(this IEnumerable enumerable) public static void Add(this IEnumerable enumerable, object? item) { - if (enumerable is ICollection collection) - collection.Add(item); + if (enumerable is IList list) + list.Add(item); if (enumerable is ICollectionView collectionView) collectionView.Add(item); @@ -95,8 +95,8 @@ public static void Add(this IEnumerable enumerable, object? item) public static void Insert(this IEnumerable enumerable, int index, object? item) { - if (enumerable is ICollection collection) - collection.Insert(index, item); + if (enumerable is IList list) + list.Insert(index, item); if (enumerable is ICollectionView collectionView) collectionView.Insert(index, item); @@ -104,8 +104,8 @@ public static void Insert(this IEnumerable enumerable, int index, object? item) public static void Remove(this IEnumerable enumerable, object? item) { - if (enumerable is ICollection collection) - collection.Remove(item); + if (enumerable is IList list) + list.Remove(item); if (enumerable is ICollectionView collectionView) collectionView.Remove(item); @@ -113,8 +113,8 @@ public static void Remove(this IEnumerable enumerable, object? item) public static void Clear(this IEnumerable enumerable) { - if (enumerable is ICollection collection) - collection.Clear(); + if (enumerable is IList list) + list.Clear(); if (enumerable is ICollectionView collectionView) collectionView.Clear(); From 2320d082b691d79f7bfdb5101ed8af69e0670138 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Tue, 3 Feb 2026 17:38:03 +0500 Subject: [PATCH 5/5] Add unit tests for ICollectionView --- tests/CollectionExtensionsTests.cs | 149 +++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/tests/CollectionExtensionsTests.cs b/tests/CollectionExtensionsTests.cs index 74a8d349..799529d4 100644 --- a/tests/CollectionExtensionsTests.cs +++ b/tests/CollectionExtensionsTests.cs @@ -1,4 +1,6 @@ +using Microsoft.UI.Xaml.Data; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -97,6 +99,46 @@ public void IndexOf_IList_EmptyList_ReturnsNegativeOne() Assert.AreEqual(-1, list.IndexOf("a")); } + [UITestMethod] + public void IndexOf_ICollectionView_FindsItemAtCorrectIndex() + { + var list = new List { "a", "b", "c" }; + var collectionView = new CollectionViewSource { Source = list }.View; + Assert.AreEqual(1, collectionView.IndexOf("b")); + } + + [UITestMethod] + public void IndexOf_ICollectionView_ReturnsNegativeOneWhenNotFound() + { + var list = new List { "a", "b", "c" }; + var collectionView = new CollectionViewSource { Source = list }.View; + Assert.AreEqual(-1, collectionView.IndexOf("d")); + } + + [UITestMethod] + public void IndexOf_ICollectionView_FindsNullItem() + { + var list = new List { "a", null, "c" }; + var collectionView = new CollectionViewSource { Source = list }.View; + Assert.AreEqual(1, collectionView.IndexOf(null)); + } + + [UITestMethod] + public void IndexOf_ICollectionView_ReturnsFirstOccurrence() + { + var list = new List { 1, 2, 3, 2, 4 }; + var collectionView = new CollectionViewSource { Source = list }.View; + Assert.AreEqual(1, collectionView.IndexOf(2)); + } + + [UITestMethod] + public void IndexOf_ICollectionView_EmptyList_ReturnsNegativeOne() + { + var list = new List(); + var collectionView = new CollectionViewSource { Source = list }.View; + Assert.AreEqual(-1, collectionView.IndexOf("a")); + } + [TestMethod] public void IndexOf_ArrayList_FindsItemAtCorrectIndex() { @@ -151,6 +193,14 @@ public void IsReadOnly_ReadOnlyCollection_ReturnsTrue() Assert.IsTrue(readOnly.IsReadOnly()); } + [UITestMethod] + public void IsReadOnly_ICollectionView_ReturnsFalse() + { + var list = new List { 1, 2, 3 }; + var collectionView = new CollectionViewSource { Source = list }.View; + Assert.IsFalse(collectionView.IsReadOnly()); + } + [TestMethod] public void IsReadOnly_Array_ReturnsFalse() { @@ -199,6 +249,24 @@ public void Add_IList_AddsNullItem() CollectionAssert.AreEqual(new[] { "a", "b", null }, list); } + [UITestMethod] + public void Add_ICollectionView_AddsItem() + { + var list = new List { "a", "b" }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Add("c"); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); + } + + [UITestMethod] + public void Add_ICollectionView_AddsNullItem() + { + var list = new List { "a", "b" }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Add(null); + CollectionAssert.AreEqual(new[] { "a", "b", null }, list); + } + [TestMethod] public void Add_ArrayList_AddsItem() { @@ -267,6 +335,42 @@ public void Insert_IList_InsertsNull() CollectionAssert.AreEqual(new[] { "a", null, "b" }, list); } + [UITestMethod] + public void Insert_ICollectionView_InsertsItemAtIndex() + { + var list = new List { "a", "c" }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Insert(1, "b"); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); + } + + [UITestMethod] + public void Insert_ICollectionView_InsertsAtBeginning() + { + var list = new List { 2, 3 }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Insert(0, 1); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, list); + } + + [UITestMethod] + public void Insert_ICollectionView_InsertsAtEnd() + { + var list = new List { 1, 2 }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Insert(2, 3); + CollectionAssert.AreEqual(new[] { 1, 2, 3 }, list); + } + + [UITestMethod] + public void Insert_ICollectionView_InsertsNull() + { + var list = new List { "a", "b" }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Insert(1, null); + CollectionAssert.AreEqual(new[] { "a", null, "b" }, list); + } + [TestMethod] public void Insert_ArrayList_InsertsItemAtIndex() { @@ -335,6 +439,42 @@ public void Remove_IList_NonExistentItem_DoesNothing() CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); } + [UITestMethod] + public void Remove_ICollectionView_RemovesItem() + { + var list = new List { "a", "b", "c" }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Remove("b"); + CollectionAssert.AreEqual(new[] { "a", "c" }, list); + } + + [UITestMethod] + public void Remove_ICollectionView_RemovesFirstOccurrence() + { + var list = new List { 1, 2, 3, 2, 4 }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Remove(2); + CollectionAssert.AreEqual(new[] { 1, 3, 2, 4 }, list); + } + + [UITestMethod] + public void Remove_ICollectionView_RemovesNull() + { + var list = new List { "a", null, "c" }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Remove(null); + CollectionAssert.AreEqual(new[] { "a", "c" }, list); + } + + [UITestMethod] + public void Remove_ICollectionView_NonExistentItem_DoesNothing() + { + var list = new List { "a", "b", "c" }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Remove("d"); + CollectionAssert.AreEqual(new[] { "a", "b", "c" }, list); + } + [TestMethod] public void Remove_ArrayList_RemovesItem() { @@ -379,6 +519,15 @@ public void Clear_IList_RemovesAllItems() Assert.AreEqual(0, list.Count); } + [UITestMethod] + public void Clear_ICollectionView_RemovesAllItems() + { + var list = new List { 1, 2, 3, 4, 5 }; + var collectionView = new CollectionViewSource { Source = list }.View; + collectionView.Clear(); + Assert.AreEqual(0, list.Count); + } + [TestMethod] public void Clear_ArrayList_RemovesAllItems() {