Search code examples
c#wpfxamllistviewwpf-grid

WPF: How can I use multiple list views in a control size correctly?


This is my XAML right now:

<Window x:Class="GridDemo.SubWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:GridDemo"
    d:DataContext="{d:DesignInstance local:ViewModel, IsDesignTimeCreatable=True}"
    mc:Ignorable="d"
    Title="Window" Width="300">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <ListView
        ItemsSource="{Binding Animals}"
        SelectedItem="{Binding SelectedAnimal}"
        Grid.Row="0"/>
    <Button
        Command="{Binding AddAnimalCommand}"
        Content="Add Animal"
        HorizontalAlignment="Right"
        Grid.Row="1"/>
    <ListView
        ItemsSource="{Binding Vegetables}"
        SelectedItem="{Binding SelectedVegetable}"
        Grid.Row="2"/>
    <Button
        Command="{Binding AddVegetableCommand}"
        Content="Add Vegetable"
        HorizontalAlignment="Right"
        Grid.Row="3"/>
</Grid>

When I run, it shows this:

enter image description here

Problem right away, it's using too much vertical space.

A thing I like and want to keep is that if I shrink it so that it's too small to show both listviews in their entirety, the listboxes shrink and automatically get scroll bars added, but the buttons stay visible no matter what.

enter image description here

But the main problem happens when the listviews are different sizes. They will always use half of the available space to them. Suppose I have ten animals and five vegetables, and my window is tall enough to hold 15 items. I would want the vegetable listview to ask for only as much space as it needs, letting the animal listview ask for all of the remaining space. Instead, both listviews ask for 50% of the remaining space, causing the animal listview to be too small and the vegetable listview to be too big.

enter image description here

I think that what I want is for the listview rows to behave like Height="Auto" when there's enough room for them, and to behave like Height="*" when there isn't.


Solution

  • To get the effect you're looking for, I'd do something like this:

    <Window x:Class="GridDemo.SubWindow" x:Name="win"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:GridDemo"
            d:DataContext="{d:DesignInstance local:ViewModel, IsDesignTimeCreatable=True}"
            mc:Ignorable="d"
            Title="Window" Width="300">
        <Grid x:Name="grd">
            <Grid.RowDefinitions>
                <RowDefinition>
                    <RowDefinition.Height>
                        <MultiBinding Converter="{StaticResource gpc}">
                            <MultiBinding.Bindings>
                                <Binding Path="Animals" />
                                <Binding ElementName="win" Path="ActualHeight" />
                            </MultiBinding.Bindings>
                        </MultiBinding>
                    </RowDefinition.Height>
                </RowDefinition>
                <RowDefinition Height="Auto"/>
                <RowDefinition>
                    <RowDefinition.Height>
                        <MultiBinding Converter="{StaticResource gpc}">
                            <MultiBinding.Bindings>
                                <Binding Path="Vegetables" />
                                <Binding ElementName="win" Path="ActualHeight" />
                            </MultiBinding.Bindings>
                        </MultiBinding>
                    </RowDefinition.Height>
                </RowDefinition>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <ListView
                ItemsSource="{Binding Animals}"
                SelectedItem="{Binding SelectedAnimal}"
                Grid.Row="0"/>
            <Button
                Command="{Binding AddAnimalCommand}"
                Content="Add Animal"
                HorizontalAlignment="Right"
                Grid.Row="1"/>
            <ListView
                ItemsSource="{Binding Vegetables}"
                SelectedItem="{Binding SelectedVegetable}"
                Grid.Row="2"/>
            <Button
                Command="{Binding AddVegetableCommand}"
                Content="Add Vegetable"
                HorizontalAlignment="Right"
                Grid.Row="3"/>
        </Grid>
    </Window>
    

    I've used an implementation of IMultiValueConverter to generate the row height proportions. It's in a resource dictionary with a key of "gpc" and is implemented as follows:

    public class GridProportionConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values.Count() == 2 && values[0] is ICollection && values[1] is double && ((ICollection)values[0]).Count > 0)
            {
                ICollection collection = (ICollection)values[0];
                double windowHeight = (double)values[1];
    
                if (windowHeight < 350)
                {
                    return new GridLength(1, GridUnitType.Star);
                }
    
                return new GridLength(collection.Count, GridUnitType.Star);
            }
    
            return new GridLength(1, GridUnitType.Star);
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    I have used ICollection assuming you have some kind of ObservableCollection<T> for your collections.

    I find this works quite well, but obviously one thing it depends on is each ListViewItem being roughly the same height in each ListView, otherwise you may need to cater for the height difference.

    I've also put a bit of a fail-safe in, where if the window's height goes below a certain value, the proportion becomes 1:1.

    If the proportions are ever out of control, e.g., 300:1, you could always pass in both collections and calculate more appropriate proportions based on that, e.g., you might decide that 4:1 is the biggest difference you'd tolerate and you'd default to that if it ever got bigger.