SimonG
SimonG

Reputation: 322

FirstOrDefault on ObservableCollection gets an ArgumentOutOfRange Exception

I've got an Observable Collection CurrentItemSource where I get an ArgumentOutOfRangeException when I try to call FirstOrDefault on it.

CommandViewModel item = CurrentItemSource?.FirstOrDefault();

I don't understand how this is possible since this Microsoft Article describes that the only Exception that could be thrown from a FirstOrDefault is an ArgumentNullException.

The Exception I get:

ArgumentOutOfRangeException [2]: Der Index lag außerhalb des Bereichs. Er darf nicht negativ und kleiner als die Auflistung sein. Parametername: index

How can that happen and are there any possibilities to fix this problem?

I instantiate the CurrentItemSource in the Constructor:

CurrentItemSource = new ObservableCollection<CommandViewModel>();

if (Application.Current != null)
{
    Application.Current.Dispatcher.BeginInvoke(new Action(() => 
    { 
        CurrentItemSource.EnableCollectionSynchronization();
    }));
}

Then I also enable CollectionSynchronization to have a thread-safe ObservableCollection.

I also have a Binding in my XAML-File:

<ComboBox ItemsSource="{Binding CurrentItemSource}"/>

The Context of my FirstOrDefault-Call:

if (ComboBoxText.IsNullOrEmpty())
{
    //Do Something
}
else
{
    CommandViewModel item = CurrentItemSource?.FirstOrDefault(); //Original Line

    if (item != null)
    {
        if (item is Type1)
        {
            //Do Something
        }
        else if (item is Type2)
        {
            //Do Something else
        }
    }   
}

Stacktrace:

Stacktrace [2]
       bei System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
       bei System.Collections.Generic.List1.get_Item(Int32 index)
       bei System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable1 source)
       bei Gui.ViewModels.ComboBoxViewModel.SetDisplayedText(String comboBoxText) in D:\workspace\Gui\ViewModels\ComboBoxViewModel.cs:Zeile 707.
       bei Gui.ViewModels.ComboBoxViewModel.set_ComboBoxText(String value) in D:\workspace\Gui\ViewModels\ComboBoxViewModel.cs:Zeile 209.
       bei Gui.ViewModels.OutputViewModel.CreateContextMenu(ComboBoxViewModel comboBox) in D:\workspace\Gui\ViewModels\OutputViewModel.cs:Zeile 160.
       bei Gui.ViewModels.ComboBoxViewModel.CreateContextMenuAsync() in D:\workspace\Gui\ViewModels\ComboBoxViewModel.cs:Zeile 1051.
       bei Gui.ViewModels.ComboBoxViewModel.<BuildCurrentItemSource>b__135_0() in D:\workspace\Gui\ViewModels\ComboBoxViewModel.cs:Zeile 1031.
       bei System.Threading.Tasks.Task.Execute()
    --- Ende der Stapelüberwachung vom vorhergehenden Ort, an dem die Ausnahme ausgelöst wurde ---
       bei System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
       bei System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
       bei System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
       bei Gui.ViewModels.ComboBoxViewModel.<BuildCurrentItemSource>d__135.MoveNext() in D:\workspace\Gui\ViewModels\ComboBoxViewModel.cs:Zeile 1031.

Upvotes: 0

Views: 1977

Answers (1)

Water Cooler v2
Water Cooler v2

Reputation: 33880

A brief summary of the cause for the exception

Your code is throwing an exception because of thread-unsafe access to the underlying IList<T> that the ObservableCollection represents.

The EnableCollectionSynchronization does not provide thread-safety of its own. It merely guarantees that the CustomView will use the same synchronization technique for accessing the elements of the collection in a thread-safe manner as the one you provide.

Note that the documentation reads:

While you must synchronize your application's access to the collection, you must also guarantee that access from WPF (specifically from CollectionView) participates in the same synchronization mechanism. You do this by calling the EnableCollectionSynchronization method.

Solution

Since you are using the parameterless overload, you need to secure your access to the observable collection via a lock statement (or the System.Threading.Monitor class).

Therefore, you must do this:

// class level field
private object _lock = new object();

...

CommandViewModel item = null;

lock(_lock)
{
  item = CurrentItemSource?.FirstOrDefault();
}

More explanation about the cause of the exception

The source of the exception is most likely one of the two following places. If you post the stack trace, you may see that it has in its path one of the two code paths:

One: System.Linq.Enumerable.FirstOrDefault<TSource>()

The if condition that checks for the nullability of the underlying list and then indexes.

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source)
{
    IList<TSource> list = source as IList<TSource>;

    // The following is re-entrant by multiple threads
    // in a thread-unsafe manner.
    if (list != null)
    {
        if (list.Count > 0)
        {
            // This code is contentious and most likely
            // the place where your code bombs.
            return list[0];
        }
    }
    else
        ...

Two: In the MoveNext method of the System.Collections.Generic.List<T>+Enumerator<T> class

Since you call the parameterless constructor of the ObservableCollection class, it defaults to using a System.Collections.Generic.List<T>, which is thread-unsafe by default. And when any code that you or the WPF framework wrote foreaches over the collection, the MoveNext method of the List<T> class is called, which calls an implementation of the MoveNext method inside a private / nested class named Enumerator<T>. This may be another place where the thread-unsafe code could throw the ArgumentOutOfRangeException.

public bool MoveNext()
{
    List<T> list = this.list;

    // This if condition might be entered into by a second thread
    // after the first thread modified the list
    if ((this.version == list._version) && (this.index < list._size))
    {
        // This line is contentious and might be
        // the source of your exception.
        this.current = list._items[this.index];


        this.index++;
        return true;
    }
    return this.MoveNextRare();
}

As you have now posted the stack trace, you see that it bombs off in the first of the two possible code paths I listed above. That is, in the System.Linq.Enumerable.FirstOfDefault method body.

Upvotes: 2

Related Questions