Skip to content

Commit 5c06649

Browse files
vladimir-krestovRussKie
authored andcommitted
Fixing IndexOutOfRangeException throwing by DGV when disposing its DataSource (dotnet#4551)
to DataGridView and BindingNavigator. Fixes dotnet#4216 A DataGridView threw IndexOutOfRangeException when its DataSource is already disposed and the DataGridView try to redraw itself, because its Rows and Columns are not updated but DataSource is already released. The DataGridView try to draw rows that are not exist in DataConnection, it send some index (eg. 5) to items collection and catch the exception because this index is out of empty items collection range. Initially, the issue repoduced when a user closes a form with DataGridView and BindingNavigator, because the form disposes BindingSource when closing and then disposes BindingNavigator, that try to redraw DataGridView. It is due to BindingNavigator send UiaReturnRawElementProvider message to Windows and it redraws DGV sometimes (looks like a bug). We tried to cancel DGV redwawing if a form is closing. Then we found the second case: we cought this IndexOutOfRangeException if to just dispose DataSource without form closing. So the issue is DataGridView Rows and Columns are not updated when DataSource disposing. This fix uses Dispose events to set null for DataGridView.DataSource and BindingNavigator.BindingSource thereby call refresh of internal collections of their items. (cherry picked from commit 3f9c8e7)
1 parent 937b05a commit 5c06649

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

src/System.Windows.Forms/src/System/Windows/Forms/BindingNavigator.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,14 @@ private void OnBindingSourceStateChanged(object sender, EventArgs e)
767767
RefreshItemsInternal();
768768
}
769769

770+
/// <summary>
771+
/// Refresh tool strip items when the BindingSource is disposed.
772+
/// </summary>
773+
private void OnBindingSourceDisposed(object sender, EventArgs e)
774+
{
775+
BindingSource = null;
776+
}
777+
770778
/// <summary>
771779
/// Refresh tool strip items when something changes in the BindingSource's list.
772780
/// </summary>
@@ -895,6 +903,7 @@ private void WireUpBindingSource(ref BindingSource oldBindingSource, BindingSour
895903
oldBindingSource.DataSourceChanged -= new EventHandler(OnBindingSourceStateChanged);
896904
oldBindingSource.DataMemberChanged -= new EventHandler(OnBindingSourceStateChanged);
897905
oldBindingSource.ListChanged -= new ListChangedEventHandler(OnBindingSourceListChanged);
906+
oldBindingSource.Disposed -= new EventHandler(OnBindingSourceDisposed);
898907
}
899908

900909
if (newBindingSource != null)
@@ -905,6 +914,7 @@ private void WireUpBindingSource(ref BindingSource oldBindingSource, BindingSour
905914
newBindingSource.DataSourceChanged += new EventHandler(OnBindingSourceStateChanged);
906915
newBindingSource.DataMemberChanged += new EventHandler(OnBindingSourceStateChanged);
907916
newBindingSource.ListChanged += new ListChangedEventHandler(OnBindingSourceListChanged);
917+
newBindingSource.Disposed += new EventHandler(OnBindingSourceDisposed);
908918
}
909919

910920
oldBindingSource = newBindingSource;

src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.Methods.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14903,6 +14903,14 @@ protected virtual void OnDataSourceChanged(EventArgs e)
1490314903
}
1490414904
}
1490514905

