0
votes

I am trying to bind one data grid's header's width to another datagrid's headers.

So, I have added a datagrid for each row of the parent data grid. Now, I am trying to control the column size of the child data grid using the Parent header's columns.

So in the headertemplate of the last column, I added a datagrid with just Column Headers, no rows. In this column's celltemplate, I have added another datagrid, with no headers, just datarows.

Is there any way in XAML to resize the celltemplate's datagrid column when I resize the headertemplate datagrid column header.

Read alot of anwers on SO plus few posts on CodeProject etc., but I am not able to make it work. Is it possible to do?

XAML

 <Grid>
            <DataGrid x:Name="Test"
                      AutoGenerateColumns="False"
                      CanUserAddRows="False"
                      ItemsSource="{Binding AllAssets}"
                      CanUserResizeColumns="True">
                <DataGrid.Resources>               
                    <DataTemplate x:Key="NewKey2">
                        <DataGrid Name="dgC"
                                  AutoGenerateColumns="False"
                                  HeadersVisibility="None"
                                  ItemsSource="{Binding months}"
                                  CanUserResizeColumns="True">
                            <DataGrid.Columns>
                                <DataGridTextColumn x:Name="Col1" Binding="{Binding value}" />
                                <DataGridTextColumn x:Name="Col2" Binding="{Binding MonthName}" />
                            </DataGrid.Columns>
                        </DataGrid>
                    </DataTemplate>
                    <DataTemplate x:Key="NewKey3">
                        <StackPanel>
                            <Label HorizontalAlignment="Center">All Headers</Label>
                            <DataGrid Name="dgH">
                                <DataGrid.Columns>
                                    <DataGridTextColumn Width="{Binding ElementName=Col1, Path=ActualWidth}" Header="Value" />
                                    <DataGridTextColumn Width="{Binding ElementName=Col2, Path=ActualWidth}" Header="Month" />
                                    <!--Error or no result on these attempts
                                    <DataGridTextColumn Header="Month" Width="{Binding Source={x:Reference Col2}, Path=ActualWidth}"/>--><!--
                                    <DataGridTextColumn >
                                        <DataGridTextColumn.Header>
                                            <TextBlock Width="{Binding Source={x:Reference Col1}, Path=ActualWidth}" >Value</TextBlock>
                                        </DataGridTextColumn.Header>
                                    </DataGridTextColumn>
                                    <DataGridTextColumn  Header="Month" />-->
                                </DataGrid.Columns>
                            </DataGrid>
                        </StackPanel>
                    </DataTemplate>
                </DataGrid.Resources>
                <DataGrid.Columns>
                    <DataGridTemplateColumn Header="Id">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Label Content="{Binding id}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Header="Name">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Label Content="{Binding name}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn CellTemplate="{StaticResource NewKey2}"
                                            HeaderTemplate="{StaticResource NewKey3}" />
                </DataGrid.Columns>

            </DataGrid>
        </Grid>

C# Class data for example:

namespace WpfApplication1
{
    public class Assets
    {
        public List<Asset> AllAssets { get; set; }

        public Assets()
        {
            AllAssets = new List<Asset>();

            for (int i = 1; i < 3; i++)
            {
                Asset asset = new Asset();

                asset.id = i;
                asset.name = "asset " + i.ToString();

                for (int x = 1; x < 3; x++)
                {
                    MonthsData months = new MonthsData();
                    months.MonthName = "Month " + x.ToString();
                    months.value = x;
                    asset.months.Add(months);
                }

                AllAssets.Add(asset);
            }
        }

    }

    public class Asset
    {
        public int id { get; set; }
        public string name { get; set; }
        public List<MonthsData> months { get; set; }

        public Asset()
        {
            months = new List<MonthsData>();
        }
    }

    public class MonthsData 
    {
        public string MonthName { get; set; }
        public int value { get; set; }
    }
}

enter image description here

2
Try binding minwidth property of the child to the parent - JohnChris

2 Answers

5
votes

Assuming you can define a couple of properties in the ViewModel

public class Assets : MVVM.ViewModel.ViewModelBase
{
    public List<Asset> AllAssets { get; set; }
    private double col1Width;
    private double col2Width;
    public double Col1Width
    {
        get { return col1Width; }
        set { col1Width = value; OnPropertyChanged("Col1Width"); }
    }
    public double Col2Width
    {
        get { return col2Width; }
        set { col2Width = value; OnPropertyChanged("Col2Width"); }
    }

then you'll be able to read the actual widths at the Load event from the XAML by means of an helper

    private void Grid_Loaded(object sender, RoutedEventArgs e)
    {
        var dg = FindVisualChildByName<DataGrid>(Test, "dgC");
        assets.Col1Width = dg.Columns[0].ActualWidth;
        assets.Col2Width = dg.Columns[1].ActualWidth;

    }

and set them back to your XAML bindings from the grid's DataContext

