Click here to Skip to main content
15,880,469 members
Articles / Programming Languages / C#
Article

Multiselect Treeview Implementation

Rate me:
Please Sign up or sign in to vote.
4.85/5 (25 votes)
20 Sep 2007CPOL3 min read 169.2K   8.2K   45   44
An extension of the .NET Treeview control to provide multiselect capabilities
Screenshot - app.gif

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

C#
#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 );
            }
        }
    }
}

// Note we use the new keyword to Hide the native treeview's 
// SelectedNode property.
private TreeNode m_SelectedNode;
public new TreeNode SelectedNode
{
    get
    {
        return m_SelectedNode;
    }
    set
    {
        ClearSelectedNodes();
        if( value != null )
        {
            SelectNode( value );
        }
    }
}

#endregion  

Overridden Events

C#
#region Overridden Events

protected override void OnGotFocus( EventArgs e )
{
    // Make sure at least one node has a selection
    // this way we can tab to the ctrl and use the
    // keyboard to select nodes
    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 )
{
    // If the user clicks on a node that was not
    // previously selected, select it now.
    try
    {
        base.SelectedNode = null;

        TreeNode node = this.GetNodeAt( e.Location );
        if( node != null )
        {
            //Allow user to click on image
            int leftBound = node.Bounds.X; // - 20; 
            // Give a little extra room
            int rightBound = node.Bounds.Right + 10; 
            if( e.Location.X > leftBound && e.Location.X < rightBound )
            {
                if( ModifierKeys == 
                    Keys.None && ( m_SelectedNodes.Contains( node ) ) )
                {
                    // Potential Drag Operation
                    // Let Mouse Up do select
                }
                else
                {
                    SelectNode( node );
                }
            }
        }

        base.OnMouseDown( e );
    }
    catch( Exception ex )
    {
        HandleException( ex );
    }
}