14906+
/// <summary>
14907+
/// Refresh items when the DataSource is disposed.
14908+
/// </summary>
14909+
private void OnDataSourceDisposed(object sender, EventArgs e)
14910+
{
14911+
DataSource = null;
14912+
}
14913+
1490614914
protected virtual void OnDefaultCellStyleChanged(EventArgs e)
1490714915
{
1490814916
if (e is DataGridViewCellStyleChangedEventArgs dgvcsce && !dgvcsce.ChangeAffectsPreferredSize)

src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,6 +2029,16 @@ public object DataSource
20292029
{
20302030
if (value != DataSource)
20312031
{
2032+
if (DataSource is Component oldDataSource)
2033+
{
2034+
oldDataSource.Disposed -= OnDataSourceDisposed;
2035+
}
2036+
2037+
if (value is Component newDataSource)
2038+
{
2039+
newDataSource.Disposed += OnDataSourceDisposed;
2040+
}
2041+
20322042
CurrentCell = null;
20332043
if (DataConnection is null)
20342044
{

src/System.Windows.Forms/tests/UnitTests/BindingNavigatorTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Collections.Generic;
66
using System.ComponentModel;
7+
using System.Data;
78
using System.Diagnostics;
89
using Moq;
910
using Xunit;
@@ -129,5 +130,86 @@ public void BindingNavigator_ConstructorBool()
129130
Assert.Equal(bn.AddNewItem, bn.Items[index++]);
130131
Assert.Equal(bn.DeleteItem, bn.Items[index++]);
131132
}
133+
134+
[WinFormsFact]
135+
public void BindingNavigator_UpdatesItsItems_AfterDataSourceDisposing()
136+
{
137+
using BindingNavigator control = new BindingNavigator(true);
138+
int rowsCount = 5;
139+
BindingSource bindingSource = GetTestBindingSource(rowsCount);
140+
control.BindingSource = bindingSource;
141+
142+
Assert.Equal("1", control.PositionItem.Text);
143+
Assert.Equal($"of {rowsCount}", control.CountItem.Text);
144+
145+
bindingSource.Dispose();
146+
147+
// The BindingNavigator updates its PositionItem and CountItem values
148+
// after its BindingSource is disposed
149+
Assert.Equal("0", control.PositionItem.Text);
150+
Assert.Equal("of 0", control.CountItem.Text);
151+
}
152+
153+
[WinFormsFact]
154+
public void BindingNavigator_BindingSource_IsNull_AfterDisposing()
155+
{
156+
using BindingNavigator control = new BindingNavigator();
157+
BindingSource bindingSource = GetTestBindingSource(5);
158+
control.BindingSource = bindingSource;
159+
160+
Assert.Equal(bindingSource, control.BindingSource);
161+
162+
bindingSource.Dispose();
163+
164+
Assert.Null(control.BindingSource);
165+
}
166+
167+
[WinFormsFact]
168+
public void BindingNavigator_BindingSource_IsActual_AfterOldOneIsDisposed()
169+
{
170+
using BindingNavigator control = new BindingNavigator(true);
171+
int rowsCount1 = 3;
172+
BindingSource bindingSource1 = GetTestBindingSource(rowsCount1);
173+
int rowsCount2 = 5;
174+
BindingSource bindingSource2 = GetTestBindingSource(rowsCount2);
175+
control.BindingSource = bindingSource1;
176+
177+
Assert.Equal(bindingSource1, control.BindingSource);
178+
Assert.Equal("1", control.PositionItem.Text);
179+
Assert.Equal($"of {rowsCount1}", control.CountItem.Text);
180+
181+
control.BindingSource = bindingSource2;
182+
183+
Assert.Equal(bindingSource2, control.BindingSource);
184+
Assert.Equal("1", control.PositionItem.Text);
185+
Assert.Equal($"of {rowsCount2}", control.CountItem.Text);
186+
187+
bindingSource1.Dispose();
188+
189+
// bindingSource2 is actual for the BindingNavigator
190+
// so it will contain correct PositionItem and CountItem values
191+
// even after bindingSource1 is disposed.
192+
// This test checks that Disposed events unsubscribed correctly
193+
Assert.Equal(bindingSource2, control.BindingSource);
194+
Assert.Equal("1", control.PositionItem.Text);
195+
Assert.Equal($"of {rowsCount2}", control.CountItem.Text);
196+
}
197+
198+
private BindingSource GetTestBindingSource(int rowsCount)
199+
{
200+
DataTable dt = new DataTable();
201+
dt.Columns.Add("Name");
202+
dt.Columns.Add("Age");
203+
204+
for (int i = 0; i < rowsCount; i++)
205+
{
206+
DataRow dr = dt.NewRow();
207+
dr[0] = $"User{i}";
208+
dr[1] = i * 3;
209+
dt.Rows.Add(dr);
210+
}
211+
212+
return new() { DataSource = dt };
213+
}
132214
}
133215
}

src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataGridViewTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Numerics;
1212
using static System.Windows.Forms.Metafiles.DataHelpers;
1313
using static Interop;
14+
using System.Data;
1415

1516
namespace System.Windows.Forms.Tests
1617
{
@@ -2801,6 +2802,92 @@ public void DataGridView_OnRowHeadersWidthSizeModeChanged_NullE_ThrowsNullRefere
28012802
Assert.Throws<NullReferenceException>(() => control.OnRowHeadersWidthSizeModeChanged(null));
28022803
}
28032804

2805+
[WinFormsFact]
2806+
public void DataGridView_UpdatesItsItems_AfterDataSourceDisposing()
2807+
{
2808+
using DataGridView control = new DataGridView();
2809+
int rowsCount = 5;
2810+
BindingSource bindingSource = GetTestBindingSource(rowsCount);
2811+
BindingContext context = new BindingContext();
2812+
context.Add(bindingSource, bindingSource.CurrencyManager);
2813+
control.BindingContext = context;
2814+
control.DataSource = bindingSource;
2815+
2816+
// The TestBindingSource table contains 2 columns
2817+
Assert.Equal(2, control.Columns.Count);
2818+
// The TestBindingSource table contains some rows + 1 new DGV row (because AllowUserToAddRows is true)
2819+
Assert.Equal(rowsCount + 1, control.Rows.Count);
2820+
2821+
bindingSource.Dispose();
2822+
2823+
// The DataGridView updates its Rows and Columns collections after its DataSource is disposed
2824+
Assert.Empty(control.Columns);
2825+
Assert.Empty(control.Rows);
2826+
}
2827+
2828+
[WinFormsFact]
2829+
public void DataGridView_DataSource_IsNull_AfterDisposing()
2830+
{
2831+
using DataGridView control = new DataGridView();
2832+
BindingSource bindingSource = GetTestBindingSource(5);
2833+
control.DataSource = bindingSource;
2834+
2835+
Assert.Equal(bindingSource, control.DataSource);
2836+
2837+
bindingSource.Dispose();
2838+
2839+
Assert.Null(control.DataSource);
2840+
}
2841+
2842+
[WinFormsFact]
2843+
public void DataGridView_DataSource_IsActual_AfterOldOneIsDisposed()
2844+
{
2845+
using DataGridView control = new DataGridView();
2846+
int rowsCount1 = 3;
2847+
BindingSource bindingSource1 = GetTestBindingSource(rowsCount1);
2848+
int rowsCount2 = 5;
2849+
BindingSource bindingSource2 = GetTestBindingSource(rowsCount2);
2850+
BindingContext context = new BindingContext();
2851+
context.Add(bindingSource1, bindingSource1.CurrencyManager);
2852+
control.BindingContext = context;
2853+
control.DataSource = bindingSource1;
2854+
2855+
Assert.Equal(bindingSource1, control.DataSource);
2856+
Assert.Equal(2, control.Columns.Count);
2857+
Assert.Equal(rowsCount1 + 1, control.Rows.Count); // + 1 is the new DGV row
2858+
2859+
control.DataSource = bindingSource2;
2860+
2861+
Assert.Equal(bindingSource2, control.DataSource);
2862+
Assert.Equal(2, control.Columns.Count);
2863+
Assert.Equal(rowsCount2 + 1, control.Rows.Count); // + 1 is the new DGV row
2864+
2865+
bindingSource1.Dispose();
2866+
2867+
// bindingSource2 is actual for the DataGridView so it will contain correct Rows and Columns counts
2868+
// even after bindingSource1 is disposed. This test checks that Disposed events unsubscribed correctly
2869+
Assert.Equal(bindingSource2, control.DataSource);
2870+
Assert.Equal(2, control.Columns.Count);
2871+
Assert.Equal(rowsCount2 + 1, control.Rows.Count); // + 1 is the new DGV row
2872+
}
2873+
2874+
private BindingSource GetTestBindingSource(int rowsCount)
2875+
{
2876+
DataTable dt = new DataTable();
2877+
dt.Columns.Add("Name");
2878+
dt.Columns.Add("Age");
2879+
2880+
for (int i = 0; i < rowsCount; i++)
2881+
{
2882+
DataRow dr = dt.NewRow();
2883+
dr[0] = $"User{i}";
2884+
dr[1] = i * 3;
2885+
dt.Rows.Add(dr);
2886+
}
2887+
2888+
return new() { DataSource = dt };
2889+
}
2890+
28042891
private class SubDataGridViewCell : DataGridViewCell
28052892
{
28062893
}

0 commit comments

Comments
 (0)