3

I have a custom control ListBox that I would like to adjust its size according to the size of its ItemsPanel. For the items panel, I have a custom control WrapPanel that arranges the items accordingly. As you can see in the screen shot, the ListBox prefers to size itself to its parent control or availableSize.

So then I tried to make a custom control Grid, that had an ItemsSource property that handed the items to its listbox. But that didn't work either. When the Grid would arrange, then the ListBox would Arrange, which would cause the Grid to Arrange and so on.

So my question is, how to I create a custom control that has an ItemsSource property and an ItemsPresenter that sizes itself according to the contents of its children??

Screenshot of ListBox Custom Control

Askolein
  • 3,250
  • 3
  • 28
  • 40
Jesse Seger
  • 951
  • 1
  • 13
  • 31

2 Answers2

2

You just have to set your ListBox HorizontalAlignment to Left, and VerticalAlignment to Top. This should do the trick.

Simple example :

MainWindow.xaml

<Window x:Class="ListBoxFitItemsPanel.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="400" Width="400">
    <ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Disabled" Background="Red" HorizontalAlignment="Left" VerticalAlignment="Top" >
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel Background="AntiqueWhite" Margin="5" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>

        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
    </ListBox>
</Window>

enter image description here

EDIT : It also works in a Binding scenario :

MainWindow.xaml

<Window x:Class="ListBoxFitItemsPanel.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ListBoxFitItemsPanel"
        Title="MainWindow" Height="400" Width="400">
    <Window.Resources>
        <DataTemplate DataType="{x:Type local:Item}">
            <Rectangle Width="100" Height="100" Fill="LightSlateGray" Stroke="Black" StrokeThickness="1" Margin="5" />
        </DataTemplate>
    </Window.Resources>
    <ListBox ItemsSource="{Binding Items}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Disabled" Background="Red" HorizontalAlignment="Left" VerticalAlignment="Top" >
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel Background="AntiqueWhite" Margin="5" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
</Window>

MainWindow.xaml.cs

using System.Collections.Generic;
using System.Windows;

namespace ListBoxFitItemsPanel
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        public IEnumerable<Item> Items
        {
            get
            {
                for (int i = 0; i < 9; i++)
                {
                    yield return new Item();
                }
            }
        }
    }

    public class Item { }
}
Sisyphe
  • 4,626
  • 1
  • 25
  • 39
  • That's not quit it. You see, the ListBox still arranges to fit within the window. Then the WrapPanel arranges its children. What I want is the ListBox to fit nicely around its own ItemsPanel which has already determined its size. – Jesse Seger Feb 08 '13 at 16:07
  • +1 This is the correct answer. The `ListBox` is filling it's container panel. You need to align it within it's parent by using `HorizontalAlignment` and `VerticalAlignment` – Cameron MacFarland Feb 08 '13 at 16:10
  • @Jesse : In that case, it does not. The double layout pass measures and arranges the top container based on the size returned by all children. In the screenshot I posted, the Window backgroudn is white whereas the Listbox backgroudn is red. I set a margin to make it visible. You can see that the ListBox only took the space that was needed by the wrap panel. If you tried to fix the wrap panel height or width, the ListBox would not take more. – Sisyphe Feb 08 '13 at 16:12
  • You guys are exactly correct. But I have a custom wrap panel as the ItemsPanel that says I'm going to by 800 x 600. It does not arrange itself based on the available size. It is only concerned with it's child components. So, once the wrap panel is sized, how do you set the ListBox size? – Jesse Seger Feb 08 '13 at 16:58
  • I think I figured something out. My MeasureOverride on my custom WrapPanel was not working correctly. I'll post an update once I pin point the issue. – Jesse Seger Feb 08 '13 at 18:52
1

This is what I had to do to get the functionality I wanted. First I have to create my SquareWrapPanel. This is where the magic happens and arranges my items the way I want.

protected override Size ArrangeOverride(Size finalSize)
    {
        return ArrangeByChildCount(finalSize);
    }

    private Size ArrangeByChildCount(Size finalSize)
    {

        double childWidth = 0;
        double childHeight = 0;


        foreach (UIElement e in Children)
        {
            e.Measure(finalSize);

            childWidth = e.DesiredSize.Width;
            childHeight = e.DesiredSize.Height;
        }

        if (Children.Count > 0)
        {
            int square = (int)Math.Sqrt(Children.Count);


            int rowCount = square + Children.Count % square;
            int columnCount = square;

            double height = rowCount * childHeight;
            double width = columnCount * childWidth;

            Size size = new Size(width, height);
            base.ArrangeOverride(size);
            return size;
        }
        else
        {
            return new Size(300, 300);
        }
    }

Then I created a custom panel that extended ItemsControl. That way I could bind a collection of items to it. There is nothing in the code behind, but here is the style I had to use.

    <Style TargetType="{x:Type local:SquareItemsPanel}" BasedOn="{StaticResource {x:Type ItemsControl}}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ItemsControl">
                <Border BorderBrush="Black" BorderThickness="2" CornerRadius="4">
                    <Expander x:Name="exp" Header="View">
                        <local:SquareWrapPanel IsItemsHost="True"/>
                    </Expander>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Jesse Seger
  • 951
  • 1
  • 13
  • 31