mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 14:37:19 +08:00
934 lines
28 KiB
C#
934 lines
28 KiB
C#
using System.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Windows.Input;
|
|
using AtomUI.Controls;
|
|
using Avalonia;
|
|
using Avalonia.Collections;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Input;
|
|
using Avalonia.Threading;
|
|
using ReactiveUI;
|
|
using ReactiveUI.Avalonia;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.NavMenu;
|
|
|
|
public class NavMenuNodeCommandTests
|
|
{
|
|
static NavMenuNodeCommandTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Prepared_Container_Receives_Node_Command_And_Explicit_Parameter()
|
|
{
|
|
var command = new TrackingCommand();
|
|
var parameter = new object();
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
ItemKey = "command-item",
|
|
Command = command,
|
|
CommandParameter = parameter
|
|
};
|
|
|
|
ShowInWindow(node, (_, item) =>
|
|
{
|
|
item.Command.ShouldBeSameAs(command);
|
|
item.CommandParameter.ShouldBeSameAs(parameter);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Prepared_Container_Preserves_Null_CommandParameter_When_ItemKey_Is_Set()
|
|
{
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
ItemKey = "command-item",
|
|
Command = new TrackingCommand()
|
|
};
|
|
|
|
ShowInWindow(node, (_, item) => item.CommandParameter.ShouldBeNull());
|
|
}
|
|
|
|
[Fact]
|
|
public void Realized_Container_Tracks_Node_Command_And_Parameter_Changes()
|
|
{
|
|
var oldCommand = new TrackingCommand();
|
|
var newCommand = new TrackingCommand();
|
|
var oldParameter = new object();
|
|
var newParameter = new object();
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = oldCommand,
|
|
CommandParameter = oldParameter
|
|
};
|
|
|
|
ShowInWindow(node, (_, item) =>
|
|
{
|
|
node.Command = newCommand;
|
|
node.CommandParameter = newParameter;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
item.Command.ShouldBeSameAs(newCommand);
|
|
item.CommandParameter.ShouldBeSameAs(newParameter);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Realized_Container_Tracks_Observable_Custom_Node_Command_Changes()
|
|
{
|
|
var oldCommand = new TrackingCommand();
|
|
var newCommand = new TrackingCommand();
|
|
var oldParameter = new object();
|
|
var newParameter = new object();
|
|
var node = new ObservableNavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = oldCommand,
|
|
CommandParameter = oldParameter
|
|
};
|
|
|
|
ShowInWindow(node, (_, item) =>
|
|
{
|
|
node.Command = newCommand;
|
|
node.CommandParameter = newParameter;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
item.Command.ShouldBeSameAs(newCommand);
|
|
item.CommandParameter.ShouldBeSameAs(newParameter);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Prepared_Container_Receives_NonObservable_Custom_Node_Command_Values()
|
|
{
|
|
var command = new TrackingCommand();
|
|
var parameter = new object();
|
|
var node = new PlainNavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = command,
|
|
CommandParameter = parameter
|
|
};
|
|
|
|
ShowInWindow(node, (_, item) =>
|
|
{
|
|
item.Command.ShouldBeSameAs(command);
|
|
item.CommandParameter.ShouldBeSameAs(parameter);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Legacy_Custom_Node_Uses_Null_Command_Defaults()
|
|
{
|
|
INavMenuNode node = new LegacyNavMenuNode
|
|
{
|
|
Header = "Legacy item",
|
|
ItemKey = "legacy-item"
|
|
};
|
|
|
|
node.Command.ShouldBeNull();
|
|
node.CommandParameter.ShouldBeNull();
|
|
|
|
ShowInWindow(node, (_, item) =>
|
|
{
|
|
item.Command.ShouldBeNull();
|
|
item.CommandParameter.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Pointer_Submission_Executes_Node_Command_Once()
|
|
{
|
|
var command = new TrackingCommand();
|
|
var parameter = new object();
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = command,
|
|
CommandParameter = parameter
|
|
};
|
|
|
|
ShowInWindow(node, (window, item) =>
|
|
{
|
|
item.ItemHeader.ShouldNotBeNull();
|
|
var header = item.ItemHeader;
|
|
var point = header.TranslatePoint(
|
|
new Point(header.Bounds.Width / 2, header.Bounds.Height / 2),
|
|
window);
|
|
point.ShouldNotBeNull();
|
|
|
|
window.MouseMove(point.Value);
|
|
window.MouseDown(point.Value, MouseButton.Left);
|
|
window.MouseUp(point.Value, MouseButton.Left);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
command.ExecuteCount.ShouldBe(1);
|
|
command.LastParameter.ShouldBeSameAs(parameter);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Pointer_Submission_With_ReactiveCommand_Coalesces_Transient_CanExecute_Changes()
|
|
{
|
|
NavMenuItem? rootItem = null;
|
|
NavMenuItem? parentItem = null;
|
|
NavMenuItem? siblingItem = null;
|
|
NavMenuItem? leafItem = null;
|
|
bool? siblingEnabledDuringExecution = null;
|
|
bool? leafEnabledDuringExecution = null;
|
|
bool? rootSelectedPathDuringExecution = null;
|
|
bool? parentSelectedPathDuringExecution = null;
|
|
var command = ReactiveCommand.Create<string>(_ =>
|
|
{
|
|
siblingEnabledDuringExecution = siblingItem!.IsEffectivelyEnabled;
|
|
leafEnabledDuringExecution = leafItem!.IsEffectivelyEnabled;
|
|
rootSelectedPathDuringExecution = rootItem!.IsInSelectedPath;
|
|
parentSelectedPathDuringExecution = parentItem!.IsInSelectedPath;
|
|
}, outputScheduler: AvaloniaScheduler.Instance);
|
|
var sibling = new NavMenuNode
|
|
{
|
|
Header = "Option 1",
|
|
ItemKey = "option-1",
|
|
Command = command,
|
|
CommandParameter = "option-1"
|
|
};
|
|
var leaf = new NavMenuNode
|
|
{
|
|
Header = "Option 2",
|
|
ItemKey = "option-2",
|
|
Command = command,
|
|
CommandParameter = "option-2"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Item 1",
|
|
ItemKey = "item-1"
|
|
};
|
|
parent.Children.Add(sibling);
|
|
parent.Children.Add(leaf);
|
|
var root = new NavMenuNode
|
|
{
|
|
Header = "Navigation Three - Submenu",
|
|
ItemKey = "navigation-three"
|
|
};
|
|
root.Children.Add(parent);
|
|
var menu = CreateMenu(new[] { root });
|
|
|
|
ShowInWindow(menu, (window, realizedRootItem) =>
|
|
{
|
|
rootItem = realizedRootItem;
|
|
rootItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
parentItem = rootItem.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
parentItem.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
leafItem = parentItem.ContainerFromItem(leaf).ShouldBeOfType<NavMenuItem>();
|
|
siblingItem = parentItem.ContainerFromItem(sibling).ShouldBeOfType<NavMenuItem>();
|
|
var header = leafItem.ItemHeader.ShouldNotBeNull();
|
|
var point = header.TranslatePoint(
|
|
new Point(header.Bounds.Width / 2, header.Bounds.Height / 2),
|
|
window).ShouldNotBeNull();
|
|
|
|
window.MouseMove(point);
|
|
window.MouseDown(point, MouseButton.Left);
|
|
window.MouseUp(point, MouseButton.Left);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
siblingEnabledDuringExecution.ShouldBe(true);
|
|
leafEnabledDuringExecution.ShouldBe(true);
|
|
rootSelectedPathDuringExecution.ShouldBe(true);
|
|
parentSelectedPathDuringExecution.ShouldBe(true);
|
|
siblingItem.IsEffectivelyEnabled.ShouldBeTrue();
|
|
leafItem.IsEffectivelyEnabled.ShouldBeTrue();
|
|
rootItem.IsInSelectedPath.ShouldBeTrue();
|
|
parentItem.IsInSelectedPath.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Keyboard_Submission_Executes_Node_Command_Once()
|
|
{
|
|
var command = new TrackingCommand();
|
|
var parameter = new object();
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = command,
|
|
CommandParameter = parameter
|
|
};
|
|
|
|
ShowInWindow(node, (window, _) =>
|
|
{
|
|
var menu = window.Content.ShouldBeOfType<AtomUI.Desktop.Controls.NavMenu>();
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
window.KeyPress(Key.Down, RawInputModifiers.None, PhysicalKey.ArrowDown, null);
|
|
window.KeyPress(Key.Enter, RawInputModifiers.None, PhysicalKey.Enter, null);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
command.ExecuteCount.ShouldBe(1);
|
|
command.LastParameter.ShouldBeSameAs(parameter);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Node_Command_CanExecute_Controls_Realized_Container_Effective_Enabled_State()
|
|
{
|
|
var command = new TrackingCommand
|
|
{
|
|
CanExecuteValue = false
|
|
};
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = command
|
|
};
|
|
|
|
ShowInWindow(node, (_, item) =>
|
|
{
|
|
item.IsEffectivelyEnabled.ShouldBeFalse();
|
|
|
|
command.CanExecuteValue = true;
|
|
command.RaiseCanExecuteChanged();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
item.IsEffectivelyEnabled.ShouldBeTrue();
|
|
node.IsEnabled.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Replacing_Node_Command_Unsubscribes_Old_Command_And_Subscribes_New_Command()
|
|
{
|
|
var oldCommand = new TrackingCommand();
|
|
var newCommand = new TrackingCommand();
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = oldCommand
|
|
};
|
|
|
|
ShowInWindow(node, (_, item) =>
|
|
{
|
|
oldCommand.SubscriberCount.ShouldBe(1);
|
|
|
|
node.Command = newCommand;
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
item.Command.ShouldBeSameAs(newCommand);
|
|
oldCommand.SubscriberCount.ShouldBe(0);
|
|
newCommand.SubscriberCount.ShouldBe(1);
|
|
});
|
|
|
|
newCommand.SubscriberCount.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Detaching_And_Reattaching_Menu_Does_Not_Duplicate_Command_Subscriptions()
|
|
{
|
|
var command = new TrackingCommand();
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = command
|
|
};
|
|
var menu = CreateMenu(new[] { node });
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 360,
|
|
Height = 260,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
var item = menu.ContainerFromItem(node).ShouldBeOfType<NavMenuItem>();
|
|
command.SubscriberCount.ShouldBe(1);
|
|
|
|
command.CanExecuteValue = false;
|
|
command.RaiseCanExecuteChanged();
|
|
window.Content = null;
|
|
command.CanExecuteValue = true;
|
|
Dispatcher.UIThread.RunJobs();
|
|
command.SubscriberCount.ShouldBe(0);
|
|
item.IsEffectivelyEnabled.ShouldBeTrue(
|
|
"detaching the container must cancel a queued CanExecute update before it can write stale state.");
|
|
|
|
window.Content = menu;
|
|
Dispatcher.UIThread.RunJobs();
|
|
command.SubscriberCount.ShouldBe(1);
|
|
}
|
|
finally
|
|
{
|
|
window.Content = null;
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
command.SubscriberCount.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Removing_Node_Clears_Container_Command_And_Releases_Subscriptions()
|
|
{
|
|
var command = new TrackingCommand();
|
|
var parameter = new object();
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = command,
|
|
CommandParameter = parameter
|
|
};
|
|
var nodes = new AvaloniaList<INavMenuNode> { node };
|
|
var menu = CreateMenu(nodes);
|
|
|
|
ShowInWindow(menu, (_, item) =>
|
|
{
|
|
command.SubscriberCount.ShouldBe(1);
|
|
|
|
nodes.Remove(node);
|
|
RunDispatcherJobsUntil(() => menu.ContainerFromItem(node) is null);
|
|
|
|
item.Command.ShouldBeNull();
|
|
item.CommandParameter.ShouldBeNull();
|
|
command.SubscriberCount.ShouldBe(0);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Clearing_ItemsSource_Releases_All_Node_Command_Subscriptions()
|
|
{
|
|
var firstCommand = new TrackingCommand();
|
|
var secondCommand = new TrackingCommand();
|
|
var firstNode = new NavMenuNode
|
|
{
|
|
Header = "First item",
|
|
Command = firstCommand
|
|
};
|
|
var secondNode = new NavMenuNode
|
|
{
|
|
Header = "Second item",
|
|
Command = secondCommand
|
|
};
|
|
var nodes = new AvaloniaList<INavMenuNode> { firstNode, secondNode };
|
|
var menu = CreateMenu(nodes);
|
|
|
|
ShowInWindow(menu, (_, firstItem) =>
|
|
{
|
|
var secondItem = menu.ContainerFromItem(secondNode).ShouldBeOfType<NavMenuItem>();
|
|
firstCommand.SubscriberCount.ShouldBe(1);
|
|
secondCommand.SubscriberCount.ShouldBe(1);
|
|
|
|
nodes.Clear();
|
|
RunDispatcherJobsUntil(() => menu.ContainerFromItem(firstNode) is null &&
|
|
menu.ContainerFromItem(secondNode) is null);
|
|
|
|
firstItem.Command.ShouldBeNull();
|
|
secondItem.Command.ShouldBeNull();
|
|
firstCommand.SubscriberCount.ShouldBe(0);
|
|
secondCommand.SubscriberCount.ShouldBe(0);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Replacing_ItemsSource_Releases_Old_Node_Command_Subscription()
|
|
{
|
|
var oldCommand = new TrackingCommand();
|
|
var newCommand = new TrackingCommand();
|
|
var oldNode = new NavMenuNode
|
|
{
|
|
Header = "Old item",
|
|
Command = oldCommand
|
|
};
|
|
var newNode = new NavMenuNode
|
|
{
|
|
Header = "New item",
|
|
Command = newCommand
|
|
};
|
|
var menu = CreateMenu(new[] { oldNode });
|
|
|
|
ShowInWindow(menu, (_, oldItem) =>
|
|
{
|
|
oldCommand.SubscriberCount.ShouldBe(1);
|
|
|
|
menu.ItemsSource = new[] { newNode };
|
|
RunDispatcherJobsUntil(() => menu.ContainerFromItem(newNode) is NavMenuItem);
|
|
|
|
oldItem.Command.ShouldBeNull();
|
|
oldCommand.SubscriberCount.ShouldBe(0);
|
|
newCommand.SubscriberCount.ShouldBe(1);
|
|
});
|
|
|
|
newCommand.SubscriberCount.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rebinding_The_Same_Container_Releases_Previous_Node_Command_Bindings()
|
|
{
|
|
var oldCommand = new TrackingCommand();
|
|
var newCommand = new TrackingCommand();
|
|
var oldNode = new NavMenuNode
|
|
{
|
|
Header = "Old item",
|
|
Command = oldCommand
|
|
};
|
|
var newNode = new NavMenuNode
|
|
{
|
|
Header = "New item",
|
|
Command = newCommand
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu();
|
|
var item = new NavMenuItem();
|
|
|
|
var oldBindings = item.ResetNodeBindingDisposables();
|
|
NavMenuItemContainerBinder.BindNode(item, oldNode, menu, oldBindings);
|
|
item.Command.ShouldBeSameAs(oldCommand);
|
|
|
|
var newBindings = item.ResetNodeBindingDisposables();
|
|
item.Command.ShouldBeNull();
|
|
NavMenuItemContainerBinder.BindNode(item, newNode, menu, newBindings);
|
|
item.Command.ShouldBeSameAs(newCommand);
|
|
|
|
item.ClearNodeBindingDisposables();
|
|
item.Command.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Reapplying_Menu_Template_Does_Not_Duplicate_Command_Subscriptions()
|
|
{
|
|
var command = new TrackingCommand();
|
|
var node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = command
|
|
};
|
|
|
|
ShowInWindow(node, (window, oldItem) =>
|
|
{
|
|
var menu = window.Content.ShouldBeOfType<AtomUI.Desktop.Controls.NavMenu>();
|
|
var template = menu.Template;
|
|
template.ShouldNotBeNull();
|
|
command.SubscriberCount.ShouldBe(1);
|
|
|
|
menu.Template = null;
|
|
menu.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
command.SubscriberCount.ShouldBeLessThan(2);
|
|
|
|
menu.Template = template;
|
|
menu.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
command.SubscriberCount.ShouldBe(1);
|
|
var currentItem = menu.ContainerFromItem(node).ShouldBeOfType<NavMenuItem>();
|
|
if (!ReferenceEquals(currentItem, oldItem))
|
|
{
|
|
oldItem.Command.ShouldBeNull();
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Removing_Custom_Node_Releases_PropertyChanged_Subscriptions()
|
|
{
|
|
var node = new ObservableNavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = new TrackingCommand()
|
|
};
|
|
var nodes = new AvaloniaList<INavMenuNode> { node };
|
|
var menu = CreateMenu(nodes);
|
|
|
|
ShowInWindow(menu, (_, item) =>
|
|
{
|
|
var realizedSubscriberCount = node.PropertyChangedSubscriberCount;
|
|
realizedSubscriberCount.ShouldBeGreaterThan(1);
|
|
|
|
nodes.Remove(node);
|
|
RunDispatcherJobsUntil(() => menu.ContainerFromItem(node) is null);
|
|
|
|
node.PropertyChangedSubscriberCount.ShouldBeLessThan(realizedSubscriberCount);
|
|
item.Command.ShouldBeNull();
|
|
|
|
node.Command = new TrackingCommand();
|
|
node.CommandParameter = new object();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
item.Command.ShouldBeNull();
|
|
item.CommandParameter.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Removed_Observable_Custom_Node_Can_Be_Collected()
|
|
{
|
|
var nodeReference = CreateReleasedCustomNodeReference();
|
|
|
|
CollectGarbage();
|
|
|
|
nodeReference.IsAlive.ShouldBeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public void Removed_Node_Command_And_Owner_Graph_Can_Be_Collected()
|
|
{
|
|
var references = CreateReleasedCommandGraph();
|
|
|
|
CollectGarbage();
|
|
|
|
references.Node.IsAlive.ShouldBeFalse();
|
|
references.Command.IsAlive.ShouldBeFalse();
|
|
references.Owner.IsAlive.ShouldBeFalse();
|
|
}
|
|
|
|
private static void ShowInWindow(INavMenuNode node, Action<AvaloniaWindow, NavMenuItem> assertion)
|
|
{
|
|
var menu = CreateMenu(new[] { node });
|
|
ShowInWindow(menu, assertion);
|
|
}
|
|
|
|
private static AtomUI.Desktop.Controls.NavMenu CreateMenu(IEnumerable<INavMenuNode> nodes)
|
|
{
|
|
return new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false,
|
|
ItemsSource = nodes
|
|
};
|
|
}
|
|
|
|
private static void ShowInWindow(
|
|
AtomUI.Desktop.Controls.NavMenu menu,
|
|
Action<AvaloniaWindow, NavMenuItem> assertion)
|
|
{
|
|
var node = menu.ItemsSource!.Cast<INavMenuNode>().First();
|
|
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 360,
|
|
Height = 260,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var item = menu.ContainerFromItem(node).ShouldBeOfType<NavMenuItem>();
|
|
assertion(window, item);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static ReleasedCommandGraphReferences CreateReleasedCommandGraph()
|
|
{
|
|
CommandOwner? owner = new CommandOwner();
|
|
NavMenuNode? node = new NavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = owner.Command
|
|
};
|
|
AvaloniaList<INavMenuNode>? nodes = new AvaloniaList<INavMenuNode> { node };
|
|
AtomUI.Desktop.Controls.NavMenu? menu = CreateMenu(nodes);
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 360,
|
|
Height = 260,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var command = owner.Command;
|
|
command.SubscriberCount.ShouldBe(1);
|
|
nodes.Remove(node);
|
|
RunDispatcherJobsUntil(() => menu.ContainerFromItem(node) is null);
|
|
command.SubscriberCount.ShouldBe(0);
|
|
|
|
var references = new ReleasedCommandGraphReferences(
|
|
new WeakReference(node),
|
|
new WeakReference(command),
|
|
new WeakReference(owner));
|
|
menu.ItemsSource = null;
|
|
window.Content = null;
|
|
owner = null;
|
|
node = null;
|
|
nodes = null;
|
|
menu = null;
|
|
return references;
|
|
}
|
|
finally
|
|
{
|
|
window.Content = null;
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private static WeakReference CreateReleasedCustomNodeReference()
|
|
{
|
|
ObservableNavMenuNode? node = new ObservableNavMenuNode
|
|
{
|
|
Header = "Command item",
|
|
Command = new TrackingCommand()
|
|
};
|
|
AvaloniaList<INavMenuNode>? nodes = new AvaloniaList<INavMenuNode> { node };
|
|
AtomUI.Desktop.Controls.NavMenu? menu = CreateMenu(nodes);
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 360,
|
|
Height = 260,
|
|
Content = menu
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
menu.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
nodes.Remove(node);
|
|
RunDispatcherJobsUntil(() => menu.ContainerFromItem(node) is null);
|
|
|
|
var nodeReference = new WeakReference(node);
|
|
menu.ItemsSource = null;
|
|
window.Content = null;
|
|
node = null;
|
|
nodes = null;
|
|
menu = null;
|
|
return nodeReference;
|
|
}
|
|
finally
|
|
{
|
|
window.Content = null;
|
|
window.Close();
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
private static void RunDispatcherJobsUntil(Func<bool> condition, int maxPasses = 128)
|
|
{
|
|
for (var i = 0; i < maxPasses; i++)
|
|
{
|
|
if (condition())
|
|
{
|
|
return;
|
|
}
|
|
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
condition().ShouldBeTrue("The expected NavMenu container state was not reached before the dispatcher pass limit.");
|
|
}
|
|
|
|
private static void CollectGarbage()
|
|
{
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
GC.Collect();
|
|
GC.WaitForPendingFinalizers();
|
|
GC.Collect();
|
|
}
|
|
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
private sealed record ReleasedCommandGraphReferences(
|
|
WeakReference Node,
|
|
WeakReference Command,
|
|
WeakReference Owner);
|
|
|
|
private sealed class CommandOwner
|
|
{
|
|
public CommandOwner()
|
|
{
|
|
Command = new TrackingCommand(this);
|
|
}
|
|
|
|
public TrackingCommand Command { get; }
|
|
}
|
|
|
|
private sealed class TrackingCommand : ICommand
|
|
{
|
|
private readonly object? _retainedState;
|
|
private EventHandler? _canExecuteChanged;
|
|
|
|
public TrackingCommand(object? retainedState = null)
|
|
{
|
|
_retainedState = retainedState;
|
|
}
|
|
|
|
public int ExecuteCount { get; private set; }
|
|
|
|
public object? LastParameter { get; private set; }
|
|
|
|
public int SubscriberCount => _canExecuteChanged?.GetInvocationList().Length ?? 0;
|
|
|
|
public bool CanExecuteValue { get; set; } = true;
|
|
|
|
public event EventHandler? CanExecuteChanged
|
|
{
|
|
add
|
|
{
|
|
_canExecuteChanged += value;
|
|
}
|
|
remove
|
|
{
|
|
_canExecuteChanged -= value;
|
|
}
|
|
}
|
|
|
|
public bool CanExecute(object? parameter) => CanExecuteValue;
|
|
|
|
public void Execute(object? parameter)
|
|
{
|
|
ExecuteCount++;
|
|
LastParameter = parameter;
|
|
}
|
|
|
|
public void RaiseCanExecuteChanged()
|
|
{
|
|
_canExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
private sealed class ObservableNavMenuNode : INavMenuNode, INotifyPropertyChanged
|
|
{
|
|
private ICommand? _command;
|
|
private object? _commandParameter;
|
|
private PropertyChangedEventHandler? _propertyChanged;
|
|
|
|
public object? Header { get; set; }
|
|
|
|
public IDataTemplate? HeaderTemplate { get; set; }
|
|
|
|
public EntityKey? ItemKey { get; set; }
|
|
|
|
public PathIcon? Icon { get; set; }
|
|
|
|
public bool IsEnabled { get; set; } = true;
|
|
|
|
public ICommand? Command
|
|
{
|
|
get => _command;
|
|
set => SetField(ref _command, value);
|
|
}
|
|
|
|
public object? CommandParameter
|
|
{
|
|
get => _commandParameter;
|
|
set => SetField(ref _commandParameter, value);
|
|
}
|
|
|
|
public ITreeNode<INavMenuNode>? ParentNode { get; private set; }
|
|
|
|
public IEnumerable<INavMenuNode> Children { get; } = [];
|
|
|
|
public int PropertyChangedSubscriberCount { get; private set; }
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged
|
|
{
|
|
add
|
|
{
|
|
_propertyChanged += value;
|
|
PropertyChangedSubscriberCount++;
|
|
}
|
|
remove
|
|
{
|
|
_propertyChanged -= value;
|
|
PropertyChangedSubscriberCount--;
|
|
}
|
|
}
|
|
|
|
public void UpdateParentNode(INavMenuNode? parentNode)
|
|
{
|
|
ParentNode = parentNode;
|
|
}
|
|
|
|
private void SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
|
{
|
|
if (EqualityComparer<T>.Default.Equals(field, value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
field = value;
|
|
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
}
|
|
|
|
private sealed class PlainNavMenuNode : INavMenuNode
|
|
{
|
|
public object? Header { get; set; }
|
|
|
|
public IDataTemplate? HeaderTemplate { get; set; }
|
|
|
|
public EntityKey? ItemKey { get; set; }
|
|
|
|
public PathIcon? Icon { get; set; }
|
|
|
|
public bool IsEnabled { get; set; } = true;
|
|
|
|
public ICommand? Command { get; set; }
|
|
|
|
public object? CommandParameter { get; set; }
|
|
|
|
public ITreeNode<INavMenuNode>? ParentNode { get; private set; }
|
|
|
|
public IEnumerable<INavMenuNode> Children { get; } = [];
|
|
|
|
public void UpdateParentNode(INavMenuNode? parentNode)
|
|
{
|
|
ParentNode = parentNode;
|
|
}
|
|
}
|
|
|
|
private sealed class LegacyNavMenuNode : INavMenuNode
|
|
{
|
|
public object? Header { get; set; }
|
|
|
|
public IDataTemplate? HeaderTemplate { get; set; }
|
|
|
|
public EntityKey? ItemKey { get; set; }
|
|
|
|
public PathIcon? Icon { get; set; }
|
|
|
|
public bool IsEnabled { get; set; } = true;
|
|
|
|
public ITreeNode<INavMenuNode>? ParentNode { get; private set; }
|
|
|
|
public IEnumerable<INavMenuNode> Children { get; } = [];
|
|
|
|
public void UpdateParentNode(INavMenuNode? parentNode)
|
|
{
|
|
ParentNode = parentNode;
|
|
}
|
|
}
|
|
}
|