Introduction
Using a ListViewLayoutManager
allows controlling the behavior of the column layout of ListView
/GridView
controls:
- Fixed Column: Column with fixed column width
- Range Column: Column with minimal and/or maximal column width
- Proportional Column: Column with proportional column width
The Range Column allows to restrict the column width as well as to fill the remaining visible area with the column.
As known from HTML tables or the Grid
control, the Proportional Column determines the column widths on a percentage basis. The following factors determine the width of a proportional column:
- Visibility of the vertical
ListView
scrollbars - Changes of the
ListView
control width - Changes of the width of a non-proportional column
The implementation supports both controlling through XAML or Code Behind. Usage of XAML styles allows a ListViewLayoutManager
to be 'attached' to an existing ListView
control.
The class ConverterGridColumn
offers object specific binding by using the interface IValueConverter
. Using the ImageGridViewColumn
class allows representing a column as an image/icon using a DataTemplate
.
In the article User Settings Applied, I describe how you can persist order and size of the ListView
columns.
ListView/GridView Layout in XAML
Fixed Column
The following example shows controlling columns with fixed widths using XAML:
<ListView
Name="MyListView"
ctrl:ListViewLayoutManager.Enabled="true">
<ListView.View>
<GridView>
<GridViewColumn
DisplayMemberBinding="{Binding Path=Name}"
ctrl:FixedColumn.Width="100"
Header="Name" />
<GridViewColumn
DisplayMemberBinding="{Binding Path=City}"
ctrl:FixedColumn.Width="300"
Header="City" />
</GridView>
</ListView.View>
</ListView>
Setting the property Enabled
binds the ListViewLayoutManager
to the ListView
control. The property FixedColumn.Width
determines the column width and prevents resizing using the mouse.
Proportional Column
The following example shows controlling columns with proportional widths using XAML:
<ListView
Name="MyListView"
ctrl:ListViewLayoutManager.Enabled="true">
<ListView.View>
<GridView>
<GridViewColumn
DisplayMemberBinding="{Binding Path=Name}"
ctrl:ProportionalColumn.Width="1"
Header="Name" />
<GridViewColumn
DisplayMemberBinding="{Binding Path=City}"
ctrl:ProportionalColumn.Width="3"
Header="City" />
</GridView>
</ListView.View>
</ListView>
Matching the RowDefinition.Width
of the Grid
control, the value of ProportionalColumn.Width
represents the percentage. The scenario above sets the column Name
to 25% and the column City
to 75% of the total width. Analogous to the fixed columns, resizing with the mouse is disabled.
Range Column
The following example shows controlling ranged columns with minimal/maximal widths using XAML:
<ListView
Name="MyListView"
ctrl:ListViewLayoutManager.Enabled="true">
<ListView.View>
<GridView>
<GridViewColumn
DisplayMemberBinding="{Binding Path=Name}"
ctrl:RangeColumn.MinWidth="100"
Width="150"
Header="Name" />
<GridViewColumn
DisplayMemberBinding="{Binding Path=City}"
ctrl:RangeColumn.MaxWidth="200"
Width="150"
Header="City" />
<GridViewColumn
DisplayMemberBinding="{Binding Path=Country}"
Width="100"
ctrl:RangeColumn.MinWidth="50"
ctrl:RangeColumn.MaxWidth="150"
Header="Country" />
<GridViewColumn
DisplayMemberBinding="{Binding Path=State}"
Width="100"
ctrl:RangeColumn.MinWidth="100"
ctrl:RangeColumn.IsFillColumn="true"
Header="Country" />
</GridView>
</ListView.View>
</ListView>
The first range column which has the value IsFillColumn
set to true
, will automatically get resized to the remaining space. The column will not get filled if the ListView
contains a proportional column.
Dragging the mouse out of the configured range when resizing, the mouse cursor changes its representation to indicate this.
Combined Usage
In real life, it is common to combine these column types. Their order can be varied as required:
<ListView
Name="MyListView"
ctrl:ListViewLayoutManager.Enabled="true">
<ListView.View>
<GridView>
<GridViewColumn
DisplayMemberBinding="{Binding Path=State}"
ctrl:FixedColumn.Width="25"
Header="Name" />
<GridViewColumn
DisplayMemberBinding="{Binding Path=Name}"
Width="150"
ctrl:RangeColumn.MinWidth="100"
ctrl:RangeColumn.MaxWidth="200"
Header="City" />
<GridViewColumn
DisplayMemberBinding="{Binding Path=City}"
ctrl:ProportionalColumn.Width="1"
Header="Zip" />
<GridViewColumn
DisplayMemberBinding="{Binding Path=Country}"
ctrl:ProportionalColumn.Width="2"
Header="Country" />
</GridView>
</ListView.View>
</ListView>
ListView/GridView Layout using Code Behind
It is also possible to setup the column layout in the source code:
ListView listView = new ListView();
new ListViewLayoutManager( listView );
GridView gridView = new GridView();
gridView.Columns.Add( FixedColumn.ApplyWidth( new MyGridViewColumn( "State" ), 25 ) );
gridView.Columns.Add( RangeColumn.ApplyWidth( new MyGridViewColumn( "Name" ), 100,
150, 200 ) );
gridView.Columns.Add( ProportionalColumn.ApplyWidth( new MyGridViewColumn( "City" ),
1 ) );
gridView.Columns.Add( ProportionalColumn.ApplyWidth( new MyGridViewColumn(
"Country" ), 2 ) );
listView.View = gridView;
Columns with Custom Representation
The class ConverterGridColumn
serves as a base class for binding table columns to individual objects:
public class Customer
{
public Customer()
{
}
public string FirstName
{
get { return this.firstName; }
set { this.firstName = value; }
}
public string LastName
{
get { return this.lastName; }
set { this.lastName = value; }
}
private string firstName;
private string lastName;
}
public class CustomerFullNameColumn : ConverterGridViewColumn
{
public CustomerGridViewColumn() :
base( typeof( Customer ) )
{
}
protected override object ConvertValue( object value )
{
Customer customer = value as Customer;
return string.Concat( customer.FirstName, " ", customer.LastName );
}
}
Columns Represented as Images
The class ImageGridColumn
serves as a base class for binding table columns to images/icons:
public class Customer
{
public Customer()
{
}
public bool IsActive
{
get { return this.isActive; }
set { this.isActive = value; }
}
private bool isActive;
}
public class CustomerActiveColumn : ImageGridViewColumn
{
public CustomerActiveColumn()
{
}
protected override ImageSource GetImageSource( object value )
{
Customer customer = value as Customer;
if ( customer != null )
{
return new BitmapImage( new Uri( customer.IsActive ? "Active.png" :
"Inactive.png" ) );
}
return null;
}
}
Points of Interest
At the core of layouting the ListView
control lies the ListViewLayoutManager
with the following responsibilities:
- Preventing resizing columns of type
Fixed
and Proportional
- Enforcing the range of allowed widths for type
Range
- Updating the column layout upon changes to the size of the
ListView
control - Updating the column layout upon changes to the widths of individual columns
To properly receive the required information, it is necessary to analyze the Visual Tree of the ListView
control. The object Thumb
provides the events for changes to the column width. To ensure correctly representing the mouse cursor, the events PreviewMouseMove
and PreviewMouseLeftButtonDown
are being handled.
The event ScrollChanged
of the class ScrollViewer
triggers updates due to control size changes. Only changes to the size of the control Viewport
are relevant for resizing (ScrollChangedEventArgs.ViewportWidthChange
).
Tracking the property Width
of class GridViewColumn
using DependencyPropertyDescriptor
gives notification of changes to column widths.
To support integration in existing systems, the required column data is kept in Attached Properties. Using the method DependencyProperty.ReadLocalValue()
allows detecting whether the own property is present in an object.
The class ConverterGridViewColumn
uses a simple Binding
and at the same time represents the converter (interface IValueConverter
).
The class ImageGridViewColumn
uses the FrameworkElementFactory
in a DataTemplate
to embed the image dynamically. By default, the images in the ListView
/GridView
control will be stretched automatically (property Image.Stretch
). Because the image in a DataTemplate
gets created dynamically, the property value of the template element must be assigned using a Binding
:
protected ImageGridViewColumn( Stretch imageStretch )
{
FrameworkElementFactory imageElement = new FrameworkElementFactory( typeof( Image ) );
Binding imageStretchBinding = new Binding();
imageStretchBinding.Source = imageStretch;
imageElement.SetBinding( Image.StretchProperty, imageStretchBinding );
DataTemplate template = new DataTemplate();
template.VisualTree = imageElement;
CellTemplate = template;
}
History
- 28th September, 2012
- Added projects and solutions for Visual Studio 2010
- ReSharped source code
ListViewLayoutManager
: Using a value range to check if the scroll viewer has been changedListViewLayoutManager
: New public method Refresh
ListViewLayoutManager
: Hide fixed column thumb - thanks Name taken
- 3rd August, 2009
RangeColumn
now supports columns with automatic width (Width="Auto"
) - thanks inv_inv
- 27th November, 2008
- Fixed memory leak by un-registering the
ListView
events - thanks Dave and John
- 3rd November, 2008
- Fixed infinite recursion of column resizing in some rare cases - thanks ascalonx
- Fixed column resizing in case of nested
ListView
- thanks mburi
- 19th September, 2008
- Fill column can now be at any position - the first occurrence will be used
- 12th June, 2008
- Added
RangeColumn.IsFillColumn
to fill the last column to the available visible area
- 3rd June, 2008
- Considering limits of range column during auto-resize (mouse double click) - thanks paul
- 14th May, 2008
- Fixed horizontal scrolling in case of absent proportional columns - thanks vernarim
- 10th May, 2008
- 23rd April, 2008
- Added screenshot descriptions
- Minor bug fixes
- 6th April, 2008