5,699,431 members and growing! (16,275 online)
Email Password   helpLost your password?
Desktop Development » List Controls » ListView controls     Intermediate

A Bindable, Sortable, Autosizing ListView

By Steef (Stephan Deckers)

A listview with support for Databinding, Sorting & Autofit and upon rebinding data reselection of a previous selected item
C#, Windows, .NET 1.1, .NET, ADO.NET, GDI+, Visual Studio, VS.NET2003, Dev

Posted: 17 May 2005
Updated: 17 May 2005
Views: 56,891
Bookmarked: 59 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
7 votes for this Article.
Popularity: 3.73 Rating: 4.41 out of 5
0 votes, 0.0%
1
0 votes, 0.0%
2
0 votes, 0.0%
3
1 vote, 14.3%
4
6 votes, 85.7%
5

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

About the Author

Steef (Stephan Deckers)


Stephan is a software engineer at Methec in Dieren, the Netherlands, and is specialized in Web-services, Remoting, Windows Services and the Microsoft.Net platform in General. He has a background as a GIS-specialist and started his career at Intergraph Benelux in Hoofdorp as Application Engineer Utilities.
Stephan is married and he and his wife are expecting their first child in July.
Occupation: Web Developer
Location: Netherlands Netherlands

Other popular List Controls articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 3 of 3 (Total in Forum: 3) (Refresh)FirstPrevNext
GeneralHiding columnsmemberLukky1:41 3 Apr '06  
GeneralDragDrop Registration FailedmemberPablo123414:35 15 Nov '05  
GeneralVery, very nice, but,.....memberJoão Rezende3:20 24 May '05  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

PermaLink | Privacy | Terms of Use
Last Updated: 17 May 2005
Editor: Smitha Vijayan
Copyright 2005 by Steef (Stephan Deckers)
Everything else Copyright © CodeProject, 1999-2008
Web12 | Advertise on the Code Project