Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Relative Column Widths Not Working When DataGrid is Nested Inside A Grouped DataGrid

Tags:

c#

wpf

datagrid

Suppose I have an object with 6 attributes:

public class MyClass
{
    public string Attribute1 { get; set; }
    public string Attribute2 { get; set; }
    public string Attribute3 { get; set; }
    public string Attribute4 { get; set; }
    public string Attribute5 { get; set; }
    public string Attribute6 { get; set; }
}

I display a collection of these objects in a DataGrid:

<Grid>
    <DataGrid x:Name="myGrid" Margin="5, 5, 5, 5" AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True">
        <DataGrid.GroupStyle>
            <GroupStyle ContainerStyle="{StaticResource GroupHeaderStyle}">
                <GroupStyle.Panel>
                    <ItemsPanelTemplate>
                        <DataGridRowsPresenter/>
                    </ItemsPanelTemplate>
                </GroupStyle.Panel>
            </GroupStyle>
        </DataGrid.GroupStyle>
        <DataGrid.Columns>
            <DataGridTextColumn Header="Attribute1" Binding="{Binding Attribute1}" Width="5*"/>
            <DataGridTextColumn Header="Attribute2" Binding="{Binding Attribute2}" Width="5*"/>
            <DataGridTextColumn Header="Attribute3" Binding="{Binding Attribute3}" Width="10*"/>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

I populate my Grid like this, grouping by "Attribute1"...

    public MainWindow()
    {
        InitializeComponent();

        ObservableCollection<MyClass> data = new ObservableCollection<MyClass>();
        data.Add(new MyClass { Attribute1 = "Red", Attribute4 = "Circle", Attribute5 = "Large", Attribute6 = "Transparent" });
        data.Add(new MyClass { Attribute1 = "Red", Attribute4 = "Square", Attribute5 = "Medium", Attribute6 = "Opaque" });
        data.Add(new MyClass { Attribute1 = "Red", Attribute4 = "Triangle", Attribute5 = "Large", Attribute6 = "Opaque" });
        data.Add(new MyClass { Attribute1 = "Yellow", Attribute4 = "Square", Attribute5 = "Large", Attribute6 = "Transparent" });
        data.Add(new MyClass { Attribute1 = "Blue", Attribute4 = "Triangle", Attribute5 = "Small", Attribute6 = "Transparent" });
        data.Add(new MyClass { Attribute1 = "Blue", Attribute4 = "Sphere", Attribute5 = "Small", Attribute6 = "Opaque" });


        ListCollectionView lcv = new ListCollectionView(data);
        lcv.GroupDescriptions.Add(new PropertyGroupDescription("Attribute1"));
        myGrid.ItemsSource = lcv;

    }

I style my GroupItems to show a nested DataGrid within an expander:

<Window.Resources>
    <Style x:Key="GroupHeaderStyle" TargetType="{x:Type GroupItem}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type GroupItem}">
                    <Expander x:Name="exp" IsExpanded="True" Foreground="Black">
                        <Expander.Header>
                                <TextBlock Foreground="Black" Text="{Binding Name}"/>
                        </Expander.Header>
                        <DockPanel>
                            <DataGrid ItemsSource="{Binding Items}" x:Name="subGrid" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False">
                                <DataGrid.Columns>
                                    <DataGridTextColumn Header="Attribute 4" Binding="{Binding Attribute4}" Width="Auto"/>
                                    <DataGridTextColumn Header="Attribute 5" Binding="{Binding Attribute5}" Width="Auto"/>
                                    <DataGridTextColumn Header="Attribute 6" Binding="{Binding Attribute6}" Width="Auto"/>
                                </DataGrid.Columns>
                            </DataGrid>
                        </DockPanel>
                    </Expander>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

With my nested DataGrid Column widths set to "Auto"... it works great:

enter image description here

The problem is when I try to use relative widths...

<DataGrid ItemsSource="{Binding Items}" x:Name="subGrid" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Attribute 4" Binding="{Binding Attribute4}" Width="1*"/>
        <DataGridTextColumn Header="Attribute 5" Binding="{Binding Attribute5}" Width="1*"/>
        <DataGridTextColumn Header="Attribute 6" Binding="{Binding Attribute6}" Width="2*"/>
    </DataGrid.Columns>
</DataGrid>

Now it looks like this... enter image description here

Not only are the columns super skinny... they are not re-sizable with the mouse, as usual.

This issue really has me puzzled. I've searched SO and tried several somewhat-related issues without any luck.

How can I set the columns on my nested DataGrid to use relative widths?

like image 377
Gordon True Avatar asked Apr 22 '15 17:04

Gordon True


1 Answers

I think the problem is that WPF doesn't know how wide the DockPanel is so you get those skinny columns.

One solution is to set the width of the DockPanel (or the DataGrid) to a fixed width like 500 pixels.

Another solution is to bind the Width of the DataGrid to the ActualWidth of the DockPanel. This works but the DataGrid will only grow in size, it will not shrink when the window gets smaller.

<DataGrid ItemsSource="{Binding Items}" 
          x:Name="subGrid" 
          AutoGenerateColumns="False" 
          IsReadOnly="True" 
          CanUserAddRows="False" 
          Width="{Binding 
                  RelativeSource={RelativeSource AncestorType=DockPanel}, Path=ActualWidth}">

Yet another solution is to bind the Width of the DockPanel to the ActualWidth of the Expander. The problem with this is that it doesn't work right... the Expander gets bigger because the DockPanel gets bigger and we get into a loop. What we really want is the ActualWidth of the Expander minus enough to not cause the Expander to increase it's width. I've experimented a bit and "ActualWidth - 3" seems like it works (but idk why 3...). If you add Paddings or Margins, this 3 may need to change. To get this into a binding, we need an IValueConverter.

public class ActualWidthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value is double)
            return (double)value - 3;
        return value;
    }
    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

You'll need to add the converter as a resource (app or Window or whatever):

<Window ....
    xmlns:app="clr-namespace:WpfApplication25">
<Window.Resources>
    <app:ActualWidthConverter x:Key="ActualWidthConverter" />
</Window.Resources>

And, of course, you'll need the binding applied to the Width of the DockPanel:

<DockPanel Width="{Binding RelativeSource={RelativeSource AncestorType=Expander}, Path=ActualWidth, Converter={StaticResource ActualWidthConverter}}">

It's not a perfect solution but maybe it'll be helpful? An alternative version of this method could use a MultiBinding; passing in the Expander's ActualWidth and the DockPanel's Margin: ActualWidth - Margin.Left - Margin.Right - 3 (I'm still wondering why 3? And will it be 3 on everyone else's computer too?).

like image 147
J.H. Avatar answered Sep 19 '22 20:09

J.H.