3

In my Metro application, I have a data source containing a certain number of items (say 25). I have a ListView that presents those items. My problem is that the ListView have a size that allows it to display, say, 6.5 items, so that the last item it displays is cut in half. If the resolution changes, it might display 4 items, or 8.2 items, or whatever. What I'd like is that the ListView shows exactly the number of items that fits in the height of the control, instead of clipping the last item.

Right now, I see two possible half-solutions, none of which is optimal:

  1. Set the height of the ListView to a fixed height that is a multiple of the item size. This does not scale with changes in resolution.

  2. Limit the number of items in the data source. This does not scale either.

So my question is, how can I get the ListView to only display complete items (items where all edges are inside the viewport/listview), and hide the rest?

dbc
  • 104,963
  • 20
  • 228
  • 340
Hallgeir
  • 1,213
  • 1
  • 14
  • 29
  • 1
    I think option #1 that you mentioned can be made to scale with some additional code to handle resolution / orientation / view-state (filled, snapped etc) changes. – Krishna Sep 11 '12 at 07:15
  • 1
    A simple thing to do here is to get at runtime the ActualHeight of the list then the ActualHeight of a single listitem then you can calculate how many items can fit in available space and change the height of the list. I'm not sure if it's the best solution but i did used it and worked pretty well for me. – Novitchi S Sep 11 '12 at 07:33
  • @NovitchiS Thanks, that seems like the simplest way to do it for now... If you post this as an answer I'll accept this as the answer to my question. – Hallgeir Sep 11 '12 at 11:33

3 Answers3

4

ListView inherits from ItemsControl, so one more optimized solution consists in injecting custom panel (overriding measure by custom clipping display) in ItemsPanel

something like this(sorry, i did not try to compile):

protected override Size MeasureOverride(Size constraint)
{
 if (this.VisualChildrenCount <= 0)
  return base.MeasureOverride(constraint);
 var size = ne Size(constraint.Width,0);
 for(int i = 0; i < this.visualChildrenCount; i++)
 {
  child.Measure(size);
  if(size.height + child.desiredSize > constraint.height)
   break;
  size.Height += child.DesiredSize;
 }
 return size;
}
JesuX
  • 41
  • 2
  • 1
    Thanks, but it appears that the constraint's Height is set to infinity, and the child's DesiredSize height is set to 0 after measurement... – Hallgeir Sep 11 '12 at 08:46
3

My final solution was to combine the suggestions of @NovitchiS and @JesuX.

I created a stack panel override, and listened to the LayoutUpdated event. My final solution:

class HeightLimitedStackPanel : StackPanel
{
    public HeightLimitedStackPanel() : base()
    {
        this.LayoutUpdated += OnLayoutUpdated;
    }

    double GetSizeOfVisibleChildren(double parentHeight)
    {
        double currentSize = 0;
        bool hasBreaked = false;
        for (int i = 0; i < Children.Count; i++)
        {
            var child = Children[i];
            if (currentSize + child.DesiredSize.Height > parentHeight)
            {
                hasBreaked = true;
                break;
            }
            currentSize += child.DesiredSize.Height;
        }
        if (hasBreaked) return currentSize;

        return parentHeight;
    }

    double ParentHeight
    {
        get 
        {
            ItemsPresenter parent = VisualTreeHelper.GetParent(this) as ItemsPresenter;
            if (parent == null)
                return 0;

            return parent.ActualHeight;
        }
    }

    double previousHeight = 0;
    int previousChildCount = 0;
    protected void OnLayoutUpdated(object sender, object e)
    {
        double height = ParentHeight;
        if (height == previousHeight && previousChildCount == Children.Count) return;
        previousHeight = height;
        previousChildCount = Children.Count;

        this.Height = GetSizeOfVisibleChildren(height);
    }
}
Hallgeir
  • 1,213
  • 1
  • 14
  • 29
0

The answer from @JesuX is the better approach -- if done correctly. The following ListView subclass works fine for me:

public sealed class IntegralItemsListView : ListView
{
    protected override Size MeasureOverride(Size availableSize)
    {
        Size size = base.MeasureOverride(availableSize);
        double height = 0;
        if (Items != null)
        {
            for (int i = 0; i < Items.Count; ++i)
            {
                UIElement itemContainer = (UIElement)ContainerFromIndex(i);
                if (itemContainer == null)
                {
                    break;
                }

                itemContainer.Measure(availableSize);
                double childHeight = itemContainer.DesiredSize.Height;
                if (height + childHeight > size.Height)
                {
                    break;
                }

                height += childHeight;
            }
        }

        size.Height = height;
        return size;
    }
}

One caveat -- if you plop an IntegralItemsListView into a Grid, it will have

VerticalAlignment="Stretch"

by default, which defeats the purpose of this class.

Also: If the items are of uniform height, the method can obviously be simplified:

protected override Size MeasureOverride(Size availableSize)
{
    Size size = base.MeasureOverride(availableSize);
    size.Height = (int)(size.Height / ItemHeight) * ItemHeight;
    return size;
}
Petter Hesselberg
  • 5,062
  • 2
  • 24
  • 42