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:
What does it take to implement such a control in .NET MAUI? Let's see...
Step 1. Add small images for the TreeView nodes
Step 2. Create a folder. Name it Models. Then, add a couple of classes inside:
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; }
}
}
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; }
}
}
Step 3. Create a folder. Name it Controls. Then, add four classes inside:
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);
}
}
}
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
};
}
}
}
}
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 aTapGestureRecognizer
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 aIList<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();
}
}
}
3d. And finally, the TreeView
control implementation!
The public collection
RootNodes
needs to be assigned (later) in order to display the nodes. It's aList
ofTreeViewNode
.You can call the public
ProcessXamlItemGroups
method to map the hierarchy of nodes (xamlItemGroups
) into anObservableCollection
ofTreeViewNode
.
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;
}
}
}
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
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; }
}
}
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; }
}
}
Employee:
namespace MauiTreeView.Sample.Models
{
public class Employee
{
public int EmployeeId { get; set; }
public string EmployeeName { get; set; }
public int DepartmentId { get; set; }
}
}
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 }
};
}
}
}
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 aXamlItemGroup
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;
}
}
}
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>
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;
}
}
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>();
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>
Step 6. Time to test our application!
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)
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.
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?
hopdev.hashnode.dev/ and github.com/str37/PowerTree
Nice 🙏🥰
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