Introduction
The built in UWP GridView will wrap items to a new row when the space on one row is exceeded. However this can leave ugly empty space to the right of the control before or after overflow occurs. This behaviour fixes that problem by calculating how many items can fit on a row (based on a min item size) and then setting the width of all items in order to fill the entire row.
Background
See the following article for an introduction to the concept of attached behaviours.
http://www.codeproject.com/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF
Please note that the included behaviour class also includes code outlined in this article
https://marcominerva.wordpress.com/2013/03/07/how-to-bind-the-itemclick-event-to-a-command-and-pass-the-clicked-item-to-it/
Using the code
The behaviour exposes two properties
The first is MinItemWidth which specifies the minimum width you desire for each item.
public static readonly DependencyProperty MinItemWidthProperty =
DependencyProperty.RegisterAttached("MinItemWidth", typeof(double),
typeof(ListViewBehaviour), new PropertyMetadata(0, OnMinItemWidthChanged));
The second property is the badly named FillBeforeWrap which essentially means that the row will be filled even before overflow occurs. This would be in a scenario if there are less items than what a single row will accomodate.
public static readonly DependencyProperty FillBeforeWrapProperty =
DependencyProperty.RegisterAttached("FillBeforeWrap", typeof(bool),
typeof(ListViewBehaviour), new PropertyMetadata(false));
In the property changed callback, handling is attached for the size changed event. ListView base is used for compatibility with both ListView and GridView.
public static void OnMinItemWidthChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
if (s is ListViewBase)
{
ListViewBase f = s as ListViewBase;
f.SizeChanged -= listView_SizeChanged;
if (((double)e.NewValue) > 0)
{
f.SizeChanged += listView_SizeChanged;
}
}
}
In the size changed event handler the calculation for item width is performed. Note that the items panel root of the control must be a ItemsWrapGrid.
private static void listView_SizeChanged(object sender, SizeChangedEventArgs e)
{
var itemsControl = sender as ListViewBase;
ItemsWrapGrid itemsPanel = itemsControl.ItemsPanelRoot as ItemsWrapGrid;
if (itemsPanel != null)
{
var total = e.NewSize.Width - 10;
var itemMinSize = (double)itemsControl.GetValue(MinItemWidthProperty);
var canBeFit = Math.Floor(total / itemMinSize);
if ((bool)itemsControl.GetValue(FillBeforeWrapProperty) &&
itemsControl.Items.Count > 0 &&
itemsControl.Items.Count < canBeFit)
{
canBeFit = itemsControl.Items.Count;
}
itemsPanel.ItemWidth = total / canBeFit;
}
}
To use the behaviour class in your view first add a namespace reference
xmlns:behave="using:UtilitiesUniversal.Behaviours"
Then you use the attached properties as follows. Note that you also need to override the container style so it stretches along with the items.
<GridView Name="gridData" Margin="14,10,0,10" behave:ListViewBehaviour.MinItemWidth="{Binding FakeBinding, FallbackValue=250}" behave:ListViewBehaviour.FillBeforeWrap="{Binding FakeBinding, FallbackValue=True}">
<GridView.ItemContainerStyle>
<Style TargetType="GridViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style>
</GridView.ItemContainerStyle>
<GridView.ItemTemplate>
<DataTemplate>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
Points of Interest
It was my experience that unlike WPF, UWP will only allow the setting of an attached property value through a binding. That is the reason for the FakeBinding notation in the XAML above. I'm leveraging the FallBackValue argument for non existing binding in order to set the value.
History
Version 1.0