The blog of dlaa.me

Sometimes all it takes is a little encouragement [How to: Automatically update the widths of ListView columns]

I was working on a WPF project the other day and wanted an easy way to display data in a simple tabular format: a few columns (with headers) that would automatically size to fit their contents. The obvious choice was the ListView control and its GridView View which do exactly this. As you might expect, using this control was straightforward and it worked just like I wanted. Well, almost... There was one small catch: my ListView was hooked up to a data source that changed dynamically (via a Binding on its ItemsSource property) and I noticed that when the data source was updated, the widths of the columns were not automatically adjusted to fit the new content.

Here's a sample application I wrote for this post - notice how the text in the first ListView's "Value" column is truncated because the columns widths have not been updated:

ListViewColumnWidthAutoUpdate sample

This behavior was kind of annoying - and a brief web search showed that I'm hardly the first person to want to change it. There are a few different ways to tell the ListView to update its columns - the one I prefer looks something like this:

// Technique for updating column widths of a ListView's GridView manually
public static void UpdateColumnWidths(GridView gridView)
{
    // For each column...
    foreach (var column in gridView.Columns)
    {
        // If this is an "auto width" column...
        if (double.IsNaN(column.Width))
        {
            // Set its Width back to NaN to auto-size again
            column.Width = 0;
            column.Width = double.NaN;
        }
    }
}

Calling this method after a ListView's ItemsSource property changes is simple and does exactly what we want. So if your scenario is such that you always know exactly when your data changes, you can add a call to this method after that happens and stop reading now because your problem is already solved. :)

Okay, so if you're still reading, then your scenario is probably like mine: changes to the data source can occur without the application explicitly knowing about it. That last bit may not make a lot of sense until you realize that it's possible to implement a great deal of an application's functionality entirely in XAML. Specifically, it's quite easy to connect a ListView to the SelectedItem property of a ListBox so that changes to the selected item of the ListBox automatically re-populate the data in the ListView. Because this can be done entirely in XAML, these updates aren't automatically visible to the application.

The solution for the slightly more complicated scenario begins by realizing that it's necessary to know when the ItemsSource Binding updates. Fortunately, this is quite easy in WPF! :) By setting the NotifyOnTargetUpdated property of the ItemsSource Binding to true and handling the Binding.TargetUpdated attached event on the ListView, we have a fairly simple way of generating an event that can run a bit of code that calls the above method to update the column widths. What's more, this technique is fairly designer-friendly because it gives the designer complete freedom to set up such cross-control Bindings in their XAML without having to be intimately involved with the developer responsible for the application's code. Granted, the developer needs to implement the handler for the generated event, but that code is completely general and can be reused across multiple different ListViews.

The second ListView of the sample application uses this approach; notice how the column widths are correct in the image above. The XAML looks like this:

<ListView
    ItemsSource="{Binding Details, NotifyOnTargetUpdated=True}"
    Binding.TargetUpdated="ListViewTargetUpdated"
    ...

And the code for the event handler looks like this:

// Handler for the ListView's TargetUpdated event
private void ListViewTargetUpdated(object sender, DataTransferEventArgs e)
{
    // Get a reference to the ListView's GridView...
    var listView = sender as ListView;
    if (null != listView)
    {
        var gridView = listView.View as GridView;
        if (null != gridView)
        {
            // ... and update its column widths
            ListViewBehaviors.UpdateColumnWidths(gridView);
        }
    }
}

I'd arrived at the above solution and was going to consider the problem solved - and that's when Dr. WPF suggested I could use an attached behavior to encapsulate what I'd done into something that would be even simpler to use from XAML and wouldn't require the developer's involvement at all (aside from referencing the code that implements the attached behavior, of course). Attached behaviors are a powerful technique that allow the introduction of changes to the functionality of a control simply by setting an attached property on it. (If you're not familiar with attached behaviors, you can read more about them in this post by John Gossman or this article by Josh Smith.)

