nnnnnn
nnnnnn

Reputation: 150080

Can a WPF ListBox's height be set to a multiple of its item height?

Is there some way to set the Height attribute of a WPF multi-select ListBox to be a multiple of the item height, similar to setting the size attribute of an html select element?

I have a business requirement to not have half an item showing at the bottom of the list (if it's a long list with a scrollbar), and not have extra white space at the bottom (if it's a short list with all items showing), but the only method I can find to do this is to just keep tweaking the Height until it looks about right.

(What else have I tried? I've asked colleagues, searched MSDN and StackOverflow, done some general Googling, and looked at what VS Intellisense offered as I edited the code. There's plenty of advice out there about how to set the height to fit the ListBox's container, but that's the opposite of what I'm trying to do.)

Upvotes: 1

Views: 4807

Answers (2)

George
George

Reputation: 702

This is done by setting parent control Height property to Auto, without setting any size to the Listbox itself (or also setting to Auto). To limit the list size you should also specify MaxHeight Property

Upvotes: 3

Uri London
Uri London

Reputation: 10797

Yeah, one could imagine there would be an easier way to do it (a single snapToWholeElement property). I couldn't find this property as well.

To achieve your requirement, I've wrote a little logic. Basically, In my Windows object I've a public property lbHeight which is calculate the listbox height by calculating the height of each individual item.

First, let's take a look at the XAML:

<Window
    x:Class="SO.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="120" SizeToContent="Height"
    Title="SO Sample"    
    >
    <StackPanel>
        <ListBox x:Name="x_list" Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=lbHeight}" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border x:Name="x" Background="Gray" Margin="4" Padding="3">
                        <TextBlock Text="{Binding}" />
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>        
</Window>

Note that the ItemTemplate is somewhat non trivial. One important thing to notice is that I gave this item a Name - so I can find it later.

In the code-behind constructor I put some data in the list box:

public MainWindow( )
{
    InitializeComponent( );
    this.x_list.ItemsSource = Enumerable.Range( 0, 100 );
}

next, I'm implementing a findVisualItem - to find the root element of the data template. I've made this function a little generic, so it get a predicate p which identify whether this is the element I want to find:

private DependencyObject findVisualItem( DependencyObject el, Predicate<DependencyObject> p )
{
    DependencyObject found = null;

    if( p(el) ) {
        found = el;
    }
    else {
        int count = VisualTreeHelper.GetChildrenCount( el );
        for( int i=0; i<count; ++i ) {
            DependencyObject c = VisualTreeHelper.GetChild( el, i );
            found = findVisualItem( c, p );
            if( found != null )
                break;
        }
    }
    return found;
}

I'll use the following predicate, which returns true if the element I'm looking for is a border, and its name is "x". You should modify this predicate to match your root element of your ItemTemplate.

findVisualItem(
    x_list,
    el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
    );

Finally, the lbHeight property:

public double lbHeight
{
    get {
        FrameworkElement item = findVisualItem( 
            x_list,
            el => { return ( el is Border ) ? ( (FrameworkElement)el ).Name == "x" : false; }
            ) as FrameworkElement;
        if( item != null ) {
            double h = item.ActualHeight + item.Margin.Top + item.Margin.Bottom;
            return h * 12;
        }
        else {
            return 120;
        }
    }
}

I've also made the Window implementing INotifyPropertyChanged, and when the items of the list box were loaded (Loaded event of ListBox) I fired a PropertyChanged event for the 'lbHeight' property. At some point it was necessary, but at the end WPF fetched the lbHeight property when I already have a rendered Item.

It is possible your Items aren't identical in Height, in which case you'll have to sum all the Items in the VirtualizedStackPanel. If you have a Horizontal scroll bar, you'll have to consider it for the total height of course. But this is the overall idea. It is only 3 hours since you published your question - I hope someone will come with a simpler answer.

Upvotes: 7

Related Questions