Creating a Derived Accordion Control with FlexGrid

The FlexGrid for WinForms control is known for its flexibility. The control can be used in more ways than one and you can customize it easily enough to avoid having to write a custom solution from scratch in many cases. In previous posts we demonstrated how you can use the FlexGrid as a Calendar control, a TreeListView and as a ListBox. Some developers have gone as far as using it to make Visio style diagrams just using FlexGrid. In this post I will show how to derive a custom control from FlexGrid to make an Accordion control, just like the one used in the Studio for WinForms Control Explorer. The FlexAccordion component derives from FlexGrid and is designed to display a hierarchical list of items with 1 level deep. It was modeled specifically to fit the needs of the Control Explorer, but it can be modified further for more scenarios. FlexGrid makes it easy to create such a control because it gives us a backbone to start with that has provides hit testing, scrolling, item selection, tags for user data, and custom cell styles.

Extending FlexGrid

To create your own custom component which derives from C1FlexGrid, first add a new Component Class to your solution. Add a project reference to the C1.Win.C1FlexGrid assembly. In your derived class declaration, specify the base class name as C1FlexGrid using the syntax as seen below.


using System;  
using System.Collections.Generic;  
using System.ComponentModel;  
using System.Data;  
using System.Drawing;  
using System.Text;  
using System.Windows.Forms;  
using C1.Win.C1FlexGrid;  

namespace MyNameSpace  
{  
    public partial class FlexAccordion : C1FlexGrid  
    {  
        public FlexAccordion()  
        {  
            InitializeComponent();  
        }  
    }  
}  

Define the Object Model

Next, define the object model or the properties. The FlexAccordion will add several unique properties in addition to everything available to us from C1FlexGrid.

ExpandedIndex (int) - Gets or sets the currently expanded header row index SelectedItemIndex (int) - Gets or sets the index of the currently selected item AnimationDelay (int) - Gets or sets the animation delay in milliseconds CanExpand (Boolean) - Gets or sets whether the control can expand/collapse HeaderStyle (C1.Win.C1FlexGrid.CellStyle) - Gets or sets the CellStyle object associated with item headers ItemStyle (C1.Win.C1FlexGrid.CellStyle) - Gets or sets the CellStyle object associated with items

Most of these properties will simply hold an integer or Boolean value that will later be referenced during implementation. The two styles are custom CellStyles added to the base C1FlexGrid component. Here we are taking advantage of this CellStyle feature to make styling the headers and items more straightforward. Below is the code used to declare the object model as described. Note that all of this code is to be placed within the FlexAccordion class above or below the constructor.


# region private members  

private int _expandedIndex;  
private int _selectedItemIndex;  
private int _animationDelay;  
private bool _canExpand;  
private CellStyle _headerStyle;  
private CellStyle _itemStyle;  

# endregion  

# region public members  
/// <summary>  
/// Gets or sets the currently expanded row index  
/// </summary>  
public int ExpandedIndex  
{  
    get { return _expandedIndex; }  
    set { _expandedIndex = value; }  
}  

/// <summary>  
/// Gets or sets the currently selected item  
/// </summary>  
public int SelectedItemIndex  
{  
    get { return _selectedItemIndex; }  
    set  
    {  
        if (_selectedItemIndex != -1)  
        {  
            this.SetCellStyle(\_selectedItemIndex, 0, \_itemStyle);  
        }  
        _selectedItemIndex = value;  
    }  
}  

/// <summary>  
/// Gets or sets the animation delay in milliseconds.  
/// </summary>  
public int AnimationDelay  
{  
    get { return _animationDelay; }  
    set { _animationDelay = value; }  
}  

/// <summary>  
/// Gets or sets whether the control can expand/collapse  
/// </summary>  
public bool CanExpand  
{  
    get { return _canExpand; }  
    set { _canExpand = value; }  
}  

/// <summary>  
/// Gets or sets the CellStyle object associated with item headers.  
/// </summary>  
public CellStyle HeaderStyle  
{  
    get { return _headerStyle; }  
    set { _headerStyle = value; }  
}  

/// <summary>  
/// Gets or sets the CellStyle object associated with items.  
/// </summary>  
public CellStyle ItemStyle  
{  
    get { return _itemStyle; }  
    set { _itemStyle = value; }  
}  

