Skip to content

fix: restore connection tree state when the search filter is cleared - #159

Open
jafin wants to merge 2 commits into
robertpopa22:mainfrom
jafin:fix/connection-tree-filter-state-main
Open

fix: restore connection tree state when the search filter is cleared#159
jafin wants to merge 2 commits into
robertpopa22:mainfrom
jafin:fix/connection-tree-filter-state-main

Conversation

@jafin

@jafin jafin commented Aug 6, 2026

Copy link
Copy Markdown

Problem

Filtering the Connections tree with the search box and then clearing it leaves the tree broken: folders that were expanded before the filter come back collapsed, expand/collapse gets out of sync with what the tree reports, and in one case EnsureVisible threw on an invalid index.

Root cause

Two compounding defects in ConnectionTree.RemoveFilter():

1. The "pre-filter" snapshot was not a snapshot. ApplyFilter stored ExpandedObjects, whose getter returns a live Dictionary.KeyCollection over the tree model's expansion map (TreeListView.cs:317) rather than a copy. The ExpandAll() immediately after rewrote that same map, so the saved state became the filtered state. The ExpandedObjects setter then does Clear() before enumerating the value it was given — the very collection it just emptied — so RemoveFilter restored nothing and every folder collapsed.

2. The display was never rebuilt. Assigning ExpandedObjects only updates the model's map; OLV documents that RebuildAll must follow. RemoveFilter instead rebuilt first (via ResetColumnFiltering) and mutated the map afterwards, leaving the branch structure and row list on the filtered layout. Expansion state and row indexes then read from different sources, which is how IndexOf handed EnsureVisible a row index past the end of the list.

Fix

  • _preFilterExpandedObjects is materialized into a List<object> so it survives the ExpandAll().
  • RemoveFilter calls RebuildAll(SelectedObjects, _preFilterExpandedObjects, null), restoring expansion, selection and the row list in one consistent pass.

Follow-up commit: batching

Clearing the filter runs three passes — UseFiltering and the column filter reset each trigger UpdateFiltering, then the tree rebuild above. Each pass repainted and re-ran AutoResizeColumn, which measures the text of every visible row. The second commit holds painting across the whole method and collapses the three auto-resizes into one against the restored rows.

Note Freeze()/Unfreeze() is not usable here: the if (Frozen) return; guard lives in ObjectListView.BuildList(bool), but VirtualObjectListView overrides that method without the guard, and TreeListView is a virtual list. Freezing would suspend painting while adding one more BuildList in DoUnfreeze.

Tests

Two regression tests in ConnectionTreeExpansionTests, both verified to fail against the unfixed code:

Test Failure without the fix
RemoveFilter_RestoresPreFilterExpansionState Expected: True / But was: False — root and Folder1 came back collapsed
RemoveFilter_RebuildsRowsToMatchRestoredExpansionState Expected: 4 / But was: 5 — rows kept the filtered layout

Full suite on this branch: 6343 passed, 2 failed. The failure (StartupConnectionPathReturnsSavedPathWhenItIsTheSoleCandidate, counted twice by overlapping groups) is pre-existing and unrelated — connections-file discovery picking up a candidate next to the test binary on the dev box; it fails in isolation on an unmodified tree too.

jafin added 2 commits August 6, 2026 18:54
Clearing the search box left the tree in a broken state: folders that were
expanded before filtering came back collapsed, and the tree could throw an
invalid-index exception from EnsureVisible.

Two compounding defects in RemoveFilter():

ApplyFilter stored ExpandedObjects as the pre-filter state, but that getter
returns a live view over the tree model's expansion map rather than a
snapshot. The ExpandAll() that follows rewrote the same map, and the
ExpandedObjects setter clears the map before enumerating the value it was
handed - the very collection it just emptied - so nothing was restored.

Assigning ExpandedObjects also only updates the model's map; the branch
structure and row list are not rebuilt. RemoveFilter rebuilt first (via
ResetColumnFiltering) and mutated the map afterwards, leaving expansion
state and row indexes reading from different sources. That mismatch is what
let IndexOf hand EnsureVisible a row past the end of the list.

The pre-filter state is now materialized into a list, and RemoveFilter uses
RebuildAll to restore expansion, selection and the row list together.
Clearing the filter runs three passes: UseFiltering and the column filter
reset each trigger UpdateFiltering, then the tree is rebuilt to restore the
pre-filter expansion state. Each pass repainted and re-ran the column
auto-resize, which measures the text of every visible row.

Hold painting across the whole method and suspend the per-pass auto-resize,
doing it once at the end against the restored rows. The early return when
there is no saved expansion state became a conditional so that final resize
runs on every path.

Freeze()/Unfreeze() would not help here: TreeListView is a virtual list, and
VirtualObjectListView.BuildList(bool) overrides the base without the
"if (Frozen) return" guard, so freezing suspends painting while adding one
more BuildList in DoUnfreeze. Collapsing the two UpdateFiltering passes into
one would mean changing the vendored ObjectListView filtering workflow, which
is out of scope here.

