ygoe
ygoe

Reputation: 20354

UniformGrid with per-row MinHeight

My application displays multiple items in an ItemsControl that uses a UniformGrid as its ItemsPanel.

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <!-- Arrange all items vertically, distribute the space evenly -->
        <UniformGrid Columns="1"/>
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

Now each of the items will take as much space as there is and the full width. That's how it should be. But each item must not get smaller than a certain height, say 250 pixels. Therefore, my ItemsControl is put inside a ScrollViewer which will start to scroll if the minimum height doesn't fit on the screen anymore (but fill until then).

Setting each item's MinHeight property to 250 will make it taller but won't change how the UniformGrid arranges them in any way, so they're clipped. That's not the solution.

Setting the entire ItemsControl's or the UniformGrid's MinHeight property does work, but then I need to calculate it from the number of items. A good place to get the updated number of items is to override the OnItemsChanged method in my ItemsControl code-behind. But that won't give me the actual items panel. I have the following code for that in another project, but it always returns null here:

ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);

So that doesn't work, too.

Walking the visual tree is probably not an option, too, because it's too late. I need to set the minimum height before that items panel is arranged so that it won't flicker. (I generate complex content on a size change so any late layouting must be avoided.)

What options do I have to get what I need?

I may drop the UniformGrid in favour of something else, if it's possible. Any suggestions?

Upvotes: 2

Views: 2984

Answers (3)

ygoe
ygoe

Reputation: 20354

I found that I've solved the problem with the following code some time ago. I have a derived class in my application so I can override the MeasureOverride method of ItemsControl.

class MyItemsControl : ItemsControl
{
    // ...

    protected override Size MeasureOverride(Size constraint)
    {
        Size size = base.MeasureOverride(constraint);

        // Keep minimum height per item
        int minHeight = Items.Count * 250;
        if (size.Height < minHeight)
        {
            size.Height = minHeight;
        }
        return size;
    }
}

The XAML code of my class looks like this:

<ItemsControl x:Class="MyItemsControl">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- Arrange all items vertically, distribute the space evenly -->
            <UniformGrid Columns="1"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <!-- Templates and other stuff ... -->
</ItemsControl>

Upvotes: 0

Saraf Talukder
Saraf Talukder

Reputation: 376

You can employ a lot of techniques to achieve this. However on the approach is to extend the UniformGrid.

The solution in this case is to override the MeasureOverride() to calculate the new size either based on the min height of all the children in the row or the MinRowHeight, which ever is greater.

Usage :

<UserControl x:Class="SOF.UniformGridMinHeight"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:sof="clr-namespace:SOF"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
<sof:UniformGridEx Columns="3" MinRowHeight="100">
        <Border Background="Red"/>
        <Border Background="Blue"/>
        <Border Background="Green"/>
        <Border Background="Yellow"/>
        <Border Background="Pink"/>
        <Border Background="Purple"/>
        <Border Background="LightCoral"/>
        <Border Background="DimGray"/>
        <Border Background="OrangeRed"/>
        <Border Background="Olive"/>
        <Border Background="Salmon"/>
</sof:UniformGridEx>

Extended UniformGrid:

public class UniformGridEx : UniformGrid
{
    public static readonly DependencyProperty MinRowHeightProperty = DependencyProperty.Register(
        "MinRowHeight", typeof(double), typeof(UniformGrid), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure));

    private int _columns;
    private int _rows;

    public double MinRowHeight
    {
        get { return (double)GetValue(MinRowHeightProperty); }
        set { SetValue(MinRowHeightProperty, value); }
    }

    protected override Size MeasureOverride(Size constraint)
    {
        UpdateComputedValues();
        var calculatedSize = base.MeasureOverride(constraint);
        if (MinRowHeight > 0)
        {
            calculatedSize = new Size(calculatedSize.Width, Math.Max(calculatedSize.Height, _rows * MinRowHeight));
        }
        return calculatedSize;
    }

    private void UpdateComputedValues()
    {
        _columns = Columns;
        _rows = Rows;

        if (FirstColumn >= _columns)
        {
            FirstColumn = 0;
        }

        if ((_rows == 0) || (_columns == 0))
        {
            int nonCollapsedCount = 0;

            for (int i = 0, count = InternalChildren.Count; i < count; ++i)
            {
                UIElement child = InternalChildren[i];
                if (child.Visibility != Visibility.Collapsed)
                {
                    nonCollapsedCount++;
                }
            }

            if (nonCollapsedCount == 0)
            {
                nonCollapsedCount = 1;
            }

            if (_rows == 0)
            {
                if (_columns > 0)
                {
                    _rows = (nonCollapsedCount + FirstColumn + (_columns - 1)) / _columns;
                }
                else
                {
                    _rows = (int)Math.Sqrt(nonCollapsedCount);
                    if ((_rows * _rows) < nonCollapsedCount)
                    {
                        _rows++;
                    }
                    _columns = _rows;
                }
            }
            else if (_columns == 0)
            {
                _columns = (nonCollapsedCount + (_rows - 1)) / _rows;
            }
        }
    }
}

Upvotes: 4

Sheridan
Sheridan

Reputation: 69959

A UniformGrid performs a very exact task... providing all items with the exact same sized space to render themselves. If you don't require that functionality, then you are using the wrong Panel. Try using the WrapPanel Class instead, which can better handle different sized children:

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <WrapPanel />
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

Upvotes: 0

Related Questions