4

I am trying to customize a DataGridColumnHeader to show multiple text fields instead of
showing only the header text provided by DataGridColumn.Header property.
If i didn't miss something, i just have to create a DataTemplate and bind to the properties
of the parent object. This works fine for the DataGridColumn.Header property but
binding to attached property fails.

For the sake of completeness, implementation of the attached property:

public static class CustomHeader
{
    public static string GetUnit(DependencyObject obj) { return (string)obj.GetValue(UnitProperty); }
    public static void SetUnit(DependencyObject obj, string value) { obj.SetValue(UnitProperty, value); }

    public static readonly DependencyProperty UnitProperty = DependencyProperty.RegisterAttached(
        "Unit", typeof(string), typeof(CustomHeader), new FrameworkPropertyMetadata(null));
}

Usage in the Xaml-Markup:

<DataGrid x:Name="tObjectDataGrid" Margin="10,50,10,10" 
                  AutoGenerateColumns="False" EnableRowVirtualization="True"  
                  ItemsSource="{Binding ObjectList}"  
                  RowDetailsVisibilityMode="VisibleWhenSelected" >
  <DataGrid.Resources>  
    <DataTemplate x:Key="CustomHeaderTemplate">  
      <StackPanel>  
        <TextBlock Text="{Binding}" />  
        <TextBlock Text="{Binding Path=(cust:CustomHeader.Unit)}" /> <-- attached binding doesn't work :( 
      </StackPanel>  
    </DataTemplate>  
  </DataGrid.Resources>  

  <DataGrid.Columns>  
    <DataGridTextColumn x:Name="SpeedColumn"
                        Width="1*"
                        Binding="{Binding Speed}"
                        Header="Speed"
                        HeaderTemplate="{StaticResource CustomHeaderTemplate}"
                        cust:CustomHeader.Unit="[m/s]" />  
  </DataGrid.Columns>  
</DataGrid>  

I really appreciate any comment or weblink that clarifies what i am missing here. Thanks in advance.

LaWi
  • 71
  • 1
  • 1
  • 6
  • Where have you set this `attached property`? – Rohit Vats Jul 27 '13 at 15:49
  • It is set in DataGridTextColumn's last line ... cust:CustomHeader.Unit="[m/s]". The class 'CustomHeader' is situated in the same assembly (in a testproject) and in the same namespace as the rest of the testproject. "cust" is xmlns:cust="clr-namespace:DependencyTest" (to be complete) ... does this setup maybe introduce unexpected sideeffects? – LaWi Jul 27 '13 at 17:26
  • Behaviour is completely fine since you set attached property on `DataGridTextColumn` and `HeaderTemplate` not lies in the `Visual Tree` of your column. Hence not inheriting the attached property. If you set the default value for your property to say `TestString`, that value will be shown in header. – Rohit Vats Jul 28 '13 at 07:12

2 Answers2

4

You should use multi value converter (msdn).

XAML:

<DataGrid x:Name="tObjectDataGrid" Margin="10,50,10,10" 
          AutoGenerateColumns="False" EnableRowVirtualization="True"  
          ItemsSource="{Binding ObjectList}"  
          RowDetailsVisibilityMode="VisibleWhenSelected" >
    <DataGrid.Resources>
        <cust:UnitConverter x:Key="unitCon" />
        <DataTemplate x:Key="CustomHeaderTemplate">
            <StackPanel>
                <TextBlock Text="{Binding}" />
                <TextBlock>
                    <TextBlock.Text>
                        <MultiBinding Converter="{StaticResource unitCon}">
                            <Binding Path="Columns" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=DataGrid}" />
                            <Binding Path="." />
                        </MultiBinding>
                    </TextBlock.Text>
                </TextBlock>
            </StackPanel>
        </DataTemplate>
    </DataGrid.Resources>

    <DataGrid.Columns>
        <DataGridTextColumn x:Name="SpeedColumn"
                Width="1*"
                Binding="{Binding Speed}"
                Header="Speed"
                HeaderTemplate="{StaticResource CustomHeaderTemplate}"
                cust:CustomHeader.Unit="[m/s]" />

        <DataGridTextColumn x:Name="SpeedColumn2"
                Width="1*"
                Binding="{Binding Speed2}"
                Header="Speed2"
                HeaderTemplate="{StaticResource CustomHeaderTemplate}"
                cust:CustomHeader.Unit="[km/h]" />
    </DataGrid.Columns>
</DataGrid>

Code-behind:

public class UnitConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string result = null;

        ObservableCollection<DataGridColumn> columns = values[0] as ObservableCollection<DataGridColumn>;
        string headerName = values[1].ToString();

        var coll = columns.Where(x => x.Header.ToString() == headerName).FirstOrDefault();

        if (coll != null)
            result = CustomHeader.GetUnit(coll);

        return result;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

I think that better solution will be create new class e.g HeaderData and after that you can create instance of it in xaml and binding to this class.

Example:

Class to hold header data:

class HeaderData
{
    public string Name { get; set; }
    public string Unit { get; set; }
}

XAML code:

<DataGrid x:Name="tObjectDataGrid" Margin="10,50,10,10" 
          AutoGenerateColumns="False" EnableRowVirtualization="True"  
          ItemsSource="{Binding ObjectList}"  
          RowDetailsVisibilityMode="VisibleWhenSelected" >
    <DataGrid.Resources>
        <DataTemplate x:Key="CustomHeaderTemplate">
            <StackPanel>
                <TextBlock Text="{Binding Name}" />
                <TextBlock Text="{Binding Unit}" />
            </StackPanel>
        </DataTemplate>
    </DataGrid.Resources>
    <DataGrid.Columns>
        <DataGridTextColumn x:Name="SpeedColumn"
                        Width="1*"
                        Binding="{Binding Speed}"                                
                        HeaderTemplate="{StaticResource CustomHeaderTemplate}">             
            <DataGridTextColumn.Header>
                <cust:HeaderData Name="Speed" Unit="[m/s]" />
            </DataGridTextColumn.Header>
        </DataGridTextColumn>
        <DataGridTextColumn x:Name="SpeedColumn2"
                        Width="1*"
                        Binding="{Binding Speed2}"                                
                        HeaderTemplate="{StaticResource CustomHeaderTemplate}">
            <DataGridTextColumn.Header>
                <cust:HeaderData Name="Speed2" Unit="[km/h]" />
            </DataGridTextColumn.Header>
        </DataGridTextColumn>
    </DataGrid.Columns>
</DataGrid>
kmatyaszek
  • 19,016
  • 9
  • 60
  • 65
  • Thank you. The second approach looks very nice from the easyness viewpoint. So i marked that as answer. I am just wondering because i thought attached properties should solve exactly such a point i encountered. When i want to bind the two textvalues in cust:HeaderData i guess i have to implement the properties as dependencyproperties? Or do i have to derive from dependencyobject? – LaWi Jul 27 '13 at 17:15
  • No, you can create simple properties and you don't have to derive from DependencyObject. In my answer I showed you example class. – kmatyaszek Jul 27 '13 at 18:21
  • I'm trying to use this solution but with two-way binding with `TextBox`. So, instead of having hard-coded value `Unit="[km/h]"`, I would like to have something like `Unit="{Binding Unit, Mode=TwoWay}"`. I tried with attached property and with separate class, but I can't get the value to propagate back to my view-model. In attached property example, the problem is in `ConvertBack` as I don't have information how to call `SetUnit(obj, value)`. In `HeaderData` case, I get the value to the `HeaderData.Unit` property, but not all the way back to my view-model. I'm completely stuck now. – lukeguy Mar 28 '15 at 10:07
1

At first glance, your code is normal, should work. But when your dependency property is set to DataGridTextColumn, SetUnit not called and the variable Unit value NULL. I tried to assign a value of attached dependency property in Window (since it attached, you can set its value anywhere) in this case must work:

<Window x:Class="DataGridAttachedHelp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:DataGridAttachedHelp"
    local:CustomHeader.Unit="[m/s]"
    Name="MyWindow"
    Title="MainWindow" Height="350" Width="525"
    WindowStartupLocation="CenterScreen">

<Grid>
    <DataGrid x:Name="tObjectDataGrid" Margin="10,50,10,10" 
              AutoGenerateColumns="False" EnableRowVirtualization="True"                    
              RowDetailsVisibilityMode="VisibleWhenSelected" >

        <DataGrid.Resources>
            <DataTemplate x:Key="CustomHeaderTemplate">
                <StackPanel>
                    <TextBlock Text="{Binding}" />
                    <TextBlock Text="{Binding Path=(local:CustomHeader.Unit), ElementName=MyWindow}" />
                </StackPanel>  
            </DataTemplate>  
        </DataGrid.Resources>  

        <DataGrid.Columns>  
            <DataGridTextColumn x:Name="SpeedColumn" Width="1*" Binding="{Binding Speed}" Header="Speed"
                    HeaderTemplate="{StaticResource CustomHeaderTemplate}" />  
        </DataGrid.Columns>  
    </DataGrid>      
</Grid>
</Window>

In your case the property did not work, because it is necessary to indicate the source of the properties, for example: ElementName, Source. Therefore, simply add the name of DataGridTextColumn in parameter ElementName:

<TextBlock Text="{Binding Path=(local:CustomHeader.Unit), ElementName=SpeedColumn}" />
Anatoliy Nikolaev
  • 22,370
  • 15
  • 69
  • 68
  • 1
    Thank you for your reply. This solves it for one DataColumn. I also tried to bind using RelativeSource but failed to get the proper ancestor in my example. My intention is, to easily apply some more information on the header using a DynamicResource string, image and so on for each column. So directly managing it over an attached property at window/usercontrol-level is not fully matching my needs . – LaWi Jul 27 '13 at 16:52