DEV Community

Luis Beltran
Luis Beltran

Posted on • Edited on

Creating a TreeView control in .NET MAUI

This article is part of the #MAUIUIJuly initiative by Matt Goldman. You'll find other helpful articles and tutorials published daily by community members and experts there, so make sure to check it out every day.

A few years ago, a student asked me if it was possible to integrate a TreeView control in Xamarin.Forms in order to display a hierarchical set of items. The specific case: A project has tasks, each task is assigned to an employee. We found an interesting project here and modified it a bit here.

This is what we got in the end:

TreeView implementation in Xamarin.Forms

What does it take to implement such a control in .NET MAUI? Let's see...

Step 1. Add small images for the TreeView nodes

Images

Step 2. Create a folder. Name it Models. Then, add a couple of classes inside:

Models

2a. Every node will be represented as a XamlItem with a Key and Id.



namespace MauiTreeView.Models
{
    [Serializable]
    public class XamlItem
    {
        public string Key { get; set; }
        public int ItemId { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

2b. Every group will be represented as a XamlItemGroup with a Name, GroupId, a collection of nodes (XamlItems) and a collection of sub-groups (Children) to represent the hierarchy.



namespace MauiTreeView.Models
{
    [Serializable]
    public class XamlItemGroup
    {
        public List<XamlItemGroup> Children { get; } = new ();
        public List<XamlItem> XamlItems { get; } = new ();

        public string Name { get; set; }
        public int GroupId { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

Step 3. Create a folder. Name it Controls. Then, add four classes inside:

Custom controls

3a. Class ResourceImage assigns the image (icon) for a node in the TreeView. For example, an open folder when the user expands the node. It uses a BindableProperty and extends an Image control.



namespace MauiTreeView.Controls
{
    public class ResourceImage : Image
    {
        public static readonly BindableProperty ResourceProperty = BindableProperty.Create(nameof(Resource), typeof(string), typeof(string), null, BindingMode.OneWay, null, ResourceChanged);

        private static void ResourceChanged(BindableObject bindable, object oldvalue, object newvalue)
        {
            var resourceString = (string)newvalue;
            var imageControl = (Image)bindable;

            imageControl.Source = ImageSource.FromFile(resourceString);
        }

        public string Resource
        {
            get => (string)GetValue(ResourceProperty);
            set => SetValue(ResourceProperty, value);
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

3b. Next, we have the ExpandButtonContent class, which sets the specific icons that will be displayed for expanded/Collapsed Leaf Nodes or expanded, on-request nodes. It uses the ResourceImage and extends a ContentView. Although we have not created the TreeViewNode class, don't worry. We will in a minute :)



namespace MauiTreeView.Controls
{
    public class ExpandButtonContent : ContentView
    {
        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();

            var node = BindingContext as TreeViewNode;
            bool isLeafNode = (node.ChildrenList == null || node.ChildrenList.Count == 0);

            //empty nodes have no icon to expand unless showExpandButtonIfEmpty is et to true which will show the expand
            //icon can click and populated node on demand propably using the expand event.
            if ((isLeafNode) && !node.ShowExpandButtonIfEmpty)
            {
                Content = new ResourceImage
                {
                    Resource = isLeafNode ? "blank.png" : "folderopen.png",
                    HeightRequest = 16,
                    WidthRequest = 16
                };
            }
            else
            {
                Content = new ResourceImage
                {
                    Resource = node.IsExpanded ? "openglyph.png" : "collpsedglyph.png",
                    HeightRequest = 16,
                    WidthRequest = 16
                };
            }
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

3c. Now we have the TreeViewNode class implementation. This is a recursive StackLayout with several elements. Among others:

  • A BoxView (_SpacerBoxView) for sub-levels identation
  • A ContentView (_ExpandButtonContent) with a TapGestureRecognizer that is used to determine if the user tapped on the image (to expand/hide the inner content).
  • The TreeViewNode content is configured in the constructor.
  • The ChildrenList is a IList<TreeViewNode>, so that means this control is recursive: A node can contain other (sub)nodes.


using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace MauiTreeView.Controls
{
    public class TreeViewNode : StackLayout
    {
        private DataTemplate _ExpandButtonTemplate = null;

        private TreeViewNode _ParentTreeViewItem;

        private DateTime _ExpandButtonClickedTime;

        private readonly BoxView _SpacerBoxView = new BoxView() { Color = Colors.Transparent };
        private readonly BoxView _EmptyBox = new BoxView { BackgroundColor = Colors.Blue, Opacity = .5 };

        private const int ExpandButtonWidth = 32;
        private ContentView _ExpandButtonContent = new ();

        private readonly Grid _MainGrid = new Grid
        {
            VerticalOptions = LayoutOptions.Start,
            HorizontalOptions = LayoutOptions.Fill,
            RowSpacing = 2
        };

        private readonly StackLayout _ContentStackLayout = new StackLayout { Orientation = StackOrientation.Horizontal };

        private readonly ContentView _ContentView = new ContentView
        {
            HorizontalOptions = LayoutOptions.Fill,
        };

        private readonly StackLayout _ChildrenStackLayout = new StackLayout
        {
            Orientation = StackOrientation.Vertical,
            Spacing = 0,
            IsVisible = false
        };

        private IList<TreeViewNode> _Children = new ObservableCollection<TreeViewNode>();
        private readonly TapGestureRecognizer _TapGestureRecognizer = new TapGestureRecognizer();
        private readonly TapGestureRecognizer _ExpandButtonGestureRecognizer = new TapGestureRecognizer();
        private readonly TapGestureRecognizer _DoubleClickGestureRecognizer = new TapGestureRecognizer();

        internal readonly BoxView SelectionBoxView = new BoxView { Color = Colors.Blue, Opacity = .5, IsVisible = false };

        private TreeView ParentTreeView => Parent?.Parent as TreeView;
        private double IndentWidth => Depth * SpacerWidth;
        private int SpacerWidth { get; } = 30;
        private int Depth => ParentTreeViewItem?.Depth + 1 ?? 0;

        private bool _ShowExpandButtonIfEmpty = false;
        private Color _SelectedBackgroundColor = Colors.Blue;
        private double _SelectedBackgroundOpacity = .3;

        public event EventHandler Expanded;

        /// <summary>
        /// Occurs when the user double clicks on the node
        /// </summary>
        public event EventHandler DoubleClicked;

        protected override void OnParentSet()
        {
            base.OnParentSet();
            Render();
        }

        public bool IsSelected
        {
            get => SelectionBoxView.IsVisible;
            set => SelectionBoxView.IsVisible = value;
        }
        public bool IsExpanded
        {
            get => _ChildrenStackLayout.IsVisible;
            set
            {
                _ChildrenStackLayout.IsVisible = value;

                Render();
                if (value)
                {
                    Expanded?.Invoke(this, new EventArgs());
                }
            }
        }

        /// <summary>
        /// set to true to show the expand button in case we need to poulate the child nodes on demand
        /// </summary>
        public bool ShowExpandButtonIfEmpty
        {
            get { return _ShowExpandButtonIfEmpty; }
            set { _ShowExpandButtonIfEmpty = value; }
        }

        /// <summary>
        /// set BackgroundColor when node is tapped/selected
        /// </summary>
        public Color SelectedBackgroundColor
        {
            get { return _SelectedBackgroundColor; }
            set { _SelectedBackgroundColor = value; }
        }

        /// <summary>
        /// SelectedBackgroundOpacity when node is tapped/selected
        /// </summary>
        public Double SelectedBackgroundOpacity
        {
            get { return _SelectedBackgroundOpacity; }
            set { _SelectedBackgroundOpacity = value; }
        }

        /// <summary>
        /// customize expand icon based on isExpanded property and or data 
        /// </summary>
        public DataTemplate ExpandButtonTemplate
        {
            get { return _ExpandButtonTemplate; }
            set { _ExpandButtonTemplate = value; }
        }

        public View Content
        {
            get => _ContentView.Content;
            set => _ContentView.Content = value;
        }

        public IList<TreeViewNode> ChildrenList
        {
            get => _Children;
            set
            {
                if (_Children is INotifyCollectionChanged notifyCollectionChanged)
                {
                    notifyCollectionChanged.CollectionChanged -= ItemsSource_CollectionChanged;
                }

                _Children = value;

                if (_Children is INotifyCollectionChanged notifyCollectionChanged2)
                {
                    notifyCollectionChanged2.CollectionChanged += ItemsSource_CollectionChanged;
                }

                TreeView.RenderNodes(_Children, _ChildrenStackLayout, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), this);

                Render();
            }
        }

        /// <summary>
        /// TODO: Remove this. We should be able to get the ParentTreeViewNode by traversing up through the Visual Tree by 'Parent', but this not working for some reason.
        /// </summary>
        public TreeViewNode ParentTreeViewItem
        {
            get => _ParentTreeViewItem;
            set
            {
                _ParentTreeViewItem = value;
                Render();
            }
        }

        /// <summary>
        /// Constructs a new TreeViewItem
        /// </summary>
        public TreeViewNode()
        {
            var itemsSource = (ObservableCollection<TreeViewNode>)_Children;
            itemsSource.CollectionChanged += ItemsSource_CollectionChanged;

            _TapGestureRecognizer.Tapped += TapGestureRecognizer_Tapped;
            GestureRecognizers.Add(_TapGestureRecognizer);

            _MainGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
            _MainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
            _MainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });

            _MainGrid.Children.Add(SelectionBoxView);

            _ContentStackLayout.Children.Add(_SpacerBoxView);
            _ContentStackLayout.Children.Add(_ExpandButtonContent);
            _ContentStackLayout.Children.Add(_ContentView);

            SetExpandButtonContent(_ExpandButtonTemplate);

            _ExpandButtonGestureRecognizer.Tapped += ExpandButton_Tapped;
            _ExpandButtonContent.GestureRecognizers.Add(_ExpandButtonGestureRecognizer);

            _DoubleClickGestureRecognizer.NumberOfTapsRequired = 2;
            _DoubleClickGestureRecognizer.Tapped += DoubleClick;
            _ContentView.GestureRecognizers.Add(_DoubleClickGestureRecognizer);

            _MainGrid.SetRow((IView)_ChildrenStackLayout, 1);
            _MainGrid.SetColumn((IView)_ChildrenStackLayout, 0);

            _MainGrid.Children.Add(_ContentStackLayout);
            _MainGrid.Children.Add(_ChildrenStackLayout);

            base.Children.Add(_MainGrid);

            HorizontalOptions = LayoutOptions.Fill;
            VerticalOptions = LayoutOptions.Start;

            Render();
        }

        void _DoubleClickGestureRecognizer_Tapped(object sender, EventArgs e)
        {
        }

        private void ChildSelected(TreeViewNode child)
        {
            //Um? How does this work? The method here is a private method so how are we calling it?
            ParentTreeViewItem?.ChildSelected(child);
            ParentTreeView?.ChildSelected(child);
        }

        private void Render()
        {
            _SpacerBoxView.WidthRequest = IndentWidth;

            if ((ChildrenList == null || ChildrenList.Count == 0) && !ShowExpandButtonIfEmpty)
            {
                SetExpandButtonContent(_ExpandButtonTemplate);
                return;
            }

            SetExpandButtonContent(_ExpandButtonTemplate);

            foreach (var item in ChildrenList)
            {
                item.Render();
            }
        }

        /// <summary>
        /// Use DataTemplae 
        /// </summary>
        private void SetExpandButtonContent(DataTemplate expandButtonTemplate)
        {
            if (expandButtonTemplate != null)
            {
                _ExpandButtonContent.Content = (View)expandButtonTemplate.CreateContent();
            }
            else
            {
                _ExpandButtonContent.Content = (View)new ContentView { Content = _EmptyBox };
            }
        }

        private void ExpandButton_Tapped(object sender, EventArgs e)
        {
            _ExpandButtonClickedTime = DateTime.Now;
            IsExpanded = !IsExpanded;
        }

        private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
        {
            //TODO: Hack. We don't want the node to become selected when we are clicking on the expanded button
            if (DateTime.Now - _ExpandButtonClickedTime > new TimeSpan(0, 0, 0, 0, 50))
            {
                ChildSelected(this);
            }
        }

        private void DoubleClick(object sender, EventArgs e)
        {
            DoubleClicked?.Invoke(this, new EventArgs());
        }

        private void ItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            TreeView.RenderNodes(_Children, _ChildrenStackLayout, e, this);
            Render();
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

3d. And finally, the TreeView control implementation!

  • The public collection RootNodes needs to be assigned (later) in order to display the nodes. It's a List of TreeViewNode.

  • You can call the public ProcessXamlItemGroups method to map the hierarchy of nodes (xamlItemGroups) into an ObservableCollection of TreeViewNode.



using MauiTreeView.Models;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace MauiTreeView.Controls
{
    public class TreeView : ScrollView
    {
        private readonly StackLayout _StackLayout = new StackLayout { Orientation = StackOrientation.Vertical };

        //TODO: This initialises the list, but there is nothing listening to INotifyCollectionChanged so no nodes will get rendered
        private IList<TreeViewNode> _RootNodes = new ObservableCollection<TreeViewNode>();
        private TreeViewNode _SelectedItem;

        /// <summary>
        /// The item that is selected in the tree
        /// TODO: Make this two way - and maybe eventually a bindable property
        /// </summary>
        public TreeViewNode SelectedItem
        {
            get => _SelectedItem;

            set
            {
                if (_SelectedItem == value)
                {
                    return;
                }

                if (_SelectedItem != null)
                {
                    _SelectedItem.IsSelected = false;
                }

                _SelectedItem = value;

                SelectedItemChanged?.Invoke(this, new EventArgs());
            }
        }


        public IList<TreeViewNode> RootNodes
        {
            get => _RootNodes;
            set
            {
                _RootNodes = value;

                if (value is INotifyCollectionChanged notifyCollectionChanged)
                {
                    notifyCollectionChanged.CollectionChanged += (s, e) =>
                    {
                        RenderNodes(_RootNodes, _StackLayout, e, null);
                    };
                }

                RenderNodes(_RootNodes, _StackLayout, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset), null);
            }
        }

        /// <summary>
        /// Occurs when the user selects a TreeViewItem
        /// </summary>
        public event EventHandler SelectedItemChanged;

        public TreeView()
        {
            Content = _StackLayout;
        }

        private void RemoveSelectionRecursive(IEnumerable<TreeViewNode> nodes)
        {
            foreach (var treeViewItem in nodes)
            {
                if (treeViewItem != SelectedItem)
                {
                    treeViewItem.IsSelected = false;
                }

                RemoveSelectionRecursive(treeViewItem.ChildrenList);
            }
        }

        private static void AddItems(IEnumerable<TreeViewNode> childTreeViewItems, StackLayout parent, TreeViewNode parentTreeViewItem)
        {
            foreach (var childTreeNode in childTreeViewItems)
            {
                if (!parent.Children.Contains(childTreeNode))
                {
                    parent.Children.Add(childTreeNode);
                }

                childTreeNode.ParentTreeViewItem = parentTreeViewItem;
            }
        }

        /// <summary>
        /// TODO: A bit stinky but better than bubbling an event up...
        /// </summary>
        internal void ChildSelected(TreeViewNode child)
        {
            SelectedItem = child;
            child.IsSelected = true;
            child.SelectionBoxView.Color = child.SelectedBackgroundColor;
            child.SelectionBoxView.Opacity = child.SelectedBackgroundOpacity;
            RemoveSelectionRecursive(RootNodes);
        }

        internal static void RenderNodes(IEnumerable<TreeViewNode> childTreeViewItems, StackLayout parent, NotifyCollectionChangedEventArgs e, TreeViewNode parentTreeViewItem)
        {
            if (e.Action != NotifyCollectionChangedAction.Add)
            {
                //TODO: Reintate this...
                //parent.Children.Clear();
                AddItems(childTreeViewItems, parent, parentTreeViewItem);
            }
            else
            {
                AddItems(e.NewItems.Cast<TreeViewNode>(), parent, parentTreeViewItem);
            }
        }

        // Main code: 
        private TreeViewNode CreateTreeViewNode(object bindingContext, Label label, bool isItem)
        {
            var node = new TreeViewNode
            {
                BindingContext = bindingContext,
                Content = new StackLayout
                {
                    Children =
                    {
                        new ResourceImage
                        {
                            Resource = isItem? "item.png" :"folderopen.png" ,
                            HeightRequest= 16,
                            WidthRequest = 16
                        },
                        label
                    },
                    Orientation = StackOrientation.Horizontal
                }
            };

            //set DataTemplate for expand button content
            node.ExpandButtonTemplate = new DataTemplate(() => new ExpandButtonContent { BindingContext = node });

            return node;
        }

        private void CreateXamlItem(IList<TreeViewNode> children, XamlItem xamlItem)
        {
            var label = new Label
            {
                VerticalOptions = LayoutOptions.Center,
                TextColor = Colors.Black
            };
            label.SetBinding(Label.TextProperty, "Key");

            var xamlItemTreeViewNode = CreateTreeViewNode(xamlItem, label, true);
            children.Add(xamlItemTreeViewNode);
        }

        public ObservableCollection<TreeViewNode> ProcessXamlItemGroups(XamlItemGroup xamlItemGroups)
        {
            var rootNodes = new ObservableCollection<TreeViewNode>();

            foreach (var xamlItemGroup in xamlItemGroups.Children.OrderBy(xig => xig.Name))
            {
                var label = new Label
                {
                    VerticalOptions = LayoutOptions.Center,
                    TextColor = Colors.Black
                };
                label.SetBinding(Label.TextProperty, "Name");

                var groupTreeViewNode = CreateTreeViewNode(xamlItemGroup, label, false);

                rootNodes.Add(groupTreeViewNode);

                groupTreeViewNode.ChildrenList = ProcessXamlItemGroups(xamlItemGroup);

                foreach (var xamlItem in xamlItemGroup.XamlItems)
                {
                    CreateXamlItem(groupTreeViewNode.ChildrenList, xamlItem);
                }
            }

            return rootNodes;
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

And that's it! Well, it needs a bit polishing (implement some bindables for example, make it a Nuget package...), but for the moment let's test it.

Step 4. Create the following structure in your project

Sample TreeView

4a. Models. For this example, the hierarchy will be like this: A company has departments, and each department includes employees. So the classes (models) are as follows:

Company:



namespace MauiTreeView.Sample.Models
{
    public class Company
    {
        public int CompanyId { get; set; }
        public string CompanyName { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

Department:



namespace MauiTreeView.Sample.Models
{
    public class Department
    {
        public int DepartmentId { get; set; }
        public string DepartmentName { get; set; }
        public int ParentDepartmentId { get; set; }
        public int CompanyId { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

Employee:



namespace MauiTreeView.Sample.Models
{
    public class Employee
    {
        public int EmployeeId { get; set; }
        public string EmployeeName { get; set; }
        public int DepartmentId { get; set; }
    }
}


Enter fullscreen mode Exit fullscreen mode

4b. For the Services layer, data is hard-coded. The DataService contains methods to return a collection of Department, Employee, and Company:



using MauiTreeView.Sample.Models;

namespace MauiTreeView.Sample.Services
{
    public class DataService
    {
        public Company GetCompany()
        {
            return new Company()
            {
                CompanyId = 1,
                CompanyName = "TC Solutions"
            };
        }

        public IEnumerable<Department> GetDepartments()
        {
            return new List<Department>()
            {
                new Department() { CompanyId = 1, DepartmentId = 1, DepartmentName = "IT", ParentDepartmentId = -1 },
                new Department() { CompanyId = 1,  DepartmentId = 2, DepartmentName = "Accounting", ParentDepartmentId = -1 },
                new Department() { CompanyId = 1,  DepartmentId = 3, DepartmentName = "Production", ParentDepartmentId = -1 },
                new Department() { CompanyId = 1,  DepartmentId = 4, DepartmentName = "Software", ParentDepartmentId = 1 },
                new Department() { CompanyId = 1,  DepartmentId = 5, DepartmentName = "Support", ParentDepartmentId = 1 },
                new Department() { CompanyId = 1,  DepartmentId = 6, DepartmentName = "Testing", ParentDepartmentId = 4 },
                new Department() { CompanyId = 1,  DepartmentId = 7, DepartmentName = "Accounts receivable", ParentDepartmentId = 2 },
                new Department() { CompanyId = 1,  DepartmentId = 8, DepartmentName = "Accounts payable", ParentDepartmentId = 2 },
                new Department() { CompanyId = 1,  DepartmentId = 9, DepartmentName = "Customers and services", ParentDepartmentId = 8 }
            };
        }

        public IEnumerable<Employee> GetEmployees()
        {
            return new List<Employee>()
            {
                new Employee() { EmployeeId = 1, EmployeeName = "Luis", DepartmentId = 1 },
                new Employee() { EmployeeId = 2, EmployeeName = "Pepe", DepartmentId = 1 },
                new Employee() { EmployeeId = 3, EmployeeName = "Juan", DepartmentId = 2 },
                new Employee() { EmployeeId = 4, EmployeeName = "Inés", DepartmentId = 3 },
                new Employee() { EmployeeId = 5, EmployeeName = "Sara", DepartmentId = 3 },
                new Employee() { EmployeeId = 6, EmployeeName = "Sofy", DepartmentId = 4 },
                new Employee() { EmployeeId = 7, EmployeeName = "Hugo", DepartmentId = 5 },
                new Employee() { EmployeeId = 8, EmployeeName = "Gema", DepartmentId = 5 },
                new Employee() { EmployeeId = 9, EmployeeName = "Olga", DepartmentId = 6 },
                new Employee() { EmployeeId = 1, EmployeeName = "Otto", DepartmentId = 6 },
                new Employee() { EmployeeId = 2, EmployeeName = "Axel", DepartmentId = 6 },
                new Employee() { EmployeeId = 3, EmployeeName = "Eloy", DepartmentId = 7 },
                new Employee() { EmployeeId = 4, EmployeeName = "Flor", DepartmentId = 8 },
                new Employee() { EmployeeId = 5, EmployeeName = "Aída", DepartmentId = 9 },
                new Employee() { EmployeeId = 6, EmployeeName = "Ruth", DepartmentId = 9 }
            };
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

4c. Class CompanyTreeViewBuilder is a specific mapping from our hierarchy to XamlItemGroup hierarchy (which is used by the TreeView control).

  • FindParentDepartment method compares the ParentDepartmentId to evaluate if the current department belongs to another one (or if it's a root one).

  • The public method GroupData maps your hierarchy to a XamlItemGroup instance. It requires a DataService connection to obtain the data (companies, departments, and employees).



using MauiTreeView.Models;
using MauiTreeView.Sample.Models;
using MauiTreeView.Sample.Services;

namespace MauiTreeView.Sample.Helpers
{
    public class CompanyTreeViewBuilder
    {
        private XamlItemGroup FindParentDepartment(XamlItemGroup group, Department department)
        {
            if (group.GroupId == department.ParentDepartmentId)
                return group;

            if (group.Children != null)
            {
                foreach (var currentGroup in group.Children)
                {
                    var search = FindParentDepartment(currentGroup, department);

                    if (search != null)
                        return search;
                }
            }

            return null;
        }

        public XamlItemGroup GroupData(DataService service)
        {
            var company = service.GetCompany();
            var departments = service.GetDepartments().OrderBy(x => x.ParentDepartmentId);
            var employees = service.GetEmployees();

            var companyGroup = new XamlItemGroup();
            companyGroup.Name = company.CompanyName;

            foreach (var dept in departments)
            {
                var itemGroup = new XamlItemGroup();
                itemGroup.Name = dept.DepartmentName;
                itemGroup.GroupId = dept.DepartmentId;

                // Employees first
                var employeesDepartment = employees.Where(x => x.DepartmentId == dept.DepartmentId);

                foreach (var emp in employeesDepartment)
                {
                    var item = new XamlItem();
                    item.ItemId = emp.EmployeeId;
                    item.Key = emp.EmployeeName;

                    itemGroup.XamlItems.Add(item);
                }

                // Departments now
                if (dept.ParentDepartmentId == -1)
                {
                    companyGroup.Children.Add(itemGroup);
                }
                else
                {
                    XamlItemGroup parentGroup = null;

                    foreach (var group in companyGroup.Children)
                    {
                        parentGroup = FindParentDepartment(group, dept);

                        if (parentGroup != null)
                        {
                            parentGroup.Children.Add(itemGroup);
                            break;
                        }
                    }
                }
            }

            return companyGroup;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

4d. Then, CompanyPage is where we will use our control! There is XAML and C# code required for the setup. The XAML part is simple:



<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiTreeView.Sample.Views.CompanyPage"
             xmlns:controls="clr-namespace:MauiTreeView.Controls"
             Title="Company Page">

    <controls:TreeView BackgroundColor="White"
                       Margin="4"
                       x:Name="TheTreeView"/>

</ContentPage>


Enter fullscreen mode Exit fullscreen mode

And regarding the code-behind, here it is:



//S6 and S7
using MauiTreeView.Sample.Services;
using MauiTreeView.Sample.Helpers;

namespace MauiTreeView.Sample.Views;

public partial class CompanyPage : ContentPage
{
    DataService service;
    CompanyTreeViewBuilder companyTreeViewBuilder;

    public CompanyPage(DataService service, CompanyTreeViewBuilder companyTreeViewBuilder)
    {
        InitializeComponent();

        this.service = service;
        this.companyTreeViewBuilder = companyTreeViewBuilder;

        ProcessTreeView();
    }

    private void ProcessTreeView()
    {
        var xamlItemGroups = companyTreeViewBuilder.GroupData(service);
        var rootNodes = TheTreeView.ProcessXamlItemGroups(xamlItemGroups);
        TheTreeView.RootNodes = rootNodes;
    }
}


Enter fullscreen mode Exit fullscreen mode

As you can see, you simply:

  • call the GroupData (from CompanyTreeViewBuilder) public method passing a DataService instance.

  • then you pass the XamlItemGroup instance to ProcessXamlItemGroups method (from TreeView control) to actually display the content.

Step 5. Don't forget to configure your DI in MauiProgram.cs!



        builder.Services.AddSingleton<DataService>();
        builder.Services.AddSingleton<CompanyTreeViewBuilder>();
        builder.Services.AddTransient<CompanyPage>();


Enter fullscreen mode Exit fullscreen mode

And set the initial page in AppShell.xaml:



<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="MauiTreeView.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MauiTreeView"
    xmlns:samples="clr-namespace:MauiTreeView.Sample.Views"
    Shell.FlyoutBehavior="Disabled">

    <ShellContent
        Title="Home"
        ContentTemplate="{DataTemplate samples:CompanyPage}"
        Route="MainPage" />

</Shell>



Enter fullscreen mode Exit fullscreen mode

Step 6. Time to test our application!

Windows:
Image description

Android:
Image description

Cool, right? :-)

By the way, the project is available on GitHub.

I hope that this blog post was interesting and useful for you. I invite you to visit my blog for more technical posts about _Xamarin, .NET MAUI, Azure, and more. I write in Spanish language =)

Thanks for your time, and enjoy the rest of the #MAUIUIJuly publications!

See you next time,
Luis

Top comments (5)

Collapse
 
hopdev profile image
Alan Stratton

I took up your challenge Luis. No NuGet package yet, but some significant improvements. Check it out at and go to to see my motivation and more details.

Collapse
 
hopdev profile image
Alan Stratton

This is a very good sample. A TreeView control currently is not included in MAUI or the CommunityToolkit. On your github repository I see no LICENSE. If this is open source could you add a LICENSE to the github repo? Assuming this is open source, I would like to fork this code, create a new project, or submit some pull requests to you. What are your preferences?

Collapse
 
hopdev profile image
Alan Stratton
Collapse
 
mohamed_kaba_1539291e6fff profile image
Mohamed Kaba

Nice 🙏🥰

Collapse
 
moonligeht profile image
DingShun

The StackLayout of the parent node and the StackLayout of the child node are different.Therefore, the child expansion of the current child node affects the other parent nodes and their expansion items