Forum Xamarin.iOS

Bluetooth Scan throwing ArgumentOutOfRangeException in Xamarin.iOS

stefanhkstefanhk Member ✭✭
edited May 12 in Xamarin.iOS

Posted this on Stack Overflow as well, but re-posting here in case there's anyone who can help.

I have a BLE scanning app that uses this plugin to continuously scan for nearby devices. When devices are discovered, they are added to an ObservableRangeCollection (from James Montemagno's MvvmHelpers). If the device is already in the list and some of its data (eg, RSSI strength) changes, the collection is updated using ReplaceRange(). If the device has been in the list for too long without updating, it is removed from collection.

This is all working smoothly on Android, but it throws an ArgumentOutOfRange exception on iOS. The exception doesn't always come at the same time, but if I leave the app running for long enough, it will eventually throw. Unfortunately, the stack trace doesn't point to anything in my code, so it's hard to tell whether this is something going on with the BLE Plugin, the ObservableRangeCollection plugin, or even Xamarin.Forms.

Here is the exception message and stack trace:

{System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.Parameter name: index
  at Xamarin.Forms.ListProxy.get_Item (System.Int32 index) [0x0000b] in D:\a\1\s\Xamarin.Forms.Core\ListProxy.cs:129 
  at Xamarin.Forms.ListProxy.System.Collections.IList.get_Item (System.Int32 index) [0x00000] in D:\a\1\s\Xamarin.Forms.Core\ListProxy.cs:443 
  at Xamarin.Forms.Internals.TemplatedItemsList`2[TView,TItem].get_Item (System.Int32 index) [0x00008] in <7d3a3d515f644268b15520ab8aaf1bfc>:0 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer+ListViewDataSource.GetCellForPath (Foundation.NSIndexPath indexPath) [0x00007] in D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:1327 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer+ListViewDataSource.GetCell (UIKit.UITableView tableView, Foundation.NSIndexPath indexPath) [0x00021] in D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:1036 
  at (wrapper managed-to-native) ObjCRuntime.Messaging.void_objc_msgSend(intptr,intptr)
  at UIKit.UITableView.EndUpdates () [0x0000d] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.16.0.13/src/Xamarin.iOS/UITableView.g.cs:294 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer+<>c__DisplayClass52_0.<DeleteRows>b__0 () [0x00048] in D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:599 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer.DeleteRows (System.Int32 oldStartingIndex, System.Int32 oldItemsCount, System.Int32 section) [0x00046] in D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:603 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer.UpdateItems (System.Collections.Specialized.NotifyCollectionChangedEventArgs e, System.Int32 section, System.Boolean resetWhenGrouped) [0x00119] in D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:542 
  at Xamarin.Forms.Platform.iOS.ListViewRenderer.OnCollectionChanged (System.Object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x00000] in D:\a\1\s\Xamarin.Forms.Platform.iOS\Renderers\ListViewRenderer.cs:332 
  at Xamarin.Forms.Internals.TemplatedItemsList`2[TView,TItem].OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x0000a] in <7d3a3d515f644268b15520ab8aaf1bfc>:0 
  at Xamarin.Forms.Internals.TemplatedItemsList`2[TView,TItem].OnProxyCollectionChanged (System.Object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x0045f] in <7d3a3d515f644268b15520ab8aaf1bfc>:0 
  at Xamarin.Forms.ListProxy.OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedEventArgs e) [0x0000a] in D:\a\1\s\Xamarin.Forms.Core\ListProxy.cs:232 
  at (wrapper other) System.Object.gsharedvt_out_sig(object&,intptr)
  at Xamarin.Forms.ListProxy+<>c__DisplayClass34_0.<OnCollectionChanged>b__0 () [0x00018] in D:\a\1\s\Xamarin.Forms.Core\ListProxy.cs:208 
  at (wrapper other) System.Object.__interp_lmf_mono_interp_entry_from_trampoline(intptr,intptr)
  at Foundation.NSAsyncActionDispatcher.Apply () [0x00000] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.16.0.13/src/Xamarin.iOS/Foundation/NSAction.cs:152 
  at (wrapper managed-to-native) UIKit.UIApplication.UIApplicationMain(int,string[],intptr,intptr)
  at UIKit.UIApplication.Main (System.String[] args, System.IntPtr principal, System.IntPtr delegate) [0x00005] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.16.0.13/src/Xamarin.iOS/UIKit/UIApplication.cs:86 
  at UIKit.UIApplication.Main (System.String[] args, System.String principalClassName, System.String delegateClassName) [0x0000e] in /Library/Frameworks/Xamarin.iOS.framework/Versions/13.16.0.13/src/Xamarin.iOS/UIKit/UIApplication.cs:65 
  at SDA.iOS.Application.Main (System.String[] args) [0x00001] in C:\Users\shodg\SDA\SDA\SDA.iOS\Main.cs:12 

Here is the relevant code in the viewmodel for adding/updating/removing devices from the ObservableRangeCollection:

        public ObservableRangeCollection<DeviceListItemViewModel> Devices { get; set; } = new ObservableRangeCollection<DeviceListItemViewModel>();

        private void OnDeviceAdded(BleDevice device)
        {
            Devices.Add(new DeviceListItemViewModel(device));
        }

        private void OnDeviceUpdated(BleDevice device)
        {
            Devices.ReplaceRange(Devices.OrderByDescending(x => x.Rssi).ToList());
        }

        private void OnDeviceExpired(BleDevice device)
        {
            var vm = Devices.FirstOrDefault(d => d.Id == device.Id);
            Devices.Remove(vm);
        }

It appears that the exception has to do with ListView updates on iOS, and I've seen some threads on problems with ObservableCollections in Xamarin.iOS, but I'm having trouble determining if this issue lies in the Montemagno plugin or elsewhere, and what the best path forward is.

Any insight would be most welcome!

Best Answer

  • stefanhkstefanhk ✭✭
    Accepted Answer

    Thanks! Yes, the BLE scan runs continuously, so it was likely producing a race condition where an item was being removed at the same time that the list was being updated with removed devices. Invoking on main thread and adding a lock to the property seems to keep that exception from throwing.

    In case anyone is interested, here's how I've set it up:

            private object _deviceListLock = new object();
            public ObservableRangeCollection<DeviceListItemViewModel> Devices { get; set; } = new ObservableRangeCollection<DeviceListItemViewModel>();
    
            private void OnDeviceAdded(BleDevice device)
            {
                InvokeOnMainThread(() =>
                {
                    lock (_deviceListLock)
                    {
                        Devices.Add(new DeviceListItemViewModel(device));
                    }
                });
            }
    
            private void OnDeviceUpdated(BleDevice device)
            {
                InvokeOnMainThread(() =>
                {
                    lock (_deviceListLock)
                    {
                        Devices.ReplaceRange(Devices.OrderByDescending(x => x.Rssi).ToList());
                    }
                });
            }
    
            private void OnDeviceExpired(BleDevice device)
            {
                InvokeOnMainThread(() =>
                {
                    lock (_deviceListLock)
                    {
                        var vm = Devices.FirstOrDefault(d => d.Id == device.Id);
                        Devices.Remove(vm);
                    }
                });
            }
    

    Out of curiosity, does anyone know why this technique would be necessary for iOS, but not for Android?

Answers

  • LandLuLandLu Member, Xamarin Team Xamurai

    What is this ReplaceRange used for?
    Perhaps it fires at the same time when OnDeviceExpired triggers.
    Therefore, the sorted method will get the wrong index as some items have been removed.
    We should try to avoid executing add, update and remove actions at the same time.

  • stefanhkstefanhk Member ✭✭
    Accepted Answer

    Thanks! Yes, the BLE scan runs continuously, so it was likely producing a race condition where an item was being removed at the same time that the list was being updated with removed devices. Invoking on main thread and adding a lock to the property seems to keep that exception from throwing.

    In case anyone is interested, here's how I've set it up:

            private object _deviceListLock = new object();
            public ObservableRangeCollection<DeviceListItemViewModel> Devices { get; set; } = new ObservableRangeCollection<DeviceListItemViewModel>();
    
            private void OnDeviceAdded(BleDevice device)
            {
                InvokeOnMainThread(() =>
                {
                    lock (_deviceListLock)
                    {
                        Devices.Add(new DeviceListItemViewModel(device));
                    }
                });
            }
    
            private void OnDeviceUpdated(BleDevice device)
            {
                InvokeOnMainThread(() =>
                {
                    lock (_deviceListLock)
                    {
                        Devices.ReplaceRange(Devices.OrderByDescending(x => x.Rssi).ToList());
                    }
                });
            }
    
            private void OnDeviceExpired(BleDevice device)
            {
                InvokeOnMainThread(() =>
                {
                    lock (_deviceListLock)
                    {
                        var vm = Devices.FirstOrDefault(d => d.Id == device.Id);
                        Devices.Remove(vm);
                    }
                });
            }
    

    Out of curiosity, does anyone know why this technique would be necessary for iOS, but not for Android?

Sign In or Register to comment.