Skip to content

[iOS] Fix ArgumentOutOfRangeException in ObservableGroupedSource when removing a group from grouped CollectionView#34692

Open
baaaaif wants to merge 2 commits intodotnet:mainfrom
baaaaif:fix/observable-grouped-source-remove-crash
Open

[iOS] Fix ArgumentOutOfRangeException in ObservableGroupedSource when removing a group from grouped CollectionView#34692
baaaaif wants to merge 2 commits intodotnet:mainfrom
baaaaif:fix/observable-grouped-source-remove-crash

Conversation

@baaaaif
Copy link
Copy Markdown

@baaaaif baaaaif commented Mar 27, 2026

Summary

Fixes #34691ArgumentOutOfRangeException in ObservableGroupedSource.GetGroupCount when removing a group from a grouped CollectionView on iOS.

Root Cause

CollectionChanged(NotifyCollectionChangedEventArgs) called UpdateSection() before processing the Remove action:

var collectionView = controller.CollectionView;
UpdateSection(collectionView);  // ← called here, before Remove()

switch (args.Action) {
    case NotifyCollectionChangedAction.Remove:
        Remove(args);  // ← _groupSource already has N-1 items

Standard INotifyCollectionChanged semantics require the item to be already removed from the backing collection before CollectionChanged fires. So when MAUI receives CollectionChanged(Remove), _groupSource has N-1 items — but UIKit still has N sections.

UpdateSection iterates UIKit's N sections and calls NumberOfItemsInSection(N-1), which re-enters the data source:

UpdateSection  →  NumberOfItemsInSection(N-1)
  →  ItemCountInGroup(N-1)  →  GetGroupCount(N-1)
    →  _groupSource[N-1]    ← out of range → 💥

The Compatibility layer (Microsoft.Maui.Controls.Compatibility.Platform.iOS.ObservableGroupedSource) does not call UpdateSection before Remove and is unaffected.

Fix

  1. Skip UpdateSection before Remove — the post-processing call (after DeleteSections completes) already handles UIKit reconciliation.

  2. Defensive bounds check in GetGroupCount — guards against other re-entrancy scenarios where UIKit calls back synchronously with a stale section index during DeleteSections.

Testing

  • Verified the fix prevents the crash in a grouped CollectionView with dynamic section removal on iOS
  • UpdateSection is still called for Add, Replace, Move, and Reset — no regression expected for those actions
  • The post-processing UpdateSection still runs for Remove after DeleteSections completes

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 27, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34692

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34692"

@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Mar 27, 2026
@baaaaif
Copy link
Copy Markdown
Author

baaaaif commented Mar 27, 2026

@dotnet-policy-service agree company="audius GmbH"

@baaaaif baaaaif force-pushed the fix/observable-grouped-source-remove-crash branch from bf07aeb to aacbd45 Compare March 27, 2026 07:06
baaaaif added 2 commits March 27, 2026 08:07
…hen removing a group on iOS

Skip the pre-processing UpdateSection call for Remove actions. Standard INCC semantics
require the item to already be removed from the backing collection before CollectionChanged
fires. This means _groupSource has N-1 items when MAUI receives CollectionChanged(Remove),
but UIKit still has N sections. Calling UpdateSection at this point iterates all N of UIKit's
sections and calls GetGroupCount(N-1) -> _groupSource[N-1] -> ArgumentOutOfRangeException.

The post-processing UpdateSection call (after DeleteSections) already handles reconciliation.

Also add a defensive bounds check in GetGroupCount to guard against other re-entrancy
scenarios where UIKit calls back synchronously with a stale section index during DeleteSections.

Fixes: dotnet#34691
… on iOS

Adds three device tests to CollectionViewTests.iOS.cs verifying that removing sections
from a grouped CollectionView does not throw ArgumentOutOfRangeException (issue dotnet#34691):

- ItemsSourceGroupedRemoveLastSectionDoesNotCrash: removes the last group, which was
  the classic crash case where _groupSource[N-1] was accessed with only N-1 items.
- ItemsSourceGroupedRemoveFirstSectionDoesNotCrash: removes from the beginning/middle.
- ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash: removes all groups one by
  one, exercising the full range of section indices.
@baaaaif baaaaif force-pushed the fix/observable-grouped-source-remove-crash branch from aacbd45 to de54a54 Compare March 27, 2026 07:07
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 30, 2026

🚦 Gate - Test Before and After Fix

📊 Expand Full Gatede54a54 · test: add regression tests for grouped CollectionView section removal on iOS

Gate Result: ❌ FAILED

Platform: IOS · Base: main · Merge base: 794a9fa6

Test Without Fix (expect FAIL) With Fix (expect PASS)
📱 CollectionViewTests (ItemsSourceGroupedRemoveLastSectionDoesNotCrash, ItemsSourceGroupedRemoveFirstSectionDoesNotCrash, ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash) Category=CollectionView ❌ PASS — 486s ✅ PASS — 204s
🔴 Without fix — 📱 CollectionViewTests (ItemsSourceGroupedRemoveLastSectionDoesNotCrash, ItemsSourceGroupedRemoveFirstSectionDoesNotCrash, ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash): PASS ❌ · 486s

(truncated to last 15,000 chars)

Run + 1208
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000182d009e8 CoreFoundation`CFRunLoopRunSpecific + 572
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x00000001842d0c78 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001843443a4 Foundation`-[NSRunLoop(NSRunLoop) runUntilDate:] + 100
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x00000001029d6f28 mlaunch`xamarin_dyn_objc_msgSend + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x000000010820e68c
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x0000000108574008
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x0000000108207f6c
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x00000001081a10b4
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x00000001079bcd34
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x0000000104894c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #16: 0x0000000104712d30 libcoreclr.dylib`MethodDescCallSite::CallTargetWorker(unsigned long long const*, unsigned long long*, int) + 836
�[40m�[37mdbug�[39m�[22m�[49m: frame #17: 0x0000000104619350 libcoreclr.dylib`RunMain(MethodDesc*, short, int*, PtrArray**) + 648
�[40m�[37mdbug�[39m�[22m�[49m: frame #18: 0x0000000104619688 libcoreclr.dylib`Assembly::ExecuteMainMethod(PtrArray**, int) + 264
�[40m�[37mdbug�[39m�[22m�[49m: frame #19: 0x000000010464129c libcoreclr.dylib`CorHost2::ExecuteAssembly(unsigned int, char16_t const*, int, char16_t const**, unsigned int*) + 640
�[40m�[37mdbug�[39m�[22m�[49m: frame #20: 0x0000000104607650 libcoreclr.dylib`coreclr_execute_assembly + 232
�[40m�[37mdbug�[39m�[22m�[49m: frame #21: 0x00000001029d2140 mlaunch`mono_jit_exec + 204
�[40m�[37mdbug�[39m�[22m�[49m: frame #22: 0x00000001029d5ecc mlaunch`xamarin_main + 884
�[40m�[37mdbug�[39m�[22m�[49m: frame #23: 0x00000001029d71f4 mlaunch`main + 64
�[40m�[37mdbug�[39m�[22m�[49m: frame #24: 0x0000000182876b98 dyld`start + 6076
�[40m�[37mdbug�[39m�[22m�[49m: thread #2
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd5c34 libsystem_kernel.dylib`mach_msg2_trap + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182be83a0 libsystem_kernel.dylib`mach_msg2_internal + 76
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000182bde764 libsystem_kernel.dylib`mach_msg_overwrite + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x0000000182bd5fa8 libsystem_kernel.dylib`mach_msg + 24
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001046052f4 libcoreclr.dylib`MachMessage::Receive(unsigned int) + 80
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x000000010460461c libcoreclr.dylib`SEHExceptionThread(void*) + 164
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #3, name = '.NET SynchManager'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bdbd04 libsystem_kernel.dylib`kevent + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x00000001045f9304 libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ReadBytesFromProcessPipe(int, unsigned char*, int) + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001045f89f0 libcoreclr.dylib`CorUnix::CPalSynchronizationManager::WorkerThread(void*) + 164
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #4
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd78b0 libsystem_kernel.dylib`__workq_kernreturn + 8
�[40m�[37mdbug�[39m�[22m�[49m: thread #5, name = '.NET EventPipe'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bde498 libsystem_kernel.dylib`poll + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x00000001048f4e90 libcoreclr.dylib`ds_ipc_poll(_DiagnosticsIpcPollHandle*, unsigned long, unsigned int, void (*)(char const*, unsigned int)) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001049a2bb0 libcoreclr.dylib`ds_ipc_stream_factory_get_next_available_stream(void (*)(char const*, unsigned int)) + 756
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001049a0a68 libcoreclr.dylib`server_thread(void*) + 372
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #6, name = '.NET DebugPipe'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd6678 libsystem_kernel.dylib`__open + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182be16a4 libsystem_kernel.dylib`open + 64
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001048f5a84 libcoreclr.dylib`TwoWayPipe::WaitForConnection() + 40
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001048f0578 libcoreclr.dylib`DbgTransportSession::TransportWorker() + 232
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001048ef5c8 libcoreclr.dylib`DbgTransportSession::TransportWorkerStatic(void*) + 40
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #7, name = '.NET Debugger'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd93cc libsystem_kernel.dylib`__psynch_cvwait + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182c1809c libsystem_pthread.dylib`_pthread_cond_wait + 984
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001045f6f6c libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ThreadNativeWait(CorUnix::_ThreadNativeWaitData*, unsigned int, CorUnix::ThreadWakeupReason*, unsigned int*) + 320
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001045f6bec libcoreclr.dylib`CorUnix::CPalSynchronizationManager::BlockThread(CorUnix::CPalThread*, unsigned int, bool, bool, CorUnix::ThreadWakeupReason*, unsigned int*) + 380
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001045fb0cc libcoreclr.dylib`CorUnix::InternalWaitForMultipleObjectsEx(CorUnix::CPalThread*, unsigned int, void* const*, int, unsigned int, int, int) + 1600
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x00000001048edda8 libcoreclr.dylib`DebuggerRCThread::MainLoop() + 228
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x00000001048edc70 libcoreclr.dylib`DebuggerRCThread::ThreadProc() + 256
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x00000001048eda24 libcoreclr.dylib`DebuggerRCThread::ThreadProcStatic(void*) + 56
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #8
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd93cc libsystem_kernel.dylib`__psynch_cvwait + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182c1809c libsystem_pthread.dylib`_pthread_cond_wait + 984
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x00000001045f6f6c libcoreclr.dylib`CorUnix::CPalSynchronizationManager::ThreadNativeWait(CorUnix::_ThreadNativeWaitData*, unsigned int, CorUnix::ThreadWakeupReason*, unsigned int*) + 320
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x00000001045f6bec libcoreclr.dylib`CorUnix::CPalSynchronizationManager::BlockThread(CorUnix::CPalThread*, unsigned int, bool, bool, CorUnix::ThreadWakeupReason*, unsigned int*) + 380
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x00000001045fb0cc libcoreclr.dylib`CorUnix::InternalWaitForMultipleObjectsEx(CorUnix::CPalThread*, unsigned int, void* const*, int, unsigned int, int, int) + 1600
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x0000000104748078 libcoreclr.dylib`FinalizerThread::WaitForFinalizerEvent(CLREvent*) + 240
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x00000001047481d8 libcoreclr.dylib`FinalizerThread::FinalizerThreadWorker(void*) + 264
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x00000001046e5fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001046e648c libcoreclr.dylib`ManagedThreadBase::FinalizerBase(void (*)(void*)) + 36
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x0000000104748350 libcoreclr.dylib`FinalizerThread::FinalizerThreadStart(void*) + 88
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #9, name = '.NET SigHandler'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd67dc libsystem_kernel.dylib`read + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000102a90e98 libSystem.Native.dylib`SignalHandlerLoop + 96
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #10
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bdbd04 libsystem_kernel.dylib`kevent + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000102a8f4a4 libSystem.Native.dylib`SystemNative_WaitForSocketEvents + 80
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x000000010831cc1c
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x000000010831c804
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x000000010831c5bc
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x00000001082014c0
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000108201218
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x0000000108201108
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x0000000104894c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x0000000104712988 libcoreclr.dylib`DispatchCallSimple(unsigned long*, unsigned int, unsigned long long, unsigned int) + 268
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x0000000104724c6c libcoreclr.dylib`ThreadNative::KickOffThread_Worker(void*) + 148
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x00000001046e5fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x00000001046e645c libcoreclr.dylib`ManagedThreadBase::KickOff(void (*)(void*), void*) + 32
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x0000000104724d44 libcoreclr.dylib`ThreadNative::KickOffThread(void*) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #11
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd5c34 libsystem_kernel.dylib`mach_msg2_trap + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182be83a0 libsystem_kernel.dylib`mach_msg2_internal + 76
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000182bde764 libsystem_kernel.dylib`mach_msg_overwrite + 484
�[40m�[37mdbug�[39m�[22m�[49m: frame #3: 0x0000000182bd5fa8 libsystem_kernel.dylib`mach_msg + 24
�[40m�[37mdbug�[39m�[22m�[49m: frame #4: 0x0000000182d02c0c CoreFoundation`__CFRunLoopServiceMachPort + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #5: 0x0000000182d01528 CoreFoundation`__CFRunLoopRun + 1208
�[40m�[37mdbug�[39m�[22m�[49m: frame #6: 0x0000000182d009e8 CoreFoundation`CFRunLoopRunSpecific + 572
�[40m�[37mdbug�[39m�[22m�[49m: frame #7: 0x00000001842d0c78 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212
�[40m�[37mdbug�[39m�[22m�[49m: frame #8: 0x00000001029d6f28 mlaunch`xamarin_dyn_objc_msgSend + 160
�[40m�[37mdbug�[39m�[22m�[49m: frame #9: 0x000000010856a47c
�[40m�[37mdbug�[39m�[22m�[49m: frame #10: 0x000000010856a340
�[40m�[37mdbug�[39m�[22m�[49m: frame #11: 0x000000010856a174
�[40m�[37mdbug�[39m�[22m�[49m: frame #12: 0x00000001085670f8
�[40m�[37mdbug�[39m�[22m�[49m: frame #13: 0x0000000108201468
�[40m�[37mdbug�[39m�[22m�[49m: frame #14: 0x0000000108201218
�[40m�[37mdbug�[39m�[22m�[49m: frame #15: 0x0000000108201108
�[40m�[37mdbug�[39m�[22m�[49m: frame #16: 0x0000000104894c04 libcoreclr.dylib`CallDescrWorkerInternal + 132
�[40m�[37mdbug�[39m�[22m�[49m: frame #17: 0x0000000104712988 libcoreclr.dylib`DispatchCallSimple(unsigned long*, unsigned int, unsigned long long, unsigned int) + 268
�[40m�[37mdbug�[39m�[22m�[49m: frame #18: 0x0000000104724c6c libcoreclr.dylib`ThreadNative::KickOffThread_Worker(void*) + 148
�[40m�[37mdbug�[39m�[22m�[49m: frame #19: 0x00000001046e5fa8 libcoreclr.dylib`ManagedThreadBase_DispatchOuter(ManagedThreadCallState*) + 248
�[40m�[37mdbug�[39m�[22m�[49m: frame #20: 0x00000001046e645c libcoreclr.dylib`ManagedThreadBase::KickOff(void (*)(void*), void*) + 32
�[40m�[37mdbug�[39m�[22m�[49m: frame #21: 0x0000000104724d44 libcoreclr.dylib`ThreadNative::KickOffThread(void*) + 172
�[40m�[37mdbug�[39m�[22m�[49m: frame #22: 0x00000001046020fc libcoreclr.dylib`CorUnix::CPalThread::ThreadEntry(void*) + 364
�[40m�[37mdbug�[39m�[22m�[49m: frame #23: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #12, name = 'com.apple.CFSocket.private'
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182be0c2c libsystem_kernel.dylib`__select + 8
�[40m�[37mdbug�[39m�[22m�[49m: frame #1: 0x0000000182d28a80 CoreFoundation`__CFSocketManager + 704
�[40m�[37mdbug�[39m�[22m�[49m: frame #2: 0x0000000182c17bc8 libsystem_pthread.dylib`_pthread_start + 136
�[40m�[37mdbug�[39m�[22m�[49m: thread #13
�[40m�[37mdbug�[39m�[22m�[49m: frame #0: 0x0000000182bd78b0 libsystem_kernel.dylib`__workq_kernreturn + 8
�[40m�[37mdbug�[39m�[22m�[49m: (lldb) detach
�[40m�[37mdbug�[39m�[22m�[49m: Process 9458 detached
�[40m�[37mdbug�[39m�[22m�[49m: (lldb) quit
�[40m�[37mdbug�[39m�[22m�[49m: 9458 Execution timed out after 60 seconds and the process was killed.
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 137
�[40m�[37mdbug�[39m�[22m�[49m: Failed to list crash reports from device.
�[40m�[37mdbug�[39m�[22m�[49m: Test run started but crashed and no test results were reported
�[40m�[37mdbug�[39m�[22m�[49m: No crash reports, waiting 30 seconds for the crash report service...
�[41m�[30mfail�[39m�[22m�[49m: Application test run crashed
      Failed to launch the application, please try again. If the problem persists, try rebooting MacOS
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling the application 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405):
�[40m�[37mdbug�[39m�[22m�[49m: Unable to lookup in current state: Shutdown
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 149
�[41m�[30mfail�[39m�[22m�[49m: Failed to uninstall the app bundle! Check logs for more details!
XHarness exit code: 83 (APP_LAUNCH_FAILURE)
  Passed: 0
  Failed: 0
  Tests completed with exit code: 83

🟢 With fix — 📱 CollectionViewTests (ItemsSourceGroupedRemoveLastSectionDoesNotCrash, ItemsSourceGroupedRemoveFirstSectionDoesNotCrash, ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash): PASS ✅ · 204s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
  Graphics -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Graphics/Release/net10.0-ios26.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
  Essentials -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Essentials/Release/net10.0-ios26.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
  Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core/Release/net10.0-ios26.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.BindingSourceGen/Release/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
  Controls.Core -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Core/Release/net10.0-ios26.0/Microsoft.Maui.Controls.dll
  TestUtils.DeviceTests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests/Release/net10.0-ios/Microsoft.Maui.TestUtils.DeviceTests.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13750190
  Controls.Xaml -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.Xaml/Release/net10.0-ios26.0/Microsoft.Maui.Controls.Xaml.dll
  TestUtils.DeviceTests.Runners -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests.Runners/Release/net10.0-ios/Microsoft.Maui.TestUtils.DeviceTests.Runners.dll
  Core.DeviceTests.Shared -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Core.DeviceTests.Shared/Release/net10.0-ios/Microsoft.Maui.DeviceTests.Shared.dll
  TestUtils.DeviceTests.Runners.SourceGen -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/TestUtils.DeviceTests.Runners.SourceGen/Release/netstandard2.0/Microsoft.Maui.TestUtils.DeviceTests.Runners.SourceGen.dll
  Detected signing identity:
    Code Signing Key: "" (-)
    Provisioning Profile: "" () - no entitlements
    Bundle Id: com.microsoft.maui.controls.devicetests
    App Id: com.microsoft.maui.controls.devicetests
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.iOS.cs(121,5): warning xUnit2013: Do not use Assert.Equal() to check for collection size. Use Assert.Single instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net10.0-ios]
  Controls.DeviceTests -> /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.dll
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  Optimizing assemblies for size. This process might take a while.
  IL stripping assemblies

Build succeeded.

/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.iOS.cs(121,5): warning xUnit2013: Do not use Assert.Equal() to check for collection size. Use Assert.Single instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/DeviceTests/Controls.DeviceTests.csproj::TargetFramework=net10.0-ios]
    1 Warning(s)
    0 Error(s)

Time Elapsed 00:02:28.24
[11.0.0-prerelease.26107.1+bfbac237157e59cdbd19334325b2af80bd6e9828] XHarness command issued: apple test --app artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app --target ios-simulator-64_18.6 --device 61D65259-33D1-401B-AACB-B08BD2DD9597 -o artifacts/log --timeout 01:00:00 -v --set-env=TestFilter=Category=CollectionView
�[40m�[32minfo�[39m�[22m�[49m: Preparing run for ios-simulator-64_18.6 targeting 61D65259-33D1-401B-AACB-B08BD2DD9597
�[40m�[32minfo�[39m�[22m�[49m: Looking for available ios-simulator-64_18.6 simulators..
�[40m�[37mdbug�[39m�[22m�[49m: Looking for available ios-simulator-64_18.6 simulators. Storing logs into list-ios-simulator-64_18.6-20260405_111627.log
�[40m�[32minfo�[39m�[22m�[49m: Found simulator device 'iPhone 11 Pro'
�[40m�[32minfo�[39m�[22m�[49m: Getting app bundle information from '/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app'..
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /usr/libexec/PlistBuddy
�[40m�[37mdbug�[39m�[22m�[49m: Process PlistBuddy exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling any previous instance of 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'com.microsoft.maui.controls.devicetests' was uninstalled successfully
�[40m�[32minfo�[39m�[22m�[49m: Installing application 'Microsoft.Maui.Controls.DeviceTests' on 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: Installing '/Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app' to 'iPhone 11 Pro' (142.15 MB)
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Users/cloudtest/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26107.1/tools/net10.0/any/../../../runtimes/any/native/mlaunch/bin/mlaunch
�[40m�[37mdbug�[39m�[22m�[49m: Using Xcode 26.1.1 found in /Applications/Xcode_26.1.1.app
�[40m�[37mdbug�[39m�[22m�[49m: xcrun simctl list --json --json-output /tmp/tmpzwNzID.tmp
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: No need to boot (already booted): iPhone 11 Pro
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: Installing on iPhone 11 Pro (61D65259-33D1-401B-AACB-B08BD2DD9597) by executing 'xcrun simctl install 61D65259-33D1-401B-AACB-B08BD2DD9597 /Users/cloudtest/vss/_work/1/s/artifacts/bin/Controls.DeviceTests/Release/net10.0-ios/iossimulator-arm64/Microsoft.Maui.Controls.DeviceTests.app'
�[40m�[37mdbug�[39m�[22m�[49m: Xamarin.Hosting: The bundle id com.microsoft.maui.controls.devicetests was successfully installed.
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'Microsoft.Maui.Controls.DeviceTests' was installed successfully on 'iPhone 11 Pro'
�[40m�[32minfo�[39m�[22m�[49m: Starting test run for com.microsoft.maui.controls.devicetests..
�[40m�[37mdbug�[39m�[22m�[49m: Test log server listening on: 0.0.0.0:56972
�[40m�[37mdbug�[39m�[22m�[49m: *** Executing 'Microsoft.Maui.Controls.DeviceTests' on ios-simulator-64_18.6 'iPhone 11 Pro' ***
�[40m�[37mdbug�[39m�[22m�[49m: System log for the 'iPhone 11 Pro' simulator is: /Users/cloudtest/Library/Logs/CoreSimulator/61D65259-33D1-401B-AACB-B08BD2DD9597/system.log
�[40m�[37mdbug�[39m�[22m�[49m: Simulator 'iPhone 11 Pro' is already booted
�[40m�[37mdbug�[39m�[22m�[49m: Scanning log stream for Microsoft.Maui.Controls.DeviceTests into '/Users/cloudtest/vss/_work/1/s/artifacts/log/Microsoft.Maui.Controls.DeviceTests.log'..
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Launching the app
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Users/cloudtest/.nuget/packages/microsoft.dotnet.xharness.cli/11.0.0-prerelease.26107.1/tools/net10.0/any/../../../runtimes/any/native/mlaunch/bin/mlaunch
�[40m�[37mdbug�[39m�[22m�[49m: Connection from 127.0.0.1:56992 saving logs to /Users/cloudtest/vss/_work/1/s/artifacts/log/test-ios-simulator-64_18.6-20260405_111630.log
�[40m�[37mdbug�[39m�[22m�[49m: Tests have finished executing
�[40m�[37mdbug�[39m�[22m�[49m: Process mlaunch exited with 0
�[40m�[37mdbug�[39m�[22m�[49m: Test run completed
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /bin/bash
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 137
�[40m�[37mdbug�[39m�[22m�[49m: cp: /Users/cloudtest/Library/Developer/CoreSimulator/Devices/61D65259-33D1-401B-AACB-B08BD2DD9597/data/Containers/Data/Application/E2D337C1-9763-4A92-A68A-681F12AD0934/Documents/test-results.xml: No such file or directory
�[40m�[37mdbug�[39m�[22m�[49m: Process bash exited with 1
�[40m�[37mdbug�[39m�[22m�[49m: Test run succeeded
�[40m�[37mdbug�[39m�[22m�[49m: No crash reports, waiting 0 seconds for the crash report service...
�[40m�[32minfo�[39m�[22m�[49m: Application finished the test run successfully
�[40m�[32minfo�[39m�[22m�[49m: Tests run: 46 Passed: 42 Inconclusive: 0 Failed: 0 Ignored: 4
�[40m�[32minfo�[39m�[22m�[49m: Uninstalling the application 'com.microsoft.maui.controls.devicetests' from 'iPhone 11 Pro'
�[40m�[37mdbug�[39m�[22m�[49m: 
�[40m�[37mdbug�[39m�[22m�[49m: Running /Applications/Xcode_26.1.1.app/Contents/Developer/usr/bin/simctl
�[40m�[37mdbug�[39m�[22m�[49m: Process simctl exited with 0
�[40m�[32minfo�[39m�[22m�[49m: Application 'com.microsoft.maui.controls.devicetests' was uninstalled successfully
XHarness exit code: 0
  Passed: 84
  Failed: 0
  Tests completed successfully

⚠️ Issues found
  • CollectionViewTests (ItemsSourceGroupedRemoveLastSectionDoesNotCrash, ItemsSourceGroupedRemoveFirstSectionDoesNotCrash, ItemsSourceGroupedRemoveAllSectionsOneByOneDoesNotCrash) PASSED without fix (should fail) — tests don't catch the bug
📁 Fix files reverted (2 files)
  • eng/pipelines/ci-copilot.yml
  • src/Controls/src/Core/Handlers/Items/iOS/ObservableGroupedSource.cs

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Mar 31, 2026

🤖 AI Summary

📊 Expand Full Reviewde54a54 · test: add regression tests for grouped CollectionView section removal on iOS
🔍 Pre-Flight — Context & Validation

Issue: #34691 - [iOS] CollectionView (grouped): ArgumentOutOfRangeException in ObservableGroupedSource.GetGroupCount when removing a section
PR: #34692 - [iOS] Fix ArgumentOutOfRangeException in ObservableGroupedSource when removing a group from grouped CollectionView
Platforms Affected: iOS only (Items/ handler — Items2/ has no grouped source; Compatibility layer unaffected)
Files Changed: 1 implementation (ObservableGroupedSource.cs), 1 test (CollectionViewTests.iOS.cs)

Key Findings

  • Root cause: ObservableGroupedSource.CollectionChanged calls UpdateSection(collectionView) BEFORE processing Remove. Per INotifyCollectionChanged semantics, _groupSource already has N-1 items when the event fires, but UIKit still has N sections. UpdateSection iterates N sections and calls NumberOfItemsInSection(N-1), re-entering the data source via ItemCountInGroup(N-1)GetGroupCount(N-1)_groupSource[N-1] which is out of range → ArgumentOutOfRangeException.
  • The Compatibility layer (Microsoft.Maui.Controls.Compatibility.Platform.iOS.ObservableGroupedSource) does NOT call UpdateSection before Remove and is unaffected.
  • Gate FAILED: Tests pass even WITHOUT the fix — the crash is timing-sensitive and the test setup (simple Task.Delay) doesn't consistently trigger the synchronous UIKit callback sequence that causes the exception.
  • Test quality concern: Tests use Task.Delay (unreliable for UI synchronization) and only verify post-removal counts; they don't assert crash absence.
  • PR's fix has two parts: (1) skip UpdateSection before Remove, (2) add defensive bounds check in GetGroupCount.
  • Prior agent review exists at commit de54a540 — pre-flight, try-fix, and report were all COMPLETE. This is a fresh re-review with the same codebase state.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34692 Skip UpdateSection before Remove + defensive bounds check in GetGroupCount ⏳ PENDING (Gate ❌ FAILED — tests pass without fix) ObservableGroupedSource.cs Two-pronged: prevents crash + adds safety net

🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-opus-4.6) Clamp UpdateSection loop to Math.Min(numberOfSections, _groupSource.Count) ✅ PASS (ios) 1 file (+5/-2) Single change; intrinsically safe for all actions
2 try-fix (claude-sonnet-4.6) Remove pre-processing UpdateSection entirely (all actions) ⚠️ BLOCKED (AOT artifact staleness) 1 file (-3) Logic sound; build env issue
3 try-fix (gpt-5.3-codex) Re-entrancy guard _isUpdatingSection bool; GetGroupCount returns 0 only during UpdateSection for OOB ✅ PASS (ios) 1 file (+17/-2) Preserves both call sites; adds state
4 try-fix (gpt-5.4) Temporary pre-remove section snapshot — feed UIKit old count during reconciliation ❌ FAIL (exit 143 — test killed) 1 file Complex; unverified
PR PR #34692 Skip UpdateSection before Remove + defensive bounds check in GetGroupCount ❌ Gate FAILED (tests pass without fix) 1 fix + 1 test Two-part; well-motivated; tests don't prove regression

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 Yes PerformBatchUpdates wrapping all UIKit mutations — UIKit-native atomicity, eliminates re-entrant callbacks. Not pursued: complex refactor; doesn't address UpdateSection probe itself
claude-sonnet-4.6 2 Yes Same PerformBatchUpdates idea — structural change replacing UpdateSection probing with UIKit batch protocol. Not pursued: same reasoning
gpt-5.3-codex 2 Yes Section-consistency check before incremental update — if NumberOfSections != _groupSource.Count, fallback to ReloadData. Not pursued: loses section animations; broader than needed

Exhausted: Yes — solution space covered (skip, clamp, guard, delete, snapshot all tried; PerformBatchUpdates is a refactor beyond targeted fix scope)
Selected Fix: Attempt 1 (clamp UpdateSection loop to Math.Min(numberOfSections, _groupSource.Count)) — simplest single-location change, intrinsically safe for all actions without conditional logic or new state. Alternatively, the PR's two-part fix is more targeted (Remove-only skip + safety net).


📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE iOS only; Items/ handler; 1 fix file + 1 test file
Gate ❌ FAILED Tests PASS even without fix — regression not proven
Try-Fix ✅ COMPLETE 4 attempts; 2 passing alternatives found (Attempts 1 & 3)
Report ✅ COMPLETE

Summary

The PR fixes a real and well-diagnosed crash (ArgumentOutOfRangeException in ObservableGroupedSource.GetGroupCount on iOS when removing a group from a grouped CollectionView). The root cause analysis is correct and the code fix is sound. However, the gate failed: the three regression tests added by the PR pass even without the fix, meaning they do not prove the fix addresses the crash. This is the primary blocker.

Try-Fix found two independently passing alternatives, confirming the general approach space is valid. The simplest alternative (Attempt 1: clamp UpdateSection loop bound) is a single-location change that's arguably cleaner than the PR's two-part approach.

Root Cause

ObservableGroupedSource.CollectionChanged calls UpdateSection(collectionView) before the Remove action is processed. Per INotifyCollectionChanged semantics, _groupSource already has N-1 items when the event fires, but UIKit still has N sections. UpdateSection iterates N sections and calls NumberOfItemsInSection(N-1), which re-enters the data source via ItemCountInGroup(N-1)GetGroupCount(N-1)_groupSource[N-1] which is out of range.

Fix Quality

The PR's two-part fix is well-motivated and targeted:

  1. Skip UpdateSection before Remove — directly addresses the root cause for the Remove case. Conservative (only changes the Remove path; Add/Replace/Move/Reset are unaffected).
  2. Defensive bounds check in GetGroupCount — good defense-in-depth for other re-entrancy scenarios (UIKit can call back synchronously during DeleteSections).

However, the tests are inadequate:

  • Tests pass even WITHOUT the fix — the crash is timing-sensitive and the test setup does not consistently trigger the synchronous UIKit re-entrancy sequence.
  • Tests use Task.Delay(1000) / Task.Delay(500) / Task.Delay(300) — arbitrary delays are unreliable for device test synchronization.
  • Tests only verify post-removal count (Assert.Equal(2, groupData.Count)) — they don't verify the crash path was exercised.

Try-Fix found a simpler alternative (Attempt 1):
Clamping UpdateSection's loop bound to Math.Min(numberOfSections, _groupSource.Count) (+5/-2 lines in one method) is intrinsically safe for all actions without conditional logic. It preserves the pre-processing call's intent while making it harmless when called with a stale UIKit count. This is arguably a better fix than the PR's two-part approach because it eliminates the hazard at the source rather than working around it action-by-action.

Requested Changes

  1. Fix the tests so they actually detect the regression. The tests must FAIL on the broken baseline (without the fix). Options:

    • Wrap RemoveAt in a try/catch and assert that ArgumentOutOfRangeException is NOT thrown (or use Assert.DoesNotThrow).
    • Use await collectionView.WaitForLayout() or EnsureHandlerCreated() patterns (consistent with other CollectionView device tests in the file) instead of arbitrary Task.Delay.
    • Consider whether the crash only manifests on physical device vs. simulator; if simulator-only tests can't reproduce it reliably, document this and mark the tests appropriately.
  2. Consider the simpler Attempt 1 fix: Clamping UpdateSection loop to Math.Min(numberOfSections, _groupSource.Count) achieves the same safety with a single change and no conditional per-action logic. The PR author may prefer their own two-part approach — that is fine — but should be aware of this alternative.

  3. Minor: The three test methods have nearly identical boilerplate setup. Consider extracting shared setup into a helper method.

The code fix itself (skipping UpdateSection for Remove + bounds check in GetGroupCount) is correct and should be kept once the tests are strengthened.


@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 31, 2026
@baaaaif
Copy link
Copy Markdown
Author

baaaaif commented Apr 1, 2026

If I understand correctly, a change is optional, and conservative code is preferable. So no change is necessary?

By the way, the issue and pull request are generated automatically by Copilot (CLI) from a frequently occurring exception in Sentry. I think it's great that it works so well.

Use Sentry MCP, check if it's our own code or Maui, and fix it directly in Maui.

@MauiBot MauiBot added the s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates label Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] CollectionView (grouped): ArgumentOutOfRangeException in ObservableGroupedSource.GetGroupCount when removing a section

2 participants