Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clear ObservableCollection throws exception

I have a Xamarin app implementing a search functionality where results are grouped. Therefore I used a Grouped Listview.

private async void SearchRecipient()
{
    IList<Recipient> recipients = null;

    if (!string.IsNullOrWhiteSpace(RecipientSearched))
    {   
        recipients = await _service.GetRecipients(RecipientSearched.ToLower());

        FilteredRecipients.Clear();
        _userGroupedList.Clear();
        _officeGroupedList.Clear();

        if (recipients != null)
        {
            foreach (var r in recipients)
            {
                // Some logic to populate collections
                _userGroupedList.Add(selectable);
                _officeGroupedList.Add(selectable);
            }

            if (_userGroupedList.Count > 0)
                FilteredRecipients.Add(_userGroupedList);
            if (_officeGroupedList.Count > 0)
                FilteredRecipients.Add(_officeGroupedList);
        }
    }
}

FilteredRecipients is an ObservableCollection, while _userGroupedList and _officeGroupedList are List.

public SearchRecipientPageModel()
{
    FilteredRecipients = new ObservableCollection<GroupedRecipientModel>();
    _userGroupedList = new GroupedRecipientModel("User");
    _officeGroupedList = new GroupedRecipientModel("Office");
}

Search works and grouping as well. The problem happens when I repeat a search a second time and FilteredRecipients.Clear() throws the following exception:

System.ArgumentOutOfRangeException: 'Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index'

UPDATE Problems seems to happen only when some item of the result is selected with a checkbox. I think is due to my Checkbox Renderer implementation, because I substituted Checkbox with a Switch and it seems to work. I have had some problems to make it working in TwoWay Mode Binding, but maybe I didn't fix it correctly.

    public class CustomCheckBox : View
{
    public bool Checked
    {
        get => (bool)GetValue(CheckedProperty);
        set => SetValue(CheckedProperty, value);
    }

    public ICommand Command
    {
        get => (ICommand)GetValue(CommandProperty);
        set => SetValue(CommandProperty, value);
    }

    public object CommandParameter
    {
        get => GetValue(CommandParameterProperty);
        set => SetValue(CommandParameterProperty, value);
    }

    public static readonly BindableProperty CommandParameterProperty =
                                BindableProperty.Create("CommandParameter", typeof(object), typeof(CustomCheckBox), default(object));

    public static readonly BindableProperty CheckedProperty =
                                BindableProperty.Create("Checked", typeof(bool), typeof(CustomCheckBox), default(bool), propertyChanged: OnChecked);

    public static readonly BindableProperty CommandProperty =
                BindableProperty.Create("Command", typeof(ICommand), typeof(CustomCheckBox), default(ICommand));


    private static void OnChecked(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is CustomCheckBox checkbox)
        {
            object parameter = checkbox.CommandParameter ?? newValue;

            if (checkbox.Command != null && checkbox.Command.CanExecute(parameter))
                checkbox.Command.Execute(parameter);
        }
    }
}

Renderer

public class CustomCheckBoxRenderer : ViewRenderer<CustomCheckBox, CheckBox>
{
    protected override void OnElementChanged(ElementChangedEventArgs<CustomCheckBox> e)
    {
        base.OnElementChanged(e);
        if (Control == null && Element != null)
            SetNativeControl(new CheckBox());

        if (Control != null)
        {                
            Control.IsChecked = Element.Checked;
            Control.Checked += (s, r) => { Element.Checked = true; };
            Control.Unchecked += (s, r) => { Element.Checked = false; };
        }
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if (e.PropertyName == nameof(Element.Checked))
            Control.IsChecked = Element.Checked;
    }
}

Moreover I am still investigating a bug of this renderer since Checked event is raised twice each time.

like image 809
user2297037 Avatar asked Nov 17 '17 09:11

user2297037


2 Answers

Scary this error is still being observed in iOS ObservableCollection.Clear() after 2 years! (android has no problem with this.) It was very hard to reproduce but I noticed it happens inside some async methods and anonymous functions. (I used those often on commands inside my viewModels)

Part of the solution that solved it for me was wrapping in BeginInvokeOnMainThread().

Device.BeginInvokeOnMainThread(() =>
{
    FilteredRecipients.Clear();
});

Instead of creating a custom ObservableCollection i created an extension method:

public static void SafeClear<T>(this ObservableCollection<T> observableCollection)
{
    if(!MainThread.IsMainThread)
    {
        Device.BeginInvokeOnMainThread(() =>
        {
            while (observableCollection.Any())
            {
                 ObservableCollection.RemoveAt(0);
            }
        });
    }
    else
    {
        while (observableCollection.Any())
        {
             ObservableCollection.RemoveAt(0);
        }
    }
}

So in the OP's example, he would call the extension method like so:

FilteredRecipients.SafeClear();
_userGroupedList.SafeClear();
_officeGroupedList.SafeClear();

EDIT - After further testing, clear() was still crashing with out of index error. using @Павел Воронцов 's answer in combination with an extension method is what we went with in the end.

like image 62
Solarcloud Avatar answered Nov 15 '22 14:11

Solarcloud


You can just use this ObservableCollection to avoid issues with clearing:

public class SafeObservableCollection<T> : ObservableCollection<T>
{
    /// <summary>
    /// Normal ObservableCollection fails if you are trying to clear ObservableCollection<ObservableCollection<T>> if there is data inside
    /// this is workaround till it won't be fixed in Xamarin Forms
    /// </summary>
    protected override void ClearItems()
    {
        while (this.Items.Any())
        {
            this.Items.RemoveAt(0);
        }
    }
}
like image 25
Павел Воронцов Avatar answered Nov 15 '22 16:11

Павел Воронцов