Thursday, August 07, 2008
 
   
 
Welcome to my site

First let me say thanks for stopping by my site. My name is David Hanson-Graville and I am a IT consultant working in the UK. Let me make it clear, I am passionate about technology and specifically .net and its various forms. I've programmed in a range of langages, but I can say, I am now at my happiest when coding with c#. I hope my blog is an enjoyable & educational read and please feel free to email me at David.Hanson@OnTheBlog.net if you have any questions. 

Search Minimize
Print  
Archive Minimize
Print  
Dipstick Survey Minimize
What technology are you most excited about?











Submit Survey  View Results
Print  
Contact me Minimize
Print  
Silverlight News Minimize
silverlight - Google News
Print  
WPF: How to stretch columns in a ListView Minimize
Location: BlogsOnTheBlog    
Posted by: David Hanson Sun, 13 Apr 2008 21:21:11 GMT
The ListView control in WPF is a powerful option when trying to present tabular data to users. It supports many of the common behaviours found in grid controls as well as the full WFP templating architecture we have all come to love. Below is a simple of example of a ListView which has been bound to a collection of strings. The example uses a template GridVewColumn and a 3 standard GridViewColumn’s to display the data.
 

And here is the associated XAML.
 
<Window x:Class="WPFSamples.ListViewDemo"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:s="clr-namespace:System;assembly=mscorlib"
    xmlns:Extensions="clr-namespace:Demo.Extension.Properties"
    Title="ListViewDemo" Height="353" Width="714">
    <Grid>
        <Grid.Resources>
           
            <x:Array Type="{x:Type s:String}" x:Key="ourData">
                <s:String>Scott Gus:String>
                <s:String>Tim Sneaths:String>
                <s:String>Rockford Lhotkas:String>
                <s:String>Robert Scobles:String>
                <s:String>Ayende Rahiens:String>
            x:Array>
        Grid.Resources>
       
        <ListView Name="myListView"
                  Background="LightBlue"
                  ItemsSource="{StaticResource ourData}">
            <ListView.View>
                <GridView>
                   
                    <GridViewColumn Header="Names" >
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Border>
                                    <TextBlock Text="{Binding .}"
                                               Foreground="Black"
                                               FontWeight="Bold" />
                                Border>
                            DataTemplate>
                        GridViewColumn.CellTemplate>
                    GridViewColumn>
                   
                    <GridViewColumn Header="Length"
                                    Width="50"  DisplayMemberBinding="{Binding Length}" />
                    <GridViewColumn Header="Names"  DisplayMemberBinding="{Binding}" />
                    <GridViewColumn Header="Length"
                                    Width="50" DisplayMemberBinding="{Binding Length}" />
                GridView>
            ListView.View>
        ListView>
    Grid>
Window>
 
 
As you can see, I have highlighted the column’s in our GridView that have explicit width settings.  Now looking at what we get when we run this sample you can see that the default behaviour of the GridViewColumn is to set the width to AUTO thus fitting the content being displayed.  However, there often situations where you would rather the GridViewColum’s stretch to fit the remaining available space of its container. To achieve this you would imagine you could set the width of the GridViewColumn to “*”. Unfortunately this is not supported and you will be prompted with the following message.
 
<GridViewColumn Header="Names" Width="*">
    <GridViewColumn.CellTemplate>
        <DataTemplate>
            <Border>
                <TextBlock Text="{Binding .}"
                           Foreground="Black"
                           FontWeight="Bold" />
            Border>
        DataTemplate>
    GridViewColumn.CellTemplate>
GridViewColumn>
 
Cannot convert string '*' in attribute 'Width' to object of type 'System.Double'. '*'
 
I find this inconsistent use of the * in WPF frustrating, I am sure the WPF team have a valid reason for why this is not supported but it just feels like it should be. As the * is not supported on a GridViewColumn’s width I tried many other way to get our GridViewColumn to stretch, all to no avail.
 
<ListView HorizontalAlignment="Stretch" ... Makes no difference.
<ListView HorizontalContentAlignment="Stretch" ... Makes no difference.
<GridViewColumn HorizontalAlignment="Stretch" ... Not supported.
<GridViewColumn HorizontalContentAlignment="Stretch" ... Not supported.
 
