Search code examples
c#recursionblazortreeviewmudblazor

C# MudBlazor Trying to Populate a Treeview With List<T>


I have a List that looks like:

public class CategoryViewModel 
{
    public int Id { get; set; }

    public string? Name { get; set; }    

    public int? ParentCategoryId { get; set; }    
}

I'm using the TreeItemData class from the Mudblazor site Mudblazor.

public class TreeItemData
{
    public int Id { get; set; }

    public int? ParentId { get; set; }

    public string Text { get; set; }

    public string Icon { get; set; }

    public bool IsExpanded { get; set; } = true;

    public bool HasChild => TreeItems != null && TreeItems.Count > 0;

    public HashSet<TreeItemData> TreeItems { get; set; } = new HashSet<TreeItemData>();

    public TreeItemData(string text, string icon)
    {
        Text = text;
        Icon = icon;            
    }
}

I am struggling with how to iterate over my populated List and populate Hashset<TreeItemData> TreeItems. The Mudblazor Treeview requires a Hashset.

My attempt, BuildTreeItems is not working:

private void BuildTreeItems(int? ParentId = null)
{
    if (Categories != null)
    {

        foreach (CategoryViewModel item in Categories.Where(i => i.ParentCategoryId == ParentId))
        {
            if (!Categories.Where(c => c.ParentCategoryId == item.Id).Any())
            {// no children
                TreeItems.Add(new TreeItemData(item.Name ?? string.Empty, Icons.Custom.FileFormats.FileDocument));

                BuildTreeItems(item.ParentCategoryId);
            }
            else
            {// has children
                TreeItems.Add(new TreeItemData(item.Name, Icons.Custom.FileFormats.FileDocument) 
                {
                    TreeItems = new HashSet<TreeItemData>()
                    {

                    }
                });

                BuildTreeItems(item.Id);
            }
        }
    }
}

I have actually tried various alternatives to this code, and I got close with using Blazor nested components, but the Mudblazor Treeview does not work well with when using nested components. I think the best option for me would be try to get the MudTreeView all in 1 component, and just figure out how to use recursion to convert my List to a Hashset suitable for the Mudblazor Treeview.

I'm not sure what I am missing. I'm trying to mimic the CustomTree example from the website: Mudblazor Treeview

It looks something like this:

protected override void OnInitialized()
    {
        TreeItems.Add(new TreeItemData(".azure", Icons.Custom.Brands.MicrosoftAzure));
        TreeItems.Add(new TreeItemData(".github", Icons.Custom.Brands.GitHub));
        TreeItems.Add(new TreeItemData(".vscode", Icons.Custom.Brands.MicrosoftVisualStudio));
        TreeItems.Add(new TreeItemData("content", Icons.Custom.FileFormats.FileDocument));
        TreeItems.Add(new TreeItemData("src", Icons.Custom.FileFormats.FileCode)
        {
            TreeItems = new HashSet<TreeItemData>()
            {
                new TreeItemData("MudBlazor", Icons.Custom.Brands.MudBlazor),
                new TreeItemData("MudBlazor.Docs", Icons.Custom.FileFormats.FileDocument)
                {
                    TreeItems = new HashSet<TreeItemData>()
                    {
                        new TreeItemData("_Imports.razor", Icons.Material.Filled.AlternateEmail),
                        new TreeItemData( "compilerconfig.json", Icons.Custom.FileFormats.FileImage),
                        new TreeItemData( "MudBlazor.Docs.csproj", Icons.Custom.Brands.MicrosoftVisualStudio),
                        new TreeItemData("NewFilesToBuild.txt" , Icons.Custom.FileFormats.FileDocument),
                    }
                },
                new TreeItemData("MudBlazor.Docs.Client", Icons.Material.Filled.Folder),
                new TreeItemData("MudBlazor.Docs.Compiler", Icons.Material.Filled.Folder),
                new TreeItemData("MudBlazor.Docs.Server", Icons.Material.Filled.Folder),
                new TreeItemData("MudBlazor.UnitTests", Icons.Material.Filled.Folder),
                new TreeItemData("MudBlazor.UnitTests.Viewer", Icons.Material.Filled.Folder),
                new TreeItemData(".editorconfig", Icons.Custom.FileFormats.FileCode),
                new TreeItemData("MudBlazor.sln", Icons.Custom.Brands.MicrosoftVisualStudio)
            }
        });
        TreeItems.Add(new TreeItemData("History", Icons.Material.Filled.Folder));
    }

I need to do this dynamically with an unknown number of categories and unknown number of levels.

What am I missing? Any help would be appreciated.

Sample Sample Data and what my treeview would look like.

Id | Name | ParentCategoryId
1 | Pictures         | NULL
2 | Audio            | NULL
3 | Animals         | 1
4 | Dogs             | 3
5 | Ring Tones    | 2
8 | Cats               | 3
9 | Photographs | 1

Pictures
    Animals
         Dogs
         Cats
    Photographs
Audio
     Ring Tones

Showing the Solution

private void BuildTree()
{
    if (Categories != null)
    {
        TreeItems = new HashSet<TreeItemData>();

        var subItems = Categories.Where(item => item.ParentCategoryId == null);
        foreach (var item in subItems)
        {
            BuildTreeItem(item, null);
        } 
    }
}

