TreeView
TreeView is a component that displays a hierarchical list of items. It's a MAUI layer control with complex logic. It also comes with some pre-built behaviors like hierarchical selection.
Usage
TreeView is included in the UraniumUI.Material.Controls namespace. You should add it to your XAML like this:
xmlns:material="http://schemas.enisn-projects.io/dotnet/maui/uraniumui/material"
xmlns:m="clr-namespace:UraniumUI.Icons.MaterialSymbols;assembly=UraniumUI.Icons.MaterialSymbols"
Then you can use it like this:
<material:TreeView ItemsSource="{Binding Nodes}" />
TreeView doesn't have any visual appearance without data. You should bind some data to see the control.
For large data sets, let
TreeViewown the scrolling surface. Do not wrap it in aScrollView; place it in a constrained layout such as aGridrow with*height so the internal virtualized list can recycle rows correctly.
Data Binding
Prepare a ViewModel that contain a list of a node object. Node means a single item in the tree. It can be a folder or a file. It can be a person or a company. It can be a country or a city. It can be anything. It's up to you. So there is no type limitation or no interface implementation required for your object. It can be a simple object that should have a Children property that includes list of same type of the object when hierarchical data is required.
A simple example of a node object:
public class MyItem
{
public MyItem()
{
}
public MyItem(string name) // For easy initialization (optional)
{
Name = name;
}
public virtual string Name { get; set; }
public virtual IList<MyItem> Children { get; set; } = new ObservableCollection<MyItem>();
}
Initialize your ViewModel with some data:
public class TreeViewPageViewModel : BindableObject
{
public ObservableCollection<MyItem> Nodes { get; set; } = new();
public TreeViewPageViewModel()
{
Nodes.Add(new MyItem("A")
{
Children =
{
new MyItem("A.1"),
new MyItem("A.2"),
}
});
Nodes.Add(new MyItem("B")
{
Children =
{
new MyItem("B.1")
{
Children =
{
new MyItem("B.1.a"),
new MyItem("B.1.b"),
new MyItem("B.1.c"),
new MyItem("B.1.d"),
}
},
new MyItem("B.2"),
}
});
Nodes.Add(new MyItem("C"));
Nodes.Add(new MyItem("D"));
}
}
Now you can bind your ViewModel to the TreeView:
<material:TreeView ItemsSource="{Binding Nodes}" />
| Light | Dark |
|---|---|
![]() |
![]() |
ItemTemplate
You can customize nodes with ItemTemplate. It's just like a ListView or CollectionView:
<material:TreeView ItemsSource="{Binding Nodes}">
<material:TreeView.ItemTemplate>
<DataTemplate>
<HorizontalStackLayout Spacing="5">
<Image Source="{FontImageSource FontFamily=MaterialOutlined, Glyph={x:Static m:MaterialOutlined.Folder}, Color={StaticResource Primary}}" />
<Label Text="{Binding Name}" FontAttributes="Bold" />
<Label Text="{Binding Children.Count, StringFormat='({0})'}" />
</HorizontalStackLayout>
</DataTemplate>
</material:TreeView.ItemTemplate>
</material:TreeView>
| Light | Dark |
|---|---|
![]() |
![]() |
IsExpandedPropertyName
You can bind expanding property to a property of your node object. It's useful when you want to save the state of the tree. For example, you can save the state of the tree in the local storage and restore it when the user opens the app again. To do this, you should set IsExpandedPropertyName property of the TreeView to the name of the property that contains the state of the node. For example, if you have a IsExtended property in your node object, you should set IsExpandedPropertyName to IsExtended:
<material:TreeView ItemsSource="{Binding Nodes}" IsExpandedPropertyName="IsExtended">
public TreeViewPageViewModel()
{
// ...
Nodes[1].IsExtended = true;
Nodes[1].Children[0].IsExtended = true;
}
They will be rendered as expanded when treeview is initialized.
IsLeafPropertyName
If the node object contain its children in a different property, you can set IsLeafPropertyName property of the TreeView to the name of the property that contains the state of the node. Default value is IsLeaf. If you have a IsLeaf property in your node object, it'll be automatically binded. If your property that declares leaf state has a different name, you should consider to set IsLeafPropertyName.
<material:TreeView ItemsSource="{Binding Nodes}" IsLeafPropertyName="IsLeaf" />
[!NOTE] Leaf status is already managed by the TreeView. But when lazy-loading is used, TreeView can't know if the node is a leaf or not. So you should set
IsLeafPropertyNameto the name of the property that contains the state of the node.
ChildrenBinding
If the node object contain its children in a different property, you can set ChildrenBinding property of the TreeView to the name of the property that contains the children. For example, if you have a SubItems property in your node object, you should set ChildrenBinding to SubItems:
<material:TreeView ItemsSource="{Binding Nodes}" ChildrenBinding="{Binding SubItems}" />
Selection
TreeView supports single selection and multiple selection. You can set SelectionMode property of the TreeView to Single or Multiple to enable selection. Default value is None. You can bind SelectedItem or SelectedItems property of the TreeView to a property in your ViewModel to get the selected item or items.
If you prefer event or command-based handling instead of binding to a reactive property, TreeView also provides SelectedItemChanged / SelectedItemChangedCommand for single selection and SelectedItemsChanged / SelectedItemsChangedCommand for multiple selection.
Single Selection
<material:TreeView ItemsSource="{Binding Nodes}" SelectionMode="Single" SelectedItem="{Binding SelectedNode}" />
<material:TreeView
ItemsSource="{Binding Nodes}"
SelectionMode="Single"
SelectedItemChangedCommand="{Binding NodeSelectedCommand}" />
| Light | Dark |
|---|---|
![]() |
![]() |
Multiple Selection
<material:TreeView ItemsSource="{Binding Nodes}" SelectionMode="Multiple" SelectedItems="{Binding SelectedNodes}" />
<material:TreeView
ItemsSource="{Binding Nodes}"
SelectionMode="Multiple"
SelectedItemsChangedCommand="{Binding SelectedNodesChangedCommand}" />
| Light | Dark |
|---|---|
![]() |
![]() |
Hierarchical Selection (CheckBox)
TreeView has a special behavior for CheckBoxes. TreeView supports hierarchical selection. It means that when you select a parent node, all of its children will be selected. When you deselect a parent node, all of its children will be deselected. When some of children is selected, CheckBox will enter semi-selected state. It's useful when you want to select a group of items. For example, you can select all files in a folder.
TreeView provides TreeViewHierarchicalSelectBehavior that can be used only with CheckBox when it's directly ItemTemplate of the TreeView.
<material:TreeView ItemsSource="{Binding Nodes}">
<material:TreeView.ItemTemplate>
<DataTemplate>
<material:CheckBox Text="{Binding Name}">
<material:CheckBox.Behaviors>
<material:TreeViewHierarchicalSelectBehavior />
</material:CheckBox.Behaviors>
</material:CheckBox>
</DataTemplate>
</material:TreeView.ItemTemplate>
</material:TreeView>
Accessibility
Tree views usually combine selection, expansion, and custom item templates. The built-in node row is already wrapped by UraniumUI's interactive row surface, so you do not need to wrap the whole ItemTemplate in another ButtonView or StatefulContentView just to make the row selectable.
Keep item text available through labels, do not rely on indentation or color alone, and provide semantic descriptions for icon-only custom node actions. If your ItemTemplate adds extra actions inside the row, such as a delete icon or secondary menu, make those actions real focusable controls instead of passive layouts with only TapGestureRecognizer. If you use TreeViewHierarchicalSelectBehavior, keep the checkbox Text bound to the node label so the option can be announced.
See Clickable Areas for custom row guidance.
Customizations
TreeView allows you to customize its appearance in a couple of ways.
Styles
You can customize the appearance of the TreeView with styles. You override the following styles in your Page or App.xaml:
<!--TreeView Selection Background Color-->
<Style TargetType="material:TreeView">
<Setter Property="SelectionColor" Value="Red" />
</Style>
<!--TreeView Regular Label -->
<Style TargetType="Label" Class="TreeView.Label" BaseResourceKey="Microsoft.Maui.Controls.Label">
<Setter Property="TextColor" Value="Red" />
</Style>
<!--TreeView Selected Label -->
<Style TargetType="Label" Class="TreeView.Label.Selected" BaseResourceKey="Microsoft.Maui.Controls.Label" >
<Setter Property="TextColor" Value="White" />
</Style>
<!--TreeView Regular Arrow -->
<Style TargetType="Path" Class="TreeView.Arrow" BaseResourceKey="Microsoft.Maui.Controls.Shapes.Path">
<Setter Property="Fill" Value="Red" />
</Style>
<!--TreeView Selected Arrow -->
<Style TargetType="Path" Class="TreeView.Arrow.Selected" BaseResourceKey="Microsoft.Maui.Controls.StyleClass.TreeView.Arrow">
<Setter Property="Fill" Value="White" />
</Style>
[!NOTE] Make sure
materialnamespace of TreeView is added to your file, you can addxmlns:material="http://schemas.enisn-projects.io/dotnet/maui/uraniumui/material"
Spacing
You can change the spacing between the arrow icon and the content with Spacing property. Default value is 10.
<material:TreeView ItemsSource="{Binding Nodes}" Spacing="25"/>
| Light | Dark |
|---|---|
![]() |
![]() |
ExpanderTemplate
You can completely customize the expander with ExpanderTemplate property. It's an arrow by default. You can use any view as an expander. For example, you can use a Switch to expand and collapse nodes.
You can use following binding properties in the ExpanderTemplate:
IsExpanded-trueif the node is expanded, otherwisefalse.IsLeaf-trueif the node is a leaf, otherwisefalse. You can use it to manage visibility of the control.
<material:TreeView ItemsSource="{Binding Nodes}">
<material:TreeView.ExpanderTemplate>
<DataTemplate>
<Switch IsToggled="{Binding IsExpanded}" />
</DataTemplate>
</material:TreeView.ExpanderTemplate>
</material:TreeView>
UseAnimation
Determines whether to use animations when expanding and collapsing nodes. Default value is true. You may want to disable animations if you want to improve performance while working with huge amount of tree nodes. On Windows, setting this property to false also disables the native CollectionView item transition animations used by the virtualized TreeView surface.
<material:TreeView ItemsSource="{Binding Nodes}" UseAnimation="False"/>
| Enabled | Disabled |
|---|---|
![]() |
![]() |
Lazy-Loading
TreeView supports lazy-loading of children. It means that children will be loaded only when the node is expanded. TreeView executes LoadChildrenCommand command with node item that is expanded as parameter when the node is expanded. You can set LoadChildrenCommand property of the TreeView to the command that will be executed when the node is expanded. For example, you can load children from the database when the node is expanded.
Following properies can be used to define a propert lazy-loading behavior:
IsLeafPropertyName- The name of the property that contains the state of the node. Default value isIsLeaf.LoadChildrenCommand- The command that will be executed when the node is expanded.
Example
A file system is a good example of lazy-loading. When you open a folder, you don't want to load all of its children. You want to load children only when the folder is expanded. You can use LoadChildrenCommand to load children from the database when the folder is expanded.
public class TreeViewFileSystemViewModel : UraniumBindableObject
{
private static readonly EnumerationOptions EnumerationOptions = new()
{
IgnoreInaccessible = true,
RecurseSubdirectories = false,
ReturnSpecialDirectories = false,
AttributesToSkip = 0,
};
public ObservableCollection<NodeItem> Nodes { get; private set; }
public ICommand LoadChildrenCommand { get; }
public TreeViewFileSystemViewModel()
{
InitializeNodes();
LoadChildrenCommand = new Command<NodeItem>(async node => await LoadChildrenAsync(node));
}
async Task LoadChildrenAsync(NodeItem node)
{
if (node is null || !node.IsDirectory || node.HasLoadedChildren)
{
return;
}
node.HasLoadedChildren = true;
var children = await Task.Run(() => GetContent(node.Path).ToArray());
node.Children = new ObservableCollection<NodeItem>(children);
node.IsLeaf = node.Children.Count == 0;
}
void InitializeNodes()
{
var path = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (DeviceInfo.Platform == DevicePlatform.WinUI)
{
path = "C:\\";
}
Nodes = new ObservableCollection<NodeItem>(
GetContent(path));
}
IEnumerable<NodeItem> GetContent(string dir)
{
foreach (var directory in Directory.EnumerateDirectories(dir, "*", EnumerationOptions))
{
yield return new NodeItem
{
Name = Path.GetFileName(directory),
Path = directory,
IsDirectory = true,
IsLeaf = false,
};
}
foreach (var file in Directory.EnumerateFiles(dir, "*", EnumerationOptions))
{
yield return new NodeItem
{
Name = Path.GetFileName(file),
Path = file,
IsDirectory = false,
IsLeaf = true,
};
}
}
public class NodeItem : UraniumBindableObject
{
private bool isLeaf;
private bool hasLoadedChildren;
private IList<NodeItem> children = new ObservableCollection<NodeItem>();
public string Name { get; set; }
public string Path { get; set; }
public bool IsDirectory { get; set; }
public bool IsLeaf { get => isLeaf; set => SetProperty(ref isLeaf, value); }
public bool HasLoadedChildren { get => hasLoadedChildren; set => SetProperty(ref hasLoadedChildren, value); }
public IList<NodeItem> Children { get => children; set => SetProperty(ref children, value); }
}
}
<material:TreeView
ItemsSource="{Binding Nodes}"
IsLeafPropertyName="IsLeaf"
LoadChildrenCommand="{Binding LoadChildrenCommand}">
<material:TreeView.ItemTemplate>
<DataTemplate>
<HorizontalStackLayout Spacing="5" VerticalOptions="Center">
<Image>
<Image.Triggers>
<DataTrigger TargetType="Image" Binding="{Binding IsDirectory}" Value="True">
<Setter Property="Source" Value="{FontImageSource FontFamily=MaterialOutlined, Glyph={x:Static m:MaterialOutlined.Folder}, Color={AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}}" />
</DataTrigger>
<DataTrigger TargetType="Image" Binding="{Binding IsDirectory}" Value="False">
<Setter Property="Source" Value="{FontImageSource FontFamily=MaterialOutlined, Glyph={x:Static m:MaterialOutlined.File_present}, Color={AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}}" />
</DataTrigger>
</Image.Triggers>
</Image>
<Label Text="{Binding Name}" FontAttributes="Bold" />
</HorizontalStackLayout>
</DataTemplate>
</material:TreeView.ItemTemplate>
</material:TreeView>
| Dark |
|---|
![]() |















