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 TreeView own the scrolling surface. Do not wrap it in a ScrollView; place it in a constrained layout such as a Grid row 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
TreeView Light TreeView 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
MAUI Custom TreeView MAUI Custom TreeView

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 IsLeafPropertyName to 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
TreeView Light TreeView 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
TreeView Light TreeView 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 material namespace of TreeView is added to your file, you can add xmlns:material="http://schemas.enisn-projects.io/dotnet/maui/uraniumui/material"

MAUI TreeView Customizations

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"/>

TreeView Spacing

Light Dark
TreeView Light TreeView 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 - true if the node is expanded, otherwise false.
  • IsLeaf - true if the node is a leaf, otherwise false. 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>

Treeview Expander

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
TreeView Animations Enabled TreeView Animations 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 is IsLeaf.
  • 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
MAUI TreeView lazy-loading
In this document