private void BuildTreeItem(CategoryViewModel categoryItem, TreeItemData? data)
{
    if (Categories != null)
    {
        var treeItem = new TreeItemData(categoryItem.Name ?? string.Empty, Icons.Material.Filled.AccountCircle);
        var subItems = Categories.Where(item => item.ParentCategoryId == categoryItem.Id);

        foreach (var subItem in subItems)
        {
            BuildTreeItem(subItem, treeItem);
        }

        if (data is null)
            TreeItems.Add(treeItem);

        else
            data.TreeItems.Add(treeItem); 
    }
}

Solution

  • Here are two approaches.

    1. Use RenderFragments to build out the MudTreeViewItem component structure.
    2. Build the HashSet.

    I answered another similar question recently so I've lifted some code from that answer and abreviated your data objects to simplify the code.

    A simple readonly data object and a data provider.

    public record Taxonomy(int Id, string Name, int? ParentId);
    
    public static class DataProvider
    {
        private static List<Taxonomy>? _taxonomies;
    
        public static IEnumerable<Taxonomy> Items
        {
            get
            {
                if (_taxonomies == null)
                    Populate();
    
                return _taxonomies?.AsEnumerable() ?? Enumerable.Empty<Taxonomy>();
            }
        }
    
        private static void Populate()
        {
            _taxonomies = new List<Taxonomy>();
    
            _taxonomies.Add(new(1, "World", 0));
            _taxonomies.Add(new(2, "Europe", 1));
            _taxonomies.Add(new(3, "Asia", 1));
            _taxonomies.Add(new(4, "Africa", 1));
            _taxonomies.Add(new(5, "France", 2));
            _taxonomies.Add(new(6, "Spain", 2));
            _taxonomies.Add(new(7, "Portugal", 2));
            _taxonomies.Add(new(8, "Cordoba", 6));
            _taxonomies.Add(new(9, "Algeciras", 6));
        }
    }
    

    The RenderFragment version.

    @page "/"
    @using SO77657548.Data
    
    <PageTitle>Index</PageTitle>
    
    <MudText Typo="Typo.h3" GutterBottom="true">Hello, world!</MudText>
    <MudText Class="mb-8">Welcome to your new app, powered by MudBlazor!</MudText>
    
    <div>
        @BuildTreeFragment
    </div>
    
    
    @code {
        private IEnumerable<Taxonomy> _items = DataProvider.Items;
    
        private RenderFragment BuildTreeFragment => __builder =>
        {
            var subItems = _items.Where(item => item.ParentId == 0);
    
            <MudTreeView T="string">
                @foreach (var item in subItems)
                {
                    @BuildTreeItemFragment(item)
                }
            </MudTreeView>
        };
    
        private RenderFragment<Taxonomy> BuildTreeItemFragment => taxonomy => __builder =>
        {
            var items = _items.Where(item => item.ParentId == taxonomy.Id);
    
            <MudTreeViewItem Value="@(taxonomy.Name)">
                @foreach (var item in items)
                {
                    @BuildTreeItemFragment(item)
                }
            </MudTreeViewItem>
        };
    }
    

    The HashSet version:

    @page "/"
    @using SO77657548.Data
    
    <PageTitle>Index</PageTitle>
    
    <MudText Typo="Typo.h3" GutterBottom="true">Hello, world!</MudText>
    <MudText Class="mb-8">Welcome to your new app, powered by MudBlazor!</MudText>
    
    <MudPaper Width="300px" Elevation="0">
        <MudTreeView ServerData="LoadServerData" Items="_treeItems">
            <ItemTemplate>
                <MudTreeViewItem Value="@context" Icon="@context.Icon" LoadingIconColor="Color.Info" CanExpand="@context.CanExpand" Text="@context.Title" EndText="@context.Number?.ToString()" EndTextTypo="@Typo.caption" />
            </ItemTemplate>
        </MudTreeView>
    </MudPaper>
    
    @code {
        private IEnumerable<Taxonomy> _items = DataProvider.Items;
        private HashSet<TreeItemData> _treeItems { get; set; } = new HashSet<TreeItemData>();
    
        protected override void OnInitialized()
        {
            BuildTree();
        }
    
        // Not sure what this is supposed to do - I'm not too familiar with MudBlazor
        public async Task<HashSet<TreeItemData>> LoadServerData(TreeItemData parentNode)
        {
            await Task.Yield();
            return parentNode.TreeItems;
        }
    
        private void BuildTree()
        {
            _treeItems = new HashSet<TreeItemData>();
    
            var subItems = _items.Where(item => item.ParentId == 0);
            foreach (var item in subItems)
            {
                BuildTreeItem(item, null);
            }
        }
    
        private void BuildTreeItem(Taxonomy taxonomyItem, TreeItemData? data)
        {
            var treeItem = new TreeItemData(taxonomyItem.Name, Icons.Material.Filled.AccountCircle);
            var subItems = _items.Where(item => item.ParentId == taxonomyItem.Id);
    
            @foreach (var subItem in subItems)
            {
                BuildTreeItem(subItem, treeItem);
            }
    
            if (data is null)
                _treeItems.Add(treeItem);
    
            else
                data.TreeItems.Add(treeItem);
        }
    }
    

    enter image description here