Click here to Skip to main content
15,867,686 members
Articles / Desktop Programming / Windows Forms

Virtual Mode TreeListView

Rate me:
Please Sign up or sign in to vote.
4.79/5 (27 votes)
1 Oct 2012CPOL7 min read 143K   13.6K   145   41
A treelistview running in virtual mode

Introduction

This VS 2010 C# project offers a solution to use a Windows Forms ListView in combination with a TreeView. Regarding Windows Forms, only the ListView is used. There are several projects here on CodeProject, showing the combination of a treeview and a listview. However I have created my own which runs in virtual mode and provides a good performance, even when used with a large number of lines/nodes.

How does it look like? Here's a screen shot with an example model:

screenshot of virtual mode treelistview control

Background

I had to create a prototype that deals with tree-organized data whereby expanding a single node/line resulted in showing the nodes/lines children with a count of 65535. Expanding/collapsing 65535 lines is not a performance problem when using the virtual mode that gathers only data that is in view.

Using the Code

So what does the trick? Well, I just use a regular ListView. Because it runs in virtual mode, this property "VirtualMode" of the ListView has to be set to true. Also I use the property "CheckBoxes" set to true, because I use it to provide the state whether a line is expanded or collapsed. The checkbox of course will not be displayed. I also use the "OwnerDraw" mode property set to true and draw an arrow-east when the line is not expanded, and an arrow-southeast when a line is expanded.

When the ListView runs in virtual mode, you must never "fill" the control in order that it will display items. The data is taken directly from the data model, whenever the visible lines of the control come into view. Therefore an event handler named "RetrieveVirtualItem" has to be provided. Now all the control needs to know is, how many lines it has.

When a line is expanded, the number of lines of course increases and the control can adjust its scrollbar. When a line gets collapsed, the number of lines decreases.

The ListView just knows it has lines. But with a TreeListView, tree-like organized data has to be mapped to linear organized lines. To visualize the hierarchy, indentation is used as a normal TreeView does in the same way.

How do we map tree-like organized data to linear organized data? Well, first of all, the control sends events for the visual lines in order to get data for these lines. The event handler "RetrieveVirtualItem" therefore must provide appropriate data for each line. Within this handler, the zero based line position is passed and we have to provide a ListViewItem to the event arguments. I use some kind of "mapper", which is a List of IDataNode.

C#
private readonly List<IDataNode> m_Mapper = new List<IDataNode>(64);

Because the mapper can be accessed as a zero-based array, every item in the mapper corresponds to a line within the listview. A data model can be loaded from an XML-file, I have provided an example model with the project. All nodes within the XML-file will be a DataNode object. When traversing the XML-file's DOM, the tree-organized model will be created, also the indent level will be set within each DataNode that has children. Have a look at the source files IModel.cs as well as Model.cs in order to see the code in detail.

The mapper afterwards will get initialized this way:

C#
private void InitVirtualListViewNodes()
        {
            m_Mapper.Clear();

            ObtainAllNodes(m_Model.DataPool); //obtain top level nodes

            listView1.VirtualListSize = m_Mapper.Count;
            listView1.VirtualMode = true;
            listView1.Invalidate();
        }  

This method adds IDataNode references to the mapper:

C#
private void ObtainAllNodes(List<IDataNode> nds)
        {
            foreach (IDataNode dn in nds)
            {
                m_Mapper.Add(dn);
                if (dn.Expanded)
                {
                    ObtainAllNodes(dn.Children);
                }
            }
        }      

Usually when you first load the model, all lines will be collapsed. Now if you click on an expandable line, the mouse click event handler will obtain the line number and the mapper which holds references to DataNodes, will allow to obtain the appropriate DataNode. Now the model is organized as a composite pattern which simply means that each DataNode holds a collection to child DataNodes. If a line must get expanded, the child DataNode's references will be inserted into the mapper at the given position. After this, the mapper's items grow according to the number of children. After this is done, the control's virtual item count will be set to the mapper's count property. This causes the control to fire new RetrieveVirtualItem events and so, the new expanded lines will get displayed.

The DataNode class looks like this:

