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:
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
.
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:
private void InitVirtualListViewNodes()
{
m_Mapper.Clear();
ObtainAllNodes(m_Model.DataPool);
listView1.VirtualListSize = m_Mapper.Count;
listView1.VirtualMode = true;
listView1.Invalidate();
}
This method adds IDataNode
references to the mapper:
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:
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:
private void ListView1RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
e.Item = MakeListViewItem(m_Mapper[e.ItemIndex]);
}
The method MakelistViewItem
looks like this:
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:
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:
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:
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:
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.