CommunityToolkit / CommunityToolkit/Windows

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

Aperta
#324 1 commento 0 reazioni 0 assegnatari Vedi su GitHub
components::collections feature request :mailbox_with_mail:
Lingua principale
C#
Stelle
1.1k
Fork
166
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

### 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.

Guida per i contributori

Apri la guida per i contributori

Direzione di ricerca

Start with the AdvancedCollectionView implementation and commit 72bf0b1a006685ccb3595621972cd27b75bfddee, then reproduce the issue using the custom ObservableMap shown in the report. Done means AdvancedCollectionView handles non-trivial generic collections without making incorrect item-type assumptions.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
csharp
Ambito
frontend
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Ferma
Chiarezza
Abbastanza chiara
Idoneità per principianti
35/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.