No behavior change; the existing filter regression tests cover it.
Copilot AI lite review requested due to automatic review settings August 6, 2026 08:58
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Restore connection tree state when clearing search filter

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Snapshot expanded tree state before filtering to restore it after clearing the search
• Rebuild tree rows and expansion together to avoid invalid indexes and UI desync
• Batch filter-removal updates to reduce redundant repaints and column auto-resizes
Diagram

graph TD
  U["Search box"] --> AF["ApplyFilter()"] --> SNAP["Expanded snapshot (List)"]
  U --> RF["RemoveFilter() (batched)"] --> RB["RebuildAll() restore state"] --> UF["AutoResize column"]
  SNAP --> RB --> TM[("Tree model state")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Store expansion state as stable IDs/paths
  • ➕ More robust than object references if model objects are recreated
  • ➕ Can survive model reloads or rehydration scenarios
  • ➖ Requires stable identifiers and mapping logic
  • ➖ More code and higher test surface than the current fix
2. Always call RebuildAll on filter changes (apply + remove)
  • ➕ Simplifies consistency: one canonical rebuild path for row list and expansion
  • ➕ Reduces chances of model/view desync across future filter features
  • ➖ Potentially heavier work on every keystroke while filtering
  • ➖ May require throttling/debouncing to avoid UI lag
3. Use Freeze()/Unfreeze() for batching
  • ➕ Standard ObjectListView pattern for suppressing repaints and intermediate states
  • ➖ Not reliable for TreeListView/VirtualObjectListView due to overridden BuildList behavior
  • ➖ Could add extra BuildList passes during unfreeze

Recommendation: The chosen approach (materialize ExpandedObjects into a List and restore via RebuildAll) is the best fit for this defect: it directly fixes the live-collection snapshot bug and the model/view row-list mismatch in a single consistent rebuild step. The BeginUpdate + temporary auto-resize suppression is a pragmatic batching strategy given Freeze/Unfreeze limitations in the virtual TreeListView implementation.

Files changed (2) +123 / -8

Bug fix (1) +38 / -8
ConnectionTree.csSnapshot expansion state and rebuild tree on filter clear (batched) +38/-8

Snapshot expansion state and rebuild tree on filter clear (batched)

• Fixes RemoveFilter() to restore pre-filter expansion and rows consistently by materializing ExpandedObjects into a List and calling RebuildAll with restored selection/expansion. Batches the multi-pass filtering updates using BeginUpdate/EndUpdate and temporarily suppresses UpdateFiltering-triggered AutoResizeColumn, then performs a single resize at the end.

mRemoteNG/UI/Controls/ConnectionTree/ConnectionTree.cs

Tests (1) +85 / -0
ConnectionTreeExpansionTests.csAdd regression tests for expansion/row consistency when clearing filter +85/-0

Add regression tests for expansion/row consistency when clearing filter

• Adds two tests validating that clearing the search filter restores the pre-filter expansion state and that the row list is rebuilt to match (preventing invalid index issues in EnsureModelVisible/EnsureVisible). Tests exercise filter apply/remove behavior under a message pump.

mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (3) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. UI tests lack message pump 📘 Rule violation ▣ Testability
Description
The newly added UI tests rely on RunWithMessagePump, but that helper does not start a WinForms
message loop via Application.Run(...) nor ensure Application.ExitThread() in a finally/cleanup
path. This can lead to flaky or invalid UI test execution for ObjectListView/TreeListView-based
controls.
Code

mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[R179-182]

+            RunWithMessagePump(tree =>
+            {
+                var model = new ConnectionTreeModel();
+                var root = new RootNodeInfo(RootNodeType.Connection);
Evidence
PR Compliance ID 104328 requires ObjectListView/FrmOptions-related tests that create/show forms to
run a message loop via Application.Run and ensure Application.ExitThread() in cleanup. The new
tests invoke RunWithMessagePump(...), but the helper shows a form with form.Show() and never
calls Application.Run(...) or Application.ExitThread().

Rule 104328: Use RunWithMessagePump pattern for ObjectListView/FrmOptions tests
mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[176-182]
mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[16-51]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Tests that exercise `ConnectionTree` (TreeListView/ObjectListView-based UI) must run under a real WinForms message loop. The current `RunWithMessagePump` helper uses `form.Show()` on an STA thread without `Application.Run(form)` and without guaranteed `Application.ExitThread()` cleanup.

## Issue Context
The compliance rule requires the RunWithMessagePump pattern: `Application.Run(form)` and a try/finally that calls `Application.ExitThread()`.

## Fix Focus Areas
- mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[16-51]
- mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[176-259]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Underscore-prefixed private fields added 📘 Rule violation ⚙ Maintainability
Description
The newly introduced private fields use a leading underscore (e.g., _preFilterExpandedObjects),
which violates the required camelCase naming convention for non-public fields. This reduces
consistency with the enforced style rule and can cause style-check failures.
Code

mRemoteNG/UI/Controls/ConnectionTree/ConnectionTree.cs[R38-39]

+        private List<object>? _preFilterExpandedObjects;
+        private bool _columnAutoResizeSuspended;
Evidence
PR Compliance ID 1562273 requires non-public field names to use camelCase and explicitly disallows
leading underscores. The new fields _preFilterExpandedObjects and _columnAutoResizeSuspended are
non-public fields introduced with leading underscores.

Rule 1562273: Use camelCase for non-public field names
mRemoteNG/UI/Controls/ConnectionTree/ConnectionTree.cs[37-40]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New non-public fields were added with leading underscores, which violates the non-public field naming rule.

## Issue Context
The rule requires non-public field names to be camelCase with no leading underscores.

## Fix Focus Areas
- mRemoteNG/UI/Controls/ConnectionTree/ConnectionTree.cs[38-39]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. New tests violate underscore pattern 📘 Rule violation ▣ Testability
Description
The newly added test method names use only two underscore-separated segments instead of the required
three-part MethodName_Scenario_ExpectedBehavior pattern. This can break consistency and any
tooling that enforces/depends on the naming convention.
Code

mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[R176-178]

+        [Test]
+        public void RemoveFilter_RestoresPreFilterExpansionState()
+        {
Evidence
PR Compliance ID 1563791 requires test method names to have exactly three underscore-separated
segments. The new tests RemoveFilter_RestoresPreFilterExpansionState and
RemoveFilter_RebuildsRowsToMatchRestoredExpansionState have only two segments (one underscore).

Rule 1563791: Test method names must follow MethodName_Scenario_ExpectedBehavior pattern
mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[176-219]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New NUnit tests were added whose method names do not follow the required `MethodName_Scenario_ExpectedBehavior` naming pattern.

## Issue Context
The rule requires exactly three non-empty segments separated by single underscores.

## Fix Focus Areas
- mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[176-219]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 52 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +38 to +39
private List<object>? _preFilterExpandedObjects;
private bool _columnAutoResizeSuspended;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

1. Underscore-prefixed private fields added 📘 Rule violation ⚙ Maintainability

The newly introduced private fields use a leading underscore (e.g., _preFilterExpandedObjects),
which violates the required camelCase naming convention for non-public fields. This reduces
consistency with the enforced style rule and can cause style-check failures.
Agent Prompt
## Issue description
New non-public fields were added with leading underscores, which violates the non-public field naming rule.

## Issue Context
The rule requires non-public field names to be camelCase with no leading underscores.

## Fix Focus Areas
- mRemoteNG/UI/Controls/ConnectionTree/ConnectionTree.cs[38-39]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +176 to +178
[Test]
public void RemoveFilter_RestoresPreFilterExpansionState()
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. New tests violate underscore pattern 📘 Rule violation ▣ Testability

The newly added test method names use only two underscore-separated segments instead of the required
three-part MethodName_Scenario_ExpectedBehavior pattern. This can break consistency and any
tooling that enforces/depends on the naming convention.
Agent Prompt
## Issue description
New NUnit tests were added whose method names do not follow the required `MethodName_Scenario_ExpectedBehavior` naming pattern.

## Issue Context
The rule requires exactly three non-empty segments separated by single underscores.

## Fix Focus Areas
- mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[176-219]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +179 to +182
RunWithMessagePump(tree =>
{
var model = new ConnectionTreeModel();
var root = new RootNodeInfo(RootNodeType.Connection);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Ui tests lack message pump 📘 Rule violation ▣ Testability

The newly added UI tests rely on RunWithMessagePump, but that helper does not start a WinForms
message loop via Application.Run(...) nor ensure Application.ExitThread() in a finally/cleanup
path. This can lead to flaky or invalid UI test execution for ObjectListView/TreeListView-based
controls.
Agent Prompt
## Issue description
Tests that exercise `ConnectionTree` (TreeListView/ObjectListView-based UI) must run under a real WinForms message loop. The current `RunWithMessagePump` helper uses `form.Show()` on an STA thread without `Application.Run(form)` and without guaranteed `Application.ExitThread()` cleanup.

## Issue Context
The compliance rule requires the RunWithMessagePump pattern: `Application.Run(form)` and a try/finally that calls `Application.ExitThread()`.

## Fix Focus Areas
- mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[16-51]
- mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs[176-259]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a regression in the Connections tree where clearing a search filter could restore an incorrect expansion state and leave the virtual row list out of sync (leading to expand/collapse inconsistencies and potential invalid-index errors in visibility/selection operations).

Changes:

  • Materialize the pre-filter expansion state into a real snapshot (List<object>) before ExpandAll() mutates the underlying expansion map.
  • When clearing the filter, rebuild the tree via RebuildAll(...) to restore expansion + selection + row layout in a consistent pass.
  • Batch UI updates during filter removal to avoid redundant repaints and repeated column auto-resize work.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
mRemoteNG/UI/Controls/ConnectionTree/ConnectionTree.cs Fixes expansion snapshotting and uses RebuildAll + update batching when clearing the search filter.
mRemoteNGTests/UI/Controls/ConnectionTreeExpansionTests.cs Adds regression tests covering expansion-state restoration and row-layout rebuild correctness after clearing a filter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants