mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
The drag-drop / file-drop callback selected the dropped assemblies with a raw SelectedItems.Clear() + Add() loop, which flashes a transient empty selection. The model has SelectNodes precisely to avoid that -- it brackets the swap so the selection-changed fan-out fires once, with the final set, never empty. A transient empty poisons the grid-sync deferred guard, after which the tree stops following tab activation. Route the callback through SelectNodes; a regression test asserts no empty SelectedItem is observed mid-drop. Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3755/head
2 changed files with 91 additions and 3 deletions
@ -0,0 +1,87 @@
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System.Collections.Generic; |
||||
using System.ComponentModel; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
|
||||
using AwesomeAssertions; |
||||
|
||||
using ICSharpCode.ILSpyX.TreeView; |
||||
|
||||
using ILSpy.AssemblyTree; |
||||
using ILSpy.TreeNodes; |
||||
using ILSpy.Views; |
||||
|
||||
using Avalonia.Headless.NUnit; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.ILSpy.Tests; |
||||
|
||||
/// <summary>
|
||||
/// Selecting the assemblies that were just dropped / reordered must go through the model's batched
|
||||
/// SelectNodes, not a raw Clear()+Add(). A raw swap flashes a transient EMPTY selection, which the
|
||||
/// model documents as poisoning the grid-sync deferred guard (the tree stops following tab
|
||||
/// activation). This pins that the drop-selection callback never emits an empty selection mid-change.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class DropSelectionBatchingTests |
||||
{ |
||||
[AvaloniaTest] |
||||
public async Task Drop_Selection_Does_Not_Flash_An_Empty_Selection() |
||||
{ |
||||
var (window, vm) = await TestHarness.BootAsync(3); |
||||
await window.WaitForComponent<AssemblyListPane>(); |
||||
var model = vm.AssemblyTreeModel; |
||||
var listRoot = (AssemblyListTreeNode)model.Root!; |
||||
|
||||
listRoot.SelectAssembliesAfterDrop.Should().NotBeNull( |
||||
"the pane wires the after-drop selection callback when it binds"); |
||||
|
||||
var assemblies = model.AssemblyList!.GetAssemblies(); |
||||
assemblies.Length.Should().BeGreaterThanOrEqualTo(2); |
||||
|
||||
// Start from a non-empty selection so a raw Clear() would transit through empty.
|
||||
model.SelectedItem = listRoot.FindAssemblyNode(assemblies[0]); |
||||
|
||||
var observed = new List<SharpTreeNode?>(); |
||||
void OnChanged(object? _, PropertyChangedEventArgs e) |
||||
{ |
||||
if (e.PropertyName == nameof(AssemblyTreeModel.SelectedItem)) |
||||
observed.Add(model.SelectedItem); |
||||
} |
||||
model.PropertyChanged += OnChanged; |
||||
try |
||||
{ |
||||
// Drive the after-drop selection the way AssemblyListTreeNode.Drop does.
|
||||
listRoot.SelectAssembliesAfterDrop!(new[] { assemblies[0], assemblies[1] }); |
||||
} |
||||
finally |
||||
{ |
||||
model.PropertyChanged -= OnChanged; |
||||
} |
||||
|
||||
observed.Should().NotContainNulls( |
||||
"the drop selection must not flash an empty SelectedItem mid-change"); |
||||
observed.Should().ContainSingle( |
||||
"the batched selection change fans out exactly once, with the final set"); |
||||
model.SelectedItems.Should().HaveCount(2, "both dropped assemblies end up selected"); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue