Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Bindable, Sortable, Autosizing ListView

0.00/5 (No votes)
17 May 2005 1  
A listview with support for Databinding, Sorting & Autofit and upon rebinding data reselection of a previous selected item

Introduction

During my latest project, I needed a ListView to display the results of database queries with features like sorting, autosizing contents and re-selection of items. At the point I had finished the autofit part, I felt like this was something I needed to share, especially the autofit-part since I couldn't find any samples in the net to fit my needs.

Part 1: Databinding the ListView, creating columns and sorters

The databinding part of my 'bsaListView' primarily consists of two parts, setting the DataTable property and calling DataBind:

this.listView1.DataTable = CreateDataTable1( );
this.listView1.DataBind( );

Upon calling the setter for DataTable, the ListView will enumerate the datacolumns of the DataTable, create columns for it and create sorters for it based on the data type:

private System.Data.DataTable _dataTable;
[ Bindable      ( true), 
Category        ( "Whoua.Src"), 
Description     ( "DataTable")]
public System.Data.DataTable DataTable
{
     get{ return( _dataTable); }
     set
     {
          if( _dataTable == value)
          {
               return;
          }
          _dataTable = value;
          this.CreateListViewColumns( _dataTable);
     }
}

CreateListViewColumns creates the actual columns and looks like this:

ColumnHeader[] columnHeaders = new ColumnHeader[ dataTable.Columns.Count];
System.Windows.Forms.ColumnHeader columnHeader = null;

this.Columns.Clear( );

// --- Enumerate DataColumns and create ColumnHeaders for them (20050418 SDE)

int i = 0;
foreach( System.Data.DataColumn dataColumn in dataTable.Columns)
{
     columnHeader = getSortableListviewColumnHeader( dataColumn);
     columnHeader.Text = dataColumn.ColumnName;
     columnHeader.TextAlign = HorizontalAlignment.Left;
     columnHeaders[ i] = columnHeader;
     i++;
}

// --- Tell listview to create the columns (20050309 SDE)
this.Columns.AddRange( columnHeaders);

Nothing fancy going on over here, except that a call to getSortableListviewColumnHeader is made which returns a SortableListviewColumnHeader column header. This column header is needed for sorting and autosizing the contents.

I read the excellent article of Eddie Velasquez on implementing sorting for a ListView. In this article, Eddy describes a way to set a sort manager based on the data type a column is using. Since I'm using a databound ListView, this can be handled by the ListView itself and I moved it completely into the ListView (and also removed some other features I didn't need).

The first step in setting up sorting is performed by getSortableListviewColumnHeader, which returns a sorter based on the data type of a column:

SortableListviewColumnHeader sortableListviewColumnHeader = 
                     new SortableListviewColumnHeader( );

System.Type type = dataColumn.DataType;

if( type == typeof(System.String))
{
     sortableListviewColumnHeader.ListviewSorter = 
                    new ListViewTextCaseInsensitiveSorter( );
     return( sortableListviewColumnHeader);
}

else if( type == typeof( System.Int32))
{
     sortableListviewColumnHeader.ListviewSorter = 
                             new ListViewInt32Sorter( );
     return( sortableListviewColumnHeader);
}

//etc..

Whenever a column is clicked, the 'ListView_ColumnClick' event handler which is created in Initialize is called which will basically invoke the same sorting logic Eddy described:

this.ColumnClick += new ColumnClickEventHandler( ListView_ColumnClick);

private void ListView_ColumnClick( object sender, ColumnClickEventArgs e)
{
     // --- Perform sorting

     System.Windows.Forms.ListView listview = sender as ListView;

     SortableListviewColumnHeader sorter = 
            listview.Columns[ e.Column] as SortableListviewColumnHeader;

     if( listview.Sorting == SortOrder.None)
     {
          listview.Sorting = SortOrder.Ascending;
     }
     else if( listview.Sorting == SortOrder.Ascending)
     {
          listview.Sorting = SortOrder.Descending;
     }
     else
     {
          listview.Sorting = SortOrder.Ascending;
     }

     sorter.Column = e.Column;
     this.ListViewItemSorter = sorter;
}

The data is self created by calling DataBind which I will explain in the next part. In the following picture you can see an example of a bsaListView bound to a DataTable with types System.DateTime ("Date of Birth"), String ("Name, City & ZIP"), int ("Employee") and double ("Weight"). Whenever you click on a column header, the data is sorted.

Sorting

Part 2: Autofit contents

