相关文章推荐
含蓄的大象  ·  大语言模型 - 知乎·  2 年前    · 
严肃的茶叶  ·  Java8 ...·  2 年前    · 

Introduction

This articles explores the problems of standard WPF TreeView controls and describes a better way to display hierarchical data using a custom TreeListView control.

Background of TreeView

Windows Forms TreeView has quite a limited functionality and provides no easy way to extend it. WPF TreeView seems like a major step forward, at first glance. But in real application the lack of features like multiselection or multicolumn view become apparent. Moreover, things that were quite easy to do in Windows Forms are now much more complex in WPF because there's no simple way to get the container item for your business object displayed in the TreeView . For example, if you need to expand the currently selected node you have to use ItemContainerGenerator as described here or use a special model just to support selection/expanding (see here ).

Another weakness of the WPF TreeView is poor performance. It takes more the 10 seconds to expand a node containing 5000 subnodes! Even navigation becomes very slow — more than a second to just move a focus.

TreeListView

But there is an alternative way to display hierarchical data — use the ListView . The basic idea is the following:

  • For each tree node we create a row in the ListView .

  • The ListViewItem template contains a special control named RowExpander which allows you to expand/collapse a node and any number of additional controls to represent the data. Those controls are shifted to the right depending on the Node level.

  • When the TreeNode is expanded/collapsed we need to add/remove rows to/from the ListView

    Using this approach we get all the benefits from ListView: multiselection, a possibility to display several columns, performance improvements comparing to TreeView , less memory usage because of virtualization support (it means that visual elements will be created only for the items currently displayed on the screen).

    Model-View

    WPF TreeView is supposed to be used with HierarchicalDataTemplate where you specify a property containing child items. It's quite simple, but requires you to provide such a property in your business objects, which means that you can't display the hierarchy of simple types, like String, in the TreeView . Additionally, I prefer to use another approach — use a special interface which describes hierarchical data:

    public interface ITreeModel
        /// Get list of children of the specified parent
        IEnumerable GetChildren(object parent);
        /// returns weather specified parent has any children or not.
        bool HasChildren(object parent);
    

    The HasChildren method is used to display/hide the expander control without calling GetChildren, which can be time expensive. Note that items are loaded on demand in TreeListView, which means the GetChildren method will be called only when the corresponding parent node expands.

    Realization Details

    We create subclasses of ListView and ListviewItem:

    public class TreeList: ListView
        /// Internal collection of rows representing visible nodes, actually displayed
        /// in the ListView
        internal ObservableCollectionAdv Rows
            get;
            private set;
        protected override DependencyObject GetContainerForItemOverride()
            return new TreeListItem();
        protected override bool IsItemItsOwnContainerOverride(object item)
            return item is TreeListItem;
    

    When the Aaren node is collapsed/expanded we need to insert/remove all child nodes and notify ListView about that. As the system class ObservableCollection doesn't provide methods for that we need to create our own collection class:

    public class ObservableCollectionAdv : ObservableCollection
        public void RemoveRange(int index, int count)
            this.CheckReentrancy();
            var items = this.Items as List;
            items.RemoveRange(index, count);
            OnReset();
        public void InsertRange(int index, IEnumerable collection)
            this.CheckReentrancy();
            var items = this.Items as List;
            items.InsertRange(index, collection);
            OnReset();
    

    For every item created by the model we create a TreeNode class which will store node status (IsSelected, IsExpanded) and track changes in the model if it provides such information:

    void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
        switch (e.Action)
        case NotifyCollectionChangedAction.Add:
            if (e.NewItems != null)
                int index = e.NewStartingIndex;
                int rowIndex = Tree.Rows.IndexOf(this);
                foreach (object obj in e.NewItems)
                    Tree.InsertNewNode(this, obj, rowIndex, index);
                    index++;
            break;
        case NotifyCollectionChangedAction.Remove:
            if (Children.Count > e.OldStartingIndex)
                RemoveChildAt(e.OldStartingIndex);
            break;
        case NotifyCollectionChangedAction.Move:
        case NotifyCollectionChangedAction.Replace:
        case NotifyCollectionChangedAction.Reset:
            while (Children.Count > 0)
                RemoveChildAt(0);
            Tree.CreateChildrenNodes(this);
            break;
        HasChildren = Children.Count > 0;
        OnPropertyChanged("IsExpandable");
    

    Using the Code

    The source code of the article contains two examples using TreeListView. One uses a classic TreeView style and the other displays how to interact with the TreeListView. The other shows how several columns can be used to display system registry.

    Points of Interest

    In the current implementation of the TreeListView you have to keep the XAML markup of the TreeListItem in the client library. Which means that you have to copy it to each project using the control. Normally this information should be stored in the same library as the control itself, but it just didn't work this way. If somebody finds the way how to achieve this, don't hesitate to share it.

    Good day!
    I don't know what's wrong. I create DependencyProperty and bind in xaml.
    When the program starts for the first time, all work fine, but when i change bc.Budget, OnModelChanged not called. Maybe you know what's going on.
    MainWindow.xaml
    <tree:TreeList Name="_tree" Margin="0,39,0,0" Model="{Binding Path=Budget, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">

    MainWindow.xaml.cs
    InitializeComponent(); bc = new BudgetViewModel(con, DateTime.Now.Year); this.DataContext = bc; TreeList.cs
    get { return (ITreeModel)GetValue(ModelProperty); } set { SetValue(ModelProperty, value); } public static readonly DependencyProperty ModelProperty = DependencyProperty.Register("Model", typeof(ITreeModel), typeof(TreeList), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnModelChanged)); private static void OnModelChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) if (e.NewValue != null && e.NewValue is ITreeModel) var tree = (obj as TreeList); tree._root.Children.Clear(); tree.Rows.Clear(); tree.CreateChildrenNodes(tree._root); Sign In·View Thread  QuestionTrigger method when Expand/Collapse signs of a node is clicked? Pin
    Abbas Mousavi29-Jul-18 22:03
    Abbas Mousavi29-Jul-18 22:03  Any idea how to trigger a method when the expand or collapse signs are pressed? The example provides a command button to do that.
    Thanks
    Abbas

    modified 30-Jul-18 10:39am.

    Sign In·View Thread  Is there a way to Bind the IsExpanded Property of the TreeNode to my Elements?
    I found the answer for the IsSelected Property, but it's not 1:1 adaptable. Because "IsExpanded" is an unknown Property... :-/
    Sign In·View Thread  Hi, I have problem with updateing text in GridViewColumn.
    I have declared binding in XAML:
    <GridViewColumn x:Name="Value_Column" Header="Value" Width="100" DisplayMemberBinding="{Binding Value}"/>

    and I have list of objects referenced by this binding.
    When I changing te Value property in binded structure object, nothing happens.
    How can I change GridViewColumn text value in my ITreeModel element ?
    Sign In·View Thread  QuestionSorting capability -> Is there a way to add sorting capability to tree view columns? Pin
    Member 797920026-Sep-14 7:42
    Member 797920026-Sep-14 7:42  Is there a way to scroll CurrentItem into View (BringIntoView)?
    I tried to get TreeListItem via TreeNode - added property to TreeNode class (reference to TreeListItem Container), but it is always = null.
    Sign In·View Thread  I have been experimenting with this code, but have been unable to place data in the rows where the folder icon appears.
    Is that possible?
    Sign In·View Thread  Hello, im wondering if its possible to implement some kind of filtering algorithm based on textbox input by user?
    nice job..you r the man
    Sign In·View Thread  very impressive article.Well Done. Smile | :)
    I would like to know how can I bind to selectedNode Name property. I tried it many ways but it did not work.
    <TextBlock Text="{Binding ElementName=_tree Path=SelectedNode.Name}" />

    is this not the correct way? can you please help me out with this?
    Thanks.
    Sign In·View Thread  Following other people's comments regarding binding treemodel I was able to bind a viewmodel to the control. However, I am unable to bind IsSelected and IsExpanded properties to the viewModel. It seems these properties are affecting TreeNode class. Has anyone found any workaround on how to pass these properties to the viewModel?
    Sign In·View Thread 
    The DataModel element is returned by "Tag" property. You can use it in binding process:
    <tree:TreeList.ItemContainerStyle>
    <Style TargetType="{x:Type tree:TreeListItem}">
    <Setter Property="IsSelected" Value="{Binding Tag.IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    </Style>
    </tree:TreeList.ItemContainerStyle>
    Sign In·View Thread  Binding ItemsSource does nothing
    Model can't be binded, is't not a dependency property
    Doesn't follow WPF rules and break the code
    Sign In·View Thread  Your control is really great. Iam using it in my application where i wanna display packages, classes and methods in a tree view along with coloumns for each, where i can display some metrics value for each item. I want to add a a mouse down event to the textblock and on dbclicking the textblock i should open the method or the file. My question is to hilight the methos i need to open the file where the method resides. How can i get the parent elements textblock textcontent?? Please help me out.
    Sign In·View Thread  private void TreeViewSelectionChanged(object sender, SelectionChangedEventArgs e) var tree = sender as TreeList; var node = tree.SelectedNode; var pnode = GetAnc(node, 2); public TreeNode GetAnc(TreeNode node, int level) for (var i = 0; i < level; i++) if (node.Parent != null) node = node.Parent; return node; return null; Sign In·View Thread  It seems that a memory leak happens somehow.
    I used this nice control with large amount of rich data. So, as it happens really often - I have played with row expander by clicking it many times =) From click to click I realized the difference between two click delay whick have been becoming worse and worse.It just can be seen by watching process explorer that something wrong. I wanted to investigate this by using EQUTEK profiler and applied that to the SAMPLE application and my code. But nothing has resulted from my efforts.
    P.S. This WPF control is *purely* nice =)
    P.P.S. http://ib3.keep4u.ru/b/2012/09/14/50/503c7e04d3b497dd78dc6220fb690fd5.png[^]
    Sign In·View Thread  I am curious if you intend to any form of run-time drag-drop within this tree, or between two instances of this tree.
    thanks, Bill Woodruff
    "One of the few good things about modern times: If you die horribly on television, you will not have died in vain. You will have entertained us." Kurt Vonnegut

    Sign In·View Thread  Web02 2.8:2023-03-27:1
  •