CommunityToolkit / CommunityToolkit/Windows
`AdvancedCollectionView` made much too optimistic assumptions about generic parameters of collections.
- Dominant language
- C#
- Stars
- 1.1k
- Forks
- 166
- PR merge metrics
- No merged PRs in 30d
Description
### Describe the bug
In 72bf0b1a006685ccb3595621972cd27b75bfddee, `AdvancedCollectionView` starts to use an new way to retrieve item type. The assumption implied is too optimistic when a non-trival collection is used. For example, an custom `ObservableMap`, silimar to [IObservableMap interface](https://learn.microsoft.com/en-us/uwp/api/windows.foundation.collections.iobservablemap-2?view=winrt-22621) from WPF.
### Steps to reproduce
Here is a possible implementation of `ObservableMap` of mine, which may still contains bugs.
```cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Turbulent.Contracts.Generic;
using Turbulent.Helpers;
namespace Turbulent.Collections;
public sealed unsafe class ObservableMap : IDictionary, IReadOnlyDictionary, IList, IReadOnlyList, IList, INotifyPropertyChanged, INotifyCollectionChanged, IDisposable
{
private readonly IEqualityComparer? _comparer;
private int* _buckets;
private Entry[]? _entries;
private uint _size;
private int _count;
#if TARGET_64BIT
private ulong _fastModMultiplier;
#endif
private int* _freeList;
private uint _freeSize;
private int _freeCount;
private int _version;
public ObservableMap() : this(0, null) { }
public ObservableMap(int capacity = 0, IEqualityComparer? comparer = null)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity, nameof(capacity));
if (!typeof(TKey).IsValueType)
{
_comparer = comparer ?? EqualityComparer.Default;
}
else if (comparer is not null && comparer != EqualityComparer.Default)
{
_comparer = comparer;
}
if (capacity > 0)
{
Initialize(capacity);
}
}
~ObservableMap() => Dispose(false);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose(bool disposing)
{
NativeMemory.Free(_buckets);
_buckets = null;
}
private struct Entry
{
public TKey Key;
public TValue Value;
public int HashCode;
public int Next;
}
private Span Buckets => new(_buckets, (int)_size);
private int Initialize(int capacity)
{
int size = HashHelpers.GetPrime(capacity);
_entries = new Entry[size];
_size = (uint)size;
_buckets = (int*)NativeMemory.AllocZeroed(SimdHelpers.GetNextAligned(_size) * sizeof(int));
#if TARGET_64BIT
_fastModMultiplier = HashHelpers.GetFastModMultiplier(_length);
#endif
return size;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref int GetBucketRef(int hashCode)
{
#if TARGET_64BIT
return ref _buckets[HashHelpers.FastMod((uint)hashCode, _length, _fastModMultiplier)];
#else
return ref _buckets[(uint)hashCode % _size];
#endif
}
private void Resize() => Resize(HashHelpers.ExpandPrime(_count));
private void Resize(int size)
{
Debug.Assert(_entries != null, "_entries should be non-null");
Debug.Assert(size >= _entries.Length);
var entries = new Entry[size];
int count = _count;
Array.Copy(_entries, entries, count);
_size = (uint)size;
NativeMemory.Free(_buckets);
_buckets = (int*)NativeMemory.AllocZeroed(SimdHelpers.GetNextAligned(_size) * sizeof(int));
#if TARGET_64BIT
_fastModMultiplier = HashHelpers.GetFastModMultiplier(_length);
#endif
for (int i = 0; i < count; i++)
{
ref Entry entry = ref entries[i];
if (entry.Next >= -1)
{
ref int bucket = ref GetBucketRef(entry.HashCode);
entry.Next = bucket - 1; // Value in _buckets is 1-based
bucket = i + 1;
}
}
_entries = entries;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int ToExternalIndex(int i)
{
if (i < 0 || _freeCount == 0) return i;
int l = 0, r = _freeCount - 1, m, mid;
while (l <= r)
{
m = (l + r) >> 1;
mid = _freeList[m];
Debug.Assert(mid != i);
if (mid <= i) l = m + 1;
else r = m - 1;
}
return i - l;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int ToInternalIndex(int index)
{
if (index < 0 || _freeCount == 0) return index;
int l = 0, r = _freeCount - 1, m, mid;
while (l <= r)
{
m = (l + r) >> 1;
mid = _freeList[m];
if (mid <= index + m) l = m + 1;
else r = m - 1;
}
return index + l;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void InsertFreeIndex(int i)
{
if (i == _count - 1)
{
_count--;
while (_freeCount > 0 && _freeList[_freeCount - 1] == _count - 1)
{
_freeCount--;
_count--;
}
return;
}
if (_freeCount == _freeSize)
{
_freeSize = Math.Min(4, 2 * _freeSize);
_freeList = _freeList == null
? (int*)NativeMemory.Alloc(_freeSize * sizeof(int))
: (int*)NativeMemory.Realloc(_freeList, _freeSize * sizeof(int));
}
int l = 0, r = _freeCount - 1, m, mid;
while (l <= r)
{
m = (l + r) >> 1;
mid = _freeList[m];
Debug.Assert(mid != i);
if (mid <= i) l = m + 1;
else r = m - 1;
}
if (_freeCount == l)
{
_freeList[_freeCount] = i;
}
else
{
NativeMemory.Copy(_freeList + l, _freeList + l + 1, (uint)(_freeCount - l) * sizeof(int));
_freeList[l] = i;
}
_freeCount++;
}
private int Find(TKey key, out TValue? value, bool external = false)
{
if (_buckets != null)
{
Entry[]? entries = _entries;
Debug.Assert(entries is not null, "Expected _entries to be non-null");
uint collisionCount = 0;
IEqualityComparer? comparer = _comparer;
int hashCode;
if (typeof(TKey).IsValueType && comparer == null)
{
comparer = EqualityComparer.Default;
hashCode = key!.GetHashCode();
}
else
{
Debug.Assert(comparer is not null);
hashCode = key is null ? 0 : comparer!.GetHashCode(key);
}
int i = GetBucketRef(hashCode) - 1; // Value in _buckets is 1-based
while (i >= 0)
{
ref Entry entry = ref entries[i];
if (entry.HashCode == hashCode && comparer.Equals(entry.Key, key))
{
value = entry.Value;
return external ? ToExternalIndex(i) : i;
}
i = entry.Next;
collisionCount++;
if (collisionCount > (uint)entries.Length)
{
throw new InvalidOperationException();
}
}
}
value = default;
return -1;
}
private int FindValue(TValue value)
{
if (_buckets != null)
{
for (int i = 0; i < _count; i++)
{
ref Entry entry = ref _entries![i];
if (entry.Next >= 0 && EqualityComparer.Default.Equals(value, entry.Value))
{
return ToExternalIndex(i);
}
}
}
return -1;
}
private bool TryAdd(TKey key, TValue value, InsertionBehavior behavior, out int index, out TValue? old)
{
old = default;
if (_buckets == null)
{
Initialize(0);
}
Debug.Assert(_buckets is not null);
Entry[]? entries = _entries;
Debug.Assert(entries is not null, "Expected _entries to be non-null");
IEqualityComparer? comparer = _comparer;
int hashCode;
if (typeof(TKey).IsValueType && comparer == null)
{
comparer = EqualityComparer.Default;
hashCode = value!.GetHashCode();
}
else
{
Debug.Assert(comparer is not null);
hashCode = key is null ? 0 : comparer!.GetHashCode(key);
}
uint collisionCount = 0;
ref int bucket = ref GetBucketRef(hashCode);
int i = bucket - 1; // Value in _buckets is 1-based
while (i >= 0)
{
ref Entry entry = ref entries[i];
if (entry.HashCode == hashCode && comparer.Equals(entry.Key, key))
{
index = ToExternalIndex(i);
if (ReferenceEquals(entry.Value, value)) return false;
switch (behavior)
{
case InsertionBehavior.ThrowOnExisting:
throw new ArgumentException(string.Empty, nameof(key));
case InsertionBehavior.OverwriteExisting:
old = entry.Value;
entry.Value = value;
return true;
default:
return false;
}
}
i = entry.Next;
collisionCount++;
if (collisionCount > (uint)entries.Length)
{
throw new InvalidOperationException();
}
}
if (_freeCount > 0)
{
i = _freeList[--_freeCount];
}
else
{
int count = _count;
if (count == entries.Length)
{
Resize();
bucket = ref GetBucketRef(hashCode);
}
i = count;
_count = count + 1;
entries = _entries;
}
{
ref Entry entry = ref entries![i];
entry.HashCode = hashCode;
entry.Key = key;
entry.Value = value;
entry.Next = bucket - 1; // Value in _buckets is 1-based
bucket = i + 1;
}
if (!typeof(TKey).IsValueType && collisionCount > HashHelpers.HashCollisionThreshold)
{
Resize(entries.Length);
index = Find(key, out _, external: true);
Debug.Assert(index >= 0);
}
else
{
index = ToExternalIndex(i);
}
return true;
}
private bool TryRemove(TKey key, [NotNullWhen(true)] out TValue? value, out int index)
{
if (_buckets == null)
{
index = -1;
value = default;
return false;
}
Entry[]? entries = _entries;
Debug.Assert(entries != null, "Expected _entries to be non-null");
uint collisionCount = 0;
int last = -1;
IEqualityComparer? comparer = _comparer;
int hashCode;
if (typeof(TKey).IsValueType && comparer == null)
{
comparer = EqualityComparer.Default;
hashCode = key!.GetHashCode();
}
else
{
Debug.Assert(comparer is not null);
hashCode = key != null ? comparer!.GetHashCode(key) : 0;
}
ref int bucket = ref GetBucketRef(hashCode);
int i = bucket - 1; // Value in buckets is 1-based
while (i >= 0)
{
ref Entry entry = ref entries[i];
if (entry.HashCode != hashCode || !comparer.Equals(entry.Key, key))
{
last = i;
i = entry.Next;
collisionCount++;
if (collisionCount > (uint)entries.Length)
{
throw new InvalidOperationException();
}
continue;
}
if (last < 0)
{
bucket = entry.Next + 1; // Value in buckets is 1-based
}
else
{
entries[last].Next = entry.Next;
}
entry.Next = -1;
InsertFreeIndex(i);
value = entry.Value!;
index = ToExternalIndex(i);
if (RuntimeHelpers.IsReferenceOrContainsReferences())
{
entry.Value = default!;
}
return true;
}
index = -1;
value = default;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static KeyValuePair CastToKVPair(object? obj)
{
try
{
return (KeyValuePair)obj!;
}
catch (InvalidCastException e)
{
throw new ArgumentException(string.Empty, nameof(obj), e);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static TValue CastToValue(object? obj)
{
try
{
return (TValue)obj!;
}
catch (InvalidCastException e)
{
throw new ArgumentException(string.Empty, nameof(obj), e);
}
}
public bool IsFixedSize => false;
public bool IsReadOnly => true;
public bool IsSynchronized => false;
public object SyncRoot => this;
public int Count => _count - _freeCount;
public ICollection Keys => new KeyCollection(this);
IEnumerable IReadOnlyDictionary.Keys => Keys;
public ICollection Values => this;
IEnumerable IReadOnlyDictionary.Values => Values;
public TValue this[int index]
{
get
{
int i = ToInternalIndex(index);
ArgumentOutOfRangeException.ThrowIfNegative(i, nameof(index));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(i, _count, nameof(index));
return _entries![i].Value;
}
set
{
ArgumentOutOfRangeException.ThrowIfNegative(index, nameof(index));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, Count, nameof(index));
int i = ToInternalIndex(index);
TValue? oldValue = _entries![i].Value;
if (!ReferenceEquals(value, oldValue))
{
_entries![i].Value = value;
RaiseCollectionChanged(NotifyCollectionChangedAction.Replace, oldValue, value, index);
}
}
}
object? IList.this[int index]
{
get => this[index];
set => this[index] = CastToValue(value);
}
public TValue this[TKey key]
{
get
{
int i = Find(key, out _);
if (i < 0) throw new ArgumentException(string.Empty, nameof(key));
return _entries![i].Value;
}
set
{
if (TryAdd(key, value, InsertionBehavior.OverwriteExisting, out var index, out TValue? old))
{
if (old == null)
{
RaiseCountPropertyChanged();
RaiseIndexerPropertyChanged();
RaiseCollectionChanged(NotifyCollectionChangedAction.Add, value, index);
}
else
{
RaiseCollectionChanged(NotifyCollectionChangedAction.Replace, old, value, index);
}
}
}
}
public bool ContainsKey(TKey key) => Find(key, out _) >= 0;
public bool Contains(KeyValuePair pair)
{
var i = Find(pair.Key, out TValue? value);
return i >= 0 && EqualityComparer.Default.Equals(pair.Value, value);
}
public bool Contains(TValue value) => FindValue(value) >= 0;
bool IList.Contains(object? obj) => obj is TValue value ? Contains(value) : Contains(CastToKVPair(obj));
public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) => Find(key, out value) >= 0;
public int IndexOf(TValue value) => FindValue(value);
int IList.IndexOf(object? obj) => IndexOf(CastToValue(obj));
public void Add(TKey key, TValue value)
{
if (TryAdd(key, value, InsertionBehavior.ThrowOnExisting, out var index, out _))
{
RaiseCountPropertyChanged();
RaiseIndexerPropertyChanged();
RaiseCollectionChanged(NotifyCollectionChangedAction.Add, value, index);
}
else
{
throw new ArgumentException(string.Empty, nameof(key));
}
}
public void Add(KeyValuePair pair) => Add(pair.Key, pair.Value);
public bool Add(TValue value) => throw new NotSupportedException();
void ICollection.Add(TValue value) => Add(value);
int IList.Add(object? value) => Add(CastToValue(value)) ? _count - 1 : -1;
public void Insert(int index, TValue item) => throw new NotSupportedException();
void IList.Insert(int index, object? value) => throw new NotSupportedException();
public bool Remove(TKey key)
{
if (TryRemove(key, out TValue? value, out var index))
{
RaiseCountPropertyChanged();
RaiseIndexerPropertyChanged();
RaiseCollectionChanged(NotifyCollectionChangedAction.Remove, value, index);
return true;
}
else
{
return false;
}
}
public bool Remove(KeyValuePair pair)
{
if (Find(pair.Key, out TValue? value) >= 0)
{
if (!EqualityComparer.Default.Equals(pair.Value, value))
throw new ArgumentException(string.Empty, nameof(pair));
if (TryRemove(pair.Key, out _, out var index))
{
RaiseCountPropertyChanged();
RaiseIndexerPropertyChanged();
RaiseCollectionChanged(NotifyCollectionChangedAction.Remove, value, index);
return true;
}
else
{
throw new InvalidOperationException();
}
}
else
{
return false;
}
}
public bool Remove(TValue value) => throw new NotSupportedException();
void IList.Remove(object? item)
{
if (!Remove(CastToValue(item))) throw new ArgumentException(string.Empty, nameof(item));
}
public void RemoveAt(int index) => Remove(this[index]);
public void CopyTo(TValue[] array, int arrayIndex)
{
ArgumentNullException.ThrowIfNull(array, nameof(array));
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex, nameof(arrayIndex));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(arrayIndex, array.Length, nameof(arrayIndex));
if (array.Length - arrayIndex < Count)
{
throw new ArgumentException(string.Empty, nameof(array));
}
for (int index = 0; index < Count; index++)
{
array[arrayIndex + index] = this[index];
}
}
void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex)
{
ArgumentNullException.ThrowIfNull(array, nameof(array));
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex, nameof(arrayIndex));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(arrayIndex, array.Length, nameof(arrayIndex));
if (array.Length - arrayIndex < Count)
{
throw new ArgumentException(string.Empty, nameof(array));
}
for (int index = 0; index < Count; index++)
{
ref Entry entry = ref _entries![ToInternalIndex(index)];
array[arrayIndex + index] = KeyValuePair.Create(entry.Key, entry.Value);
}
}
void ICollection.CopyTo(Array array, int arrayIndex)
{
ArgumentNullException.ThrowIfNull(array, nameof(array));
ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex, nameof(arrayIndex));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(arrayIndex, array.Length, nameof(arrayIndex));
if (array.Length - arrayIndex < Count || array.Rank > 1)
{
throw new ArgumentException(string.Empty, nameof(array));
}
try
{
for (int index = 0; index < Count; index++)
{
array.SetValue(this[index], arrayIndex + index);
}
}
catch
{
for (int index = 0; index < Count; index++)
{
ref Entry entry = ref _entries![ToInternalIndex(index)];
array.SetValue(KeyValuePair.Create(entry.Key, entry.Value), arrayIndex + index);
}
}
}
public void Clear()
{
if (_buckets != null) Buckets.Clear();
_count = 0;
_freeCount = 0;
_version++;
RaiseCollectionReset();
}
public void KeysIntersectWith(IEnumerable other)
{
var bits = new BitArray(_count);
foreach (TKey key in other)
{
var i = Find(key, out _);
if (i >= 0) bits[i] = true;
}
for (int i = 0; i < _count; i++)
{
if (_entries![i].Next >= 0 && !bits[i] && !TryRemove(_entries![i].Key, out _, out _))
{
throw new InvalidOperationException();
}
}
}
public IEnumerator GetEnumerator() => new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerator> IEnumerable>.GetEnumerator() => new Enumerator(this, 1);
public struct Enumerator(ObservableMap map, byte returnType = 0) : IEnumerator>, IEnumerator
{
private readonly int _version = map._version;
private int _index = 0;
public readonly void Dispose() { }
public bool MoveNext()
{
if (_version != map._version)
{
throw new InvalidOperationException();
}
while ((uint)_index < (uint)map._count)
{
ref Entry entry = ref map._entries![_index++];
if (entry.Next >= -1)
{
Current = KeyValuePair.Create(entry.Key, entry.Value);
return true;
}
}
_index = map._count + 1;
Current = default!;
return false;
}
public KeyValuePair Current { get; private set; }
readonly TValue IEnumerator.Current => Current.Value;
readonly object? IEnumerator.Current
{
get
{
if (_index == 0 || (_index == map._count + 1))
{
throw new InvalidOperationException();
}
return returnType == 0 ? Current.Value : Current;
}
}
void IEnumerator.Reset()
{
if (_version != map._version)
{
throw new InvalidOperationException();
}
_index = 0;
Current = default!;
}
}
[DebuggerDisplay("Count = {Count}")]
public sealed class KeyCollection : ICollection, IReadOnlyCollection, ICollection, IEnumerable
{
private readonly ObservableMap _dictionary;
public KeyCollection(ObservableMap dictionary)
{
ArgumentNullException.ThrowIfNull(dictionary, nameof(dictionary));
_dictionary = dictionary;
}
public Enumerator GetEnumerator() => new(_dictionary);
public void CopyTo(TKey[] array, int index)
{
ArgumentNullException.ThrowIfNull(array, nameof(array));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)array.Length, nameof(index));
if (array.Length - index < _dictionary.Count)
{
throw new ArgumentException(string.Empty, nameof(array));
}
int count = _dictionary._count;
Entry[]? entries = _dictionary._entries;
for (int i = 0; i < count; i++)
{
if (entries![i].Next >= -1) array[index++] = entries[i].Key;
}
}
public int Count => _dictionary.Count;
bool ICollection.IsReadOnly => true;
void ICollection.Add(TKey item) => throw new NotSupportedException();
void ICollection.Clear() => throw new NotSupportedException();
public bool Contains(TKey item) =>
_dictionary.ContainsKey(item);
bool ICollection.Remove(TKey item) => throw new NotSupportedException();
IEnumerator IEnumerable.GetEnumerator() => Count == 0
? GenericEmptyEnumerator.Instance
: GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator();
void ICollection.CopyTo(Array array, int index)
{
ArgumentNullException.ThrowIfNull(array, nameof(array));
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, (uint)array.Length, nameof(index));
if (array.Rank != 1 || array.GetLowerBound(0) != 0 || array.Length - index < _dictionary.Count)
{
throw new ArgumentException(string.Empty, nameof(array));
}
if (array is TKey[] keys)
{
CopyTo(keys, index);
}
else
{
if (array is not object[] objects)
{
throw new ArgumentException(string.Empty, nameof(array));
}
int count = _dictionary._count;
Entry[]? entries = _dictionary._entries;
try
{
for (int i = 0; i < count; i++)
{
if (entries![i].Next >= -1) objects[index++] = entries[i].Key!;
}
}
catch (ArrayTypeMismatchException e)
{
throw new ArgumentException(string.Empty, nameof(array), e);
}
}
}
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => _dictionary.SyncRoot;
public struct Enumerator : IEnumerator, IEnumerator
{
private readonly ObservableMap _dictionary;
private int _index;
private readonly int _version;
private TKey? _currentKey;
internal Enumerator(ObservableMap dictionary)
{
_dictionary = dictionary;
_version = dictionary._version;
_index = 0;
_currentKey = default;
}
public readonly void Dispose() { }
public bool MoveNext()
{
if (_version != _dictionary._version)
{
throw new InvalidOperationException();
}
while ((uint)_index < (uint)_dictionary._count)
{
ref Entry entry = ref _dictionary._entries![_index++];
if (entry.Next >= -1)
{
_currentKey = entry.Key;
return true;
}
}
_index = _dictionary._count + 1;
_currentKey = default;
return false;
}
public readonly TKey Current => _currentKey!;
readonly object? IEnumerator.Current
{
get
{
if (_index == 0 || (_index == _dictionary._count + 1))
{
throw new InvalidOperationException();
}
return _currentKey;
}
}
void IEnumerator.Reset()
{
if (_version != _dictionary._version)
{
throw new InvalidOperationException();
}
_index = 0;
_currentKey = default;
}
}
}
private event PropertyChangedEventHandler? PropertyChanged;
event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
{
add => PropertyChanged += value;
remove => PropertyChanged -= value;
}
private void RaisePropertyChanged(PropertyChangedEventArgs e) => PropertyChanged?.Invoke(this, e);
public event NotifyCollectionChangedEventHandler? CollectionChanged;
private void RaiseCollectionChanged(NotifyCollectionChangedEventArgs e) => CollectionChanged?.Invoke(this, e);
private void RaiseCountPropertyChanged() => RaisePropertyChanged(EventArgsCache.CountPropertyChanged);
private void RaiseIndexerPropertyChanged() => RaisePropertyChanged(EventArgsCache.IndexerPropertyChanged);
private void RaiseCollectionChanged(NotifyCollectionChangedAction action, object? item, int index)
{
RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));
}
private void RaiseCollectionChanged(NotifyCollectionChangedAction action, object? item, int index, int oldIndex)
{
RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index, oldIndex));
}
private void RaiseCollectionChanged(NotifyCollectionChangedAction action, object? oldItem, object? newItem, int index)
{
RaiseCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
}
private void RaiseCollectionReset() => RaiseCollectionChanged(EventArgsCache.ResetCollectionChanged);
}
```
### Expected behavior
Maybe use `listType.GetInterfaces()` instead, and check for these generic interfaces:
* `IList` for `T`
* `ICollction` for `T`
* `IEnumerable` for `T`
### Screenshots
_No response_
### Code Platform
- [ ] UWP
- [X] WinAppSDK / WinUI 3
- [ ] Web Assembly (WASM)
- [ ] Android
- [ ] iOS
- [ ] MacOS
- [ ] Linux / GTK
### Windows Build Number
- [ ] Windows 10 1809 (Build 17763)
- [ ] Windows 10 1903 (Build 18362)
- [ ] Windows 10 1909 (Build 18363)
- [ ] Windows 10 2004 (Build 19041)
- [ ] Windows 10 20H2 (Build 19042)
- [ ] Windows 10 21H1 (Build 19043)
- [ ] Windows 10 21H2 (Build 19044)
- [ ] Windows 10 22H2 (Build 19045)
- [X] Windows 11 21H2 (Build 22000)
- [ ] Other (specify)
### Other Windows Build number
_No response_
### App minimum and target SDK version
- [ ] Windows 10, version 1809 (Build 17763)
- [ ] Windows 10, version 1903 (Build 18362)
- [ ] Windows 10, version 1909 (Build 18363)
- [ ] Windows 10, version 2004 (Build 19041)
- [ ] Windows 10, version 2104 (Build 20348)
- [ ] Windows 11, version 22H2 (Build 22000)
- [ ] Other (specify)
### Other SDK version
_No response_
### Visual Studio Version
2022
### Visual Studio Build Number
_No response_
### Device form factor
Desktop
### Additional context
_No response_
### Help us help you
Yes, I'd like to be assigned to work on this item.
Contributor guide
Assessment
This issue has not been assessed yet.