You can tell a ListView to either fit its contents, or its headers by setting the width-property of a column to -2 (fit largest item) or -1 (fit column header):

foreach( System.Windows.Forms.ColumnHeader c in this.listView1.Columns)
{
     c.Width = -2; 
}

You need to do this after the list items have been created.

Using option -2 seems like a good choice, but it isn't. Consider a user changing the width of a column because he doesn't need to see a certain column. Whenever data in the ListView is updated, the standard ListView restores the column-width to fit the largest item, which is very annoying. Also, using option -2 should fit the column header if the largest item in the column isn't larger then the column header, which is not the behaviour of the regular ListView.

In my solution, I'm calculating the width of each item using code from Pierre Arnaud's article. Data2Listview also checks if the largest item is smaller than the column header, in which case the column header width is used as the width to use for the column. This width is stored in a specialized column header, the SortableListviewColumnHeader:

private void Data2Listview( System.Data.DataTable dt)
{
     System.Diagnostics.Debug.Assert( dt.Columns.Count == this.Columns.Count, 
                      string.Format( "Columncount != listview columncount"));

     System.Windows.Forms.ListViewItem listViewItem = null;

     // --- Add a ListView-items for each row
     //     and see if it has the largest string-length
     //     of items so we can fit the columnheader (20050228 SDE)

     Graphics g = this.CreateGraphics( );

     string item = string.Empty;

     foreach( System.Data.DataRow dr in dt.Rows)
     {
          int i = 0; // --- Enumerating the columns (20050515 SDE)

          ArrayList items = new ArrayList( );
          SortableListviewColumnHeader slc = null;
          float itemWidth = 0.0F;
          float     columnWidth = 0.0F;
          float     width = 0.0F;
          int       itemLength     = 0;

          foreach( System.Data.DataColumn dc in dt.Columns)
          {
               item = dr[ dc].ToString().Trim();
               items.Add( item);
               slc = (SortableListviewColumnHeader) this.Columns[ i];

               // --- If the widht of a columnheader
               //     is larger then the largest item in the
               //     list, we use the columnheader (20050515 SDE)

               columnWidth = MeasureDisplayStringWidth( g, 
                                         slc.Text, this.Font);

               // --- Length item (20050515 SDE)

               itemLength     = item.Length;
               if( item.Trim() != string.Empty)
               {
                    itemWidth = MeasureDisplayStringWidth( g, 
                                             item, this.Font);

                    if( itemWidth > columnWidth)
                    {
                         width = itemWidth;
                    }
                    else
                    {
                         width = columnWidth;
                    }

                    if( width > slc.LargestSize)
                    {
                         slc.LargestSize = width;
                    }
               }

               // --- Use columnwidth when values are null (20050515 SDE)

               else
               {
                    width = columnWidth;

                    if( width > slc.LargestSize)
                    {
                         slc.LargestSize = width;
                    }
               }
               i++;
          }

          string[] listItems = (string []) items.ToArray( typeof( string));
          listViewItem = new ListViewItem( listItems);
          this.Items.Add( listViewItem);
     }
}

After Data2Listview has been called, DataBind calls AdjustColuzmnWidths which is responsible for setting the column widths only if a user has not changed it:

private void AdjustColumnWidths( )
{
     string s = string.Empty;

     foreach( SortableListviewColumnHeader slc in this.Columns)
     {
          // --- If the PreviousWidth equals
          //     the Width, the user hasn't modified a columns
          //     width and we need to autofit
          //     it to the largest item. Otherwise, 
          //     PreviousWidth is not equal
          //     to the width, the user has modified the width
          //     of a column, and we need to leave
          //     it the way it is (20050515 SDE)

          if( slc.PreviousWidth == slc.Width || slc.PreviousWidth == 0)
          {
               slc.Width = System.Convert.ToInt32( slc.LargestSize);
               slc.PreviousWidth = slc.Width;
          }
     }
}

This is all what is needed to bypass the shortcomings of the standard ListView-column header.

When using this ListView, the first screen might look like this:

Fit1

You can see the the columns either fit the width of their largest item (Date of Birth, Name, City & ZIP), or the width of the column header (Employee # & Weight). If you select the button 'Update ds1', the DataSet is updated and the city 'Huntsville/Birmingham AL' is assigned to the 3rd row. The width of the column is adjusted like in the following figure:

Fit2

If a user would adjust the size of the 'City' column before pressing the Update button, you would see the following picture:

Fit3

The width isn't adjusted anymore, just like you would expect.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here