Removing multiple rows while selecting the last new-item row in DataGridView leads to exception
- Dominant language
- C#
- Stars
- 4.9k
- Forks
- 1.1k
- Avg merge
- 20h 23m
- Merged PRs (30d)
- 103
Description
### .NET version
net10.0-windows
### Did it work in .NET Framework?
Yes (as in, the exception occurs the same way)
### Did it work in any of the earlier releases of .NET Core or .NET 5+?
net9.0-windows (also the same bug)
### Issue description
Select multiple rows in a `DataGridView`, including the last empty `*` row (for adding new items), and remove them by pressing Delete. Click another row or cell to make the grid lose focus. Then, clicking the `*` row in the grid again immediately causes an exception.
Exception
```
System.InvalidOperationException: Operation is not valid due to the current state of the object.
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.ProcessListChanged(ListChangedEventArgs e)
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.currencyManager_ListChanged(Object sender, ListChangedEventArgs e)
at System.Windows.Forms.CurrencyManager.List_ListChanged(Object sender, ListChangedEventArgs e)
at System.ComponentModel.BindingList`1.AddNewCore()
at System.ComponentModel.BindingList`1.System.ComponentModel.IBindingList.AddNew()
at System.Windows.Forms.CurrencyManager.AddNew()
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.AddNew()
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.OnNewRowNeeded()
at System.Windows.Forms.DataGridView.OnRowEnter(DataGridViewCell& dataGridViewCell, Int32 columnIndex, Int32 rowIndex, Boolean canCreateNewRow, Boolean validationFailureOccurred)
at System.Windows.Forms.DataGridView.SetCurrentCellAddressCore(Int32 columnIndex, Int32 rowIndex, Boolean setAnchorCellAddress, Boolean validateCurrentCell, Boolean throughMouseClick)
at System.Windows.Forms.DataGridView.OnCellMouseDown(HitTestInfo hti, Boolean isShiftDown, Boolean isControlDown)
at System.Windows.Forms.DataGridView.OnCellMouseDown(DataGridViewCellMouseEventArgs e)
at System.Windows.Forms.DataGridView.OnMouseDown(MouseEventArgs e)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(HWND hWnd, UInt32 msg, WPARAM wparam, LPARAM lparam)
```
No extra event handlers, overrides, or non-standard configuration are required. This occurs with a simple `DataGridView` bound to a `BindingList` with Plain-old-data objects that WF the grid can instantiate (i.e. they have a default ctor).
### Steps to reproduce
Minimal repro solution with a few additional conveniences: [WFGridPhantomItemBugRepro10.zip](https://github.com/user-attachments/files/32206689/WFGridPhantomItemBugRepro10.zip). _The name reflects the fact that, when the bug is triggered, an extra default-initialized item is added to the underlying list, which isn't shown in the grid. More details below._
https://github.com/user-attachments/assets/f786a6ff-5117-4a12-8c85-509687e19926
#### Code
1. Have a `DataGridView`, let's call it `grid`.
2. Have a `BindingList` that can support adding items, let's call it `list`.
3. Bind `grid.DataSource = list`.
#### Runtime
1. Have (at least) two items in the grid. For this example, let's add 3, called A, B, C.
2. Using the leftmost (row-selector) column, select the row with item C. Drag downwards to also select the empty `*` row for adding new items.
3. Press Delete, leaving A, B in the grid
4. Click A or B to deselect the `*` row.
5. Click the `*` row in the grid again to try to add a new item.
This should immediately trigger a runtime exception:
```
System.InvalidOperationException: Operation is not valid due to the current state of the object.
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.ProcessListChanged(ListChangedEventArgs e)
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.currencyManager_ListChanged(Object sender, ListChangedEventArgs e)
at System.Windows.Forms.CurrencyManager.List_ListChanged(Object sender, ListChangedEventArgs e)
at System.ComponentModel.BindingList`1.AddNewCore()
at System.ComponentModel.BindingList`1.System.ComponentModel.IBindingList.AddNew()
at System.Windows.Forms.CurrencyManager.AddNew()
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.AddNew()
at System.Windows.Forms.DataGridView.DataGridViewDataConnection.OnNewRowNeeded()
at System.Windows.Forms.DataGridView.OnRowEnter(DataGridViewCell& dataGridViewCell, Int32 columnIndex, Int32 rowIndex, Boolean canCreateNewRow, Boolean validationFailureOccurred)
at System.Windows.Forms.DataGridView.SetCurrentCellAddressCore(Int32 columnIndex, Int32 rowIndex, Boolean setAnchorCellAddress, Boolean validateCurrentCell, Boolean throughMouseClick)
at System.Windows.Forms.DataGridView.OnCellMouseDown(HitTestInfo hti, Boolean isShiftDown, Boolean isControlDown)
at System.Windows.Forms.DataGridView.OnCellMouseDown(DataGridViewCellMouseEventArgs e)
at System.Windows.Forms.DataGridView.OnMouseDown(MouseEventArgs e)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(HWND hWnd, UInt32 msg, WPARAM wparam, LPARAM lparam)
```
Abridged minimal repro code:
```csharp
public partial class Form1 : Form
{
private BindingList list = [];
public Form1()
{
InitializeComponent();
grid.DataSource = list; // grid is DataGridView, needs to support adding items
}
}
public class TestItem
{
public string? Val { get; set; }
}
```
### Additional remarks
It looks like the `*` row must be selected _last_, and at least one other row must be selected. From testing, the bug occurs when selecting (and then deleting):
- Rows `i..*` (start with any row, drag-select all the way down)
- Rows `{a, b, ..., *}` (Ctrl-select any rows, ending with the `*` row)
- All rows by clicking the top-left "fake" cell (I guess this might be like selecting `0..*`?)
It does _not_ happen when selecting:
- Rows `*..i` (a reverse range: start with the bottom-most row, drag-select up to any previous row)
- Rows `{..., *, ..., z`} (the `*` row must be selected last for the bug to occur)
- Rows `i..j` (again, the `*` row must be selected when deleting)
- Only row `*` (I don't think this actually triggers any deletion when pressing Delete)
After bringing the grid into the bugged state (but before triggering the exception by clicking the `*` row again), observing the underlying `BindingList` reveals that an extra default-initialized item is present at the end. Notably, this extra item is _not shown_ in the grid. This extra item is normally inserted when interacting with the `*` row, but deselecting the row without actually adding the item always removes it again. It looks like, when selecting multiple rows with `*` as the last, this extra placeholder item is added, but pressing Delete then fails to remove it again. When clicking the `*` row once again after triggering the bug, a second default-initialized item is added (observable in `grid.LostFocus`), after which the exception is raised.
Peeking into the debugger, it looks like the following assertion fails:
```csharp
case ListChangedType.ItemAdded:
if (_owner.NewRowIndex == -1 || e.NewIndex != _owner.Rows.Count)
{
_owner.Rows.InsertInternal(e.NewIndex, _owner.RowTemplateClone, force: true);
}
else
{
throw new InvalidOperationException();
}
```
In particular, `e.NewIndex` is equal to `_owner.Rows.Count`. In my case, at this point, both are `3`, but `list.Count == 4` (`A, B, default, default`). I suspect that this, along with the fact that the previous default-initialized placeholder item is not displayed, implies some internal desync regarding RowCollection count and the actual number of items in the BindingList.
Originally, we ran into this bug on Framework 4.8. I reproduced the bug in isolation, and then with (effectively) the same code in .NET 9.0. Notably, in 10.0, one small detail changed slightly: after triggering the bug in 9.0 (or Framework 4.8), deselecting the `*` row by clicking another button Control works, but in 10.0 it doesn't. In 9.0, clicking the button caused the grid to move the `>` selected item indicator to the previous item (the last visible item in the grid), but 10.0 doesn't do this - instead, you need to explicitly select another row or click another cell first to be able to cause the exception. It's worth noting that 10.0 _does_ still move the selection from `*` to the previous row if this deletion bug isn't triggered.
Contributor guide
Research direction
Start with the linked WFGridPhantomItemBugRepro10.zip and reproduce the failure using a DataGridView bound to a BindingList. Read DataGridView.DataGridViewDataConnection.ProcessListChanged and the OnNewRowNeeded path, focusing on the ItemAdded assertion and the differing grid and list counts. Done means the described selection and deletion sequence no longer throws and the underlying list and displayed rows remain synchronized.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100