protected override void OnMouseUp( MouseEventArgs e )
{
    // If you clicked on a node that WAS previously
    // selected then, reselect it now. This will clear
    // any other selected nodes. e.g. A B C D are selected
    // the user clicks on B, now A C & D are no longer selected.
    try
    {
        // Check to see if a node was clicked on
        TreeNode node = this.GetNodeAt( e.Location );
        if( node != null )
        {
            if( ModifierKeys == Keys.None && m_SelectedNodes.Contains( node ) )
            {
                // Allow user to click on image
                int leftBound = node.Bounds.X; // - 20; 
                // Give a little extra room
                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 )
{
    // If the user drags a node and the node being dragged is NOT
    // selected, then clear the active selection, select the
    // node being dragged and drag it. Otherwise if the node being
    // dragged is selected, drag the entire selection.
    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 )
{
    // Never allow base.SelectedNode to be set!
    try
    {
        base.SelectedNode = null;
        e.Cancel = true;

        base.OnBeforeSelect( e );
    }
    catch( Exception ex )
    {
        HandleException( ex );
    }
}

protected override void OnAfterSelect( TreeViewEventArgs e )
{
    // Never allow base.SelectedNode to be set!
    try
    {
        base.OnAfterSelect( e );
        base.SelectedNode = null;
    }
    catch( Exception ex )
    {
        HandleException( ex );
    }
}

protected override void OnKeyDown( KeyEventArgs e )
{
    // Handle all possible key strokes for the control.
    // including navigation, selection, etc.

    base.OnKeyDown( e );

    if( e.KeyCode == Keys.ShiftKey ) return;

    //this.BeginUpdate();
    bool bShift = ( ModifierKeys == Keys.Shift );

    try
    {
        // Nothing is selected in the tree, this isn't a good state
        // select the top node
        if( m_SelectedNode == null && this.TopNode != null )
        {
            ToggleNode( this.TopNode, true );
        }

        // Nothing is still selected in the tree, 
        // this isn't a good state, leave.
        if( m_SelectedNode == null ) return;

        if( e.KeyCode == Keys.Left )
        {
            if( m_SelectedNode.IsExpanded && m_SelectedNode.Nodes.Count > 0 )
            {
                // Collapse an expanded node that has children
                m_SelectedNode.Collapse();
            }
            else if( m_SelectedNode.Parent != null )
            {
                // Node is already collapsed, try to select its parent.
                SelectSingleNode( m_SelectedNode.Parent );
            }
        }
        else if( e.KeyCode == Keys.Right )
        {
            if( !m_SelectedNode.IsExpanded )
            {
                // Expand a collapsed node's children
                m_SelectedNode.Expand();
            }
            else
            {
                // Node was already expanded, select the first child
                SelectSingleNode( m_SelectedNode.FirstNode );
            }
        }
        else if( e.KeyCode == Keys.Up )
        {
            // Select the previous node
            if( m_SelectedNode.PrevVisibleNode != null )
            {
                SelectNode( m_SelectedNode.PrevVisibleNode );
            }
        }
        else if( e.KeyCode == Keys.Down )
        {
            // Select the next node
            if( m_SelectedNode.NextVisibleNode != null )
            {
                SelectNode( m_SelectedNode.NextVisibleNode );
            }
        }
        else if( e.KeyCode == Keys.Home )
        {
            if( bShift )
            {
                if( m_SelectedNode.Parent == null )
                {
                    // Select all of the root nodes up to this point
                    if( this.Nodes.Count > 0 )
                    {
                        SelectNode( this.Nodes[0] );
                    }
                }
                else
                {
                    // Select all of the nodes up to this point under 
                    // this nodes parent
                    SelectNode( m_SelectedNode.Parent.FirstNode );
                }
            }
            else
            {
                // Select this first node in the tree
                if( this.Nodes.Count > 0 )
                {
                    SelectSingleNode( this.Nodes[0] );
                }
            }
        }
        else if( e.KeyCode == Keys.End )
        {
            if( bShift )
            {
                if( m_SelectedNode.Parent == null )
                {
                    // Select the last ROOT node in the tree
                    if( this.Nodes.Count > 0 )
                    {
                        SelectNode( this.Nodes[this.Nodes.Count - 1] );
                    }
                }
                else
                {
                    // Select the last node in this branch
                    SelectNode( m_SelectedNode.Parent.LastNode );
                }
            }
            else
            {
                if( this.Nodes.Count > 0 )
                {
                    // Select the last node visible node in the tree.
                    // Don't expand branches incase the tree is virtual
                    TreeNode ndLast = this.Nodes[0].LastNode;
                    while( ndLast.IsExpanded && ( ndLast.LastNode != null ) )
                    {
                        ndLast = ndLast.LastNode;
                    }
                    SelectSingleNode( ndLast );
                }
            }
        }
        else if( e.KeyCode == Keys.PageUp )
        {
            // Select the highest node in the display
            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 )
        {
            // Select the lowest node in the display
            int nCount = this.VisibleCount;
            TreeNode ndCurrent = m_SelectedNode;
            while( ( nCount ) > 0 && ( ndCurrent.NextVisibleNode != null ) )
            {
                ndCurrent = ndCurrent.NextVisibleNode;
                nCount--;
            }
            SelectSingleNode( ndCurrent );
        }
        else
        {
            // Assume this is a search character a-z, A-Z, 0-9, etc.
            // Select the first node after the current node that
            // starts with this character
            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

C#
#region Helper Methods

private void SelectNode( TreeNode node )
{
    try
    {
        this.BeginUpdate();

        if( m_SelectedNode == null || ModifierKeys == Keys.Control )
        {
            // Ctrl+Click selects an unselected node, 
            // or unselects a selected node.
            bool bIsSelected = m_SelectedNodes.Contains( node );
            ToggleNode( node, !bIsSelected );
        }
        else if( ModifierKeys == Keys.Shift )
        {
            // Shift+Click selects nodes between the selected node and here.
            TreeNode ndStart = m_SelectedNode;
            TreeNode ndEnd = node;

            if( ndStart.Parent == ndEnd.Parent )
            {
                // Selected node and clicked node have same parent, easy case.
                if( ndStart.Index < ndEnd.Index )
                {
                    // If the selected node is beneath 
                    // the clicked node walk down
                    // selecting each Visible node until we reach the end.
                    while( ndStart != ndEnd )
                    {
                        ndStart = ndStart.NextVisibleNode;
                        if( ndStart == null ) break;
                        ToggleNode( ndStart, true );
                    }
                }
                else if( ndStart.Index == ndEnd.Index )
                {
                    // Clicked same node, do nothing
                }
                else
                {
                    // If the selected node is above the clicked node walk up
                    // selecting each Visible node until we reach the end.
                    while( ndStart != ndEnd )
                    {
                        ndStart = ndStart.PrevVisibleNode;
                        if( ndStart == null ) break;
                        ToggleNode( ndStart, true );
                    }
                }
            }
            else
            {
                // Selected node and clicked node have same parent, hard case.
                // We need to find a common parent to determine if we need
                // to walk down selecting, or walk up selecting.

                TreeNode ndStartP = ndStart;
                TreeNode ndEndP = ndEnd;
                int startDepth = Math.Min( ndStartP.Level, ndEndP.Level );

                // Bring lower node up to common depth
                while( ndStartP.Level > startDepth )
                {
                    ndStartP = ndStartP.Parent;
                }

                // Bring lower node up to common depth
                while( ndEndP.Level > startDepth )
                {
                    ndEndP = ndEndP.Parent;
                }

                // Walk up the tree until we find the common parent
                while( ndStartP.Parent != ndEndP.Parent )
                {
                    ndStartP = ndStartP.Parent;
                    ndEndP = ndEndP.Parent;
                }

                // Select the node
                if( ndStartP.Index < ndEndP.Index )
                {
                    // If the selected node is beneath 
                    // the clicked node walk down
                    // selecting each Visible node until we reach the end.
                    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
                {
                    // If the selected node is above 
                    // the clicked node walk up
                    // selecting each Visible node until we reach the end.
                    while( ndStart != ndEnd )
                    {
                        ndStart = ndStart.PrevVisibleNode;
                        if( ndStart == null ) break;
                        ToggleNode( ndStart, true );
                    }
                }
            }
        }
        else
        {
            // Just clicked a node, select it
            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 )
{
    // Perform some error handling here.
    // We don't want to bubble errors to the CLR.
    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#

Screenshot - me.gif

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) BrightStar Partners
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionPerformance improvment Pin
Mr Scotty4-Mar-13 2:14
Mr Scotty4-Mar-13 2:14 

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.