Skip to content

Tree

Tree derives from the native System.Windows.Controls.TreeView for free virtualization (VirtualizingStackPanel with Recycling at every nesting level) and native UIA ExpandCollapsePattern mapping, then layers the full WAI-ARIA APG contract on top: fully custom single- and multi-select tracking (native TreeView is single-select only), Ctrl/Shift range selection, ltr/rtl-aware expand/collapse arrow keys, the * expand-siblings shortcut, and a 500ms-reset typeahead buffer. Only the data-driven composition mode is supported: a consumer sets RootNodes to a hierarchy of NaviusTreeNode, and an implicit HierarchicalDataTemplate in Themes/Tree.xaml recurses it.

xmlns:tree="clr-namespace:Navius.Wpf.Primitives.Controls.Tree;assembly=Navius.Wpf.Primitives"
<tree:NaviusTree x:Name="FileTree"
SelectionMode="Multiple"
RootNodes="{Binding Nodes}" />
var nodes = new List<NaviusTreeNode>
{
new("src", "src", children: new[]
{
new NaviusTreeNode("src/App.cs", "App.cs"),
new NaviusTreeNode("src/Program.cs", "Program.cs"),
}),
};
FileTree.RootNodes = nodes;
Property Type Default From Description
RootNodes IReadOnlyList<NaviusTreeNode>? null NaviusTree The hierarchical data source. Setting it wires ItemsSource automatically; data-driven composition is the only supported mode.
SelectionMode NaviusTreeSelectionMode Single NaviusTree None | Single | Multiple. Both modes route through the same custom selection tracking (see Web deltas), not native TreeView selection.
SelectedValues IReadOnlyList<object> (read-only) - NaviusTree The full current selection.
SelectedValue object? (read-only) null NaviusTree Convenience first-of-SelectedValues read, meaningful mainly in Single mode. Deliberately hides (new) TreeView.SelectedItem’s own SelectedValue, since selection is fully custom-tracked here.
ActiveValue object? (read-only) null NaviusTree The roving-focus/active node’s value, mainly for tests.
Value (on NaviusTreeNode) object - NaviusTreeNode The node’s unique identity, used for selection/expansion state tracking.
Label (on NaviusTreeNode) string - NaviusTreeNode Display/typeahead-match label; overridable per node.
Children (on NaviusTreeNode) IReadOnlyList<NaviusTreeNode>? null NaviusTreeNode Child nodes; null or empty means a leaf (never gets an expand affordance).
Disabled (on NaviusTreeNode) bool false NaviusTreeNode Cannot be selected; skipped by keyboard navigation, typeahead, and select-all.
Parent (on NaviusTreeNode) NaviusTreeNode? null NaviusTreeNode Set automatically from the constructor’s children list; null for root nodes.
IsExpanded (on NaviusTreeNode) bool false NaviusTreeNode Two-way bound from the item container’s style; lives on the data node (not the container) so it survives virtualization recycling. Raises INotifyPropertyChanged.
IsSelected (on NaviusTreeNode) bool false NaviusTreeNode Selection membership, set internally by NaviusTree; read-only to consumers. Raises INotifyPropertyChanged so the item’s automation peer can push live UIA selection events.
HasChildren (on NaviusTreeNode) bool (read-only) - NaviusTreeNode True when Children has at least one entry; drives the expand affordance.
Method Description
SetSelectedValues(IEnumerable<object> values) Programmatically replaces the selection.
SelectNodeExclusive(NaviusTreeNode node) Replaces the selection with just this node. Routed target for a SelectionItemPattern.Select() call from a UIA client; also directly callable. No-op when selection is disabled or the node is disabled.
AddNodeToSelection(NaviusTreeNode node) Adds the node to the current selection. In Single mode, behaves like SelectNodeExclusive unless a different node is already selected, in which case it throws InvalidOperationException (matching UIA’s rule that adding to a single-select container’s differing non-empty selection is invalid). Routed target for SelectionItemPattern.AddToSelection().
RemoveNodeFromSelection(NaviusTreeNode node) Removes the node from the selection if present. Routed target for SelectionItemPattern.RemoveFromSelection().
HandleKey(Key key, ModifierKeys mods) Handles one key exactly per the keyboard table below; returns whether it was consumed. Public (not the more natural internal) so it is directly unit-testable without constructing real KeyEventArgs.
HandleItemClicked(NaviusTreeNode node, NaviusTreeItem container, ModifierKeys mods) Called by NaviusTreeItem on a mouse click; applies Ctrl-toggle / Shift-range mouse conventions in Multiple mode (see Web deltas), otherwise activates the node. Public for the same testability reason as HandleKey.
Event Signature Fires when
SelectedValuesChanged EventHandler<TreeSelectionChangedEventArgs> The selection set changes, carrying the full new selection.
Key Behavior
ArrowDown / ArrowUp Move to the next/previous enabled visible node, no wrap.
Shift + ArrowDown / Shift + ArrowUp (multi-select) Move and toggle the new node into the selection.
Home / End Focus the first/last enabled visible node.
Ctrl + Shift + Home / Ctrl + Shift + End (multi-select) Select the range from the current node to the start/end.
Enter Toggle expansion (if expandable) then apply selection (Single: replace, Multiple: toggle).
Space Select the focused node (Single: replace, Multiple: toggle).
Shift + Space (multi-select) Select the contiguous span from the last anchor to the current node.
* (Key.Multiply or Shift + 8) Expand all sibling nodes at the current node’s level.
ArrowRight (ltr) / ArrowLeft (rtl) On a collapsed expandable node: expand it. On an expanded one: move focus into its first child. On a leaf: no-op.
ArrowLeft (ltr) / ArrowRight (rtl) On an expanded expandable node: collapse it. Otherwise: move focus to the parent.
Ctrl + A (multi-select) Toggle select-all/deselect-all across enabled visible nodes.
A-Z, digit, or numpad-digit key (no Ctrl/Alt) Typeahead: matches node labels, cycles repeats of the same character, resets after 500ms of inactivity.

