Opened

TreeView issue

dhileaga commented edited

Hi I have this markup:

<TreeView Nodes="Nodes"
            @bind-SelectedNode="selectedNode"
            @bind-ExpandedNodes="ExpandedNodes"
            GetChildNodes="@(item => GetChildren(item))"
            HasChildNodes="@(item => HasChildren(item))" >
    <NodeContent>
        <Icon Name="IconName.Folder" />
        @context.Text
    </NodeContent>
</TreeView>

The function works well for the first level but after that no luck: What Am I doing wrong? Only the first 4 are fixed. Their children are produced by GetChildren(item)

mladenmacanovic commented

It's hard to tell without the full code. Can you post a full sample code to reproduce it on our side?

dhileaga commented

The code extracts data from a database, I'm not sure how much help will provide. The tree does not expand beyond the second level. Do the nodes need to be fixed and present at the time of rendering?

    public enum NodeType
    {
        Root,
        Name,
        Module,
        ItemClass,
        Package,
        DataItem
    }
    public partial class DataItemsTree
    {
        private IList<NodeInfo> ExpandedNodes = new List<NodeInfo>();

        private NodeInfo selectedNode = new();

        public class NodeInfo
        {
            public string UId { get; set; }
            public NodeType NodeType { get; set; }
            public string Text { get; set; }

            public IEnumerable<NodeInfo> Children { get; set; }


            public NodeInfo(string text, NodeType nodeType, string? uid = null)
            {
                NodeType = nodeType;
                Text = text;
                UId = uid is null ? string.Empty : uid;
                Children = Enumerable.Empty<NodeInfo>();
            }
            public NodeInfo()
            {
                NodeType = NodeType.Root;
                Text = string.Empty;
                UId = string.Empty;
                Children = Enumerable.Empty<NodeInfo>();
            }
        }

        private readonly IEnumerable<NodeInfo> Nodes = new[]
        {
            new NodeInfo("By Name", NodeType.Root),
            new NodeInfo("By Module", NodeType.Root),
            new NodeInfo("By Package", NodeType.Root),
            new NodeInfo("By Class", NodeType.Root)
        };

        private IEnumerable<NodeInfo> GetChildren(NodeInfo parent)
        {
            if (parent.NodeType == NodeType.DataItem)
            {
                return Enumerable.Empty<NodeInfo>();
            }

            if (parent.NodeType == NodeType.Root)
            {
                switch(parent.Text)
                {
                    case "By Module":
                        {
                            if (Modules is not null)
                            {
                                List<NodeInfo> children = new(23);

                                foreach (var module in Modules)
                                {
                                    children.Add(new(module?.Name ?? String.Empty, NodeType.Module));
                                }

                                return children;
                            }
                            return Enumerable.Empty<NodeInfo>();
                        }
                    default:
                        return Enumerable.Empty<NodeInfo>();
                }
            }

            if (parent.NodeType == NodeType.Module)
            {
                var items = DataItems?.Where(d => d.Module == parent.Text);
                if (items is not null)
                {
                    List<NodeInfo> children = new(23);
                    foreach(var item in items)
                    {
                        children.Add(new (item.Name ?? string.Empty, NodeType.DataItem, item.UId));
                    }

                    return children;
                }

                return Enumerable.Empty<NodeInfo>();
            }

            return Enumerable.Empty<NodeInfo>();
        }
        private bool HasChildren(NodeInfo parent)
        {
            return true;
        }

        [Parameter] public List<DataItemLookupViewModel>? DataItems { get; set; }
        [Parameter] public List<ModuleLookupViewModel>? Modules { get; set; }
        [Parameter] public List<DataItemClassLookupViewModel>? DataItemClasses { get; set; }
        [Parameter] public List<PackageLookupViewModel>? Packages { get; set; }
        [Parameter] public VersionViewModel? SelectedVersion { get; set; }
David-Moreira commented

Hello @dhileaga
I'll be taking a look at your examples later today.

But I can tell you right away that the full structure must exist in order for the full tree to be built correctly as we do not yet have 'ReadData' support on Treeview.

Children can be built more or less dynamically, for example, on node expand, but the full structure must always be provided to the tree currently.

dhileaga commented edited

That's the issue I guess. I can only provide the first level to be fixed. I thought the GetChildNodes callback will work with the parent reference. It seems to work for the first level (see snapshot) but if I try to expand "Developer", which has children for sure, it doesn't expand, the "plus" stay "plus". The callback gets called the children list is returned but the node doesn't expand.

dhileaga commented

The way I made it work is if I populate the entire tree before rendering. This is expensive in my case, any recommendation?

David-Moreira commented edited

I finally took a look at your example.

You don't necessarily need to populate the entire tree right away, but you do need to build and keep it's structure. Just like it's done on the Blazorise TreeView documentation.


Skip the following part if you're not interest in the technical aspect/reasons and just want the solution


You assumed that GetChildNodes would be called each time the expand arrow is clicked, and that is partially true, however, how it works, is that it is a function that is part of the internal rendering process and can be called multiple times to figure out the next children to render.
In this case, it would get the Children for the higher levels again, and then not know how to retrieve the lower levels back as the new references would not be the same as the ones in the ExpandedNodes collection, therefore not triggering GetChildNodes again for the previously SelectedNodes



So to avoid populating the entire tree right away, we can go for the following solution.
  • Change GetChildNodes so it just looks at the structure and keep the structure as it is loaded from DB.
  • Make use of ExpandedNodesChanged as that is an actual way to track wether the expand arrow was clicked and then load your actual data from DB to the Children collection.
    Since ExpandedNodes is a collection of the expanded nodes being tracked, the last item should be the one just expanded.

PS: We should be adding support for ReadData in the future and that'll make life easier for loading data from an external source into the tree.

Here's the code based on the example you've provided us with:

<TreeView 
          TNode="NodeInfo"
          Nodes="Nodes"
          @bind-SelectedNode="selectedNode"
          ExpandedNodes="@ExpandedNodes"
          GetChildNodes="@(item => item.Children)"
          HasChildNodes="@(item => HasChildren(item))"
          ExpandedNodesChanged="@OnExpandedNodes">
    <NodeContent>
        <Icon Name="IconName.Folder" />
        @context.Text
    </NodeContent>
</TreeView>

@code {
    public enum NodeType
    {
        Root,
        Name,
        Module,
        ItemClass,
        Package,
        DataItem
    }

    private IList<NodeInfo> ExpandedNodes = new List<NodeInfo>();

    private NodeInfo selectedNode = new();

    public class NodeInfo
    {
        public string UId { get; set; }
        public NodeType NodeType { get; set; }
        public string Text { get; set; }

        public IEnumerable<NodeInfo> Children { get; set; }


        public NodeInfo( string text, NodeType nodeType, string? uid = null )
        {
            NodeType = nodeType;
            Text = text;
            UId = uid is null ? string.Empty : uid;
            Children = null;
        }
        public NodeInfo()
        {
            NodeType = NodeType.Root;
            Text = string.Empty;
            UId = string.Empty;
            Children = null;
        }
    }

    private readonly IEnumerable<NodeInfo> Nodes = new[]
    {
            new NodeInfo("By Name", NodeType.Root),
            new NodeInfo("By Module", NodeType.Root),
            new NodeInfo("By Package", NodeType.Root),
            new NodeInfo("By Class", NodeType.Root)
    };

    private IEnumerable<NodeInfo> GetByModuleRoot()
    {
        //Get from BD
        return new List<NodeInfo>()
        {
            new("Developer", NodeType.Module),
            new("Test", NodeType.Module),
            new("Settings", NodeType.Module)
        };
    }

    private IEnumerable<NodeInfo> GetByModule( string module )
    {
        //Get from BD
        switch ( module )
        {
            case "Developer":
                return new List<NodeInfo>()
                {
                    new("IDE", NodeType.DataItem),
                    new("Tools", NodeType.DataItem)
                };
            case "Test":
                return new List<NodeInfo>()
                {
                    new("Integration", NodeType.DataItem),
                    new("Unit Tests", NodeType.DataItem)
                };
            case "Settings":
                return Enumerable.Empty<NodeInfo>();
            default:
                return Enumerable.Empty<NodeInfo>();
        }
    }

    private Task OnExpandedNodes( IList<NodeInfo> nodes )
    {
        ExpandedNodes = nodes;
        GetChildren(nodes.Last());
        return Task.CompletedTask;
    }
    private IEnumerable<NodeInfo> GetChildren( NodeInfo parent )
    {
        if ( parent.NodeType == NodeType.DataItem )
            parent.Children = Enumerable.Empty<NodeInfo>();

        if ( parent.NodeType == NodeType.Root )
        {
            switch ( parent.Text )
            {
                case "By Module":
                    parent.Children = GetByModuleRoot();
                    break;
                default:
                    parent.Children = Enumerable.Empty<NodeInfo>();
                    break;
            }
        }

        if ( parent.NodeType == NodeType.Module )
            parent.Children = GetByModule( parent.Text );

        return parent.Children;
    }

    private bool HasChildren( NodeInfo parent )
    {
        return parent.Children?.Any() ?? true;
    }

    [Parameter] public List<DataItemLookupViewModel>? DataItems { get; set; }
    [Parameter] public List<ModuleLookupViewModel>? Modules { get; set; }
    [Parameter] public List<DataItemClassLookupViewModel>? DataItemClasses { get; set; }
    [Parameter] public List<PackageLookupViewModel>? Packages { get; set; }
    [Parameter] public VersionViewModel? SelectedVersion { get; set; }

    public class DataItemLookupViewModel
    {
        public string Name { get; set; }
        public string Module { get; set; }
        public string UId { get; set; }
    }

    public class ModuleLookupViewModel
    {
        public string Name { get; set; }

        public IEnumerable<DataItemLookupViewModel> Children { get; set; }
    }

    public class DataItemClassLookupViewModel
    {
        public string Name { get; set; }
        public string UId { get; set; }
    }


    public class PackageLookupViewModel
    {
        public string Module { get; set; }
    }

    public class VersionViewModel
    {
        public string Module { get; set; }
    }
}

Want to comment on this issue? Log in and write your comment.
Asignee
David-Moreira
Labels
No Labels