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( );
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++;
}
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);
}
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)
{
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.
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;
Graphics g = this.CreateGraphics( );
string item = string.Empty;
foreach( System.Data.DataRow dr in dt.Rows)
{
int i = 0;
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];
columnWidth = MeasureDisplayStringWidth( g,
slc.Text, this.Font);
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;
}
}
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( 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:
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:
If a user would adjust the size of the 'City' column before pressing the Update button, you would see the following picture:
The width isn't adjusted anymore, just like you would expect.