From 347806694493f60bb4efcb65a264b1bfa33206dd Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Wed, 16 Sep 2026 17:41:15 +0300 Subject: [PATCH 1/7] Cover deferred view update scopes Signed-off-by: Zafer Balkan --- .../DeferredViewUpdateTests.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs diff --git a/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs b/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs new file mode 100644 index 0000000..4406a70 --- /dev/null +++ b/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs @@ -0,0 +1,98 @@ +using BrightIdeasSoftware; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ObjectListView2022.Tests +{ + [STATestClass] + public class DeferredViewUpdateTests + { + [TestMethod] + public void DeferViewUpdates_DefersRebuildUntilScopeIsDisposed() + { + using var listView = CreateListView(); + listView.SetObjects(new[] { new Model("first") }); + Assert.AreEqual(1, listView.GetItemCount()); + + using (listView.DeferViewUpdates()) + { + listView.SetObjects(new[] { + new Model("first"), + new Model("second"), + new Model("third") + }); + + Assert.IsTrue(listView.Frozen); + Assert.AreEqual(1, listView.GetItemCount()); + } + + Assert.IsFalse(listView.Frozen); + Assert.AreEqual(3, listView.GetItemCount()); + } + + [TestMethod] + public void DeferViewUpdates_NestedScopesRebuildOnlyAfterOutermostScope() + { + using var listView = CreateListView(); + listView.SetObjects(new[] { new Model("first") }); + + using (listView.DeferViewUpdates()) + { + using (listView.DeferViewUpdates()) + { + listView.SetObjects(new[] { + new Model("first"), + new Model("second") + }); + } + + Assert.IsTrue(listView.Frozen); + Assert.AreEqual(1, listView.GetItemCount()); + } + + Assert.IsFalse(listView.Frozen); + Assert.AreEqual(2, listView.GetItemCount()); + } + + [TestMethod] + public void DeferViewUpdates_UnfreezesWhenScopeExitsThroughException() + { + using var listView = CreateListView(); + + try + { + using (listView.DeferViewUpdates()) + { + Assert.IsTrue(listView.Frozen); + throw new TestException(); + } + } + catch (TestException) + { + } + + Assert.IsFalse(listView.Frozen); + } + + private static ObjectListView CreateListView() + { + var listView = new ObjectListView(); + listView.Columns.Add(new OLVColumn("Name", nameof(Model.Name))); + listView.CreateControl(); + return listView; + } + + private sealed class Model + { + public Model(string name) + { + Name = name; + } + + public string Name { get; } + } + + private sealed class TestException : System.Exception + { + } + } +} From aeafe87720f8f54d032d49786e2eb3997ad7550c Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Wed, 16 Sep 2026 17:41:30 +0300 Subject: [PATCH 2/7] Add deferred view update scope Signed-off-by: Zafer Balkan --- .../ObjectListView.DeferredUpdates.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/ObjectListView2022/ObjectListView.DeferredUpdates.cs diff --git a/src/ObjectListView2022/ObjectListView.DeferredUpdates.cs b/src/ObjectListView2022/ObjectListView.DeferredUpdates.cs new file mode 100644 index 0000000..132a973 --- /dev/null +++ b/src/ObjectListView2022/ObjectListView.DeferredUpdates.cs @@ -0,0 +1,45 @@ +using System; + +namespace BrightIdeasSoftware +{ + public partial class ObjectListView + { + /// + /// Freeze view updates until the returned scope is disposed. + /// + /// + /// Scopes may be nested. The view is rebuilt when the outermost scope is disposed. + /// + /// A scope that unfreezes the view when disposed. + public IDisposable DeferViewUpdates() + { + Freeze(); + return new DeferredViewUpdateScope(this); + } + + private sealed class DeferredViewUpdateScope : IDisposable + { + private ObjectListView owner; + + public DeferredViewUpdateScope(ObjectListView owner) + { + this.owner = owner; + } + + public void Dispose() + { + var listView = owner; + if (listView == null) + { + return; + } + + owner = null; + if (!listView.IsDisposed && !listView.Disposing) + { + listView.Unfreeze(); + } + } + } + } +} From 3b69a3e35844d084e075fd048c0cf0faeeaa7a0b Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Wed, 16 Sep 2026 17:41:46 +0300 Subject: [PATCH 3/7] Note deferred view updates Signed-off-by: Zafer Balkan --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 110ae39..ce3e21e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this repository are documented here. ### Added +- Added `DeferViewUpdates()` for nested, exception-safe view update batching. - Added per-monitor DPI font rescaling with non-compounding transitions, virtual-list safety, and preservation of explicit column widths. - Added a dedicated MSTest regression suite for `ObjectListView2022`, targeting .NET Framework 4.8.1. - Added behavioral coverage for filtering, sorting, grouping, fast/virtual lists, tree lists, check-state handling, Munger access, hot-item behavior, context-menu handling, state persistence, and performance-sensitive paths. From 5d84c159bc74d2e45f0878ea50ca413851b93976 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Wed, 16 Sep 2026 17:42:00 +0300 Subject: [PATCH 4/7] Cover deferred update scope lifecycle Signed-off-by: Zafer Balkan --- .../DeferredViewUpdateTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs b/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs index 4406a70..84da661 100644 --- a/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs +++ b/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs @@ -73,6 +73,32 @@ public void DeferViewUpdates_UnfreezesWhenScopeExitsThroughException() Assert.IsFalse(listView.Frozen); } + [TestMethod] + public void DeferViewUpdates_ScopeDisposeIsIdempotent() + { + using var listView = CreateListView(); + var scope = listView.DeferViewUpdates(); + + scope.Dispose(); + Assert.IsFalse(listView.Frozen); + + scope.Dispose(); + Assert.IsFalse(listView.Frozen); + } + + [TestMethod] + public void DeferViewUpdates_DisposingOwnerBeforeScopeDoesNotUnfreezeDisposedControl() + { + var listView = CreateListView(); + var scope = listView.DeferViewUpdates(); + Assert.IsTrue(listView.Frozen); + + listView.Dispose(); + scope.Dispose(); + + Assert.IsTrue(listView.IsDisposed); + } + private static ObjectListView CreateListView() { var listView = new ObjectListView(); From a7d44c2141fba99ad67aec81ee1706b271d31dfa Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Wed, 16 Sep 2026 17:42:13 +0300 Subject: [PATCH 5/7] Expand deferred update lifecycle coverage Signed-off-by: Zafer Balkan --- .../DeferredViewUpdateTests.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs b/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs index 84da661..f9825c7 100644 --- a/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs +++ b/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; +using System.Windows.Forms; using BrightIdeasSoftware; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -53,6 +55,53 @@ public void DeferViewUpdates_NestedScopesRebuildOnlyAfterOutermostScope() Assert.AreEqual(2, listView.GetItemCount()); } + [TestMethod] + public void DeferViewUpdates_NestedScopesCanBeDisposedOutOfOrder() + { + using var listView = CreateListView(); + listView.SetObjects(new[] { new Model("first") }); + var firstScope = listView.DeferViewUpdates(); + var secondScope = listView.DeferViewUpdates(); + listView.SetObjects(new[] { + new Model("first"), + new Model("second") + }); + + firstScope.Dispose(); + + Assert.IsTrue(listView.Frozen); + Assert.AreEqual(1, listView.GetItemCount()); + + secondScope.Dispose(); + + Assert.IsFalse(listView.Frozen); + Assert.AreEqual(2, listView.GetItemCount()); + } + + [TestMethod] + public void DeferViewUpdates_PreservesExistingFreezeUntilCallerUnfreezes() + { + using var listView = CreateListView(); + listView.SetObjects(new[] { new Model("first") }); + listView.Freeze(); + + using (listView.DeferViewUpdates()) + { + listView.SetObjects(new[] { + new Model("first"), + new Model("second") + }); + } + + Assert.IsTrue(listView.Frozen); + Assert.AreEqual(1, listView.GetItemCount()); + + listView.Unfreeze(); + + Assert.IsFalse(listView.Frozen); + Assert.AreEqual(2, listView.GetItemCount()); + } + [TestMethod] public void DeferViewUpdates_UnfreezesWhenScopeExitsThroughException() { @@ -99,6 +148,46 @@ public void DeferViewUpdates_DisposingOwnerBeforeScopeDoesNotUnfreezeDisposedCon Assert.IsTrue(listView.IsDisposed); } + [TestMethod] + public void DataListView_DeferViewUpdatesDefersDataSourceRefreshUntilScopeIsDisposed() + { + using var listView = CreateDataListView(); + var rows = new BindingList { new Model("first") }; + listView.DataSource = rows; + Assert.AreEqual(1, listView.GetItemCount()); + + using (listView.DeferViewUpdates()) + { + rows.Add(new Model("second")); + + Assert.IsTrue(listView.Frozen); + Assert.AreEqual(1, listView.GetItemCount()); + } + + Assert.IsFalse(listView.Frozen); + Assert.AreEqual(2, listView.GetItemCount()); + } + + [TestMethod] + public void FastDataListView_DeferViewUpdatesDefersDataSourceRefreshUntilScopeIsDisposed() + { + using var listView = CreateDataListView(); + var rows = new BindingList { new Model("first") }; + listView.DataSource = rows; + Assert.AreEqual(1, listView.GetItemCount()); + + using (listView.DeferViewUpdates()) + { + rows.Add(new Model("second")); + + Assert.IsTrue(listView.Frozen); + Assert.AreEqual(1, listView.GetItemCount()); + } + + Assert.IsFalse(listView.Frozen); + Assert.AreEqual(2, listView.GetItemCount()); + } + private static ObjectListView CreateListView() { var listView = new ObjectListView(); @@ -107,6 +196,18 @@ private static ObjectListView CreateListView() return listView; } + private static T CreateDataListView() where T : DataListView, new() + { + var listView = new T + { + BindingContext = new BindingContext(), + AutoGenerateColumns = false + }; + listView.Columns.Add(new OLVColumn("Name", nameof(Model.Name))); + listView.CreateControl(); + return listView; + } + private sealed class Model { public Model(string name) From 938d348d531f829081e05356ecb50df858810596 Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Wed, 16 Sep 2026 17:42:27 +0300 Subject: [PATCH 6/7] Clarify deferred update support Signed-off-by: Zafer Balkan --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3e21e..56bffac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this repository are documented here. ### Added -- Added `DeferViewUpdates()` for nested, exception-safe view update batching. +- Added `DeferViewUpdates()` for nested, exception-safe view update batching across standard and data-bound list variants. - Added per-monitor DPI font rescaling with non-compounding transitions, virtual-list safety, and preservation of explicit column widths. - Added a dedicated MSTest regression suite for `ObjectListView2022`, targeting .NET Framework 4.8.1. - Added behavioral coverage for filtering, sorting, grouping, fast/virtual lists, tree lists, check-state handling, Munger access, hot-item behavior, context-menu handling, state persistence, and performance-sensitive paths. From f5004d361047afd001366a3453bf02353419656c Mon Sep 17 00:00:00 2001 From: Zafer Balkan Date: Wed, 16 Sep 2026 17:42:41 +0300 Subject: [PATCH 7/7] Fix deferred data list fixtures Signed-off-by: Zafer Balkan --- .../DeferredViewUpdateTests.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs b/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs index f9825c7..d172472 100644 --- a/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs +++ b/src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs @@ -151,7 +151,7 @@ public void DeferViewUpdates_DisposingOwnerBeforeScopeDoesNotUnfreezeDisposedCon [TestMethod] public void DataListView_DeferViewUpdatesDefersDataSourceRefreshUntilScopeIsDisposed() { - using var listView = CreateDataListView(); + using var listView = CreateDataListView(); var rows = new BindingList { new Model("first") }; listView.DataSource = rows; Assert.AreEqual(1, listView.GetItemCount()); @@ -171,7 +171,7 @@ public void DataListView_DeferViewUpdatesDefersDataSourceRefreshUntilScopeIsDisp [TestMethod] public void FastDataListView_DeferViewUpdatesDefersDataSourceRefreshUntilScopeIsDisposed() { - using var listView = CreateDataListView(); + using var listView = CreateFastDataListView(); var rows = new BindingList { new Model("first") }; listView.DataSource = rows; Assert.AreEqual(1, listView.GetItemCount()); @@ -196,9 +196,21 @@ private static ObjectListView CreateListView() return listView; } - private static T CreateDataListView() where T : DataListView, new() + private static DataListView CreateDataListView() { - var listView = new T + var listView = new DataListView + { + BindingContext = new BindingContext(), + AutoGenerateColumns = false + }; + listView.Columns.Add(new OLVColumn("Name", nameof(Model.Name))); + listView.CreateControl(); + return listView; + } + + private static FastDataListView CreateFastDataListView() + { + var listView = new FastDataListView { BindingContext = new BindingContext(), AutoGenerateColumns = false