Skip to content
Introducing AI Conversations: Natural Language Interaction for Your Apps! Learn More

TreeView | Getting Started

A hierarchical TreeView for .NET MAUI and Blazor with lazy-loaded branches, configurable expand/collapse icons, single or multi-selection, per-item CanExpand / CanSelect predicates, retry on load failure, optional guide lines, and drag/drop reorder.

GitHubGitHub stars for shinyorg/controls
MAUINuGet downloads for Shiny.Maui.Controls
BlazorNuGet downloads for Shiny.Blazor.Controls
Frameworks
.NET MAUI
Blazor
InitialExpandedMulti-levelLazy loadMulti-select
InitialExpanded with guide linesMulti-level expansionLazy load spinnerMulti-selection
  • Hierarchical data binding via ItemsSource + ChildrenSelector (sync) and/or ChildrenLoader (async)
  • Lazy loading — children load on first expand; chevron is replaced with a spinner; failures show a retry icon
  • Async root loader — top-level items can also be lazy
  • Configurable expand/collapse/retry icons — pass any ImageSource (MAUI) or RenderFragment (Blazor), fall back to built-in glyphs
  • Per-item CanExpand and CanSelect predicates — gate gestures without removing rows
  • HasChildrenSelector — render true leaves with no chevron, distinct from CanExpand
  • Selection modes: None, Single (two-way SelectedItem), Multiple (SelectedItems)
  • Events + Commands (MAUI) for ItemSelected, ItemExpanded, ItemCollapsed, LoadFailed, ItemDropped
  • Programmatic API: ExpandAll / ExpandAllAsync / CollapseAll / Expand / Collapse / Refresh / ReloadAsync
  • Indent + guide lines — configurable IndentSize, toggleable ShowGuideLines
  • Drag/drop reorder — event-only (the control never mutates your data)
  • Blazor keyboard navigation — arrow keys, Enter, Home/End
  1. Install the NuGet package

    Terminal window
    dotnet add package Shiny.Maui.Controls
  2. Register in your MauiProgram.cs

    using Shiny;
    var builder = MauiApp.CreateBuilder();
    builder
    .UseMauiApp<App>()
    .UseShinyControls();
  3. Add the XAML namespace to your pages

    xmlns:shiny="http://shiny.net/maui/controls"

Bind a hierarchical model and supply a ChildrenSelector:

<shiny:TreeView ItemsSource="{Binding Roots}"
ChildrenSelector="{Binding GetChildren}"
HasChildrenSelector="{Binding IsFolder}"
SelectedItem="{Binding Selected, Mode=TwoWay}"
ItemExpanded="OnExpanded">
<shiny:TreeView.ItemTemplate>
<DataTemplate x:DataType="local:FileNode">
<HorizontalStackLayout Spacing="8">
<Label Text="{Binding Icon}" />
<Label Text="{Binding Name}" VerticalTextAlignment="Center" />
</HorizontalStackLayout>
</DataTemplate>
</shiny:TreeView.ItemTemplate>
</shiny:TreeView>

In code-behind, set the delegate properties (they aren’t bindable from XAML because they’re Func<T>):

public TreeViewPage()
{
InitializeComponent();
Tree.ChildrenSelector = item => (item is FileNode f) ? f.Children : null;
Tree.HasChildrenSelector = item => item is FileNode { IsFolder: true };
Tree.CanSelectSelector = item => item is FileNode f && !f.IsLocked;
}

Set ChildrenLoader to an async delegate. The chevron is replaced with a spinner during the load. If the loader throws, the spinner becomes a retry icon (↻) — tapping it re-runs the loader.

Tree.ChildrenLoader = async item =>
{
var children = await myService.GetChildrenAsync(item);
return children;
};

You can mix sync and lazy branches in the same tree. The selector is checked first; if it returns null and a loader is set, the loader runs:

Tree.ChildrenSelector = item => item is FileNode { LazyLoad: false } f ? f.Children : null;
Tree.ChildrenLoader = LoadRemoteChildrenAsync;

Catch load failures and react in your VM:

Tree.LoadFailed += (s, e) =>
StatusLabel.Text = $"Failed to load {((FileNode)e.Item).Name}: {e.Exception.Message}";

For trees where even the root list is expensive, set RootLoader:

Tree.RootLoader = async () => await myService.GetTopLevelAsync();

The whole tree shows a centered loading indicator until it resolves. Tap-to-retry is automatic on failure.

<shiny:TreeView SelectionMode="Single"
SelectedItem="{Binding Selected, Mode=TwoWay}" />

For multi-select:

<shiny:TreeView SelectionMode="Multiple"
SelectedItems="{Binding Selected}" />

Use CanSelectSelector to disable selection for specific items (e.g. category headers, locked rows). Their rows still render and remain visible — they just won’t fire ItemSelected.

Set ExpandedIcon, CollapsedIcon, and RetryIcon to ImageSource values (font icons, embedded resources, URIs). Defaults are the glyphs , , and .

<shiny:TreeView ChevronColor="#7C3AED" ChevronSize="14">
<shiny:TreeView.ExpandedIcon>
<FontImageSource Glyph="&#xF078;" FontFamily="FontAwesome" Color="#7C3AED" />
</shiny:TreeView.ExpandedIcon>
<shiny:TreeView.CollapsedIcon>
<FontImageSource Glyph="&#xF054;" FontFamily="FontAwesome" Color="#7C3AED" />
</shiny:TreeView.CollapsedIcon>
</shiny:TreeView>

Set EnableDragDrop="True" and handle ItemDropped. The TreeView never mutates your data — your handler decides what to do:

void OnItemDropped(object? sender, TreeItemDroppedEventArgs e)
{
var srcList = FindParentList((FileNode)e.SourceItem);
var tgtList = FindParentList((FileNode)e.TargetItem);
srcList.Remove((FileNode)e.SourceItem);
var idx = tgtList.IndexOf((FileNode)e.TargetItem);
tgtList.Insert(idx + 1, (FileNode)e.SourceItem);
Tree.ItemsSource = null;
Tree.ItemsSource = data;
}

The control automatically rejects drops onto descendants (preventing cycles).

EventCommandArgs
ItemSelectedItemSelectedCommandTreeItemEventArgs
ItemExpandedItemExpandedCommandTreeItemEventArgs
ItemCollapsedItemCollapsedCommandTreeItemEventArgs
LoadFailedLoadFailedCommandTreeLoadFailedEventArgs
ItemDroppedItemDroppedCommandTreeItemDroppedEventArgs

All event args carry the underlying Node and convenience Item property.

Tree.ExpandAll(); // sync — skips lazy branches
await Tree.ExpandAllAsync(); // awaits ChildrenLoader for every node
Tree.CollapseAll();
Tree.Expand(item);
Tree.Collapse(item);
Tree.Refresh(item); // drops cached children, re-runs loader on next expand
await Tree.ReloadAsync(); // re-runs RootLoader or rebinds ItemsSource
PropertyDefaultDescription
IndentSize20Pixels of horizontal indent per level
RowPadding8,6Padding inside each row
RowSpacing0Vertical spacing between rows
ShowGuideLinesfalseVertical lines connecting parents to children
GuideLineColor#E0E0E0Color of the guide lines
ChevronColorGrayColor of the default glyph chevron
ChevronSize16Size of the chevron in pixels
SelectedBackgroundColor#E3F2FDBackground tint of the selected row
RowBackgroundColorTransparentBackground of unselected rows
  • Blazor Usage — typed <TreeView TItem> component with RenderFragment icon slots and keyboard navigation