wpfgriditemscontrolitemssource

How to setup a grid as template for an Items control?


I'm trying to create an ItemsControl that uses a grid as its ItemsPanel in such a way that it has two columns, where the first columns width is the width of the widest item in that column, and has as may rows needed to display all the items

Basically, I want the following, but somehow within an ItemsControl so that I can bind to a collection of objects:

<Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <Label Content="{Binding Items[0].Header}"/>
        <TextBox Text="{Binding Items[0].Content}" Grid.Column="1"/>

        <Label Content="{Binding Items[1].Header}" Grid.Row="1"/>
        <TextBox Text="{Binding Items[1].Content}" Grid.Row="1" Grid.Column="1"/>

        <Label Content="{Binding Items[2].Header}" Grid.Row="2"/>
        <TextBox Text="{Binding Items[2].Content}" Grid.Row="2" Grid.Column="1"/>
    </Grid> 

Edit : Rachels answer worked great, here is a working example.

(I moved the Grid.IsSharedSizeScope="True" to the ItemsPanel, not sure if Rachel meant to put it in the ItemTemplate (which didn't work))

namespace WpfApplication23
{
    public partial class Window1 : Window
    {
        public List<Item> Items { get; set; }

        public Window1()
        {
            Items = new List<Item>() 
            { 
                new Item(){ Header="Item0", Content="someVal" },
                new Item(){ Header="Item1", Content="someVal" },
                new Item(){ Header="Item267676", Content="someVal" },
                new Item(){ Header="a", Content="someVal" },
                new Item(){ Header="bbbbbbbbbbbbbbbbbbbbbbbbbb", Content="someVal" },
                new Item(){ Header="ccccccc", Content="someVal" } 
            };

            InitializeComponent();

            DataContext = this;
        }
    }

    public class Item
    {
        public string Header { get; set; }
        public string Content { get; set; }
    }
}

<Window x:Class="WpfApplication23.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <ItemsControl ItemsSource="{Binding Items}">

        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Grid.IsSharedSizeScope="True"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>

        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition SharedSizeGroup="ColumnOne" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Label Content="{Binding Header}"/>
                    <TextBox Text="{Binding Content}" Grid.Column="1"/>
                </Grid>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Window>

Solution

  • There are multiple problems here for an ItemsControl:

    The last one is really the biggest problem, because an ItemsControl wraps each ItemTemplate in a ContentPresenter, so there is no default way of creating more than one item in the panel for each Iteration of the ItemsControl. Your end result would look like this:

    <Grid>
        ...
    
        <ContentPresenter>
            <Label Content="{Binding Items[0].Header}"/>
            <TextBox Text="{Binding Items[0].Content}" Grid.Column="1"/>
        </ContentPresenter>
        <ContentPresenter>
            <Label Content="{Binding Items[1].Header}" Grid.Row="1"/>
            <TextBox Text="{Binding Items[1].Content}" Grid.Row="1" Grid.Column="1"/>
        </ContentPresenter>
        <ContentPresenter>
            <Label Content="{Binding Items[2].Header}" Grid.Row="2"/>
            <TextBox Text="{Binding Items[2].Content}" Grid.Row="2" Grid.Column="1"/>
        </ContentPresenter>
    </Grid> 
    

    My best suggestion would be to create an ItemTemplate that contains a 1x2 Grid, and use Grid.IsSharedSizeScope to make the width of the first column shared. (The ItemsPanelTemplate would remain the default StackPanel.)

    This way, the end result would look like this:

    <StackPanel>
        <ContentPresenter>
            <Grid IsSharedSizeScope="True">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition SharedSizeGroup="ColumnOne" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Label Content="{Binding Header}"/>
                <TextBox Text="{Binding Content}" Grid.Column="1"/>
            </Grid>
        </ContentPresenter>
        <ContentPresenter>
            <Grid IsSharedSizeScope="True">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition SharedSizeGroup="ColumnOne" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <Label Content="{Binding Header}"/>
                <TextBox Text="{Binding Content}" Grid.Column="1"/>
            </Grid>
        </ContentPresenter>
        ...
    </StackPanel>