Introduction
Why doesn't .NET have a multiselect treeview? There are so many uses for one and turning on checkboxes in the treeview is a pretty lousy alternative. I tried some third party treeviews and I think what turned me off the most is that the object model is different than the .NET treeview I'm used to working with. All I wanted is the standard .NET treeview with a SelectedNodes
property as well as a SelectedNode
property. After a quick search on The Code Project, I found Jean Allisat's implementation here. I wasn't satisfied though because some things didn't behave correctly. For example, you click on a node and then as you Ctrl+Click on a second node, the first node loses its highlighting until the click operation is completed. Strange. So it looks a little bit choppy, but it works. I started with Jean's implementation and took it to the next level to try and clean up the UI behaviour a bit.
Using the Code
The "choppy" problem I was having with the original implementation of the multiselect treeview was that we were letting the treeview select and highlight the selected node, while in the overridden events, we would deal with manually highlighting other selected nodes. The conclusion I came to was to do all of the highlighting myself and not fight with the treeview. So the first thing we need to do is cripple the treeview so that it can NEVER have a SelectedNode
. We do this by overriding the OnMouseDown
, OnBeforeSelect
& OnAfterSelect
events and setting base.SelectedNode
to null
as well as setting e.Cancel
in some of the events to stop them from processing. We also hide the treeview's SelectedNode
property (with the new keyword) and reimplement our own version.
Now that we have a treeview that is crippled, we can implement new logic for selecting node(s). When you click on a node it becomes the SelectedNode
and it is highlighted. If you were not holding down a ModifierKey
, then we can clear the previous selection. If you were holding down the Ctrl ModifierKey
, then we decide whether to add this node to the selection or remove it if it was already in the selection. If you were holding down the Shift ModifierKey
, then we have to select all the nodes from the current SelectedNode
to this one. All of this logic resides in the SelectNode()
helper function.
One gotcha here. All of the treeview's KeyDown
messages are processed off of the SelectedNode
and since there never is a SelectedNode
(we've crippled it...) then you can't use the keyboard to navigate/edit the tree. Well, that's no good... So we have to trap the OnKeyDown
event and handle Left, Right, Up, Down, Home, End, Page Up, Page Down, and any alpha-numeric character. Each of these key commands can have different behaviours if the Ctrl or Shift ModifierKey
are pressed, and possibly different behaviours if a branch is expanded or not.
Selected Node(s) Properties
#region Selected Node(s) Properties
public MultiSelectTreeview()
{
m_SelectedNodes = new List<TreeNode>();
base.SelectedNode = null;
}
private List<TreeNode> m_SelectedNodes = null;
public List<TreeNode> SelectedNodes
{
get
{
return m_SelectedNodes;
}
set
{
ClearSelectedNodes();
if( value != null )
{
foreach( TreeNode node in value )
{
ToggleNode( node, true );
}
}
}
}
private TreeNode m_SelectedNode;
public new TreeNode SelectedNode
{
get
{
return m_SelectedNode;
}
set
{
ClearSelectedNodes();
if( value != null )
{
SelectNode( value );
}
}
}
#endregion
Overridden Events
#region Overridden Events
protected override void OnGotFocus( EventArgs e )
{
try
{
if( m_SelectedNode == null && this.TopNode != null )
{
ToggleNode( this.TopNode, true );
}
base.OnGotFocus( e );
}
catch( Exception ex )
{
HandleException( ex );
}
}
protected override void OnMouseDown( MouseEventArgs e )
{
try
{
base.SelectedNode = null;
TreeNode node = this.GetNodeAt( e.Location );
if( node != null )
{
int leftBound = node.Bounds.X;
int rightBound = node.Bounds.Right + 10;
if( e.Location.X > leftBound && e.Location.X < rightBound )
{
if( ModifierKeys ==
Keys.None && ( m_SelectedNodes.Contains( node ) ) )
{
}
else
{
SelectNode( node );
}
}
}
base.OnMouseDown( e );
}
catch( Exception ex )
{
HandleException( ex );
}
}
protected override void OnMouseUp( MouseEventArgs e )
{
try
{
TreeNode node = this.GetNodeAt( e.Location );
if( node != null )
{
if( ModifierKeys == Keys.None && m_SelectedNodes.Contains( node ) )
{
int leftBound = node.Bounds.X;
int rightBound = node.Bounds.Right + 10;
if( e.Location.X > leftBound && e.Location.X < rightBound )
{
SelectNode( node );
}
}
}
base.OnMouseUp( e );
}
catch( Exception ex )
{
HandleException( ex );
}
}
protected override void OnItemDrag( ItemDragEventArgs e )
{
try
{
TreeNode node = e.Item as TreeNode;
if( node != null )
{
if( !m_SelectedNodes.Contains( node ) )
{
SelectSingleNode( node );
ToggleNode( node, true );
}
}
base.OnItemDrag( e );
}
catch( Exception ex )
{
HandleException( ex );
}
}
protected override void OnBeforeSelect( TreeViewCancelEventArgs e )
{
try
{
base.SelectedNode = null;
e.Cancel = true;
base.OnBeforeSelect( e );
}
catch( Exception ex )
{
HandleException( ex );
}
}
protected override void OnAfterSelect( TreeViewEventArgs e )
{
try
{
base.OnAfterSelect( e );
base.SelectedNode = null;
}
catch( Exception ex )
{
HandleException( ex );
}
}
protected override void OnKeyDown( KeyEventArgs e )
{
base.OnKeyDown( e );
if( e.KeyCode == Keys.ShiftKey ) return;
bool bShift = ( ModifierKeys == Keys.Shift );
try
{
if( m_SelectedNode == null && this.TopNode != null )
{
ToggleNode( this.TopNode, true );
}
if( m_SelectedNode == null ) return;
if( e.KeyCode == Keys.Left )
{
if( m_SelectedNode.IsExpanded && m_SelectedNode.Nodes.Count > 0 )
{
m_SelectedNode.Collapse();
}
else if( m_SelectedNode.Parent != null )
{
SelectSingleNode( m_SelectedNode.Parent );
}
}
else if( e.KeyCode == Keys.Right )
{
if( !m_SelectedNode.IsExpanded )
{
m_SelectedNode.Expand();
}
else
{
SelectSingleNode( m_SelectedNode.FirstNode );
}
}
else if( e.KeyCode == Keys.Up )
{
if( m_SelectedNode.PrevVisibleNode != null )
{
SelectNode( m_SelectedNode.PrevVisibleNode );
}
}
else if( e.KeyCode == Keys.Down )
{
if( m_SelectedNode.NextVisibleNode != null )
{
SelectNode( m_SelectedNode.NextVisibleNode );
}
}
else if( e.KeyCode == Keys.Home )
{
if( bShift )
{
if( m_SelectedNode.Parent == null )
{
if( this.Nodes.Count > 0 )
{
SelectNode( this.Nodes[0] );
}
}
else
{
SelectNode( m_SelectedNode.Parent.FirstNode );
}
}
else
{
if( this.Nodes.Count > 0 )
{
SelectSingleNode( this.Nodes[0] );
}
}
}
else if( e.KeyCode == Keys.End )
{
if( bShift )
{
if( m_SelectedNode.Parent == null )
{
if( this.Nodes.Count > 0 )
{
SelectNode( this.Nodes[this.Nodes.Count - 1] );
}
}
else
{
SelectNode( m_SelectedNode.Parent.LastNode );
}
}
else
{
if( this.Nodes.Count > 0 )
{
TreeNode ndLast = this.Nodes[0].LastNode;
while( ndLast.IsExpanded && ( ndLast.LastNode != null ) )
{
ndLast = ndLast.LastNode;
}
SelectSingleNode( ndLast );
}
}
}
else if( e.KeyCode == Keys.PageUp )
{
int nCount = this.VisibleCount;
TreeNode ndCurrent = m_SelectedNode;
while( ( nCount ) > 0 && ( ndCurrent.PrevVisibleNode != null ) )
{
ndCurrent = ndCurrent.PrevVisibleNode;
nCount--;
}
SelectSingleNode( ndCurrent );
}
else if( e.KeyCode == Keys.PageDown )
{
int nCount = this.VisibleCount;
TreeNode ndCurrent = m_SelectedNode;
while( ( nCount ) > 0 && ( ndCurrent.NextVisibleNode != null ) )
{
ndCurrent = ndCurrent.NextVisibleNode;
nCount--;
}
SelectSingleNode( ndCurrent );
}
else
{
string sSearch = ( (char) e.KeyValue ).ToString();
TreeNode ndCurrent = m_SelectedNode;
while( ( ndCurrent.NextVisibleNode != null ) )
{
ndCurrent = ndCurrent.NextVisibleNode;
if( ndCurrent.Text.StartsWith( sSearch ) )
{
SelectSingleNode( ndCurrent );
break;
}
}
}
}
catch( Exception ex )
{
HandleException( ex );
}
finally
{
this.EndUpdate();
}
}
#endregion
Helper Functions
#region Helper Methods
private void SelectNode( TreeNode node )
{
try
{
this.BeginUpdate();
if( m_SelectedNode == null || ModifierKeys == Keys.Control )
{
bool bIsSelected = m_SelectedNodes.Contains( node );
ToggleNode( node, !bIsSelected );
}
else if( ModifierKeys == Keys.Shift )
{
TreeNode ndStart = m_SelectedNode;
TreeNode ndEnd = node;
if( ndStart.Parent == ndEnd.Parent )
{
if( ndStart.Index < ndEnd.Index )
{
while( ndStart != ndEnd )
{
ndStart = ndStart.NextVisibleNode;
if( ndStart == null ) break;
ToggleNode( ndStart, true );
}
}
else if( ndStart.Index == ndEnd.Index )
{
}
else
{
while( ndStart != ndEnd )
{
ndStart = ndStart.PrevVisibleNode;
if( ndStart == null ) break;
ToggleNode( ndStart, true );
}
}
}
else
{
TreeNode ndStartP = ndStart;
TreeNode ndEndP = ndEnd;
int startDepth = Math.Min( ndStartP.Level, ndEndP.Level );
while( ndStartP.Level > startDepth )
{
ndStartP = ndStartP.Parent;
}
while( ndEndP.Level > startDepth )
{
ndEndP = ndEndP.Parent;
}
while( ndStartP.Parent != ndEndP.Parent )
{
ndStartP = ndStartP.Parent;
ndEndP = ndEndP.Parent;
}
if( ndStartP.Index < ndEndP.Index )
{
while( ndStart != ndEnd )
{
ndStart = ndStart.NextVisibleNode;
if( ndStart == null ) break;
ToggleNode( ndStart, true );
}
}
else if( ndStartP.Index == ndEndP.Index )
{
if( ndStart.Level < ndEnd.Level )
{
while( ndStart != ndEnd )
{
ndStart = ndStart.NextVisibleNode;
if( ndStart == null ) break;
ToggleNode( ndStart, true );
}
}
else
{
while( ndStart != ndEnd )
{
ndStart = ndStart.PrevVisibleNode;
if( ndStart == null ) break;
ToggleNode( ndStart, true );
}
}
}
else
{
while( ndStart != ndEnd )
{
ndStart = ndStart.PrevVisibleNode;
if( ndStart == null ) break;
ToggleNode( ndStart, true );
}
}
}
}
else
{
SelectSingleNode( node );
}
OnAfterSelect( new TreeViewEventArgs( m_SelectedNode ) );
}
finally
{
this.EndUpdate();
}
}
private void ClearSelectedNodes()
{
try
{
foreach( TreeNode node in m_SelectedNodes )
{
node.BackColor = this.BackColor;
node.ForeColor = this.ForeColor;
}
}
finally
{
m_SelectedNodes.Clear();
m_SelectedNode = null;
}
}
private void SelectSingleNode( TreeNode node )
{
if( node == null )
{
return;
}
ClearSelectedNodes();
ToggleNode( node, true );
node.EnsureVisible();
}
private void ToggleNode( TreeNode node, bool bSelectNode )
{
if( bSelectNode )
{
m_SelectedNode = node;
if( !m_SelectedNodes.Contains( node ) )
{
m_SelectedNodes.Add( node );
}
node.BackColor = SystemColors.Highlight;
node.ForeColor = SystemColors.HighlightText;
}
else
{
m_SelectedNodes.Remove( node );
node.BackColor = this.BackColor;
node.ForeColor = this.ForeColor;
}
}
private void HandleException( Exception ex )
{
MessageBox.Show( ex.Message );
}
#endregion
Points of Interest
Check out my other post: Virtual Treeview Implementation
About the Author
Andrew D. Weiss
Software Engineer
Check out my blog: More-On C#
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.