# endregion  

Next initialize these properties in the constructor below the InitializeComponent call. Since we don't initialize AnimationDelay, this will default to 0. Also use this place to set any other base C1FlexGrid properties to a new default value. In this case, we change ScrollBars and SelectionMode to better fit with the accordion control.


public FlexAccordion()  
{  
    InitializeComponent();  
    _headerStyle = this.Styles.Add("Header");  
    _itemStyle = this.Styles.Add("Item");  
    _expandedIndex = -1;  
    _selectedItemIndex = -1;  
    _canExpand = true;  
    this.ScrollBars = ScrollBars.None;  
    this.SelectionMode = SelectionModeEnum.Row;  
}  

Expand and Collapse Implementation

To add some interaction to our custom control we can override any of the C1FlexGrid events. To detect the user clicking on a row to then expand or collapse its children, we can override the MouseClick event.


protected override void OnMouseClick(MouseEventArgs e)  
{  
    base.OnMouseClick(e);  
    if (this.MouseRow != -1)  
    {  
        Row hitRow = this.Rows[this.MouseRow];  
        if (hitRow.Index == _expandedIndex)  
        {  
            //Do nothing. Just clicked an already expanded header.  
        }  
        else if (hitRow.IsNode && this._canExpand)  
        {  
            //Expand items  
            Expand(hitRow.Index);  
            _expandedIndex = hitRow.Index;  
        }  
        else if (!hitRow.IsNode)  
        {  
            this.SelectedItemIndex = hitRow.Index;  
        }  
    }  
    this._canExpand = true;  
}  

In this implementation of an accordion, the user can click any Header to expand its items. But to collapse the items they must go click a different Header. So in the second IF statement we are detecting the user clicking on an already-expanded header so we choose to do nothing. If they hit an item row which is not a header, we set that as the Selected Item. If they hit a collapsed header we begin the collapsing and expanding in the Expand method.


//This method collapses the currently expanded items (if any) and expands at the passed row index.  
protected void Expand(int expandRow)  
{  
    int collapseRow = _expandedIndex + 1;  
    expandRow += 1;  
    bool done = false;  
    bool collapsing = false;  
    bool expanding = false;  
    while (!done)  
    {  
        //Update the current collapsing row index  
        if (collapseRow < this.Rows.Count)  
            collapsing = !this.Rows[collapseRow].IsNode;  
        else collapsing = false;  

        //Update the current expanding row index  
        if (expandRow < this.Rows.Count)  
            expanding = !this.Rows[expandRow].IsNode;  
        else expanding = false;  

        if (expanding || collapsing)  
        {  
           //Collapse and expand 1 row each at a time  
            if (collapsing)  
                this.Rows[collapseRow].Visible = !this.Rows[collapseRow].Visible;  
            if (expanding)  
                this.Rows[expandRow].Visible = !this.Rows[expandRow].Visible;  

            //Redraw FlexGrid to reflect new visible rows and sleep for animation effect  
            this.Refresh();  
            System.Threading.Thread.Sleep(_animationDelay);  
        }  
        else  
        {  
            //No more rows to expand or collapse  
            done = true;  
        }  

        //if we could expand/collapse continue to next row  
        if (collapsing) collapseRow += 1;  
        if (expanding) expandRow += 1;  
    }  
    updateScrollButtons();  
}  

In the Expand method we expand or collapse a row by setting its Visible property. This is common practice for hiding rows and columns in C1FlexGrid. We use the row's IsNode property (exposed through the base C1FlexGrid) to determine which rows are headers and which rows are items (IsNode = True == Header). A key line of code to point out is #30, this.Refresh(). This is calling the base C1FlexGrid's Refresh method which forces the control to redraw even though we are not completely finished running this method. This gives us our accordion's animation effect along with the thread sleep. Without it, the expand/collapse would appear instant (hey, that might be what you want). The trick used here to creating a smooth expand and collapse experience is to collapse and expand rows on the same thread back and forth, one row at a time. I tried using new threads for each action because that seemed to be the "right way" to do busy work, but found that the animation was very jerky to the eye.

Mouse Over Effects

As a nice touch, we can add some "built-in" mouse over effects. C1FlexGrid provides a built-in Highlight CellStyle which we can use. We can override the MouseEnterCell and MouseLeaveCell events to apply and remove this style based upon where the user's cursor is.


//Handle mouseover styles  
protected override void OnMouseEnterCell(RowColEventArgs e)  
{  
    base.OnMouseEnterCell(e);  
    if (!this.Rows[e.Row].IsNode)  
    {  
        this.SetCellStyle(e.Row, 0, this.Styles["Highlight"]);  
    }  
}  
protected override void OnMouseLeaveCell(RowColEventArgs e)  
{  
    base.OnMouseLeaveCell(e);  
    if (!this.Rows[e.Row].IsNode && e.Row != _selectedItemIndex)  
    {  
        this.SetCellStyle(e.Row, 0, _itemStyle);  
    }  
}  

You can customize the appearance of the Highlight CellStyle for every instance of FlexAccordion by setting it in the components constructor. Or you can leave it to set for each instance of FlexAccordion on each form it may display.

Scroll Buttons

By default, C1FlexGrid will give you a standard vertical scrollbar for when items overflow the space provided. We can turn off the scrollbar and add our own custom scroll buttons without much code. To turn off scrollbars for every instance of FlexAccordion you could set this in the constructor:


this.ScrollBars = ScrollBars.None;  

Or you can leave this to set on each form's instance of the control. These scroll buttons are optional and created at design-time. To add these buttons, open the FlexAccordion.cs file in Designer view and drag two Buttons from the Toolbox onto the designer surface. This is what you will see before you add the buttons. Name them btnBottomScroll and btnTopScroll. Add Click events to each button. In code we can implement a custom scrolling action by just setting the base control's TopRow property. It's little things like this which make C1FlexGrid so flexible!!!


private void btnTopScroll_Click(object sender, EventArgs e)  
{  
    //Scroll up  
    scroll(-1);  
}  

private void btnBottomScroll_Click(object sender, EventArgs e)  
{  
    //Scroll down  
    scroll(2);  
}  

//This function does a custom scroll by only counting visible rows  
private void scroll(int dir)  
{  
    int i = this.TopRow + dir;  
    while (!this.Rows[i].Visible)  
    {  
        i += dir;  
    }  
    this.TopRow = i;  
}  

The updateScrollButtons method is used internally to redraw the buttons after the control has been expanded, scrolled or resized. It's also used to position the buttons where we want them. Then we override a few base C1FlexGrid events to make calls to our updateScrollButtons method to ensure that these buttons are always updated as necessary.


//Scroll functions  
private void updateScrollButtons()  
{  
    if (this.BottomRow < LastVisibleRow())  
    {  
        btnBottomScroll.Visible = true;  
        btnBottomScroll.Location = new Point(this.Width - btnBottomScroll.Width, this.Height - btnBottomScroll.Height);  
    }  
    else  
    {  
        btnBottomScroll.Visible = false;  
    }  

    if (this.TopRow > 0)  
    {  
        btnTopScroll.Visible = true;  
        btnTopScroll.Location = new Point(this.Width - btnTopScroll.Width, 0);  
    }  
    else  
    {  
        btnTopScroll.Visible = false;  
    }  
}  

//This function determines the last visible row of the grid, which may be different than the last row.  
private int LastVisibleRow()  
{  
    int i = this.Rows.Count - 1;  
    while (!this.Rows[i].Visible)  
    {  
        i -= 1;  
    }  
    return i;  
}  

protected override void OnSizeChanged(EventArgs e)  
{  
    base.OnSizeChanged(e);  
    if(btnBottomScroll != null && this.Rows.Count > 0)  
        updateScrollButtons();  
}  

protected override void OnAfterScroll(RangeEventArgs e)  
{  
    base.OnAfterScroll(e);  
    updateScrollButtons();  
}  

The LastVisibleRow method is used to grab the last visible row as the name implies but not to be confused with the BottomRow which is the bottom-most row which can be seen by the user. A visible row in this case is one with its Visible property set to true. We need to know which is the last Visible row so we know if we need to show or hide the scroll down button.

Working with FlexAccordion

Now you can build the FlexAccordion control and use it on a form within your project. You can find your custom components in the Visual Studio toolbox after you've successfully built the project. Drag one onto the form. You can customize the Header and Item styles by setting them in code.


//Initialize Accordion Styles  
CellStyle csHeader = flexAccordion1.HeaderStyle;  
csHeader.ForeColor = Color.White;  
csHeader.TextEffect = TextEffectEnum.Raised;  
csHeader.BackColor = Color.Green;  

Set the AnimationDelay property to a higher value to have a slower animation (a good value to try is 20 milliseconds). To add items you must bind the control or add items through code. Remember that we built the accordion with the understanding that all header rows are to be marked by the IsNode property (IsNode = True == Header). Here is some sample code adding an item and setting it to be a header.


// add 1 header row  
flexAccordion1.AddItem("Header");  
flexAccordion1.Rows[0].IsNode = true;  
// add 1 item row  
flexAccordion1.AddItem("Item");  
// add 1 header row  
flexAccordion1.AddItem("Header");  
flexAccordion1.Rows[2].IsNode = true;  
// add 2 item rows  
flexAccordion1.AddItem("Item");  
flexAccordion1.AddItem("Item");  

The FlexAccordion Class

Here is the full implementation of the FlexAccordion control.


public partial class FlexAccordion : C1FlexGrid  
{  
    # region private members  

    private int _expandedIndex;  
    private int _selectedItemIndex;  
    private int _animationDelay;  
    private bool _canExpand;  
    private CellStyle _headerStyle;  
    private CellStyle _itemStyle;  

    # endregion  

    # region constructor  

    public FlexAccordion()  
    {  
        InitializeComponent();  
        _headerStyle = this.Styles.Add("Header");  
        _itemStyle = this.Styles.Add("Item");  
        _expandedIndex = -1;  
        _selectedItemIndex = -1;  
        _canExpand = true;  
        this.ScrollBars = ScrollBars.None;  
        this.SelectionMode = SelectionModeEnum.Row;  
    }  
    # endregion  

    # region public members  
    /// <summary>  
    /// Gets or sets the currently expanded row index  
    /// </summary>  
    public int ExpandedIndex  
    {  
        get { return _expandedIndex; }  
        set { _expandedIndex = value; }  
    }  

    /// <summary>  
    /// Gets or sets the currently selected item  
    /// </summary>  
    public int SelectedItemIndex  
    {  
        get { return _selectedItemIndex; }  
        set  
        {  
            if (_selectedItemIndex != -1)  
            {  
                this.SetCellStyle(\_selectedItemIndex, 0, \_itemStyle);  
            }  
            _selectedItemIndex = value;  
        }  
    }  

    /// <summary>  
    /// Gets or sets the animation delay in milliseconds.  
    /// </summary>  
    public int AnimationDelay  
    {  
        get { return _animationDelay; }  
        set { _animationDelay = value; }  
    }  

    /// <summary>  
    /// Gets or sets whether the control can expand/collapse  
    /// </summary>  
    public bool CanExpand  
    {  
        get { return _canExpand; }  
        set { _canExpand = value; }  
    }  

    /// <summary>  
    /// Gets or sets the CellStyle object associated with item headers.  
    /// </summary>  
    public CellStyle HeaderStyle  
    {  
        get { return _headerStyle; }  
        set { _headerStyle = value; }  
    }  

    /// <summary>  
    /// Gets or sets the CellStyle object associated with items.  
    /// </summary>  
    public CellStyle ItemStyle  
    {  
        get { return _itemStyle; }  
        set { _itemStyle = value; }  
    }  

    # endregion  

    # region implementation  

    //This method triggers the expand or collapse action when the control is clicked  
    protected override void OnMouseClick(MouseEventArgs e)  
    {  
        base.OnMouseClick(e);  
        if (this.MouseRow != -1)  
        {  
            Row hitRow = this.Rows[this.MouseRow];  
            if (hitRow.Index == _expandedIndex)  
            {  
                //Do nothing  
            }  
            else if (hitRow.IsNode && this._canExpand)  
            {  
                //expand/collapse items  
                Expand(hitRow.Index);  
                _expandedIndex = hitRow.Index;  
            }  
            else if (!hitRow.IsNode)  
            {  
                this.SelectedItemIndex = hitRow.Index;  
            }  
        }  
        this._canExpand = true;  
    }  

