diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/BindingNavigator.cs b/src/System.Windows.Forms/src/System/Windows/Forms/BindingNavigator.cs index 59c4a0029af..6a9a8dfd7d4 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/BindingNavigator.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/BindingNavigator.cs @@ -767,6 +767,14 @@ private void OnBindingSourceStateChanged(object sender, EventArgs e) RefreshItemsInternal(); } + /// + /// Refresh tool strip items when the BindingSource is disposed. + /// + private void OnBindingSourceDisposed(object sender, EventArgs e) + { + BindingSource = null; + } + /// /// Refresh tool strip items when something changes in the BindingSource's list. /// @@ -895,6 +903,7 @@ private void WireUpBindingSource(ref BindingSource oldBindingSource, BindingSour oldBindingSource.DataSourceChanged -= new EventHandler(OnBindingSourceStateChanged); oldBindingSource.DataMemberChanged -= new EventHandler(OnBindingSourceStateChanged); oldBindingSource.ListChanged -= new ListChangedEventHandler(OnBindingSourceListChanged); + oldBindingSource.Disposed -= new EventHandler(OnBindingSourceDisposed); } if (newBindingSource != null) @@ -905,6 +914,7 @@ private void WireUpBindingSource(ref BindingSource oldBindingSource, BindingSour newBindingSource.DataSourceChanged += new EventHandler(OnBindingSourceStateChanged); newBindingSource.DataMemberChanged += new EventHandler(OnBindingSourceStateChanged); newBindingSource.ListChanged += new ListChangedEventHandler(OnBindingSourceListChanged); + newBindingSource.Disposed += new EventHandler(OnBindingSourceDisposed); } oldBindingSource = newBindingSource; diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.Methods.cs b/src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.Methods.cs index 725b43104ff..53cd5eeaf4b 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.Methods.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.Methods.cs @@ -14903,6 +14903,14 @@ protected virtual void OnDataSourceChanged(EventArgs e) } } + /// + /// Refresh items when the DataSource is disposed. + /// + private void OnDataSourceDisposed(object sender, EventArgs e) + { + DataSource = null; + } + protected virtual void OnDefaultCellStyleChanged(EventArgs e) { if (e is DataGridViewCellStyleChangedEventArgs dgvcsce && !dgvcsce.ChangeAffectsPreferredSize) diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.cs b/src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.cs index 435e0db6979..a3a01730fe6 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/DataGridView.cs @@ -2029,6 +2029,16 @@ public object DataSource { if (value != DataSource) { + if (DataSource is Component oldDataSource) + { + oldDataSource.Disposed -= OnDataSourceDisposed; + } + + if (value is Component newDataSource) + { + newDataSource.Disposed += OnDataSourceDisposed; + } + CurrentCell = null; if (DataConnection is null) { diff --git a/src/System.Windows.Forms/tests/UnitTests/BindingNavigatorTests.cs b/src/System.Windows.Forms/tests/UnitTests/BindingNavigatorTests.cs index bdebec7d348..f820bc7041d 100644 --- a/src/System.Windows.Forms/tests/UnitTests/BindingNavigatorTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/BindingNavigatorTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel; +using System.Data; using System.Diagnostics; using Moq; using Xunit; @@ -129,5 +130,86 @@ public void BindingNavigator_ConstructorBool() Assert.Equal(bn.AddNewItem, bn.Items[index++]); Assert.Equal(bn.DeleteItem, bn.Items[index++]); } + + [WinFormsFact] + public void BindingNavigator_UpdatesItsItems_AfterDataSourceDisposing() + { + using BindingNavigator control = new BindingNavigator(true); + int rowsCount = 5; + BindingSource bindingSource = GetTestBindingSource(rowsCount); + control.BindingSource = bindingSource; + + Assert.Equal("1", control.PositionItem.Text); + Assert.Equal($"of {rowsCount}", control.CountItem.Text); + + bindingSource.Dispose(); + + // The BindingNavigator updates its PositionItem and CountItem values + // after its BindingSource is disposed + Assert.Equal("0", control.PositionItem.Text); + Assert.Equal("of 0", control.CountItem.Text); + } + + [WinFormsFact] + public void BindingNavigator_BindingSource_IsNull_AfterDisposing() + { + using BindingNavigator control = new BindingNavigator(); + BindingSource bindingSource = GetTestBindingSource(5); + control.BindingSource = bindingSource; + + Assert.Equal(bindingSource, control.BindingSource); + + bindingSource.Dispose(); + + Assert.Null(control.BindingSource); + } + + [WinFormsFact] + public void BindingNavigator_BindingSource_IsActual_AfterOldOneIsDisposed() + { + using BindingNavigator control = new BindingNavigator(true); + int rowsCount1 = 3; + BindingSource bindingSource1 = GetTestBindingSource(rowsCount1); + int rowsCount2 = 5; + BindingSource bindingSource2 = GetTestBindingSource(rowsCount2); + control.BindingSource = bindingSource1; + + Assert.Equal(bindingSource1, control.BindingSource); + Assert.Equal("1", control.PositionItem.Text); + Assert.Equal($"of {rowsCount1}", control.CountItem.Text); + + control.BindingSource = bindingSource2; + + Assert.Equal(bindingSource2, control.BindingSource); + Assert.Equal("1", control.PositionItem.Text); + Assert.Equal($"of {rowsCount2}", control.CountItem.Text); + + bindingSource1.Dispose(); + + // bindingSource2 is actual for the BindingNavigator + // so it will contain correct PositionItem and CountItem values + // even after bindingSource1 is disposed. + // This test checks that Disposed events unsubscribed correctly + Assert.Equal(bindingSource2, control.BindingSource); + Assert.Equal("1", control.PositionItem.Text); + Assert.Equal($"of {rowsCount2}", control.CountItem.Text); + } + + private BindingSource GetTestBindingSource(int rowsCount) + { + DataTable dt = new DataTable(); + dt.Columns.Add("Name"); + dt.Columns.Add("Age"); + + for (int i = 0; i < rowsCount; i++) + { + DataRow dr = dt.NewRow(); + dr[0] = $"User{i}"; + dr[1] = i * 3; + dt.Rows.Add(dr); + } + + return new() { DataSource = dt }; + } } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataGridViewTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataGridViewTests.cs index 6168f1724f0..b7aaf9becfa 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataGridViewTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataGridViewTests.cs @@ -11,6 +11,7 @@ using System.Numerics; using static System.Windows.Forms.Metafiles.DataHelpers; using static Interop; +using System.Data; namespace System.Windows.Forms.Tests { @@ -2801,6 +2802,92 @@ public void DataGridView_OnRowHeadersWidthSizeModeChanged_NullE_ThrowsNullRefere Assert.Throws(() => control.OnRowHeadersWidthSizeModeChanged(null)); } + [WinFormsFact] + public void DataGridView_UpdatesItsItems_AfterDataSourceDisposing() + { + using DataGridView control = new DataGridView(); + int rowsCount = 5; + BindingSource bindingSource = GetTestBindingSource(rowsCount); + BindingContext context = new BindingContext(); + context.Add(bindingSource, bindingSource.CurrencyManager); + control.BindingContext = context; + control.DataSource = bindingSource; + + // The TestBindingSource table contains 2 columns + Assert.Equal(2, control.Columns.Count); + // The TestBindingSource table contains some rows + 1 new DGV row (because AllowUserToAddRows is true) + Assert.Equal(rowsCount + 1, control.Rows.Count); + + bindingSource.Dispose(); + + // The DataGridView updates its Rows and Columns collections after its DataSource is disposed + Assert.Empty(control.Columns); + Assert.Empty(control.Rows); + } + + [WinFormsFact] + public void DataGridView_DataSource_IsNull_AfterDisposing() + { + using DataGridView control = new DataGridView(); + BindingSource bindingSource = GetTestBindingSource(5); + control.DataSource = bindingSource; + + Assert.Equal(bindingSource, control.DataSource); + + bindingSource.Dispose(); + + Assert.Null(control.DataSource); + } + + [WinFormsFact] + public void DataGridView_DataSource_IsActual_AfterOldOneIsDisposed() + { + using DataGridView control = new DataGridView(); + int rowsCount1 = 3; + BindingSource bindingSource1 = GetTestBindingSource(rowsCount1); + int rowsCount2 = 5; + BindingSource bindingSource2 = GetTestBindingSource(rowsCount2); + BindingContext context = new BindingContext(); + context.Add(bindingSource1, bindingSource1.CurrencyManager); + control.BindingContext = context; + control.DataSource = bindingSource1; + + Assert.Equal(bindingSource1, control.DataSource); + Assert.Equal(2, control.Columns.Count); + Assert.Equal(rowsCount1 + 1, control.Rows.Count); // + 1 is the new DGV row + + control.DataSource = bindingSource2; + + Assert.Equal(bindingSource2, control.DataSource); + Assert.Equal(2, control.Columns.Count); + Assert.Equal(rowsCount2 + 1, control.Rows.Count); // + 1 is the new DGV row + + bindingSource1.Dispose(); + + // bindingSource2 is actual for the DataGridView so it will contain correct Rows and Columns counts + // even after bindingSource1 is disposed. This test checks that Disposed events unsubscribed correctly + Assert.Equal(bindingSource2, control.DataSource); + Assert.Equal(2, control.Columns.Count); + Assert.Equal(rowsCount2 + 1, control.Rows.Count); // + 1 is the new DGV row + } + + private BindingSource GetTestBindingSource(int rowsCount) + { + DataTable dt = new DataTable(); + dt.Columns.Add("Name"); + dt.Columns.Add("Age"); + + for (int i = 0; i < rowsCount; i++) + { + DataRow dr = dt.NewRow(); + dr[0] = $"User{i}"; + dr[1] = i * 3; + dt.Rows.Add(dr); + } + + return new() { DataSource = dt }; + } + private class SubDataGridViewCell : DataGridViewCell { }