Search code examples
c#wpfdata-bindingdatagridwpf-controls

How to bind control position to scrolled position of datagrid


I want to group DataGrid columns by adding a descriptive header (Label) above:

enter image description here

("Header 1" above first two columns)


When resizing the columns the Label respones as it should:

enter image description here

("Header 1" still above first two columns)


But the label does not move its position when scrolling the DataGrid horizontally:

enter image description here

("Header 1" no longer above first two columns)


I already tried to add everything into a ScrollViewer, but then the headers of the DataGrid and also the Label on top will disappear when scrolling vertically, which they should not.

Any ideas how to bind horizontal position of the Label to the horizontal scroll position of the DataGrid? (preferably in xaml only)

Here is my code:

<Window x:Class="WpfApp2.MainWindow"
    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"
    mc:Ignorable="d"
    Title="MainWindow" Height="250" Width="600">

<StackPanel Margin="10">
    
    <Grid Height="30" >
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="7"/>
            <ColumnDefinition Width="{Binding ElementName=ColId, Path=ActualWidth, Mode=OneWay}"/>
            <ColumnDefinition Width="{Binding ElementName=ColName, Path=ActualWidth, Mode=OneWay}"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Column="1" 
               Grid.ColumnSpan="2" 
               Content="Header 1" 
               Background="LightYellow" 
               HorizontalContentAlignment="center" 
               BorderThickness="1" 
               BorderBrush="Black" 
               Margin="0,0,-1,-1"/>
    </Grid>

    <DataGrid Name="dgSimple"
              AutoGenerateColumns="False">
        <DataGrid.Columns>
            <DataGridTextColumn x:Name="ColId"
                                Header="ID"
                                Binding="{Binding Id}"/>
            <DataGridTextColumn x:Name="ColName"
                                Header="Name"
                                Binding="{Binding Name}"/>
            <DataGridTextColumn Header="Value1"
                                Binding="{Binding Value1}"/>
            <DataGridTextColumn Header="Value2"
                                Binding="{Binding Value2}"/>
        </DataGrid.Columns>
    </DataGrid>
    
</StackPanel>
using System.Collections.Generic;
using System.Windows;

namespace WpfApp2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        List<Item> items = new()
        {
            new Item()
            {
                Id = 1,
                Name = "Name1",
                Value1="Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
                Value2="Sem integer vitae justo eget. Pellentesque diam volutpat commodo sed."
            },
            new Item() { Id = 2, Name = "Name2" },
            new Item() { Id = 3, Name = "Name3" }
        };
        dgSimple.ItemsSource = items;
    }
}

public class Item
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public string Value1 { get; set; } = "";
    public string Value2 { get; set; } = "";
}

}


Solution

  • XAML is a markup language. Don't use it to implement this kind of behaviour. You cannot bind directly to the horizontal offset of the DataGrid anyway.

    You could handle the ScrollChanged event for the DataGrid, either directly in the view or in an attached behaviour:

    private void dgSimple_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        label.Margin = new Thickness(-e.HorizontalOffset, 0, -1, -1);
    }
    

    Don't forget to also set the HorizontalAlignment property of the Label to Left to prevent it from stretching. You could use a converter to set is Width property to the sum of the widths of the columns:

    public class WidthConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return (double)values[0] + (double)values[1];
        }
    
        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
    

    XAML:

    <Label x:Name="label" Grid.Column="1" 
       HorizontalAlignment="Left"
       Grid.ColumnSpan="2" 
       Content="Header 1" 
       Background="LightYellow" 
       HorizontalContentAlignment="center" 
       BorderThickness="1" 
       BorderBrush="Black" 
       Margin="0,0,-1,-1">
        <Label.Width>
            <MultiBinding>
                <MultiBinding.Converter>
                    <local:WidthConverter />
                </MultiBinding.Converter>
                <Binding Path="ActualWidth" ElementName="ColId" Mode="OneWay" />
                <Binding Path="ActualWidth" ElementName="ColName" Mode="OneWay" />
            </MultiBinding>
        </Label.Width>
    </Label>