.Net DataGridview: Change Formatting of the Active Column Header to Track User Location

Posted on October 9 2012 10:34 PM by John Atten in C#, Hacks, CodeProject, Controls   ||   Comments (0)

I bumped into a question on StackOverflow this evening that I felt might make a short post. Accompanying code is available at my  

The guy who posted observed that the standard .net DataGridview control provides a helpful little glyph next to the row which contains the active cell:

DataGridView-Standard-Active-Row-Indicator

The original poster of the question was wondering how he might include a similar glyph to indicate the active column as well. My problem with that is that there already exists an option for an arrow-like glyph in a column header. Unfortunately, THAT glyph, by convention, tends to mean “Click  here to sort on this column.”

That does NOT mean that the OP was off-base, though. I can think of many cases where it would be handy to have some sort of reference to the active column in addition to the active row.

Emphasize the Active Column with the DataGridViewColumn.HeaderCell.Style property

One way to approach this is to simply cause the text in the header cell to be bold when the user navigates to a cell within that column. We can create a class which inherits from DataGridView, and take advantage of the CellEnter Event to cause this to happen:

DataGridView-Bold-Text-Active-Row-Indicator

In the following code, we have a member variable which holds a reference to the last active column. In our constructor, we initialize this column object so that when the control is instantiated, the reference is not null.

We also add an event handler to catch the CellEnter event locally. When this event fires, the handler (dgvControl_CellEnter) catches it, and makes a call to our final method, OnColumnFocus. This method accepts a column index as a parameter, and uses the index to identify the new active column. From there, we can use the HeaderCell.Style property to set the font to “bold” for this particular column.

In our constructor, note that we have to make an initial call to the OnColumnFocus method, so that the default starting column will be highlighted when the control is displayed at first. However, we have to check to see if there are actually any columns present first. This is because the Visual Studio Designer needs to be able to draw the empty control when we first place it on a form.

DataGridView: Cause the Active Column Header to Display Bold Text
class dgvControl : DataGridView
{
    // hold a reference to the last active column:
    private DataGridViewColumn _currentColumn;
 
    public dgvControl() : base()
    {
 
        // Add a handler for the cell enter event:
        this.CellEnter += new DataGridViewCellEventHandler(dgvControl_CellEnter);

 
        // When the Control is initialized, instantiate the placeholder
        // variable as a new object:
        _currentColumn = new DataGridViewColumn();

 
        // In case there are no columns added (for the designer):
        if (this.Columns.Count > 0)
        {
            this.OnColumnFocus(0);
        }
    }
    
 
    void dgvControl_CellEnter(object sender, DataGridViewCellEventArgs e)
    {
        this.OnColumnFocus(e.ColumnIndex);        
    }

 
    void OnColumnFocus(int ColumnIndex)
    {
 
        // If the new cell is in the same column, do nothing:
        if (ColumnIndex != _currentColumn.Index)
        {
 
            // Set up a custom font to represent the current column:
            Font selectedFont = new Font(this.Font, FontStyle.Bold);

 
            // Grab a reference to the current column:
            var newColumn = this.Columns[ColumnIndex];

 
            // Change the font to indicate status:
            newColumn.HeaderCell.Style.Font = selectedFont;

 
            // Set the font of the previous column back to normal:
            _currentColumn.HeaderCell.Style.Font = this.Font;

 
            // Set the current column placeholder to refer to the new column:
            _currentColumn = newColumn;
        }
    }
}

 

What if I want More?

What if we want more than just bold text in the active header? Well, things get trickier. Manipulating the other properties of the HeaderCell Style require setting EnableHeaderVisualStyles to false. This has the unfortunate side effect of flattening out the styling which some from the Windows 7 GUI styles. The slight gradient and color scheme are replaced by a much flatter header. While we could work around this by overriding the OnPaint method (at least to a degree) and implementing our own painting scheme, the impact of the effect is not too disturbing.

For example, we could decide that in addition to bolding the text in the header, we will set the BackColor to a slightly darker gray:

DataGridView-Bold-and-Gray-Active-Row-Indicator

To do this, we need only add three lines of code. First off, in our constructor, we set the EnableHeaderVisualStyles property to false. Next, in our OnColumnFocus method, we set the Style.BackColor property of the new active column to a darker shade of gray, and restore the previous active column to the default (empty) backcolor:

DataGridView: Cause the Active Column Header to Display Bold Text with a Darker Back Color:
class dgvControl : DataGridView
{
    // hold a reference to the last active column:
    private DataGridViewColumn _currentColumn;
    public dgvControl() : base()
    {
        this.EnableHeadersVisualStyles = false;

 
        // Add a handler for the cell enter event:
        this.CellEnter += new DataGridViewCellEventHandler(dgvControl_CellEnter);

 
        // When the Control is initialized, instantiate the placeholder
        // variable as a new object:
        _currentColumn = new DataGridViewColumn();

 
        // In case there are no columns added (for the designer):
        if (this.Columns.Count > 0)
        {
            this.OnColumnFocus(0);
        }
    }

 
    void dgvControl_CellEnter(object sender, DataGridViewCellEventArgs e)
    {
        this.OnColumnFocus(e.ColumnIndex);        
    }

 
    void OnColumnFocus(int ColumnIndex)
    {
        // If the new cell is in the same column, do nothing:
        if (ColumnIndex != _currentColumn.Index)
        {
            // Set up a custom font to represent the current column:
            Font selectedFont = new Font(this.Font, FontStyle.Bold);

 
            // Grab a reference to the current column:
            var newColumn = this.Columns[ColumnIndex];

 
            // Change the font to indicate status:
            newColumn.HeaderCell.Style.Font = selectedFont;

 
            // Change the color to a slightly darker shade of gray:
            newColumn.HeaderCell.Style.BackColor = Color.LightGray;

 
            // Set the font of the previous column back to normal:
            _currentColumn.HeaderCell.Style.Font = this.Font;

 
            // Change the color of the previous column back to the default:
            _currentColumn.HeaderCell.Style.BackColor = Color.Empty;

 
            // Set the current column placeholder to refer to the new column:
            _currentColumn = newColumn;
        }
    }
}

 

There are other options you might explore. In this post, we walked through some very basic ways to provide visual feedback to the user about their location within the DataGridView control.

The source code for this post is available at my

 

Posted on October 9 2012 10:34 PM by John Atten     

Comments (0)

Visual Studio: Use Conditional Compilation to Control Runtime Settings for Different Deployment Scenarios

Posted on August 18 2012 07:11 PM by jatten in C#, CodeProject   ||   Comments (0)

Compile for Distinctly Different Environment Configurations

NOTE: Source code for the example project in this article can be found at my

I already understand most of this. Take me straight to the code!

I am currently working on a boring, enterprise-ey database application. You know the type – We needed it yesterday, it was built on the fly with minimal up-front design time, and pressure from on high to deliver SOMETHING right away, even if it sucks, they say, it will be better than what we have now. You KNOW, if you have read this far, that you have worked on projects such as I describe here as well. Probably many. You just don’t want to admit it. It’s OK. Admit it, and move forward. You’ll feel better.

Yes, yes, I know. This is where I am supposed to set management straight on the error of their ways, sell them on the idea of investing in an iterative design cycle, Agile/Test-Driven development methodology, the works.

Not gonna happen here, unfortunately.

SO. As the code progressed and bits and pieces of this monstrosity entered live testing and production, I am having to continue work on the rest of the application, adding features, fixing bugs, etc. Unfortunately, this application must be able to run in a couple of different scenarios:

  • On my Dev machine at home (locally).
  • On my Dev machine at work (locally).
  • On a test server at work (Database and file persistence via LAN)
  • On the production server at work (LAN, again)
  • On a remote laptop with no LAN connection – files and data persisted locally until home

The database connection information is different for each of these scenarios. Also, this application persists copies of photographs and other files and stashes the directory information in a database record. The actual file location is different in each scenario as well (and hidden from the user). File access is performed through the application. In all cases, the settings required to achieve all this should NOT be available to the user (although ultimately, some level of admin functionality might be incorporated. But not right now . . . ).

My Solution – Set Application Settings at Compile Time

I will say right here that this is most likely a hack solution to what is ultimately an application design issue. I have heard it said that the use of conditional compilation is often a strong code “smell” indicating a potential area for redesign. However, in my case, I needed a fast answer.

I have identified five possible environments within which the application must run, each of which require the same code base, but different application settings. For this example, I will leave the default Debug and Release configurations as they are, and simply create two additional configurations, LIVE and TEST. Once I have done this, I can define Conditional Compilation symbols, and load different application settings depending upon which build configuration is running.

Set Up Build Configurations Using Build Configuration Manager

First, I will use the Build Configuration Manager within Visual Studio to create my Build Configurations. Configuration Manager allows you to create, customize, and manage build settings for all projects within the current solution. For this example, there is only a single project. Open Build/Configuration Manager . . .

BuildConfigMenu

From the drop-down menu in the Configuration Manager, select <New>:

Configuration Manager-New

I create a new Build Configuration named LIVE, and copy settings from the default Release configuration:

Configuration Manager-Create-Live

I then repeat these steps to create a TEST configuration (and as many others as I might need).

Since I am creating a simple example project for this post, there is only one project visible in the Configuration Manager list. However, if there were additional projects within this same solution, I would be able to assign different configurations to each, an indicate specific projects within the solution to build or not.

Define Conditional Compilation Symbols for Each Build Configuration

Now we need to define Conditional Compilation symbols for each build configuration. Open the Project Properties window from the solution explorer, and select the “Build” tab. in the dropdown list of available configurations, you will now see the default Debug and Release options, as well as your newly defined LIVE and TEST configurations. First we will select the LIVE configuration, and add a Conditional Compilation symbol in the space provided. Because I like to keep things simple, we’ll use “LIVE” as our symbol:

BuildConfigMenu-Live-markup

Then select the TEST configuration from the drop-down menu, and repeat the same steps, typing “TEST” into the Conditional compilation symbols space.

We have now defined a symbol for each of our two build options. Now let’s look at how we can use these in our application to load build specific application settings at compile time.

Define Version-Specific Resources, and General Application Settings

Ok. Here’s the meat and potatoes of the whole thing. First, we need to define our application settings. One of the reasons I chose to take this approach is that application settings can be accessed and modified without having to recompile the application. Same with resource files. For ease of making changes to a compiled application which is being used for production, I am going to define a couple of output file paths, and save them as resource files. I will also create a string message for each version, which we will use for illustrative purposes in this example.

First, create text files with the following text, either in your preferred text editor, or using the “Add New Text File” option in the Resource Manager:

File Name: Text Content:
LiveOutputFolderPath.txt
C:\LiveOutputFolder\
TestOutputFolderPath.txt
C:\TestOutputFolder\
LiveVersionMessage.txt
This is the LIVE version of the Application
TestVersionMessage.txt
This is the TEST version of the application

 

Add the resource files to your project in the Resource Designer:

AddResourceNewTextFile

Next, define version-agnostic Application Settings using the Settings Designer like so (leave the values empty – these will be set at compile-time). Make sure to set the scope for each setting to “Application” and not “User”. Name one setting “BuildVersionMessage” and the other “OutputFolderPath”:

AddApplicationSettings

Ok. See that “View Code” menu item near the top of the Settings Designer window? Click on that. You should see a code file open to display a partial class file for the Settings Designer. This is where we get down to business.

Code: Setting Application Settings Values at Compile Time

When the Settings.cs file first opens, you will see some VS-generated code in the file. I deleted all that – we’re not using it here. Replace it with the following code (the complete source code for this example project is available from my Github repo):

8/19/2012 8:00 AM - UPDATE: I added similar code for the default “Debug” and “Release” configurations to the example project in my Github repo. The only difference is that these two methods explicitly specify Environment.CurrentDirectory as the root directory instead of using a resource file to grab the directory path. If you run the following code as-is under the Debug or Release build configurations, things won’t work the way you expect!

 1: using System.Diagnostics;
 2:  
 3: namespace WindowsFormsApplication1.Properties 
 4: {
 5:  
 6:     internal sealed partial class Settings 
 7:     {
 8:         public Settings() 
 9:         {
 10:             // Each method corresponds to a build version. We call both methods, because
 11:             // the conditional compilation will only compile the one indicated:
 12:             this.SetLiveApplicationSettings();
 13:             this.SetTestApplicationSettings();
 14:         }
 15:  
 16:         [Conditional("LIVE")]
 17:         private void SetLiveApplicationSettings()
 18:         {
 19:             // Set the two Settings values to use the resource files designated
 20:             // for the LIVE version of the app:
 21:             this["BuildVersionMessage"] = Resources.LiveVersionMessage;
 22:             this["OutputFolderPath"] = Resources.LiveOutputFolderPath;
 23:         }
 24:  
 25:  
 26:         [Conditional("TEST")]
 27:         private void SetTestApplicationSettings()
 28:         {
 29:             // Set the two Settings values to use the resource files designated
 30:             // for the TEST version of the app:
 31:             this["BuildVersionMessage"] = Resources.TestVersionMessage;
 32:             this["OutputFolderPath"] = Resources.TestOutputFolderPath;
 33:         }
 34:     }
 35: }

 

Note the use of the Conditional Attribute For the SetLiveApplicationSettings and SetTestApplicationSettings methods. Because we have used this attribute, and made reference to our version-specific Compilation Symbols for each, the code for either of these methods will only be compiled for that specific version identified in the Conditional Attribute. We call both methods from the Settings() constructor, so that whichever version is compiled will be called during initialization.

What is important here is to get the string representations for each of the Compilation Symbols correct, as well as the Setting names, used as keys to access the specific Settings properties. Misspellings here will cause the code to fail.

Demonstration of Conditional Compilation

To demonstrate how this works, create a simple Form in the designer, like this:

ExampleFormDesigner

Name the Label “lblHeader” and the Button “btnCopyFile” (yes, I still go a little Hungarian with my control names. Makes them easier to find with Intellisense, dammit . . .).

Now, use this code in the form (the complete source code for this article is available at my GitHub repo):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.IO;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.btnCopyFile.Click += new System.EventHandler(this.btnCopyFile_Click);

        }


        private void Form1_Load(object sender, EventArgs e)
        {
            // The BuildVersionMessage setting will be set at compile time:
            this.lblHeader.Text = Properties.Settings.Default.BuildVersionMessage;
        }


        private void btnCopyFile_Click(object sender, EventArgs e)
        {
            this.CopyFile();
        }


        private void CopyFile()
        {
            // The correct output file path will be defined at compile time, 
            // and made available through the settings file:
            string outputDirectory = Properties.Settings.Default.OutputFolderPath;

            // Make sure the directory exists:
            if (!Directory.Exists(outputDirectory))
            {
                Directory.CreateDirectory(outputDirectory);
            }

            using (var fileDialog = new OpenFileDialog())
            {
                fileDialog.InitialDirectory = 
                    Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                fileDialog.Multiselect = false;

                if (fileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    string oldFullFileName = fileDialog.FileName;
                    string safeFileName = fileDialog.SafeFileName;

                    // Create a new File Name using the output directory 
                    // defined at compile time:
                    string newFullFileName = outputDirectory + safeFileName;

                    // Check to see if a file with the same name already exists:
                    if (File.Exists(newFullFileName))
                    {
                        // File.Copy won't let us overwrite. Since the user has no knowledge 
                        // of this directory, we just delete the old, and save the new:
                        File.Delete(newFullFileName);
                    }

                    // Copy the file into our secret hidden directory:
                    File.Copy(oldFullFileName, newFullFileName);

                    // ...
                    // Add code here to persist the file path and other information 
                    // to the data store for access within the application . . .
                    // ...
                }              
            }
        }
    }
}
 

 

Now, build and run the application under each of the LIVE and TEST Build Configurations. When we set the build configuration to TEST and run, the form opens as follows:

ExampleFormRunTESTIf we press the Copy File button, select a file, and hit OK, the file will be saved in the directory specified in the file we named TestOutputDirectory.txt. We can go to that directory in Windows Explorer and sure enough, there is a copy of the original file. Likewise, the text displayed in the form label is the text we saved in the TestVersionMessage.txt file.

ExampleFormRunLIVE

 

When switch to the LIVE build configuration, we see similar results. What’s cool here is that if we needed to, we could open the text files which serve as out string resources and modify the directory paths or message text, save, and these changes would be incorporated into our running application without recompiling. While this is not a new thing, we still maintain the integrity of our build-specific settings – the LIVE version will load those settings specific to that build, and likewise the TEST version.

Wrapping up

While this example has been simplified for the purpose of explaining what I did, the reality is that I was able to address the varying database connection and output folder requirements of my distinct application versions and tailor each to the environment in which it would run. When I compile and run the application on my home machine, I simply select the HOME build configuration. No messing about setting database connection settings or file directories. Likewise when I have added some new features to the production build. I simply select the PRODUCTION build configuration, and the network serve database connection and LAN network shares are all appropriately set so that code which requires those settings simply works.

Useful Links:

More information on Conditional Compilation and Conditional Methods in C#/VS:

Source code for example project:

 

Posted on August 18 2012 07:11 PM by jatten     

Comments (0)

Extending C# Listview with Collapsible Groups (Part I)

Posted on May 9 2012 05:11 AM by jatten in C#, CodeProject, Hacks   ||   Comments (0)

NOTE: This post is kinda long. However, most of the length is a result of code postings (even after removing some extra stuff). Bear with me!

I’ve been deep in a project for work for the past two months. Sadly, it is nothing sexy, no exciting bleeding-edge technology, just another enterprise database, using the very mature and slightly dull Winforms library in the .NET platform.

However, I did stumble across an interesting project requirement for what is essentially an expandable group of the venerable Listview control, what one might get if one combined a Listview with a Treeview, or if the “groups” built into the Listview control could be expanded/collapsed, right-clicked, etc.

Fig. 1 – Single Group Expanded:

Gl-Demo-1_thumb6

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Fig. 2 – Multiple Groups Expanded (Note Scrollbar on Container Control):

Gl-Demo-3-Three-Expanded_thumb2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Fig. 3 – On Widen Column (Note Scrollbar on Specific ListGroup):

Gl-Demo-4-Widen-Column_thumb3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

For those who are about to point out that such a control exists in the ObjectListView, I am aware. However, I needed to do this using standard .NET Libraries. I am also aware that the standard Listview Group can be forced to expand/Collapse, but I needed this to be a faster solution. Also, causing the standard Listview Group to expand/collapse looked to rely on a whole lot of Windows API calls, and I am not so fluent in that arcane area.

My solution was to extend the Listview control, and then assemble multiple Listview controls within a FlowLayoutPanel control. The ColumnHeaders of each Listview double as the “Group.” Clicking on the left-most column toggles group expansion/collapse. The expanded/collapsed state is indicated by a solid arrow image at the left end of the column. In cases where the group is empty (containing no ListViewItems, and looking for all the world like a collapsed group) the arrow image is empty.

For the purpose of clarity, I refer to the aggregate control as a “GroupedListControl",” and each contained ListView as a “ListGroup".” There is a wide potential for improvement in this naming scheme, I am sure. For the purpose of this narrative, assume the following:

  1. A GroupedListControl Contains one or more ListGroups, which contain ListViewItems.
  2. The GroupedListControl is a container which inherits from FlowLoyoutPanel.
  3. The ListGroup is a container which inherits from the Winforms ListView.

Extending Native Listview Behaviors with Inner Classes

First, I needed to extend some of the basic behaviors of the stock .net Listview control. For example, in order to treat the leftmost column (Column [0]) differently, and to monitor the addition and removal of columns for the purpose of controlling and adjusting for the appearance of scrollbars, I needed an event to be fired when columns are added and removed. I did a little digging on the interwebs, and found my solution in a post on the Code Project site. I was able to take the core concept there and achieve what I needed:

A basic list of desired behaviors for each ListGroup include:

  1. Clicking on the leftmost ColumnHeader of a ListGroup should toggle the expansion/collapse of the group.
  2. When the first column is added, the Expanded/Collapsed indicator arrows should be added to the leftmost ColumnHeader.
  3. If the total width of the columns in any given ListGroup exceed the width of the client area of the containing GroupedListControl, the ListGroup should show a horizontal scrollbar.
  4. The height of each ListGroup should be adjusted such that all Listview items contained should be displayed in the expanded state, up to an optional maximum height determined either at design-time or runtime. If the number of ListViewItems contained exceeds this maximum, the Individual ListGroup Vertical Scrollbar will appear.
  5. Any time the Horizontal Scrollbar is displayed for a specific ListGroup, the client area for that ListGroup should be adjusted such that the Horizontal scrollbar does not partially obscure the last displayed ListViewItem.
  6. Detect Mouse Right-Clicks on specific ListView ColumnHeaders and allow for a context menu specific to right-clicking on ColumnHeaders vs. ListView Items.

This covers some minimums for the control to function properly. Let’s look at what a basic code skeleton would look like here. We will fill in some of the empty code stubs a little later in the post.

Complete source code for this project is available at GroupedListControl Project Source Code On GitHub. The source includes additional code not covered here. We will discuss some of it in upcoming posts.

First, I defined some Custom Event Argument Classes which will be utilized within the control. While they are functionally similar to some existing ListView Event Argument classes, I wanted to maintain clear naming to the degree possible. These are required by subsequent code.

Note that all examples require the following references at the head of your code file:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.Layout;
using System.Runtime.InteropServices;
using System.ComponentModel;

Custom Event Arguments:

    public class ListGroupColumnEventArgs : EventArgs
    {
        public ListGroupColumnEventArgs(int ColumnIndex)
        {
            this.ColumnIndex = ColumnIndex;
        }
        
        public ListGroupColumnEventArgs(int[] ColumnIndexes)
        {
            this.ColumnIndexes = ColumnIndexes;
        }

        public int ColumnIndex { get; set; }
        public int[] ColumnIndexes { get; set; }
    }



    public class ListGroupItemEventArgs : EventArgs
    {
        public ListGroupItemEventArgs(ListViewItem Item)
        {
            this.Item = Item;
        }

        public ListGroupItemEventArgs(ListViewItem[] Items)
        {
            this.Items = Items;
        }

        public ListViewItem Item { get; set; }
        public ListViewItem[] Items { get; set; }
    }
 

Now, the ListGroup Class itself. I have cut out some additional methods for the sake of brevity here (even at that, it’s a long chunk of code . . .), and left some code stubs to be filled in shortly. This is just to give an idea of the most basic class structure, and core functionality requirements. As it is right here, this code is not functional.

The ListGroup Class – Essentials and Code Stubs

    public class ListGroup : ListView
    {

        // DELEGATES AND ASSOCIATED EVENTS:

        // NOTE: The events and delegates related to Column and Item Addition/Removal are
        // called by the inner classes ListGroupColumnCollection and LIstGroupItemCollection. 
        // The proper function of the control depends upon these. 

        // Delegates to handle Column addition and removal Events:
        public delegate void ColumnAddedHandler(object sender, ListGroupColumnEventArgs e);
        public delegate void ColumnRemovedHandler(object sender, ListGroupColumnEventArgs e);

        // Events related to Column Addition and removal:
        public event ColumnAddedHandler ColumnAdded;
        public event ColumnRemovedHandler ColumnRemoved;

        // Delegates to handle Item Addition and Removal events:
        public delegate void ItemAddedHandler(object sender, ListGroupItemEventArgs e);
        public delegate void ItemRemovedHandler(object sender, ListGroupItemEventArgs e);

        // Events related to Item Addition and Removal:
        public event ItemAddedHandler ItemAdded;
        public event ItemRemovedHandler ItemRemoved;

        // Delegate and related events to process Group Expansion and Collapse:
        public delegate void GroupExpansionHandler(object sender, EventArgs e);
        public event GroupExpansionHandler GroupExpanded;
        public event GroupExpansionHandler GroupCollapsed;

        // Delegate and related Events to handle Listview Header Right Clicks:
        public delegate void ColumnRightClickHandler(object sender, ColumnClickEventArgs e);
        public event ColumnRightClickHandler ColumnRightClick;


        // PRIVATE INSTANCES OF INNER CLASSES

        // Instances of our inner classes, declared as private members so 
        // that our public Property accessors can be Read-only, yet allow 
        // direct manipulation from within the control instance
        private ListGroupItemCollection _Items;
        private ListGroupColumnCollection _Columns;


        /// <summary>
        /// Constructor Stub
        /// </summary>
        public ListGroup() : base()
        {
            // Implementation Code . . .
        }


        // INNER CLASS INSTANCE ACCESSORS:

        /// <summary>
        /// Hides the ListViewItemCollection internal to the base class, 
        /// and uses the new implementation defined as an inner class, 
        /// which sources an "ItemAdded" Event:
        /// </summary>
        public new ListGroupItemCollection Items
        {
            get { return _Items; }
        }


        /// <summary>
        /// Hides the ColumnHeaderCollection internal to the base class, 
        /// and uses the new implementation defined as an inner class, 
        /// which sources a "ColumnAdded" Event:
        /// </summary>
        public new ListGroupColumnCollection Columns
        {
            get { return _Columns; }
        }


        // INNER CLASS DEFINITIONS:

        /// <summary>
        /// Inner class used to hide the ListViewColumnHeaderCollection 
        /// built in to the ListView Control and provide required extended behaviors.
        /// </summary>
        public class ListGroupColumnCollection : ListView.ColumnHeaderCollection
        {
            // Implementation Code Here . . .
        }


        /// <summary>
        /// Inner class defined for ListGroup to contain List items. 
        /// Derived from ListViewItemCollection and modified to source events 
        /// indicating item addition and removal. 
        /// </summary>
        public class ListGroupItemCollection : ListView.ListViewItemCollection
        {
            // Implementation Code Here . . .
        }


        // ITEM ADDITION AND REMOVAL:


        /// <summary>
        /// Raises the ItemAdded Event when a new item is
        /// added to the items collection.
        /// </summary>
        private void OnItemAdded(ListViewItem Item)
        {
            // Code to set the size of the control to display all the items 
            // so far . . .

            // Raise the ItemAddded event to any subscribers:
            if (ItemAdded != null)
                this.ItemAdded(this, new ListGroupItemEventArgs(Item));
        }


        /// <summary>
        /// Raises the ItemRemoved Event when an item is
        /// removed from the items collection.
        /// </summary>
        private void OnItemRemoved(ListViewItem Item)
        {
            // Code to set the size of the control to display all the items 
            // remaining after the current one is removed . . .

            // Raise the ItemRemoved event to any subscribers:
            if (ItemRemoved != null)
                this.ItemRemoved(this, new ListGroupItemEventArgs(Item));
        }


        // COLUMN ADDITION AND REMOVAL:

        /// <summary>
        /// Raises the ColumnAdded Event when a new column is
        /// added to the ColumnHeaders collection.
        /// </summary>
        private void OnColumnAdded(int ColumnIndex)
        {
            // Code to manage column additions. The first column added
            // needs to have the Expand/Collapse/Empty image added . . .

            // Raise the ColumnAdded event to any subscribers:
            if (this.ColumnAdded != null)
                this.ColumnAdded(this, new ListGroupColumnEventArgs(ColumnIndex));
        }


        /// <summary>
        /// Raises the ColumnRemoved Event when a column is
        /// remmoved from the ColumnHeaders collection.
        /// </summary>
        private void OnColumnRemoved(int ColumnIndex)
        {
            // Code to manage column removals . . . 

            // Raise the ColumnRemoved event to any subscribers:
            if (this.ColumnRemoved != null)
                this.ColumnRemoved(this, new ListGroupColumnEventArgs(ColumnIndex));
        }


        // USER ACTIONS:

        /// <summary>
        /// Handles the ListGroup ColumnClick Event sourced by the base.
        /// </summary>
        void ListGroup_ColumnClick(object sender, ColumnClickEventArgs e)
        {
            // Code to manage ColumnHeader Clicks. If the Event is sourced from 
            // the first Column (Column[0], toggle expansion/collapse of the list . . .
        }


        // CONTROL BEHAVIORS:

        /// <summary>
        /// Causes the list of items to expand, showing all items in the 
        /// Items collection.
        /// </summary>
        public void Expand()
        {
            // Do stuff to make the control expand . . .

            // Raise the Expanded event to notify client code that the ListGroup has expanded:
            if (this.GroupExpanded != null)
                this.GroupExpanded(this, new EventArgs());
        }


        /// <summary>
        /// Causes the Displayed list of items to collapse, hiding all items and 
        /// displaying only the columnheaders. 
        /// </summary>
        public void Collapse()
        {
            // Do stuff to make the control collapse

            // Raise the Collapsed event to notify client code that the ListGroup has expanded:
            if (this.GroupCollapsed != null)
                this.GroupCollapsed(this, new EventArgs());
        }
    }
 

Now, pay close attention to the code stubs where we define our Inner Classes, ListGroupColumnCollection and ListGroupItemCollection. Note that each derives from its respective counterpart in the Winforms ListView Control. This is where we achieve a number of our custom event sourcing behaviors. Once again, I have simplified the class definitions here, leaving out various overloads of the core methods required (for example, there are multiple ways to “Add” an item to either collection – here I only cover the most basic. The rest are defined in the GroupedListControl Project Source Code, obtainable from my GitHub Repo).

Notice how the code within each inner class causes events to be raised within the containing ListGroup class? This provides our event sourcing for the addition/removal of Columns and ListViewItems, since these events are not defined in the base Winforms ListView Class. We need them in order to affect proper control expansion and collapse in response to additions and removals

NOTE: Core Concepts for the use of Inner Classes in this manner was adapted from THIS ARTICLE by Simon Segal on Code Project.

The following code replaces the code stub for the ListGroupColumnCollection Class in our ListGroup class definition:

The ListGroupColumnCollection Class

    /// <summary>
    /// Inner class defined for ListGroup to contain ColumnHeaders. 
    /// Derived from ListView.ColumnHeaderCollection and modified to 
    /// source events indicating column addition and removal. 
    /// </summary>
    public class ListGroupColumnCollection : ListView.ColumnHeaderCollection
    {
        // Reference to the containing ListGroup Control
        private ListGroup _Owner;

        public ListGroupColumnCollection(ListGroup Owner) : base(Owner)
        {
            _Owner = Owner;
        }


        /// <summary>
        /// Gets the total width of all columns currently defined in the control.
        /// </summary>
        public int TotalColumnWidths
        {
            get
            {
                int totalColumnWidths = 0;
                foreach(ColumnHeader clm in this)
                    totalColumnWidths = totalColumnWidths + clm.Width;
                return totalColumnWidths;
            }
        }


        /// <summary>
        /// Adds a column to the current collection and raises 
        /// the OnColumnAddedEvent on the parent control.
        /// </summary>
        public new ColumnHeader Add(string text, int width, HorizontalAlignment textAlign)
        {
            ColumnHeader clm = base.Add(text, width, textAlign);
            _Owner.OnColumnAdded(clm.Index);
            return clm;
        }


        /// <summary>
        /// Removes a column from the current collection and 
        /// raises the OnColumnRemoved Event on the parent control.
        /// </summary>
        public new void Remove(ColumnHeader column)
        {
            int index = column.Index;
            base.Remove(column);
            _Owner.OnColumnRemoved(index);
        }


        public new void Clear()
        {
            base.Clear();
        }

    } // ListGroupColumnCollection
 

The following code replaces the empty stub for the ListGroupItemCollection class in our original ListGroup class definition:

The ListGroupItemCollection Class

    /// <summary>
    /// Inner class defined for ListGroup to contain List items. Derived from ListViewItemCollection
    /// and modified to source events indicating item addition and removal. 
    /// </summary>
    public class ListGroupItemCollection : System.Windows.Forms.ListView.ListViewItemCollection
    {
        private ListGroup _Owner;
        public ListGroupItemCollection(ListGroup Owner) : base(Owner)
        {
            _Owner = Owner;
        }


        /// <summary>
        /// New implementation of Add method hides Add method defined on base class
        /// and causes an event to be sourced informing the parent about item additions.
        /// </summary>
        public new ListViewItem Add(string text)
        {
            ListViewItem item = base.Add(text);
            _Owner.OnItemAdded(item);
            return item;
        }


        /// <summary>
        /// New implementation of Remove method hides Remove method defined on base class
        /// and causes an event to be sourced informing the parent about item Removals.
        /// </summary>
        public new void Remove(ListViewItem Item)
        {
            base.Remove(Item);
            _Owner.OnItemRemoved(Item);
        }

    } // ListGroupItemCollection
 

 

Filling it all in

The most basic extension of the existing capability of the standard ListView control we are seeking is the ability for each ListGroup to “expand” and/or “collapse” in response to certain user inputs. We will want to define a singular method call which causes the desired action to be performed, so we’ll add a method to our ListGroup class named SetControlHeight. This method evaluates the current state of the control (collapsed or expanded), and calls the appropriate method to toggle that state to the opposite.

The minimum collapsed height of each ListGroup control should be just enough to display the Column Headers. The Expanded height may be unlimited, or may be constrained by setting the MaximumHeight property. In either case, however, the control height should include enough space to display the Column Headers, and an even number of ListViewItems such that the last item is fully visible in the display.

The collapsed state is recognizable if the height of the control is equal to the height of the Column Headers and the item count is greater than 0. Otherwise, the control must be in an expanded state. I had to arbitrarily set the header height as a constant which matches the default header height for the standard ListView Control (25). However, this can be modified to suit.

In order to accomplish all of the above, we will need to define a few more private members in our ListGroup Class. Add the following code in the declaration area of your class (I am still a little old-school, in that I place most of my declarations at the top of the class, just after the class declaration itself). In the Source file, the following appear just before the Constructor:

Additional Member Declarations:

        // Text strings used as Image keys for the expanded/Collapsed image in the 
        // left-most columnHeader:
        static string COLLAPSED_IMAGE_KEY = "CollapsedImage";
        static string EXPANDED_IMAGE_KEY = "ExpandedImageKey";
        static string EMPTY_IMAGE_KEY = "EmptyImageKey";

        // "Magic number" approximates the height of the List View Column Header:
        static int HEADER_HEIGHT = 25;

 

We also need a Constructor at this point. Note that the Constructor initializes the ListView.SmallImageList with some images stored in the project resources (Properties.Resources). The images are included with the . Replace the Constructor code stub with the following:

The Constructor:

    public ListGroup() : base()
    {
        this.Columns = new ListGroupColumnCollection(this);
        this.Items = new ListGroupItemCollection(this);

        // The Imagelist is used to hold images for the expanded and contracted icons in the
        // Left-most columnheader:
        this.SmallImageList = new ImageList();

        // The tilting arrow images are available in the app resources:
        this.SmallImageList.Images.Add
            (COLLAPSED_IMAGE_KEY, Properties.Resources.CollapsedGroupSmall_png_1616);
        this.SmallImageList.Images.Add
            (EXPANDED_IMAGE_KEY, Properties.Resources.ExpandedGroupSmall_png_1616);
        this.SmallImageList.Images.Add
            (EMPTY_IMAGE_KEY, Properties.Resources.EmptyGroupSmall_png_1616);

        // Default configuration (for this sample. Obviously, configure to fit your needs:
        this.View = System.Windows.Forms.View.Details;
        this.FullRowSelect = true;
        this.GridLines = true;
        this.LabelEdit = false;
        this.Margin = new Padding(0);
        this.SetAutoSizeMode(AutoSizeMode.GrowAndShrink);
        this.MaximumSize = new System.Drawing.Size(1000, 2000);
            
        // Subscribe to local Events:
        this.ColumnClick += new ColumnClickEventHandler(ListGroup_ColumnClick);
        this.ItemAdded += new ItemAddedHandler(ListGroup_ItemAdded);
    }

Now we can add code to manage the re-sizing of the control in response to user actions. While we have existing stubs for the Expand() and Collapse() methods because these formed obvious behaviors for our control, the next two will have to be added. The SetControlHeight() method is our one-stop call to adjust the height of the control:

The SetControlHeight Method:

        /// <summary>
        /// Adjusts the item display area of the control in response to changes in the 
        /// expanded or collapsed state of the control. 
        /// </summary>
        public void SetControlHeight()
        {
            if (this.Height == HEADER_HEIGHT && this.Items.Count != 0)
                this.Expand();
            else
                this.Collapse();
        }

Add the above, along with the next three methods to our ListGroup Class. The next three methods actually perform the heavy lifting in terms of adjusting the control expanded/collapsed state. The first is a function which returns the proper control height after evaluating several factors (explained in the comments). We don’t have a stub for this in our existing structure, so add it right under the SetControlHeight method:

PreferredControlHeight Function:

    private int PreferredControlHeight()
    {
        int output = HEADER_HEIGHT;
        int rowHeight = 0;

        // determine the height of an individual list item:
        if(this.Items.Count > 0)
            rowHeight = this.Items[0].Bounds.Height;

        // In case the horizontal scrollbar makes an appearance, we will
        // need to modify the height of the expanded list so that it does not
        // obscure the last item (default is 10 px to leave a little space 
        // no matter what):
        int horizScrollBarOffset = 10;

        // if the Width of the columns is greater than the width of the control, 
        // the vertical scroll bar will be shown. Increase that offset height by the 
        // height of the scrollbar (approximately the same as the height of a row):
        if (this.Columns.TotalColumnWidths > this.Width)
            horizScrollBarOffset = rowHeight + 10;

        // Increase the height of the control to accomodate the Columnheader, 
        // all of the current items, and the value of the 
        // horizontal scroll bar (if present):
        output = HEADER_HEIGHT + (this.Items.Count) * rowHeight 
        + horizScrollBarOffset + this.Groups.Count * HEADER_HEIGHT;

        return output;
    }

Then replace the Expand() and Collapse() code stubs with the following. While the PreferredControlHeight function provides the optimal height for a ListGroup, the Expand and Collapse methods perform the requested action and also cause the Expanded/Collapsed/Empty images to display properly in the left-most column:

The Expand and Collapse Methods:

    /// <summary>
    /// Causes the list of items to expand, showing all items in the 
    /// Items collection.
    /// </summary>
    public void Expand()
    {
        if (this.Columns.Count > 0)
        {
            this.Height = this.PreferredControlHeight();

            if (this.Items.Count > 0)
                // Set the image in the first column to indicate an expanded state:
                this.Columns[0].ImageKey = EXPANDED_IMAGE_KEY;
            else
                // Set the image in the first column to indicate an empty state:
                this.Columns[0].ImageKey = EMPTY_IMAGE_KEY;

            this.Scrollable = true;

            // Raise the Expanded event to notify client code 
            // that the ListGroup has expanded:
            if (this.GroupExpanded != null)
                this.GroupExpanded(this, new EventArgs());
        }
    }


    /// <summary>
    /// Causes the Displayed list of items to collapse, hiding all items and 
    /// displaying only the columnheaders. 
    /// </summary>
    public void Collapse()
    {
        if (this.Columns.Count > 0)
        {
            this.Scrollable = false;

            // Collapse the ListGroup to show only the header:
            this.Height = HEADER_HEIGHT;

            if (this.Items.Count > 0)
                // Set the image in the first column to indicate a collapsed state:
                this.Columns[0].ImageKey = COLLAPSED_IMAGE_KEY;
            else
                // Set the image in the first column to indicate an empty state:
                this.Columns[0].ImageKey = EMPTY_IMAGE_KEY;

            // Raise the Collapsed event to notify client code that 
            // the ListGroup has expanded:
            if (this.GroupCollapsed != null)
                this.GroupCollapsed(this, new EventArgs());
        }
    }

Wiring it all up

Now we need to wire up behaviors (Expand/Collapse) to the appropriate events. Some of these are obvious. When the user clicks on the left-most column (with the Expanded/Collapsed state image), the control should toggle this state. However, we also need the control to adjust its displayed area (and possible toggle the state image) when items are added/removed, and when columns are added/removed (because when columns are added, there may be a need to add the state image to the first column).

First, we will address Column addition/removal. When a column is added to the control, if it is the FIRST column, it will need the initial state image added, and the control will need to size itself. Since it is the first column, it is reasonably safe (but not 100%) to assume that there have been no items added yet, so the ListGroup is empty, and the state image should reflect this.

Add the highlighted items to the OnColumnAdded() Method:

    private void OnColumnAdded(int ColumnIndex)
    {
        

if (ColumnIndex == 0){this.Columns[0].ImageKey = EMPTY_IMAGE_KEY;this.SetControlHeight();}

        if(this.ColumnAdded != null)
            this.ColumnAdded(this, new ListGroupColumnEventArgs(ColumnIndex));
    }

What happens if the ListGroup is populated with items, and the last column is removed? I don’t have a good answer for this, other than to clear the ListItems. It seems to me that the control loses its identity and purpose. If you have thoughts about this, please do discuss in the comments, or fork the code on Github. In any case, when removing columns, we need to test and see if the column removed is the last column in the control. If so, call the Clear() Method:

    private void OnColumnRemoved(int ColumnIndex)
    {
        

if (this.Columns.Count == 0){this.Items.Clear(); }

        // Raise the ColumnRemoved event to any subscribers:
        if (this.ColumnRemoved != null)
            this.ColumnRemoved(this, new ListGroupColumnEventArgs(ColumnIndex));
    }

Now, the final piece in our simplified ListGroup is the whole thing where the user clicks on the left-most column (Column[0]), and is rewarded by the control expanding or collapsing. The state image provides a sort of visual cue/affordance to the user which implies the current state. All we have to do is handle the ColumnClick event (sourced by the base class ListView) and we’re done with this part of our (abbreviated) control

Replace the ListGroup_ColumnClick code stub with the following:

Handling the ColumnClick Event:

    void ListGroup_ColumnClick(object sender, ColumnClickEventArgs e)
    {
        int columnClicked = e.Column;
            
        // The first column (Column[0]) is what activates the expansion/collapse of the 
        // List view item group:
        if (columnClicked == 0)
        {
            this.SuspendLayout();
            this.SetControlHeight();
            this.ResumeLayout();
        }
    }

Summing Up Part I

Sadly, I need to break this project up into two parts. Even this one is too long (although much of the length is simply code samples.

In this first post, we have examined extending the Winforms ListView class so that it can serve as a component within a container control. We have also examined extending the events sourced by the ListView control through the use of inner classes, used to extend the ListViewColumnCollection and ListViewItemCollection classes.

In the next post, we will fold our ListGroup class into the container GroupedListControl. IN doing so , we will need to make a few calls to the Windows API. I hate it when that happens, but so it is (I hate it because I am not well-schooled in the Win32 API, so that kind of thing is HARD for me!). After that, we will examine some special methods to source a custom Context Menu specific to right-clicks on the List Group column headers, and a few other interesting tidbits which were necessary to make the overall control work properly.

This Article Continues: Extending C# Listview with Collapsible Groups (Part II)

Report Bugs. Submit Improvements. Do Good. Help Me Get Better!

I will try to get the next post up is a day or two. In the meantime, if you find over bugs in the code, or see areas for improvement in the overall implementation, please report bugs (in the comments here, or on Github), and feel free to fork the source and submit pull requests for improvements. I have said many times, I need all the help I can get!

Referenced in This Article:

 

Posted on May 9 2012 05:11 AM by jatten     

Comments (0)

About the author

My name is John Atten, and my "handle" on many of my online accounts is xivSolutions. I am Fascinated by all things technology and software development. I work mostly with C#, JavaScript/Node, and databases of many flavors. Actively learning always. I dig web development. I am always looking for new information, and value your feedback (especially where I got something wrong!). You can email me at:

jatten at typecastexception dot com

Web Hosting by