Click here to Skip to main content
15,895,011 members
Articles / Desktop Programming / XAML

UWP Stretch WrapGrid

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
25 Mar 2016CPOL2 min read 16.9K   231   2  
A Behaviour for windows universal plaform to force the WrapGrid itemscontrol of a List/GridView to dynamically change the width of childitems so as to fill the entire row space.

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.

C#
/// <summary>
/// Declare new attached property. This property specifies the minium width
/// for child items in an items wrap grid panel. Setting this property
/// to an non zero value will enable dynamic sizing of items so that
/// when items are wrapped the items control is always filled out horizontally
/// i.e. the width of items are increased to fill the empty space.
/// </summary>
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. 

C#
/// <summary>
       /// Only applicable when MinItemWidth is non zero. Typically the logic
       /// behind MinItemWidth will only trigger if the number of items is
       /// more than or equal to what a single row will accomodate. This property
       /// specifies that the layout logic is also performed when there are
       /// less items than what a single row will accomodate.
       /// </summary>
       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.

C#
/// <summary>
       /// This method will be called when the NavigateTo
       /// property is changed
       /// </summary>
       /// <param name="s">The sender (the Frame)</param>
       /// <param name="e">Some additional information</param>
       public static void OnMinItemWidthChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
       {
           //If the host object is a frame.
           if (s is ListViewBase)
           {
               //Unbox.
               ListViewBase f = s as ListViewBase;


               //Remove previous handler (if any.)
               f.SizeChanged -= listView_SizeChanged;


               //If property is a positive value.
               if (((double)e.NewValue) > 0)
               {
                   //Attach handling.
                   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.

C#
/// <summary>
      ///
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private static void listView_SizeChanged(object sender, SizeChangedEventArgs e)
      {
          //Unbox the sender.
          var itemsControl = sender as ListViewBase;


          //Retrieve items panel.
          ItemsWrapGrid itemsPanel = itemsControl.ItemsPanelRoot as ItemsWrapGrid;


          //If the items panel is a wrap grid.
          if (itemsPanel != null)
          {
              //Get total size (leave room for scrolling.)
              var total = e.NewSize.Width - 10;


              //Minimum item size.
              var itemMinSize = (double)itemsControl.GetValue(MinItemWidthProperty);


              //How many items can be fit whole.
              var canBeFit = Math.Floor(total / itemMinSize);


              //logic that if the total items
              //are less then the number of items that
              //would fit then devide the total size by
              //the number of items rather than the number
              //of items that would actually fit.
              if ((bool)itemsControl.GetValue(FillBeforeWrapProperty) &&
                  itemsControl.Items.Count > 0 &&
                  itemsControl.Items.Count < canBeFit)
              {
                  canBeFit = itemsControl.Items.Count;
              }


              //Set the items Panel item width appropriately.
              //Note you will need your container to stretch
              //along with the items panel or it will look
              //strange.
              //  <GridView.ItemContainerStyle>
              //< Style TargetType = "GridViewItem" >
              //< Setter Property = "HorizontalContentAlignment" Value = "Stretch" />
              // < Setter Property = "HorizontalAlignment" Value = "Stretch" />
              //</ Style >
              // </ GridView.ItemContainerStyle >
              itemsPanel.ItemWidth = total / canBeFit;
          }
      }

To use the behaviour class in your view first add a namespace reference

XML
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.

XML
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Ireland Ireland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --