Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to this repository are documented here.

### Added

- 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.
Expand Down
237 changes: 237 additions & 0 deletions src/ObjectListView2022.Tests/DeferredViewUpdateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
using System.ComponentModel;
using System.Windows.Forms;
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_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()
{
using var listView = CreateListView();

try
{
using (listView.DeferViewUpdates())
{
Assert.IsTrue(listView.Frozen);
throw new TestException();
}
}
catch (TestException)
{
}

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);
}

[TestMethod]
public void DataListView_DeferViewUpdatesDefersDataSourceRefreshUntilScopeIsDisposed()
{
using var listView = CreateDataListView();
var rows = new BindingList<Model> { 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 = CreateFastDataListView();
var rows = new BindingList<Model> { 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();
listView.Columns.Add(new OLVColumn("Name", nameof(Model.Name)));
listView.CreateControl();
return listView;
}

private static DataListView CreateDataListView()
{
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
};
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
{
}
}
}
45 changes: 45 additions & 0 deletions src/ObjectListView2022/ObjectListView.DeferredUpdates.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;

namespace BrightIdeasSoftware
{
public partial class ObjectListView
{
/// <summary>
/// Freeze view updates until the returned scope is disposed.
/// </summary>
/// <remarks>
/// Scopes may be nested. The view is rebuilt when the outermost scope is disposed.
/// </remarks>
/// <returns>A scope that unfreezes the view when disposed.</returns>
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();
}
}
}
}
}