2

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?

ygoe
  • 18,655
  • 23
  • 113
  • 210

3 Answers3

4

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;
            }
        }
    }
}
Saraf Talukder
  • 376
  • 2
  • 12
0

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>
Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • I need the fill feature as long as there's enough space. And even when it's too small, the space should be evenly distributed among all items. And they need to stretch the entire width. A WrapPanel can never do that, it will always keep the items as small as possible. – ygoe Jun 02 '15 at 14:03
0

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>
ygoe
  • 18,655
  • 23
  • 113
  • 210