Search code examples
xamlxamarin.formssignalrobservablecollection

Xamarin.Forms SignalR: Invalid update: invalid number of rows in section 0


I have a Xamarin.Forms app and in my crash logs, I keep seeing variations of this error:

SIGABRT: Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (60) must be equal to the number of rows contained in that section before the update (60), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).

This particular list view is a chat component and I am using an ObservableCollection and adding message entries with SignalR when a message is received. Relevant code shown below:

    public ObservableCollection<MessageModel> Messages {
        get {
            return _messages;
        }
        set {
            _messages = value;
            OnPropertyChanged();
        }
    }


        Messages = new ObservableCollection<MessageModel>();


        hubConnection.On<long, string, string, long, long>("postChat", (id, name, message, userId, eventId) => {               
            Messages.Add(new MessageModel() { Id = id, UserId = userId, User = name, Message = message, IsSystemMessage = false, IsOwnMessage = userId == currentUserId });
            this.MessageReceived.Invoke(this, new EventArgs());
        });

It feels like maybe a threading issue, since chats can theoretically come in at the same time, but I have no idea where to look. Here is the full StackTrace

CoreFoundation
__exceptionPreprocess
libobjc.A.dylib
objc_exception_throw
CoreFoundation
+[NSException raise:format:arguments:]
Foundation
-[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:]
UIKitCore
-[UITableView _Bug_Detected_In_Client_Of_UITableView_Invalid_Number_Of_Rows_In_Section:]
UIKitCore
-[UITableView _endCellAnimationsWithContext:]
UIKitCore
-[UITableView endUpdatesWithContext:]
BehaviorLive.iOS
wrapper_managed_to_native_ObjCRuntime_Messaging_objc_msgSend_intptr_intptr_11
BehaviorLive.iOS
UITableView.g.cs:292
BehaviorLive.iOS
Xamarin_Forms_Platform_iOS_ListViewRenderer__c__DisplayClass54_0__InsertRowsb__0 (D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:638)
BehaviorLive.iOS
Xamarin_Forms_Platform_iOS_ListViewRenderer_InsertRows_int_int_int (D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:642)
BehaviorLive.iOS
Xamarin_Forms_Platform_iOS_ListViewRenderer_UpdateItems_System_Collections_Specialized_NotifyCollectionChangedEventArgs_int_bool (D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:588)
BehaviorLive.iOS
Xamarin_Forms_Platform_iOS_ListViewRenderer_OnCollectionChanged_object_System_Collections_Specialized_NotifyCollectionChangedEventArgs (D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:386)
BehaviorLive.iOS
Xamarin_Forms_Internals_TemplatedItemsList_2_TView_REF_TItem_REF_OnCollectionChanged_System_Collections_Specialized_NotifyCollectionChangedEventArgs (D:\a\1\s\Xamarin.Forms.Core\TemplatedItemsList.cs:771)
BehaviorLive.iOS
Xamarin_Forms_Internals_TemplatedItemsList_2_TView_REF_TItem_REF_OnProxyCollectionChanged_object_System_Collections_Specialized_NotifyCollectionChangedEventArgs (D:\a\1\s\Xamarin.Forms.Core\TemplatedItemsList.cs:972)
BehaviorLive.iOS
Xamarin_Forms_ListProxy_OnCollectionChanged_System_Collections_Specialized_NotifyCollectionChangedEventArgs (D:\a\1\s\Xamarin.Forms.Core\ListProxy.cs:232)
BehaviorLive.iOS
Xamarin_Forms_ListProxy__c__DisplayClass34_0__OnCollectionChangedb__0 (D:\a\1\s\Xamarin.Forms.Core\ListProxy.cs:208)
BehaviorLive.iOS
NSAction.cs:152
BehaviorLive.iOS
wrapper_runtime_invoke_object_runtime_invoke_dynamic_intptr_intptr_intptr_intptr
BehaviorLive.iOS
mono_jit_runtime_invoke mini-runtime.c:3164
BehaviorLive.iOS
mono_runtime_invoke_checked object.c:3052
BehaviorLive.iOS
mono_runtime_invoke object.c:3107
BehaviorLive.iOS
native_to_managed_trampoline_11(objc_object*, objc_selector*, _MonoMethod**, unsigned int) registrar.m:400
BehaviorLive.iOS
-[__MonoMac_NSAsyncActionDispatcher xamarinApplySelector] registrar.m:8726
Foundation
__NSThreadPerformPerform
CoreFoundation
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
CoreFoundation
__CFRunLoopDoSource0
CoreFoundation
__CFRunLoopDoSources0
CoreFoundation
__CFRunLoopRun
CoreFoundation
CFRunLoopRunSpecific
GraphicsServices
GSEventRunModal
UIKitCore
-[UIApplication _run]
UIKitCore
UIApplicationMain
BehaviorLive.iOS
wrapper_managed_to_native_UIKit_UIApplication_UIApplicationMain_int_string___intptr_intptr
BehaviorLive.iOS
UIApplication.cs:86
BehaviorLive.iOS
UIApplication.cs:65
BehaviorLive.iOS
BehaviorLive_iOS_Application_Main_string__ <unknown>:1
BehaviorLive.iOS
wrapper_runtime_invoke_object_runtime_invoke_dynamic_intptr_intptr_intptr_intptr
BehaviorLive.iOS
mono_jit_runtime_invoke mini-runtime.c:3164
BehaviorLive.iOS
mono_runtime_invoke_checked object.c:3052
BehaviorLive.iOS
mono_runtime_exec_main_checked object.c:0
BehaviorLive.iOS
mono_jit_exec driver.c:1383
BehaviorLive.iOS
xamarin_main monotouch-main.m:493
BehaviorLive.iOS
main main.m:292
libdyld.dylib
start

Solution

  • Make sure you are on MainThread when you access an ObservableCollection that is bound to a UI view.

    • Test MainThread.IsMainThread during Debug testing, throw exception if not on that thread (but you thought all calling code was already on that thread).

    • Wrap code in Device.BeginInvokeOnMainThread. Doing so ensures you aren't altering the collection in middle of UI update:

    .

    Device.BeginInvokeOnMainThread( () => {
        .. do something with `Messages` collection ..
    });
    

    CAUTION: That BeginInvokeOnMainThread runs independently, so you can't rely on it "finishing" before whatever code comes next.

    If this "fire and forget" isn't sufficient/appropriate, then you'll need to use a Task Continuation instead. Or do the manual equivalent - I don't have an example at my fingertips, but the principle is that you run on main thread, then have an Action that you call when that work is done.