In the case of the attached behavior solution to this problem, we make use of the DependencyPropertyDescriptor class to attach a change handler to the ItemsSource property of the ListView - and then call the method above to actually update the widths of the columns. There end up being a few more lines of code with this solution because of what it takes to create an attached DependencyProperty and attach/remove a handler for it, but that code is completely self-contained and can live entirely in its own dedicated class (whereas the method used by the previous solution needs to be part of one of the application's classes). More importantly, the number of XAML edits drops to just one and it's no longer even necessary that a Binding changes the data source - even direct assignments to the ItemsSource property will do!

The third ListView of the simple application uses this approach; the XAML looks like this:

<ListView
    ItemsSource="{Binding Details}"
    local:ListViewBehaviors.IsAutoUpdatingColumnWidths="true"
    ...

And here's the complete implementation of the attached DependencyProperty:

// Class implementing handy behaviors for the ListView control
public static class ListViewBehaviors
{
    // Technique for updating column widths of a ListView's GridView manually
    public static void UpdateColumnWidths(GridView gridView)
    {
        // For each column...
        foreach (var column in gridView.Columns)
        {
            // If this is an "auto width" column...
            if (double.IsNaN(column.Width))
            {
                // Set its Width back to NaN to auto-size again
                column.Width = 0;
                column.Width = double.NaN;
            }
        }
    }

    // Definition of the IsAutoUpdatingColumnWidthsProperty attached DependencyProperty
    public static readonly DependencyProperty IsAutoUpdatingColumnWidthsProperty =
        DependencyProperty.RegisterAttached(
            "IsAutoUpdatingColumnWidths",
            typeof(bool),
            typeof(ListViewBehaviors),
            new UIPropertyMetadata(false, OnIsAutoUpdatingColumnWidthsChanged));

    // Get/set methods for the attached DependencyProperty
    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
        Justification = "Only applies to ListView instances.")]
    public static bool GetIsAutoUpdatingColumnWidths(ListView listView)
    {
        return (bool)listView.GetValue(IsAutoUpdatingColumnWidthsProperty);
    }
    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
        Justification = "Only applies to ListView instances.")]
    public static void SetIsAutoUpdatingColumnWidths(ListView listView, bool value)
    {
        listView.SetValue(IsAutoUpdatingColumnWidthsProperty, value);
    }

    // Change handler for the attached DependencyProperty
    private static void OnIsAutoUpdatingColumnWidthsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        // Get the ListView instance and new bool value
        var listView = o as ListView;
        if ((null != listView) && (e.NewValue is bool))
        {
            // Get a descriptor for the ListView's ItemsSource property
            var descriptor = DependencyPropertyDescriptor.FromProperty(ListView.ItemsSourceProperty, typeof(ListView));
            if ((bool)e.NewValue)
            {
                // Enabling the feature, so add the change handler
                descriptor.AddValueChanged(listView, OnListViewItemsSourceValueChanged);
            }
            else
            {
                // Disabling the feature, so remove the change handler
                descriptor.RemoveValueChanged(listView, OnListViewItemsSourceValueChanged);
            }
        }
    }

    // Handler for changes to the ListView's ItemsSource updates the column widths
    private static void OnListViewItemsSourceValueChanged(object sender, EventArgs e)
    {
        // Get a reference to the ListView's GridView...
        var listView = sender as ListView;
        if (null != listView)
        {
            var gridView = listView.View as GridView;
            if (null != gridView)
            {
                // And update its column widths
                UpdateColumnWidths(gridView);
            }
        }
    }
}

 

[Click here to download the sample application demonstrating everything described here.]

 

What's neat is how something that started out as a minor annoyance turned into a great learning opportunity! I haven't needed to use DependencyPropertyDescriptor before now, but it's definitely something I'll keep in mind next time something like this comes up. And while I've made use of attached behaviors in the past, I didn't initially think to use one here - my thanks go out to Marlon Grech and Dr. WPF for encouraging me to do so. As it turns out, I like the attached behavior solution best of all for its simplicity, clarity, and separation of concerns. I've incorporated this change into my project and now my ListViews are behaving exactly how I want them to!

Tags: WPF