Having played around for a while and running a few google searches it became apparent that a number of people have come across this issue, particularly when trying to stretch the last column to the remaining space. As a result, I decided to solve the problem using the attached property approach. This provides a simple way if implementing the stretch column behaviour on ListView’s. As I will outline, you change the default behaviour of the GridView setting the width to Auto so that be default all columns will stretch. Below is the XAML that will allow us to do just that.
 
<ListView Name="myListView"
          Background="LightBlue"
          ItemsSource="{StaticResource ourData}"
          Extensions:ListViewColumns.Stretch="true">
 
 
For us to achieve this level of simplicity in our XAML we have to place the complexity elsewhere.....in our attached property infact. The first stage when creating a new WPF attached property is to put the appropriate infrastructure in place. You will find may examples of how to create attached properties in WPF so I won’t bore you with the details here. Below is base code for our attached property.
 
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
 
namespace Demo.Extension.Properties
{
    ///
    /// ListViewColumnStretch
    ///
    public class ListViewColumns : DependencyObject
    {
 
        ///
        /// IsStretched Dependancy property which can be attached to gridview columns.
        ///
        public static readonly DependencyProperty StretchProperty =
            DependencyProperty.RegisterAttached("Stretch",
            typeof(bool),
            typeof(ListViewColumns),
            new UIPropertyMetadata(true,null,OnCoerceStretch));
 
 
        ///
        /// Gets the stretch.
        ///
        /// The obj.
        ///
        public static bool GetStretch(DependencyObject obj)
        {
            return (bool)obj.GetValue(StretchProperty);
        }
 
        ///
        /// Sets the stretch.
        ///
        /// The obj.
        /// if set to true [value].
        public static void SetStretch(DependencyObject obj, bool value)
        {
            obj.SetValue(StretchProperty, value);
        }
 
        ///
        /// Called when [coerce stretch].
        ///
        ///If this callback seems unfamilar then please read
        /// the great blog post by Paul jackson found here.
        /// http://compilewith.net/2007/08/wpf-dependency-properties.html
        /// The source.
        /// The value.
        ///
        public static object OnCoerceStretch(DependencyObject source, object value)
        {
            ListView lv = (source as ListView);
 
            //Ensure we dont have an invalid dependancy object of type ListView.
            if (lv == null)
                throw new ArgumentException("This property may only be used on ListViews");
 
