2

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));
        }
    }
}
mins
  • 6,478
  • 12
  • 56
  • 75
  • 1
    Could you add the relevant code, ideally an [mcve]? Otherwise this will soon be closed as duplicate of [this canonical question](https://stackoverflow.com/questions/20940979/what-is-an-indexoutofrangeexception-argumentoutofrangeexception-and-how-do-i-f) – René Vogt Sep 05 '19 at 12:08
  • Since it seems to happen when rendering a cell, it does not necessarily depend on the _collection_, but on the (properties of the) item you add. – René Vogt Sep 05 '19 at 12:10
  • @RenéVogt: I'll do that (I know what is an out of bound case, this is not my question) – mins Sep 05 '19 at 12:10
  • 1
    Why not use ObservableCollection rather than List? – Kevin Cook Sep 05 '19 at 12:20
  • @KevinCook: It's to learn WPF and the purpose of this chapter is to replace ObservableCollection by a custom class. – mins Sep 05 '19 at 12:28
  • Interestingly, the error will not occur when binding a ListBox instead of a DataGrid. – Clemens Sep 05 '19 at 14:35

2 Answers2

4

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));
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • That fixed my problem. This data is missing in the book. Fortunately the book, which is below the standards, is written in French an won't hurt so many learners (: – mins Sep 05 '19 at 15:37
  • I'd strongly recommend to always look at the online documentation, e.g. here: https://learn.microsoft.com/en-us/dotnet/api/system.collections.specialized.notifycollectionchangedeventargs?view=netframework-4.8. It is generally very good. – Clemens Sep 05 '19 at 15:51
  • Yes, I read it first most of the time, but in the case of `NotifyCollectionChangedEventArgs`, you can see the documentation associated with the two constructors (with and without the object) is the same, nothing allows to understand when to use one or the other. WPF is a huge library, and frankly, I'm not yet comfortable with it. Books (I've read 2 or 3 other ones) can only describe the surface of it. – mins Sep 05 '19 at 19:08
  • Have you read "WPF Unleashed" by Adam Nathan? It's excellent. – Clemens Sep 05 '19 at 19:22
  • Thanks for the suggestion, I've read good recommendations for it. I started with Yosifovich's WPF Cookbook which I think is no bad either. – mins Sep 05 '19 at 19:50
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));
    }
}
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Could you explain the need to notify changes for Count and indexer? Is it valid for my case, or general good practices just in case the need arises? – mins Sep 05 '19 at 15:42
  • 1
    @mins It is how ObservableCollection is implemented, but not required. An ItemsControl like ListBox or DataGrid only needs the INotifyCollectionChanged implementation. – Clemens Sep 06 '19 at 07:14