I know this is an old post, but for anyone coming across it who is binding to the ListView ItemsSource
property from their ViewModel, perhaps a MultiValue converter can help. I know there are other ways of solving the issue, but I like the level of control I can maintain using this method and it's equally applicable to a ListView
and GridViewColumn
.
I also like the fact that, using the converter, my ICollection
object can either contain strings/primitive types or class objects whose properties can be accessed by reflection.
In brief: the array of values passed in to the converter are those required to create a typeface and to instantiate a FormattedText
object, min and max widths of the UIElement
, and the bound ICollection
object itself. The parameter argument is actually an array containing the property name of a class object (or null if none is required) and a Thickness
object to add padding to the calculated width. The Thickness
object allows me to cater for any padding/margin designs that form part of my ListView
.
Note: the default padding on a ListView ItemsPane is {12,0,12,0} and on a GridViewColumn {6,0,6,0}. This might account for the 12-20 pixel error mentioned by BK.
The converter itself looks like this:
/// <summary>
/// Iterates a collection of items to calculate the maximum text width of those items.
/// Items can either be primitive types and strings or objects with a property that is
/// a primitive type or string.
/// </summary>
public sealed class ItemsToWidthConverter : IMultiValueConverter
{
//Constants for array indexes.
private const int FONTFAMILY_ID = 0;
private const int FONTSTYLE_ID = 1;
private const int FONTWEIGHT_ID = 2;
private const int FONTSTRETCH_ID = 3;
private const int FONTSIZE_ID = 4;
private const int FOREGROUND_ID = 5;
private const int MINWIDTH_ID = 6;
private const int MAXWIDTH_ID = 7;
private const int ICOLLECTION_ID = 8;
private const int PARAMETERPROPERTY_ID = 0;
private const int PARAMETERGAP_ID = 1;
/// <summary>
/// Converts collection items to a width.
/// Parameter[0] is the property name of an object. If no property name is needed, pass in null.
/// Parameter[1] is the padding to be added to the calculated width. If no padding is needed, pass in a Thickness of 0.
/// Note: a ListViewItem has default padding of {12,0,12,0}. A GridViewColumn has default padding of {6,0,6,0}.
/// </summary>
/// <param name="values">Array of 9 objects {FontFamily, FontStyle, FontWeight, FontStretch, double [FontSize], Brush, double [MinWidth], double [MaxWidth], ICollection}</param>
/// <param name="targetType">Double</param>
/// <param name="parameter">Array of 2 objects {string [Property Name], Thickness}</param>
/// <param name="culture">Desired CultureInfo</param>
/// <returns>Width of widest item including padding or Nan if none is calculated.</returns>
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
// Throw error if passed parameters are incorrect.
if (values.Length != 9) throw new Exception("Incorrect number of items passed in 'values'.");
if (!(parameter.GetType().IsArray)) throw new Exception("'Parameter' must be an array.");
var prm = (object[])parameter;
if (prm.Length !=2) throw new Exception("Incorrect number of items passed in 'parameter'.");
if (prm[PARAMETERPROPERTY_ID] != null && !(prm[PARAMETERPROPERTY_ID] is string property)) throw new Exception("'Parameter['" + PARAMETERPROPERTY_ID + "]' is neither null nor of type 'string'.");
if (!(prm[PARAMETERGAP_ID] is Thickness margin)) throw new Exception("'Parameter['" + PARAMETERGAP_ID + "]' is not of type 'Thickness'.");
if (values[ICOLLECTION_ID] == null) return double.NaN;
if (!(values[FONTFAMILY_ID] is FontFamily family)) throw new Exception("'Value['" + FONTFAMILY_ID + "]' is not of type 'FontFamily'.");
if (!(values[FONTSTYLE_ID] is FontStyle style)) throw new Exception("'Value['" + FONTSTYLE_ID + "]' is not of type 'FontStyle'.");
if (!(values[FONTWEIGHT_ID] is FontWeight weight)) throw new Exception("'Value['" + FONTWEIGHT_ID + "]' is not of type 'FontWeight'.");
if (!(values[FONTSTRETCH_ID] is FontStretch stretch)) throw new Exception("'Value['" + FONTSTRETCH_ID + "]' is not of type 'FontStretch'.");
if (!(values[FONTSIZE_ID] is double size)) throw new Exception("'Value['" + FONTSIZE_ID + "]' is not of type 'double'.");
if (!(values[FOREGROUND_ID] is Brush foreground)) throw new Exception("'Value['" + FOREGROUND_ID + "]' is not of type 'Brush'.");
if (!(values[MINWIDTH_ID] is double minWidth)) throw new Exception("'Value['" + MINWIDTH_ID + "]' is not of type 'double'.");
if (!(values[MAXWIDTH_ID] is double maxWidth)) throw new Exception("'Value['" + MAXWIDTH_ID + "]' is not of type 'double'.");
if (!(values[ICOLLECTION_ID] is ICollection col)) throw new Exception("'Value['" + ICOLLECTION_ID + "]' is not of type 'ICollection'.");
// Conver font properties to a typeface.
var typeFace = new Typeface(family, style, weight, stretch);
// Initialise the max_width variable at 0.
var widest = 0.0;
foreach (var item in col)
{
// If property parameter is null, assume the ICollection contains primitives or strings.
if (prm[PARAMETERPROPERTY_ID] == null)
{
if (item.GetType().IsPrimitive || item is string)
{
var text = new FormattedText(item.ToString(),
culture,
FlowDirection.LeftToRight,
typeFace,
size,
foreground,
null,
TextFormattingMode.Ideal);
if (text.WidthIncludingTrailingWhitespace > widest)
widest = text.WidthIncludingTrailingWhitespace;
}
}
else
// Property parameter contains a string, so assume ICollection is an object
// and use reflection to get property value.
{
if (item.GetType().GetProperty(prm[PARAMETERPROPERTY_ID].ToString()) != null)
{
var propertyValue = item.GetType().GetProperty(prm[PARAMETERPROPERTY_ID].ToString()).GetValue(item);
if (propertyValue.GetType().IsPrimitive || propertyValue is string)
{
var text = new FormattedText(propertyValue.ToString(),
culture,
FlowDirection.LeftToRight,
typeFace,
size,
foreground,
null,
TextFormattingMode.Display);
if (text.WidthIncludingTrailingWhitespace > widest)
widest = text.WidthIncludingTrailingWhitespace;
}
}
}
}
// If no width could be calculated, return Nan which sets the width to 'Automatic'
if (widest == 0) return double.NaN;
// Add the left and right thickness values to the calculated width and
// check result is within min and max values.
{
widest += ((Thickness)prm[PARAMETERGAP_ID]).Left + ((Thickness)prm[PARAMETERGAP_ID]).Right;
if (widest < minWidth || widest > maxWidth) return double.NaN;
return widest;
}
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And it can be implemented in XAML as below.
Eg 1 - a simple ListView within a template:
<ListView ItemsSource="{Binding MyStrings}">
<ListView.Width>
<MultiBinding Converter="{StaticResource ItemsToWidthConverter}">
<MultiBinding.ConverterParameter>
<x:Array Type="sys:Object">
<x:Null />
<Thickness>12,0,12,0</Thickness>
</x:Array>
</MultiBinding.ConverterParameter>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontFamily" />
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontStyle" />
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontWeight" />
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontStretch" />
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="FontSize" />
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Foreground" />
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="MinWidth" />
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="MaxWidth" />
<Binding Path="MyStrings" />
</MultiBinding>
</ListView.Width>
</ListView>
Eg 2 - ListView with GridView
<ListView ItemsSource="{Binding Employees}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding EmployeeName}">
<GridViewColumn.Width>
<MultiBinding Converter="{StaticResource ItemsToWidthConverter}">
<MultiBinding.ConverterParameter>
<x:Array Type="sys:Object">
<sys:String>EmployeeName</sys:String>
<Thickness>6,0,6,0</Thickness>
</x:Array>
</MultiBinding.ConverterParameter>
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontFamily" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontStyle" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontWeight" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontStretch" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="FontSize" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="Foreground" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="MinWidth" />
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ListView}" Path="MaxWidth" />
<Binding Path="Employees" />
</MultiBinding>
</GridViewColumn.Width>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
There are plenty of other binding variations/combinations, of course.
The only issue with this approach is that the MultiBinding
isn't notified if, say, an ObservableCollection
is changed (for example by adding an item), so additional notification code might be needed. I haven't encountered this issue as I mostly use this technique for conditional lists whereby the entire collection is replaced (and therefore OnPropertyChanged
is fired, which is consumed by the MultiBinding), but SO gives examples of how to code such notification.