Dynamic FluidWrapPanel using AdaptiveTriggers

A few days ago, I was asked if the FluidWrapPanel could resize its children uniformly to use the maximum available space. My initial response was that FluidWrapPanel acts as a container and does not influence its children’s sizes. If it resized its children’s size to a very small size then it may make the content unreadable.

Then I got further details of the requested scenario – The FluidWrapPanel has 12 children. When the size of the FluidWrapPanel changes the layout of the children should also change (they should also be resized to occupy the maximum available space). For example when the FluidWrapPanel is displayed in Portrait mode (having a certain width), the children should be displayed in a 6 x 2 layout, when in Landscape mode (having medium size) they should be displayed in 4 x 3 layout and when in Landscape mode (with maximum width) the layout should change to 3 x 4. All this should happen without affecting the fluidity of the FluidWrapPanel.

DynamicFWP

Well, let me explain FluidWrapPanel’s behavior in this scenario. FluidWrapPanel will not know whether the children should be displayed in a 6 x 2 layout, a 4 x 3 layout or in a single row. Its logic for arranging its children is governed by the following factors

  • ItemWidth dependency property
  • ItemHeight dependency property

If the child’s width and height are less than the ItemWidth and ItemHeight respectively, there will be a gap between adjacent children. If they are more, the adjacent children with overlap each other.

So the solution is to listen to the SizeChanged event of the FluidWrapPanel and when it occurs

  • Calculate which layout needs to be displayed in the FluidWrapPanel
  • Based on the layout and the size of the FluidWrapPanel, calculate the desired item width and height.
  • Set each of the child’s width and height to the calculated item width and height respectively.
  • Set the ItemWidth and ItemHeight dependency properties of the FluidWrapPanel to the calculated item width and height respectively.

This solution works best when all the children are of same size i.e. having width and height equal to the ItemWidth and ItemHeight respectively.

Since this solution is applicable mainly in UWP applications, I thought it was best to utilize AdaptiveTriggers to solve this issue with ease.

I made a sample UWP project to implement this solution. Here is what I did.

1. Create a New Universal Project and add reference to WPFSpark.UWP NuGet package.

2. Create a UserControl called FluidItemControl which will be a child to the FluidWrapPanel. It just consists of a border (having random background) and a textblock (showing a number).

I came upon a strange behavior of Page class. If you add a new dependency property to MainPage.xaml.cs and try to bind to it in MainPage.xaml, then that property will not be recognized. After trying out several ways unsuccessfully, I found a workaround in StackOverflow. It seems you have to define a base class, say PageBase, which derives from Page and then define your dependency property there. Then MainPage should derive from PageBase. Now the binding will work. Strange but true!
3. So here is the definition of PageBase class.
public class PageBase : Page
{
    public enum PageDisplayType
    {
        None = 0,
        Display3x4 = 1,
        Display4x3 = 2,
        Display6x2 = 3
    }

    #region PageDisplay
    /// <summary>
    /// PageDisplay Dependency Property
    /// </summary>
    public static readonly DependencyProperty PageDisplayProperty =
        DependencyProperty.Register("PageDisplay", typeof(PageDisplayType), typeof(MainPage),
            new PropertyMetadata(PageBase.PageDisplayType.None, OnPageDisplayChanged));

    /// <summary>
    /// Gets or sets the PageDisplay property. This dependency property 
    /// indicates the number of rows and columns to set in the FluidWrapPanel.
    /// </summary>
    public PageBase.PageDisplayType PageDisplay
    {
        get { return (PageBase.PageDisplayType)GetValue(PageDisplayProperty); }
        set { SetValue(PageDisplayProperty, value); }
    }

    /// <summary>
    /// Handles changes to the PageDisplay property.
    /// </summary>
    /// <param name="d">MainPage</param>
    /// <param name="e">DependencyProperty changed event arguments</param>
    private static void OnPageDisplayChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var page = (PageBase)d;
        var oldPageDisplay = (PageBase.PageDisplayType)e.OldValue;
        var newPageDisplay = page.PageDisplay;
        page.OnPageDisplayChanged(oldPageDisplay, newPageDisplay);
    }

    /// <summary>
    /// Provides derived classes an opportunity to handle changes to the PageDisplay property.
    /// </summary>
    /// <param name="oldPageDisplay">Old Value</param>
    /// <param name="newPageDisplay">New Value</param>
    async void OnPageDisplayChanged(PageBase.PageDisplayType oldPageDisplay, PageBase.PageDisplayType newPageDisplay)
    {
        switch (newPageDisplay)
        {
            case PageBase.PageDisplayType.Display3x4:
                rows = 3;
                columns = 4;
                break;
            case PageBase.PageDisplayType.Display4x3:
                rows = 4;
                columns = 3;
                break;
            case PageBase.PageDisplayType.Display6x2:
                rows = 6;
                columns = 2;
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(newPageDisplay), newPageDisplay, null);
        }

        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, RefreshPanel);
    }

    #endregion

    protected double rows = 0;
    protected double columns = 0;

    protected virtual void RefreshPanel()
    {
        
    }
}

4. I defined the AdaptiveTriggers in MainPage.xaml. These triggers will change the PageDisplay dependency property based on the width of the MainPage.

<local:PageBase x:Class="TestFWPanel.MainPage"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:local="using:TestFWPanel"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                xmlns:wpfSpark="using:WPFSpark"
                x:Name="RootPage"
                mc:Ignorable="d">
    <local:PageBase.Resources>
        <x:Double x:Key="SmallScreenWidth">0</x:Double>
        <x:Double x:Key="MediumScreenWidth">540</x:Double>
        <x:Double x:Key="LargeScreenWidth">900</x:Double>
    </local:PageBase.Resources>
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup>
                <VisualState x:Name="Small">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="{StaticResource SmallScreenWidth}" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="RootPage.PageDisplay"
                                Value="Display6x2"></Setter>
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="Medium">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="{StaticResource MediumScreenWidth}" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="RootPage.PageDisplay"
                                Value="Display4x3" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="Large">
                    <VisualState.StateTriggers>
                        <AdaptiveTrigger MinWindowWidth="{StaticResource LargeScreenWidth}" />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Target="RootPage.PageDisplay"
                                Value="Display3x4" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid x:Name="ContainerGrid"
              HorizontalAlignment="Stretch"
              VerticalAlignment="Stretch">
            <wpfSpark:FluidWrapPanel x:Name="fwPanel"
                                     Background="Beige"
                                     Margin="0"
                                     HorizontalAlignment="Stretch"
                                     VerticalAlignment="Stretch"
                                     ItemWidth="2"
                                     ItemHeight="2"
                                     IsComposing="True"
                                     SizeChanged="OnFWPSizeChanged"></wpfSpark:FluidWrapPanel>
        </Grid>
    </Grid>
</local:PageBase>

5. The size of the FluidWrapPanel also changes when the size of the MainPage changes. I am calling the RefreshPanel() method when this happens. This method updates the ItemWidth and ItemHeight properties along with the sizes of the children accordingly.

protected override void RefreshPanel()
{
    if ((rows.IsZero()) || (columns.IsZero()))
        return;

    var width = Math.Floor(fwPanel.Width/columns);
    var height = Math.Floor(fwPanel.Height/rows);

    foreach (var child in fwPanel.FluidItems.OfType<FluidItemControl>())
    {
        child.Width = width;
        child.Height = height;
    }

    fwPanel.ItemWidth = width;
    fwPanel.ItemHeight = height;
}

You can find the full source code here.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s