0

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

cyboashu
  • 10,196
  • 2
  • 27
  • 46
teddy2
  • 382
  • 4
  • 16

2 Answers2

5

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));


        }
Community
  • 1
  • 1
  • I struggle with this approach a little bit because: 1. you put properties that are relevant only for the View in the ViewModel 2. it's not very generic, It's not possible to use a dynamic list of columns/properties. Therefor it's quite hard to extend/maintain the ViewModel. Although I have to admit that my solution doesn't address that problem either ;-) – Mat Jan 30 '17 at 12:12
  • Hi @Mat 1) yes, I'm aware. If you want I can change the code so that it's all in the xaml and the view model is not needed for this. A dependency property of the Xaml can replace the corresponding one from the VM. It was only because I wanted to show how the bindings could work in a "conventional" and simplified way, avoiding a custom grid control .. 2) I don't think a datagrid is well suited for a generic number of columns: it natively manages only a generic number of rows. Again I'd avoid a more complicated solution when not strictly requested. –  Jan 30 '17 at 12:44
  • I have a special one off scenario and I know the column numbers before hand. They are not arbitrary, at least not for me. So that's fine. Not sure about the original OP. I have upved the answer. :) I can settle with this solution. – cyboashu Jan 30 '17 at 13:08
  • great.... no rush. Coming from a pure VBA background these thing are new for me. Thanks for all the help. – cyboashu Jan 30 '17 at 13:11
  • @cyboashu Nothing, you're welcome. Now, if you look at my edit, it's all managed in the code behind (no need of a ViewModel for this) and there is a link between the widths of dgC and dgH (assuming they have the same generic N columns) –  Jan 30 '17 at 13:19
  • I've also added a final (quite long, sorry) paragraph to manage column resizing (during runtime using mouse on the headers) –  Jan 30 '17 at 14:03
2

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>

Mat
  • 1,960
  • 5
  • 25
  • 38
  • First thing, I have upved your answer too. Not having to traverse the visual tree is good. :) But with Grids, I loose the ability to resize the headers/columns during run time using mouse. Is there any way to add a feature which will allow the user to resize the Columns Value and Month during run time. If yes, great. If no , no worries. :) By the way I tried GridSplitter but couldn't get it work. – cyboashu Jan 30 '17 at 13:03