C#
public class DataNode : IDataNode
    {
        private string m_Name;
        private string m_Comment;
        private bool m_Expanded;
        private int m_Level;
        internal List<IDataNode> m_Children;
        
        public DataNode(string nam, string cmt, int lvl, bool stat)
        {
            m_Name = nam;
            m_Comment = cmt;
            m_Level = lvl;
            m_Expanded = stat;
            m_Children = new List<IDataNode>(16);
        }

When the control is displayed first and whenever the control is scrolled or when the number of lines of the control changes (expand/collapse occurs), the control fires events in order to get the data for the visible lines to visualize. Therefore the event handler for the "RetrieveVirtualItem" gets called. The event's argument object provides the line number of the line that needs data. Before the method returns, a ListViewItem has to be provided and associated to the "Item" property of the event's argument object. We will use the line number to lookup the appropriate IDataNode reference in the mapper. With this reference, which is a part of the data model, we will create the ListViewItem the control expects. Here's the code of the handler:

C#
private void ListView1RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
        {
            e.Item = MakeListViewItem(m_Mapper[e.ItemIndex]);
        }

The method MakelistViewItem looks like this:

C#
private static ListViewItem MakeListViewItem(IDataNode dn)
        {
            ListViewItem lvi = new ListViewItem();
            lvi.Text = dn.Name;
            ListViewItem.ListViewSubItem lvsi1 = new ListViewItem.ListViewSubItem();
            lvsi1.Text = dn.CountChildren.ToString();
            ListViewItem.ListViewSubItem lvsi2 = new ListViewItem.ListViewSubItem();
            lvsi2.Text = dn.Level.ToString();
            ListViewItem.ListViewSubItem lvsi3 = new ListViewItem.ListViewSubItem();
            lvsi3.Text = dn.Comment;

            lvi.IndentCount = dn.Level;
            
            lvi.SubItems.Add(lvsi1);
            lvi.SubItems.Add(lvsi2);
            lvi.SubItems.Add(lvsi3);
            
            if (dn.Expanded)
                lvi.StateImageIndex = 1;
            else if (dn.CountChildren > 0)
                lvi.StateImageIndex = 0;

            return lvi;
        }

The basic stuff of course is done when you click on the arrow of a line in order to expand or collapse a line. This is the method:

C#
private void ExpandCollapseNodes(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                ListView lv = (ListView)sender;
                ListViewItem lvi = lv.GetItemAt(e.X, e.Y);
                if (lvi != null)
                {
                    IDataNode mbr = m_Mapper[lvi.Index];

                    int xfrom = lvi.IndentCount * 16;
                    int xto = xfrom + 16;

                    if ((e.X >= xfrom && e.X <= xto) || e.Clicks > 1)
                    {
                        if (mbr.CountChildren > 0)
                        {
                            mbr.Expanded = !mbr.Expanded;
                            lvi.Checked = !lvi.Checked;

                            PrepareNodes(lvi.Index, mbr.Expanded);
                        }
                    }
                }
            }
        }        

As you can see, the state whether a line is expanded or collapsed will be set within the Expanded property of the ListViewItem, but also within the DataNode object. The latter has to be done because when one collapses a line on top level and re-expands the line, the lines on a higher indent level still hold their state and appear expanded or collapsed, according to their last state.

The method PrepareNodes looks like this:

C#
private void PrepareNodes(int pos, bool add)
        {
            IDataNode mbr = m_Mapper[pos];
            pos++;

            if (add)
            {
                PopulateDescendantMembers(ref pos, mbr);
            }
            else
            {
                int kids = ObtainExpandedChildrenCount(pos-1);
                m_Mapper.RemoveRange(pos, kids);
            }
           
            listView1.VirtualListSize = m_Mapper.Count;
        } 
Setting the number of the VirtualListSize according to the mapper's count is the last job within the PrepareNodes method and is very important.

When we must expand a line, we have to obtain the children as well as sub-children. The DataNode's references will be inserted into the mapper. Here's the method that does the job:

C#
private void PopulateDescendantMembers(ref int pos, IDataNode mbr)
        {
            foreach (IDataNode m in mbr.Children)
            {
                m_Mapper.Insert(pos++, m);
                if (m.Expanded)
                {
                    PopulateDescendantMembers(ref pos, m);
                }
            }
        }

Collapsing a line is easy, just obtain the total number of expanded children and remove the number of DataNode references in the mapper.

Here's the method to obtain the number of expanded children:

C#
private int ObtainExpandedChildrenCount(int pos)
        {
            int kids = 0;
            IDataNode mi = m_Mapper[pos];
            int level = mi.Level;

            for (int i = pos + 1; i < m_Mapper.Count; i++, kids++)
            {
                IDataNode mix = m_Mapper[i];
                int lvlx = mix.Level;
                if (lvlx <= level) break;
            }

            return kids;
        }  

In earlier versions, I have obtained the number of expanded entries by traversing the model itself, but it's really easier to walk along the mapper until you reach the starting indent level again.

Of course there is also code for the owner draw part, but if you look at it you will recognize that it's just drawing the appropriate arrow and setting the bounding rectangle's x-position according to the indent level. So there's no need to explain it in detail, I think.

Points of Interest

The model I have provided as an XML-file of course is just very simple and doesn't contain lots of nodes. The VirtualModeTreeListView is however very useful when displaying thousands of nodes, especially when a huge amount of child nodes have to be expanded/collapsed. Then you'll experience a great performance, because the ListView itself must never be "filled" with data. Nevertheless some kind of "mapper" that maps line numbers to data objects must be provided and adjusted whenever a node gets expanded/collapsed. But the collection class I have used for the mapper provides "RemoveAt" and "InsertAt" methods, so the mapper must never be cleared and refilled completely, when expanding/collapsing occurs. I don't know if a "List<T>" is organized as a chained list, but I hope so. The mapper will ever be the only bottleneck if it works slowly, but I have not experienced this, even with a large amount of data nodes.

History

  • 5th September, 2009: First version of the VirtualModeListView. It has been tested using Windows Vista OS. You might experience some drawing problems when running on XP because there is a bug in the underlying common control which Microsoft is aware of.
  • 9th September, 2009: Provided a huge model which is downloadable. Just unzip the file and load the XML-file. Expand Earth, Europe, Germany, Bavaria, Lindau. Then expand Lindau and you will get 65535 citizens listed. Have fun with the great performance!
  • 27th December, 2009: Within version 1.1.1.2 the first column was not drawn correctly if a row is selected. This is however a microsoft bug and will get experienced with OS XP. There is now some hack that eliminates this problem.
  • 1st October, 2010: Eliminated warnings, removed orphaned reference, assembly now strong named.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) brightman objects software studios
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionhow to populate your tree gridview from datatable Pin
Tridip Bhattacharjee17-Dec-13 3:19
professionalTridip Bhattacharjee17-Dec-13 3:19 
AnswerRe: how to populate your tree gridview from datatable Pin
yetibrain27-Jan-14 3:03
yetibrain27-Jan-14 3:03 
QuestionNice Tree DataGridView Pin
Tridip Bhattacharjee17-Dec-13 2:48
professionalTridip Bhattacharjee17-Dec-13 2:48 
AnswerRe: Nice Tree DataGridView Pin
yetibrain27-Jan-14 3:04
yetibrain27-Jan-14 3:04 
Questionvc++ implementation Pin
vinaypeepl23-Jun-13 5:07
vinaypeepl23-Jun-13 5:07 
SuggestionRe: vc++ implementation Pin
yetibrain24-Jun-13 1:58
yetibrain24-Jun-13 1:58 
GeneralRe: vc++ implementation Pin
vinaypeepl24-Jun-13 17:24
vinaypeepl24-Jun-13 17:24 
SuggestionRe: vc++ implementation Pin
yetibrain25-Jun-13 1:54
yetibrain25-Jun-13 1:54 
GeneralRe: vc++ implementation Pin
vinaypeepl25-Jun-13 1:56
vinaypeepl25-Jun-13 1:56 
SuggestionRe: vc++ implementation Pin
yetibrain25-Jun-13 22:56
yetibrain25-Jun-13 22:56 
Questionminor editorial suggestion Pin
BillWoodruff7-Oct-12 20:44
professionalBillWoodruff7-Oct-12 20:44 
AnswerRe: minor editorial suggestion Pin
yetibrain18-Dec-12 23:34
yetibrain18-Dec-12 23:34 
SuggestionPossibly a standalone control Pin
Paul.HulleyUSDAW2-Oct-12 2:05
Paul.HulleyUSDAW2-Oct-12 2:05 
QuestionGood job, but I voted 4 Pin
Sergey Alexandrovich Kryukov16-Jan-12 19:47
mvaSergey Alexandrovich Kryukov16-Jan-12 19:47 
AnswerRe: Good job, but I voted 4 Pin
yetibrain19-Jan-12 4:13
yetibrain19-Jan-12 4:13 
GeneralGreat (Re: Good job, but I voted 4) Pin
Sergey Alexandrovich Kryukov27-Jan-12 22:07
mvaSergey Alexandrovich Kryukov27-Jan-12 22:07 
NewsRe: Great (Re: Good job, but I voted 4) Pin
yetibrain18-Dec-12 23:37
yetibrain18-Dec-12 23:37 
GeneralI want to see it! (Re: Good job, but I voted 4) Pin
Sergey Alexandrovich Kryukov19-Dec-12 5:22
mvaSergey Alexandrovich Kryukov19-Dec-12 5:22 
GeneralRe: I want to see it! (Re: Good job, but I voted 4) Pin
yetibrain24-Jun-13 2:01
yetibrain24-Jun-13 2:01 
QuestionColumn Reorder messes your control - and an additional checkbox is possible? Pin
Member 167849517-Jan-10 12:01
Member 167849517-Jan-10 12:01 
AnswerRe: Column Reorder messes your control - and an additional checkbox is possible? Pin
yetibrain18-Jan-10 13:34
yetibrain18-Jan-10 13:34 
Questionhow to convert to use in FrameWork 4.0, VS Studio .NET 2010 beta 2 Pin
BillWoodruff29-Dec-09 3:51
professionalBillWoodruff29-Dec-09 3:51 
AnswerRe: how to convert to use in FrameWork 4.0, VS Studio .NET 2010 beta 2 Pin
yetibrain29-Dec-09 11:55
yetibrain29-Dec-09 11:55 
GeneralSee also Pin
Qwertie27-Sep-09 12:48
Qwertie27-Sep-09 12:48 
GeneralRe: See also Pin
yetibrain19-Jan-12 4:17
yetibrain19-Jan-12 4:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.