65.9K
CodeProject is changing. Read more.
Home

Multiselect Treeview Implementation

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (23 votes)

Sep 20, 2007

CPOL

3 min read

viewsIcon

175074

downloadIcon

8230

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

#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

#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

#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