    //This method collapses the currently expanded items (if any) and expands at the passed row index.  
    protected void Expand(int expandRow)  
    {  
        int collapseRow = _expandedIndex + 1;  
        expandRow += 1;  
        bool done = false;  
        bool collapsing = false;  
        bool expanding = false;  
        while (!done)  
        {  
            //Update the current collapsing row index  
            if (collapseRow < this.Rows.Count)  
                collapsing = !this.Rows[collapseRow].IsNode;  
            else collapsing = false;  

            //Update the current expanding row index  
            if (expandRow < this.Rows.Count)  
                expanding = !this.Rows[expandRow].IsNode;  
            else expanding = false;  

            if (expanding || collapsing)  
            {  
               //Collapse and expand 1 row each at a time  
                if (collapsing)  
                    this.Rows[collapseRow].Visible = !this.Rows[collapseRow].Visible;  
                if (expanding)  
                    this.Rows[expandRow].Visible = !this.Rows[expandRow].Visible;  

                //Redraw FlexGrid to reflect new visible rows and sleep for animation effect  
                this.Refresh();  
                System.Threading.Thread.Sleep(_animationDelay);  
            }  
            else  
            {  
                //No more rows to expand or collapse  
                done = true;  
            }  

            //if we could expand/collapse continue to next row  
            if (collapsing) collapseRow += 1;  
            if (expanding) expandRow += 1;  
        }  
        updateScrollButtons();  
    }  

    //Handle mouseover styles  
    protected override void OnMouseEnterCell(RowColEventArgs e)  
    {  
        base.OnMouseEnterCell(e);  
        if (!this.Rows[e.Row].IsNode)  
        {  
            this.SetCellStyle(e.Row, 0, this.Styles["Highlight"]);  
        }  
    }  
    protected override void OnMouseLeaveCell(RowColEventArgs e)  
    {  
        base.OnMouseLeaveCell(e);  
        if (!this.Rows[e.Row].IsNode && e.Row != _selectedItemIndex)  
        {  
            this.SetCellStyle(e.Row, 0, _itemStyle);  
        }  
    }  

    //Scroll functions  
    private void updateScrollButtons()  
    {  
        if (this.BottomRow < LastVisibleRow())  
        {  
            btnBottomScroll.Visible = true;  
            btnBottomScroll.Location = new Point(this.Width - btnBottomScroll.Width, this.Height - btnBottomScroll.Height);  
        }  
        else  
        {  
            btnBottomScroll.Visible = false;  
        }  

        if (this.TopRow > 0)  
        {  
            btnTopScroll.Visible = true;  
            btnTopScroll.Location = new Point(this.Width - btnTopScroll.Width, 0);  
        }  
        else  
        {  
            btnTopScroll.Visible = false;  
        }  
    }  

    //This function determines the last visible row of the grid, which may be different than the last row.  
    private int LastVisibleRow()  
    {  
        int i = this.Rows.Count - 1;  
        while (!this.Rows[i].Visible)  
        {  
            i -= 1;  
        }  
        return i;  
    }  

    //This function does a custom scroll by only counting visible rows  
    private void scroll(int dir)  
    {  
        int i = this.TopRow + dir;  
        while (!this.Rows[i].Visible)  
        {  
            i += dir;  
        }  
        this.TopRow = i;  
    }  

    protected override void OnSizeChanged(EventArgs e)  
    {  
        base.OnSizeChanged(e);  
        if(btnBottomScroll != null && this.Rows.Count > 0)  
            updateScrollButtons();  
    }  

    protected override void OnAfterScroll(RangeEventArgs e)  
    {  
        base.OnAfterScroll(e);  
        updateScrollButtons();  
    }  

    private void btnTopScroll_Click(object sender, EventArgs e)  
    {  
        //Scroll up  
        scroll(-1);  
    }  

    private void btnBottomScroll_Click(object sender, EventArgs e)  
    {  
        //Scroll down  
        scroll(2);  
    }  

    # endregion  

}  

Conclusion

This post has demonstrated one way to extend the C1FlexGrid to provide a custom control to meet the exact needs of one application. Hopefully the same code may be useful to someone else out there as it shows some very basic yet useful ways to extend a ComponentOne control. You can download the full sample below. Download Sample

ComponentOne Product Manager Greg Lutz

Greg Lutz

comments powered by Disqus