CommunityToolkit / CommunityToolkit/Windows

`AdvancedCollectionView` made much too optimistic assumptions about generic parameters of collections.

Open
#324 1 comment 0 reactions 0 assignees View on GitHub
components::collections feature request :mailbox_with_mail:
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.