I realize this is an old question, but I found this post while trying to get star based solution for multiple columns in a ListView
/GridView
. So I figured I could help some future folks out, as it will answer this question as well.
I had initially implemented a WidthConverter
but this has the obvious limitation that the last column won't "fill", and it was never guaranteed that the row would fit its space, but here it is for those who are curious:
public class WidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var actualWidth = (double)value;
var desiredPercentage = SafeConvert(parameter);
if (actualWidth == 0.0 || desiredPercentage == 0.0) return double.NaN;
var result = actualWidth * (desiredPercentage / 100);
return result;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private double SafeConvert(object pInput)
{
if (pInput == null) return default;
if (double.TryParse(pInput.ToString(), out var result))
{
return result;
}
return default;
}
}
<GridViewColumn Header="My Header"
DisplayMemberBinding="{Binding MyColumnData}"
Width="{Binding Path=ActualWidth, ElementName=MyListView, Converter={StaticResource WidthConverter}, ConverterParameter='10.5'}" />
This works, but it's very reliant on the programmer to hard code parameter values and messy in the sense that the binding needs the ActualWidth
property from the ListView
element. Ultimately it's fairly limited to what it can actually do, especially considering most WPF folks are used to working with GridLength
when it comes to star sizing.
I took various bits and pieces from the solutions posted here as well as elsewhere and developed an MVVM friendly solution based on Attached Properties and Behaviors.
I created an Attached Property using GridLength
to leverage the existing absolute/auto/star logic matching XAML width patterns we're all used to.
public class ColumnAttachedProperties : DependencyObject
{
public static readonly DependencyProperty GridLength_WidthProperty = DependencyProperty.RegisterAttached(
name: "GridLength_Width",
propertyType: typeof(GridLength),
ownerType: typeof(ColumnAttachedProperties),
defaultMetadata: new FrameworkPropertyMetadata(GridLength.Auto));
public static GridLength GetGridLength_Width(DependencyObject dependencyObject)
=> (GridLength)dependencyObject.GetValue(GridLength_WidthProperty);
public static void SetGridLength_Width(DependencyObject dependencyObject, string value)
=> dependencyObject.SetValue(GridLength_WidthProperty, value);
}
The Attached Behavior hooks to both the Loaded and SizeChanged events, and performs some shared logic to size the columns.
Essentially, on the first pass over the columns, it will tally the star values (but doesn't set a width for star columns yet), and then on the second pass targets the star columns, setting the width as a percentage of the remaining width available. I am sure this could be optimized in some way.
public static class ListViewStarSizingAttachedBehavior
{
public static readonly DependencyProperty UseGridLength_WidthProperty = DependencyProperty.RegisterAttached(
name: "UseGridLength_Width",
propertyType: typeof(bool),
ownerType: typeof(ListViewStarSizingAttachedBehavior),
new UIPropertyMetadata(false, RegisterEventHandlers));
public static bool GetUseGridLength_Width(DependencyObject dependencyObject)
=> (bool)dependencyObject.GetValue(UseGridLength_WidthProperty);
public static void SetUseGridLength_Width(DependencyObject dependencyObject, bool value)
=> dependencyObject.SetValue(UseGridLength_WidthProperty, value);
private static void RegisterEventHandlers(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ListView listView)
{
if (e.NewValue is bool booleanValue && booleanValue == true)
{
listView.SizeChanged += ListView_SizeChanged;
listView.Loaded += ListView_Loaded;
}
else
{
listView.SizeChanged -= ListView_SizeChanged;
listView.Loaded -= ListView_Loaded;
}
}
}
private static void ListView_Loaded(object sender, RoutedEventArgs e)
{
CalculateGridColumnWidths(sender);
}
private static void ListView_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (sender is ListView listView && !listView.IsLoaded) return;
CalculateGridColumnWidths(sender);
}
private static void CalculateGridColumnWidths(object sender)
{
if (sender is ListView listView && listView.View is GridView gridView)
{
// the extra offset may need to be altered per your application.
var scrollOffset = SystemParameters.VerticalScrollBarWidth - 7;
var remainingWidth = listView.ActualWidth - scrollOffset;
var starTotal = 0.0;
foreach (var column in gridView.Columns)
{
var gridLength = ColumnAttachedProperties.GetGridLength_Width(column);
if (gridLength.IsStar)
{
// Get the cumlative star value while passing over the columns
// but don't set their width until absolute and auto have been set.
starTotal += gridLength.Value;
continue;
}
if (gridLength.IsAbsolute)
{
column.Width = gridLength.Value;
}
else
{
column.Width = double.NaN;
}
remainingWidth -= column.ActualWidth;
}
if (starTotal == 0.0) return;
// now eval each star column
foreach (var column in gridView.Columns)
{
var gridLength = ColumnAttachedProperties.GetGridLength_Width(column);
if (!gridLength.IsStar) continue;
var starPercent = (gridLength.Value / starTotal);
column.Width = remainingWidth * starPercent;
}
}
}
}
And finally to use it in XAML, the ListView
needs implement the attached behavior, and each of the columns implement the attached property. However, if you leave the attached property off the column, it will default Auto
as per the DependencyProperty
s defaultMetadata.
<ListView local:ListViewStarSizingAttachedBehavior.UseGridLength_Width="True"
ItemsSource="{Binding MyItems}" >
<ListView.View>
<GridView>
<GridView.Columns>
<!-- Absolute -->
<GridViewColumn local:ColumnAttachedProperties.GridLength_Width="250"
Header="Column One"
DisplayMemberBinding="{Binding Item1}" />
<!-- Star -->
<GridViewColumn local:ColumnAttachedProperties.GridLength_Width="2*"
Header="Column Two"
DisplayMemberBinding="{Binding Item2}" />
<!-- Auto -->
<GridViewColumn local:ColumnAttachedProperties.GridLength_Width="Auto"
Header="Column Three"
DisplayMemberBinding="{Binding Item3}" />
<!-- Star -->
<GridViewColumn local:ColumnAttachedProperties.GridLength_Width="*"
Header="Column Four"
DisplayMemberBinding="{Binding Item4}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
Seems to work well so far, but I am sure there are some edge cases which will need to be considered. The XAML is a ton cleaner than the width converter. The overall result is more flexible than those already posted as well.