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:
DataGrid
to a non 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));
}
}
}
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));
}
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));
}
}
User contributions licensed under CC BY-SA 3.0