Click here to Skip to main content
Click here to Skip to main content

A Bindable, Sortable, Autosizing ListView

By , 16 May 2005
Rate this:
Please Sign up or sign in to vote.

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Steef D.
Software Developer (Senior) Merkator
Netherlands Netherlands
Busy with Intergraph G/Technology-GIS
Follow on   Twitter

Comments and Discussions

 
QuestionHOW TO LET LAST ROW NOT JOININT SORT?? Pinmembervalidfiles12-Aug-09 6:42 
QuestionHOW LET LAST ROW NOT JOIN SORT?? Pinmembervalidfiles12-Aug-09 6:34 
GeneralHiding columns PinmemberLukky3-Apr-06 0:41 
Hi,
 
A nice feature would be to provide a way to make certain columns invisible.
 
Regards,
 
Luc Morin
GeneralDragDrop Registration Failed PinmemberPablo123415-Nov-05 13:35 
GeneralVery, very nice, but,..... PinmemberJoão Rezende24-May-05 2:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140421.2 | Last Updated 17 May 2005
Article Copyright 2005 by Steef D.
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid