Index ArgumentOutOfRangeException when DataGrid refreshes itself after an item is added

0

I'm using DataGrid to display a custom collection PersonCollection : List<Person>, INotifyCollectionChanged (just learning WPF with painful examples from a book). The program works if I add items to an initially empty collection, and also when I remove items from a collection created with several items. In those cases the DataGrid is correctly updated.

However there is a scenario leading to an ArgumentOutOfRangeException exception:

  • Bind DataGrid to a non empty collection.
  • Remove all items from the collection.
  • Add an item to the empty collection.

In that case I get this exception:

System.ArgumentOutOfRangeException
  HResult=0x80131502
  Message=Specified argument was out of the range of valid values.
Parameter name: index
  Source=PresentationFramework
  StackTrace:
   at System.Windows.Controls.ItemCollection.GetItemAt(Int32 index)
   at System.Windows.Controls.VirtualizedCellInfoCollection.Contains(DataGridCell cell)
   at System.Windows.Controls.DataGridCell.PrepareCell(Object item, DataGridRow ownerRow, Int32 index)
   at System.Windows.Controls.Primitives.DataGridCellsPresenter.PrepareContainerForItemOverride(DependencyObject element, Object item)
   at System.Windows.Controls.ItemsControl.MS.Internal.Controls.IGeneratorHost.PrepareItemContainer(DependencyObject container, Object item)
   at System.Windows.Controls.ItemContainerGenerator.System.Windows.Controls.Primitives.IItemContainerGenerator.PrepareItemContainer(DependencyObject container)
   at System.Windows.Controls.DataGridCellsPanel.InsertContainer(Int32 childIndex, UIElement container, Boolean isRecycled)
   at System.Windows.Controls.DataGridCellsPanel.AddContainerFromGenerator(Int32 childIndex, UIElement child, Boolean newlyRealized)
   at System.Windows.Controls.DataGridCellsPanel.GenerateChild(IItemContainerGenerator generator, Size constraint, DataGridColumn column, Int32& childIndex, Size& childSize)
   at System.Windows.Controls.DataGridCellsPanel.GenerateChildren(IItemContainerGenerator generator, Int32 startIndex, Int32 endIndex, Size constraint)
   at System.Windows.Controls.DataGridCellsPanel.GenerateAndMeasureChildrenForRealizedColumns(Size constraint)
   at System.Windows.Controls.DataGridCellsPanel.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at MS.Internal.Helper.MeasureElementWithSingleChild(UIElement element, Size constraint)
   at System.Windows.Controls.ItemsPresenter.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Control.MeasureOverride(Size constraint)
   at System.Windows.Controls.Primitives.DataGridCellsPresenter.MeasureOverride(Size availableSize)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Grid.MeasureCell(Int32 cell, Boolean forceInfinityV)
   at System.Windows.Controls.Grid.MeasureCellsGroup(Int32 cellsHead, Size referenceSize, Boolean ignoreDesiredSizeU, Boolean forceInfinityV, Boolean& hasDesiredSizeUChanged)
   at System.Windows.Controls.Grid.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Border.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.Control.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.Controls.VirtualizingStackPanel.MeasureChild(IItemContainerGenerator& generator, IContainItemStorage& itemStorageProvider, IContainItemStorage& parentItemStorageProvider, Object& parentItem, Boolean& hasUniformOrAverageContainerSizeBeenSet, Double& computedUniformOrAverageContainerSize, Double& computedUniformOrAverageContainerPixelSize, Boolean& computedAreContainersUniformlySized, Boolean& hasAnyContainerSpanChanged, IList& items, Object& item, IList& children, Int32& childIndex, Boolean& visualOrderChanged, Boolean& isHorizontal, Size& childConstraint, Rect& viewport, VirtualizationCacheLength& cacheSize, VirtualizationCacheLengthUnit& cacheUnit, Boolean& foundFirstItemInViewport, Double& firstItemInViewportOffset, Size& stackPixelSize, Size& stackPixelSizeInViewport, Size& stackPixelSizeInCacheBeforeViewport, Size& stackPixelSizeInCacheAfterViewport, Size& stackLogicalSize, Size& stackLogicalSizeInViewport, Size& stackLogicalSizeInCacheBeforeViewport, Size& stackLogicalSizeInCacheAfterViewport, Boolean& mustDisableVirtualization, Boolean isBeforeFirstItem, Boolean isAfterFirstItem, Boolean isAfterLastItem, Boolean skipActualMeasure, Boolean skipGeneration, Boolean& hasBringIntoViewContainerBeenMeasured, Boolean& hasVirtualizingChildren)
   at System.Windows.Controls.VirtualizingStackPanel.MeasureOverrideImpl(Size constraint, Nullable`1& lastPageSafeOffset, List`1& previouslyMeasuredOffsets, Nullable`1& lastPagePixelSize, Boolean remeasure)
   at System.Windows.Controls.VirtualizingStackPanel.MeasureOverride(Size constraint)
   at System.Windows.Controls.Primitives.DataGridRowsPresenter.MeasureOverride(Size constraint)
   at System.Windows.FrameworkElement.MeasureCore(Size availableSize)
   at System.Windows.UIElement.Measure(Size availableSize)
   at System.Windows.ContextLayoutManager.UpdateLayout()
   at System.Windows.ContextLayoutManager.UpdateLayoutCallback(Object arg)
   at System.Windows.Media.MediaContext.InvokeOnRenderCallback.DoWork()
   at System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks()
   at System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget)
   at System.Windows.Media.MediaContext.RenderMessageHandler(Object resizedCompositionTarget)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run(Window window)
   at System.Windows.Application.Run()
   at BindingCollection.App.Main()

I'm unable to understand the reason of this exception which occurs when the DataGrid redrawn itself after the new item has been added to the previously emptied collection, and I can't see the invalid value of index in VisualStudio debugger.

XAML:

<Window x:Class="WpfApp1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApp1"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">

<StackPanel>
    <DataGrid x:Name="grid"
              Width="500" Margin="10" HorizontalAlignment="Center"
              AutoGenerateColumns="True"
              ItemsSource="{Binding Collection}"/>

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
        <Button Margin="10" Content="Add" Click="AddClick"/>
        <Button Margin="10" Content="Delete" Click="DeleteClick"/>
    </StackPanel>
</StackPanel>
</Window>

C#:

using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace WpfApp1 {
    public partial class MainWindow : Window {
        ViewModel viewModel = new ViewModel ();

        public MainWindow () {
            InitializeComponent ();
            DataContext = viewModel;
        }

        private void AddClick (object sender, RoutedEventArgs e) {
            viewModel.AddNew ();
        }

        private void DeleteClick (object sender, RoutedEventArgs e) {
            int index = grid.SelectedIndex;
            if (index < 0) {
                return;
            }
            viewModel.RemoveAt (index);
        }
    }

    public class Person : INotifyPropertyChanged {
        string firstName;
        public event PropertyChangedEventHandler PropertyChanged;

        public string FirstName {
            get => firstName;
            set { firstName = value; OnPropertyChanged (); }
        }

        public Person (string firstName) {
            FirstName = firstName;
        }

        protected virtual void OnPropertyChanged ([CallerMemberName] string propertyName = null) {
            PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (propertyName));
        }
    }

    public class PersonCollection : List<Person>, INotifyCollectionChanged {
        public event NotifyCollectionChangedEventHandler CollectionChanged;

        public new void Add (Person person) {
            base.Add (person);
            NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs (
                NotifyCollectionChangedAction.Add, person);
            OnCollectionChanged (e);
        }

        public new void RemoveAt (int index) {
            Person person = base[index];
            base.RemoveAt (index);
            NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs (
                NotifyCollectionChangedAction.Remove, person);
            OnCollectionChanged (e);
        }

        protected virtual void OnCollectionChanged (NotifyCollectionChangedEventArgs e) {
            CollectionChanged?.Invoke (this, e);
        }
    }

    class ViewModel : INotifyPropertyChanged {
        private PersonCollection collection;
        public event PropertyChangedEventHandler PropertyChanged;

        public PersonCollection Collection {
            get => collection;
            set {
                collection = value;
                OnPropertyChanged ();
            }
        }

        public ViewModel () {
            Collection = new PersonCollection { new Person ("Joe") };
        }

        public void AddNew () {
            Collection.Add (new Person ("Default name"));
        }

        public void RemoveAt (int index) {
            Collection.RemoveAt (index);
        }

        protected virtual void OnPropertyChanged ([CallerMemberName] string propertyName = null) {
            PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (propertyName));
        }
    }
}
wpf
datagrid
asked on Stack Overflow Sep 5, 2019 by mins • edited Sep 5, 2019 by mins

2 Answers

2

When you remove an item, you should also pass its index to the NotifyCollectionChangedEventArgs constructor:

public new void RemoveAt(int index)
{
    var element = this[index];
    base.RemoveAt(index);

    CollectionChanged?.Invoke(this,
        new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Remove, element, index));
}

While not strictly necessary in your special case, you may of course also pass the index of an added element:

public new void Add(T element)
{
    base.Add(element);

    CollectionChanged?.Invoke(this,
        new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Add, element, Count - 1));
}
answered on Stack Overflow Sep 5, 2019 by Clemens • edited Sep 5, 2019 by Clemens
1

If you want to mimic the behaviour of an ObservableCollection<T>, you should implement the INotifyPropertyChanged interface and raise change notifications for the Count and indexer properties. You should also include the index of the added or removed item in the NotifyCollectionChangedEventArgs for the data-bound ItemsControl to work as expected:

public class PersonCollection : List<Person>, INotifyCollectionChanged, INotifyPropertyChanged
{
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    public event PropertyChangedEventHandler PropertyChanged;

    public new void Add(Person person)
    {
        int index = Count;
        base.Add(person);
        NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Add, person, Count);
        OnPropertyChanged("Count");
        OnPropertyChanged("Item[]");
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, person, index));
    }

    public new void RemoveAt(int index)
    {
        Person person = base[index];
        base.RemoveAt(index);
        OnPropertyChanged("Count");
        OnPropertyChanged("Item[]");
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, person, index));
    }

    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        CollectionChanged?.Invoke(this, e);
    }

    private void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
answered on Stack Overflow Sep 5, 2019 by mm8

User contributions licensed under CC BY-SA 3.0