Resolved via FlowDirection: ArrowRight is the expand key and ArrowLeft the collapse key under LeftToRight, mirrored under RightToLeft. All navigation is driven off the NaviusTreeNode data graph (RootNodes), not off realized containers, so it works correctly for off-screen nodes at virtualized scale; TreeContainerLocator best-effort realizes a container to move focus onto it.

No custom peer on NaviusTree beyond NaviusTreeAutomationPeer, which derives the native TreeViewAutomationPeer and does not override GetAutomationControlTypeCore(), so it reports AutomationControlType.Tree exactly as the native peer does: the control type is inherited, not customized. It adds ISelectionProvider.CanSelectMultiple (reflecting SelectionMode == Multiple, since the native peer hardcodes false) and a best-effort GetSelection() limited to currently realized containers (it never forces off-screen selected nodes to realize). Each item’s container, NaviusTreeItem, returns a NaviusTreeItemAutomationPeer (deriving TreeViewItemAutomationPeer) that re-implements ISelectionItemProvider: IsSelected reads the bound NaviusTreeNode.IsSelected (native TreeViewItem.IsSelected is never set, since selection is fully custom-tracked), and AddToSelection/RemoveFromSelection/Select route through NaviusTree.AddNodeToSelection/RemoveNodeFromSelection/SelectNodeExclusive. When a node’s IsSelected changes, NaviusTreeItem forwards it to the realized peer’s RaiseSelectionEvents, which raises the SelectionItemPattern.IsSelected property-change plus OnElementSelected/OnElementRemovedFromSelection automation events, so a UIA client observes live selection updates even though selection state lives on the data node rather than the container (this remains correct across virtualization recycling). This was a confirmed gap fixed in the M6 audit: item-level SelectionItemPattern.IsSelected previously reflected the always-false native TreeViewItem.IsSelected rather than this control’s real selection state. Virtualization (VirtualizingStackPanel with VirtualizationMode.Recycling, at both the tree and item nesting levels) is pinned by two perf-guard tests that load the real Themes/Tree.xaml dictionary onto real instances. The unit suite (tests/Navius.Wpf.Tests/TreeTests.cs, 72 tests) exercises the full keyboard table, both selection modes, the selection-event forwarding, and the automation peer provider logic.

  • Selection is fully custom for both Single and Multiple modes, tracked on NaviusTreeNode.IsSelected rather than native TreeView.SelectedItem/TreeViewItem.IsSelected. Native WPF TreeView enforces single-selection internally (setting IsSelected = true on a second item silently unselects the first), which cannot represent the web contract’s multi-select set, so one uniform custom mechanism is used for both modes instead of two parallel systems.
  • Mouse click semantics extend the web contract: the web’s ActivateAsync does not distinguish Ctrl/Shift-click at all (a plain click in multi-select always toggles). The WPF port adds Ctrl+Click (toggle) and Shift+Click (range-select from the last anchor) as native-feeling desktop conventions on top of the ported default, a deliberate addition beyond the web contract rather than a silent deviation.
  • FocusMode="activedescendant" (the web’s container-owned-focus mode for virtualized trees) was dropped. Virtualization is handled natively by VirtualizingStackPanel recycling instead, so every node gets real WPF keyboard focus; only the web’s “roving” focus model is ported.
  • Roving tabindex is approximate, not strictly enforced: native WPF Tab-in behavior (focus the last-focused item, or the first on first entry) is relied on rather than hand-managing KeyboardNavigation.IsTabStop across container recycling, a recorded simplification.
  • Manual markup composition (the web’s NaviusTreeItem/NaviusTreeGroup/etc. parts authored directly as ChildContent) was not ported; data-driven mode (RootNodes plus the implicit HierarchicalDataTemplate) is the only supported authoring path.
  • The web’s whole-tree Disabled and Label/LabelledBy parameters have no dedicated wrapper properties: Disabled maps directly to native IsEnabled (already cascades to descendants for free), and Label/LabelledBy map directly to native AutomationProperties.Name/LabeledBy. Orientation (the web’s vertical/horizontal styling hint) is out of scope; WPF has no horizontal- tree convention.

The Tree Gallery page rendered at the pinned commit, in each theme.

Tree Gallery page in the light theme

Tree Gallery page in the dark theme

Tree Gallery page in the high contrast theme