            //Setup our event handlers for this list view.
            lv.Loaded += new RoutedEventHandler(lv_Loaded);
            lv.SizeChanged += new SizeChangedEventHandler(lv_SizeChanged);
            return value;
        }
 
    }
}
 
 
Firstly, notice that dependency property inherits from DependencyObject, just about most elements in WPF derive from this type and attached properties are no exception. The remainder of the code above is fairly self explanatory, we have WPF setters and getters and we have registered the property name (DependencyProperty.RegisterAttached("Stretch") as part of this work. The only part that may not be recognised is the static method OnCoerceStretch. This method is actually just a callback that will be called prior to our property being set. It allows us to intercept the setter in order to change the the state of the value if so require.
 
public static object OnCoerceStretch(DependencyObject source, object value)
{
    ListView lv = (source as ListView);
 
    //Ensure we dont have an invalid dependancy object of type ListView.
    if (lv == null)
        throw new ArgumentException("This property may only be used on ListViews");
 
    //Setup our event handlers for this list view.
    lv.Loaded += new RoutedEventHandler(lv_Loaded);
    lv.SizeChanged += new SizeChangedEventHandler(lv_SizeChanged);
    return value;
}
 
In our coerce method above you can see we receive the dependency object the property has been attached to (in our case a listview) and then the value being set. The only this we really need to do in our method is to attach a couple of event handlers which will notify us later on about state changes in our listview.
 
    //Setup our event handlers for this list view.
    lv.Loaded += new RoutedEventHandler(lv_Loaded);
    lv.SizeChanged += new SizeChangedEventHandler(lv_SizeChanged);
 
The loaded handler is setup as we can only gain access to our listview’s internal GridView and columns once the listview has fully loaded. The SizeChanged event handler will be used to monitor when the ListView’s sizes changes, when this happens we want to be able to perform a calculation so that columns with no width having been set can stretch to fill the available space. Implementing the dependency property above allows us to use the following XAML syntax. Extensions:ListViewColumns.Stretch="true". Don’t forget to import the namespace into your XAML like this xmlns:Extensions="clr-namespace:Demo.Extension.Properties".
 
Back to our dependency property, the event handlers are once again fairly self explanatory, each event handler calls into a method called SetColumnWidths. As the SizeChanged event is called prior to the loaded event a simple check is made to ensure SetColumnWidths is not called at this point.
 
///
/// Handles the SizeChanged event of the lv control.
///
/// The source of the event.
/// The instance containing the event data.
private static void lv_SizeChanged(object sender, SizeChangedEventArgs e)
{
    ListView lv = (sender as ListView);
    if (lv.IsLoaded)
    {
        //Set our initial widths.
        SetColumnWidths(lv);
    }
}
 
///
/// Handles the Loaded event of the lv control.
///
/// The source of the event.
/// The instance containing the event data.
private static void lv_Loaded(object sender, RoutedEventArgs e)
{
    ListView lv = (sender as ListView);
    //Set our initial widths.
    SetColumnWidths(lv);
}
 
 
The final part of the puzzle is too look at our method SetColumnsWidth. This method does the majority of the work in our dependency property, it identifies columns in our GridView which do not have a width setting and then based on the available space remaining divides to those columns. The only interesting part is that the columns identified as having no width are stored in the ListView’s TAG property for reuse later.
 
///
/// Sets the column widths.
///
private static void SetColumnWidths(ListView listView)
{
    //Pull the stretch columns fromt the tag property.
    List<GridViewColumn> columns = (listView.Tag as List<GridViewColumn>);
    double specifiedWidth = 0;
    GridView gridView = listView.View as GridView;
    if (gridView != null)
    {
        if (columns == null)
        {
            //Instance if its our first run.
            columns = new List<GridViewColumn>();
            // Get all columns with no width having been set.
            foreach (GridViewColumn column in gridView.Columns)
            {
                if (!(column.Width >= 0))
                    columns.Add(column);
                else specifiedWidth += column.ActualWidth;
            }
        }
        else
        {
            // Get all columns with no width having been set.
            foreach (GridViewColumn column in gridView.Columns)
                if (!columns.Contains(column))
                    specifiedWidth += column.ActualWidth;
        }
 
        // Allocate remaining space equally.
        foreach (GridViewColumn column in columns)
        {
            double newWidth = (listView.ActualWidth - specifiedWidth) / columns.Count;
            if (newWidth >= 0) column.Width = newWidth - 10;
        }
 
        //Store the columns in the TAG property for later use.
        listView.Tag = columns;
    }
}
 
And that’s pretty much it..... were done! If we run the sample now it will look like this. You can see the two [Name] columns are stretching to fill the available space. 
 
 
 
 
 
 Sourcecode for this blog can be downloaded here.
 
 
 
 
 
 
 
Permalink |  Trackback

Comments (5)   Add Comment
Re: WPF: How to stretch columns in a ListView    By Paul on Tue, 15 Apr 2008 18:54:28 GMT
This is a very timely for me. I only change ListView to GridViewHeaderRowPresenter because I use GridViewHeaderRowPresenter object without ListView.<br>Thank you very match.

Re: WPF: How to stretch columns in a ListView    By host on Tue, 15 Apr 2008 18:45:52 GMT
Glad it helped. :-)

Re: WPF: How to stretch columns in a ListView    By Simeon on Fri, 02 May 2008 10:57:05 GMT
Thank you! It works fine!

Re: WPF: How to stretch columns in a ListView    By host on Fri, 02 May 2008 10:58:05 GMT
Welcome.

Re: WPF: How to stretch columns in a ListView    By Phil on Mon, 14 Jul 2008 20:32:11 GMT
This is a little old, but i have a problem with the code where if you turn the stretch on, it no longer allows users to resize columns. If they try, it'll just snap right back to a stretch. Is there a way around this?


Your name:
Title:
Comment:
Security Code
Enter the code shown above in the box below
Add Comment   Cancel