                            <DataGridTextColumn Header="Value" >
                                <DataGridTextColumn.HeaderStyle>
                                    <Style TargetType="DataGridColumnHeader">
                                        <Setter Property="Width" Value="{Binding ElementName=Test, Path=DataContext.Col1Width, UpdateSourceTrigger=PropertyChanged}"/>
                                    </Style>
                                </DataGridTextColumn.HeaderStyle>
                            </DataGridTextColumn>
                            <DataGridTextColumn Header="Month" >
                                <DataGridTextColumn.HeaderStyle>
                                    <Style TargetType="DataGridColumnHeader">
                                        <Setter Property="Width" Value="{Binding ElementName=Test, Path=DataContext.Col2Width, UpdateSourceTrigger=PropertyChanged}"/>
                                    </Style>
                                </DataGridTextColumn.HeaderStyle>
                            </DataGridTextColumn>

Reply to comments

As correctly pointed out in the comments below, we can avoid "properties that are relevant only for the View in the ViewModel" and manage an arbitrary (but fixed) number of columns so that the widths of dgC and dgH (assuming they have the same number of columns) are linked

    private void Grid_Loaded(object sender, RoutedEventArgs e)
    {
        var dgC = FindVisualChildByName<DataGrid>(Test, "dgC");
        var dgH = FindVisualChildByName<DataGrid>(Test, "dgH");
        for (int i = 0; i < dgC.Columns.Count; i++)
        {
            dgH.Columns[i].Width = dgC.Columns[i].ActualWidth;
        }

    }

In this case the VM bindings are not needed any longer

                            <DataGridTextColumn Header="Value" />
                            <DataGridTextColumn Header="Month" />

Managing also column resizing (skip this if it's not needed)

This is more complicated, so please go on only if it is strictly requested.

Let's change the utility so that it will return all the datagrid dgCs (there's one dgC for each row)

    public static IEnumerable<T> FindVisualChildByName<T>(DependencyObject parent, string name) where T : DependencyObject
    {
        List<T> list = new List<T>();
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);
            string controlName = child.GetValue(Control.NameProperty) as string;
            if (controlName == name)
            {
                list.Add(child as T);
            }
            else
            {
                IEnumerable<T> result = FindVisualChildByName<T>(child, name);
                if (result != null)
                    list.AddRange(result);
            }
        }
        return list;
    }

and now we'll define a dependency property for the actual width change

    private bool _columnWidthChanging;
    private void ColumnWidthPropertyChanged(object sender, EventArgs e)
    {
        // listen for when the mouse is released
        _columnWidthChanging = true;
        if (sender != null)
            Mouse.AddPreviewMouseUpHandler(this, BaseDataGrid_MouseLeftButtonUp);
    }

    void BaseDataGrid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (_columnWidthChanging)
        {
            _columnWidthChanging = false;
            var dgCs = FindVisualChildByName<DataGrid>(Test, "dgC");
            var dgH = FindVisualChildByName<DataGrid>(Test, "dgH").First();
            foreach (var dgC in dgCs)
            {
                for (int i = 0; i < dgC.Columns.Count; i++)
                {

                    var column = dgH.Columns[i];
                    dgC.Columns[i].Width = column.ActualWidth;
                }
            }
        }
    }

    private void Grid_Loaded(object sender, RoutedEventArgs e)
    {
        var dgC = FindVisualChildByName<DataGrid>(Test, "dgC").First();
        var dgH = FindVisualChildByName<DataGrid>(Test, "dgH").First();
        for (int i = 0; i < dgC.Columns.Count; i++)
        {
            var column = dgC.Columns[i];
            dgH.Columns[i].Width = column.ActualWidth;

            PropertyDescriptor pd = DependencyPropertyDescriptor
                         .FromProperty(DataGridColumn.ActualWidthProperty,
                                       typeof(DataGridColumn));

                //Add a listener for this column's width
                pd.AddValueChanged(dgH.Columns[i],
                                   new EventHandler(ColumnWidthPropertyChanged));


        }
2
votes

You can use a Grid and set the SharedSizeGroup property of ColumnDefinition. This will "automagically" resize the column defined in the Grid.IsSharedSizeScope

if you want to learn more about this built in feature you can google it or see: https://wpf.2000things.com/tag/sharedsizegroup/

I adopted your code to make it work. I replaced the DataGrid in Header/Cell templates with a Grid in order to use

XAML

<Grid Grid.IsSharedSizeScope="True">
<DataGrid AutoGenerateColumns="False"
            CanUserAddRows="False"
            ItemsSource="{Binding AllAssets}"
            CanUserResizeColumns="True">
    <DataGrid.Resources>
        <DataTemplate x:Key="NewKey2">
            <ItemsControl ItemsSource="{Binding months}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid ShowGridLines="True">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition SharedSizeGroup="A" />
                                <ColumnDefinition SharedSizeGroup="B" />
                            </Grid.ColumnDefinitions>
                            <TextBlock Text="{Binding value}"
                                        Margin="5" />
                            <TextBlock Text="{Binding MonthName}"
                                        Margin="5"
                                        Grid.Column="2" />
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </DataTemplate>
        <DataTemplate x:Key="NewKey3">
            <StackPanel>
                <Label HorizontalAlignment="Center">All Headers</Label>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition SharedSizeGroup="A" />
                        <ColumnDefinition SharedSizeGroup="B" />
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="Value"
                                Margin="5" />
                    <TextBlock Text="Month"
                                Margin="5"
                                Grid.Column="1" />
                </Grid>
            </StackPanel>
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="Id">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Label Content="{Binding id}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
        <DataGridTemplateColumn Header="Name"
                                Width="Auto">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Label Content="{Binding name}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
        <DataGridTemplateColumn CellTemplate="{StaticResource NewKey2}"
                                HeaderTemplate="{StaticResource NewKey3}" />
    </DataGrid.Columns>
</DataGrid>