32
votes

The column header of the DataGrid is not a FrameWork element for some reason, and so you cannot use bindings to set things like the header text. Please correct me if that is wrong of if that has changed with .NET 4.0 (I am using the latest WPFToolkit from CodePlex now).

I am trying to use the DataGrid for a time sheet presentation where the day's date should be part of the header text (ie, "Sun, Nov 01"), and I have the following in my XAML:

        <dg:DataGrid.Columns>
        <dg:DataGridTextColumn Header="Description" Width="Auto" Binding="{Binding Description}" IsReadOnly="True"/>
        <dg:DataGridTextColumn Header="Mon" Width="50" Binding="{Binding Allocations[0].Amount}"  />
... every other day of the week ....
        <dg:DataGridTextColumn Header="Sun" Width="50" Binding="{Binding Allocations[6].Amount}"  />
        <dg:DataGridTextColumn Header="Total" MinWidth="50" Binding="{Binding TotalAllocatedAmount}" IsReadOnly="True" />
    </dg:DataGrid.Columns>

I'd like to use the same AllocationViewModel I am using for data (ie, "{Binding Allocations[0].Amount}" and bind it's DisplayName property to the header text. Can someone show me how to do that? If I have to use a static resource, how can I get the DataContext in there?

EDIT ---------------- PREFERRED WORK-AROUND

Josh Smith had posted about a DataContextSpy awhile back, and it is the cleanest workaround I have come across to this problem. Here is the class that makes it work:

/// <summary>
/// Workaround to enable <see cref="DataContext"/> bindings in situations where the DataContext is not redily available. 
/// </summary>
/// <remarks>http://blogs.infragistics.com/blogs/josh_smith/archive/2008/06/26/data-binding-the-isvisible-property-of-contextualtabgroup.aspx</remarks>
public class DataContextSpy : Freezable
{
    public DataContextSpy()
    {
        // This binding allows the spy to inherit a DataContext.
        BindingOperations.SetBinding(this, DataContextProperty, new Binding());
    }

    public object DataContext
    {
        get { return GetValue(DataContextProperty); }
        set { SetValue(DataContextProperty, value); }
    }

    // Borrow the DataContext dependency property from FrameworkElement.
    public static readonly DependencyProperty DataContextProperty = FrameworkElement
        .DataContextProperty.AddOwner(typeof (DataContextSpy));

    protected override Freezable CreateInstanceCore()
    {
        // We are required to override this abstract method.
        throw new NotImplementedException();
    }
}

With this in place, I can hijack the DC I need in xaml:

    <dg:DataGrid.Resources>
        <behavior:DataContextSpy x:Key="spy" DataContext="{Binding Allocations}" />
    </dg:DataGrid.Resources>

And then apply as needed via binding:

            <dg:DataGridTextColumn Header="{Binding Source={StaticResource spy}, Path=DataContext[0].DisplayName}" 
                               Width="50" Binding="{Binding Allocations[0].Amount}"  />

Suh-weet!

9
Any idea how to make this work for Silverlight (where there is no Freezable) and the FrameWorkElement.DataContextProperty doesn't have an AddOwner property?Philipp Schmid
Josh Smith has definitely learned how to 'bend the spoon' in a way that I have yet to master lolGrokodile
It does not work for me (VS 2008, .NET 3.5). behavior keyword is not recognized. The blog entry you're linking to does not mention it at all. What am I missing? :(Konrad Morawski
@Morawski - the behavior: tag needs to be defined in the header of your WPF page (or whatever object you are using it in). It needs to point to the namespace that you have the DataContextSpy class in. For example: <Page x:Class="MyClass" ... xmlns:behavior="clr-namespace:WpfApplication1.MyDataContextSpyNamespace">transistor1

9 Answers

25
votes

This is the easy way to bind the DataGridTextColumn header to the data context:

<DataGrid x:Name="summaryGrid" Grid.Row="3" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False">
       <DataGrid.Columns>
            <DataGridTextColumn Header="Hard Coded Title" Width="*"/>
            <DataGridTextColumn Width="100">
                <DataGridTextColumn.Header>
                    <TextBlock Text="{Binding DataContext.SecondColumnTitle, 
                                              RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"/>
                </DataGridTextColumn.Header>
            </DataGridTextColumn>
            <DataGridTextColumn Width="150">
                <DataGridTextColumn.Header>
                    <TextBlock Text="{Binding DataContext.ThirdColumnTitle, 
                                              RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"/>
                </DataGridTextColumn.Header>
            </DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>

You will obviously need to have properties: SecondColumnTitle and ThirdColumnTitle implemented on your data context class.

I have this solution working in .net 4.5 and did not have a chance nor reason to try it in earlier versions of the framework.

7
votes

My solution allows writing a single line in DataGridColumn with the name of the property that need to bind. He has the following features:

  • There is support for DataGridTextColumn
  • There is support for DataGridTemplateColumn
  • Set StringFormat for each column
  • Specify a static value for the StringFormat
  • Fully complies with MVVM pattern

Example, which is below, includes StringFormat (he should stand before the PropertyPath):

<DataGridTextColumn Behaviors:DataGridHeader.StringFormat="StringFormat: {0:C}"
                    Behaviors:DataGridHeader.PropertyPath="HeaderValueOne" ... /> 

Equivalent to a this line:

<DataGridTextColumn HeaderStringFormat="{0:C}"
                    Header="{Binding Path=HeaderValueOne}" ... />

Who need more examples of solutions and features, please read below.

Link for the sample project.


Notes about the solution

From all the solutions that I have seen earlier, the easiest for me turned out to be this example:

<DataGridTextColumn Binding="{Binding Name}">
    <DataGridTextColumn.HeaderTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Path=DataContext.YourPropertyName,
                                      RelativeSource={RelativeSource AncestorType={x:Type SomeControl}}" />
         </DataTemplate>
    </DataGridTextColumn.HeaderTemplate>
</DataGridTextColumn>        

Please pay attention to DataGridTextColumn.HeaderTemplate, if was used DataGridTextColumn.Header, then for .NET framework below version 4.5 and for Silverlight would produce an exception:

Header property does not support UIElements

It would seem that what is necessary? I wanted to find a solution that would allow to write a single line in DataGridColumn with the name of the property that need to bind.

And here's what happened:

<DataGridTextColumn Behaviors:DataGridHeader.PropertyPath="HeaderValueOne" // Attached dependency property 

This construction similar to this:

<DataGridTextColumn Header="{Binding Path=HeaderValueOne}" ... />

Also is possible to use StringFormat for each column like this:

<DataGridTextColumn Behaviors:DataGridHeader.StringFormat="StringFormat: {0:C}"
                    Behaviors:DataGridHeader.PropertyPath="TestStringFormatValue" ... />

And there is the ability to specify a static value for the StringFormat:

<DataGridTextColumn Behaviors:DataGridHeader.StringFormat="{x:Static Member=this:TestData.TestStaticStringFormatValue}" // public static string TestStaticStringFormatValue = "Static StringFormat: {0}$";
                    Behaviors:DataGridHeader.PropertyPath="TestStringFormatValue"

Here is the original DataTemplate, which is dynamically set to the column:

<DataTemplate>
    <TextBlock Text="{Binding Path=DataContext.YourPropertyName,
                              StringFormat="YourStringFormat",
                              RelativeSource={RelativeSource AncestorType={x:Type DataGridCellsPanel}}}" />
</DataTemplate>

In order to RelativeSource did not depend on the type of DataContext, I took great solution from Mr.Bruno.

In this case, DataGridCellsPanel contains the correct DataContext, which is set for a parent DataGrid.

Below is the basic code that is performed all the magic:

IsSetHeader PropertyChanged handler

private static void IsSetHeader(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    var textColumn = sender as DataGridTextColumn;
    var templateColumn = sender as DataGridTemplateColumn;
    string path = e.NewValue as string;

    if ((textColumn == null) & (templateColumn == null)) 
    {
        return;
    }

    if (String.IsNullOrEmpty(path) == false)
    {
        currentStringFormat = ReturnStringFormat(textColumn, templateColumn);
        dataTemplate = CreateDynamicDataTemplate(path, currentStringFormat);

        if (dataTemplate != null)
        {
            if (textColumn != null)
                textColumn.HeaderTemplate = dataTemplate;

            if (templateColumn != null)
                templateColumn.HeaderTemplate = dataTemplate;
        }
    }
}

CreateDynamicDataTemplate

private static DataTemplate CreateDynamicDataTemplate(string propertyPath, string stringFormat)
{
    var pc = new ParserContext();
    MemoryStream sr = null;

    string xaml = GetXamlString(propertyPath, stringFormat);            
    sr = new MemoryStream(Encoding.ASCII.GetBytes(xaml));

    pc.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
    pc.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");

    return XamlReader.Load(sr, pc) as DataTemplate;
}

GetXamlString

private static string GetXamlString(string propertyPath, string stringFormat)
{
    #region Original PropertyPath for TextBlock

    // {Binding Path=DataContext.YourProperty, RelativeSource={RelativeSource AncestorType={x:Type DataGridCellsPanel}}}"
    // Thanks to Bruno (https://stackoverflow.com/users/248118/bruno) for this trick

    #endregion

    var sb = new StringBuilder();

    sb.Append("<DataTemplate><TextBlock Text=\"{Binding Path=DataContext.");
    sb.Append(propertyPath);
    sb.Append(", StringFormat=");
    sb.Append(stringFormat);
    sb.Append(", RelativeSource={RelativeSource AncestorType={x:Type DataGridCellsPanel}}}\" /></DataTemplate>");

    return sb.ToString();
}

StringFormat must appear before the PropertyPath, because it is optional. In order to for columns, who did not have it is not an exception occurs, I registered try-catch in GetStringFormat:

 public static string GetStringFormat(DependencyObject DepObject)
 {
    try
    {
        return (string)DepObject.GetValue(StringFormatProperty);
    }

    catch 
    {
        return String.Empty;
    }
 }

Plus: do not write in methods try-catch block, that are trying to get the value.

Minus: The minus for every missed StringFormat exception will be generated once when the program starts. If it is critical for you, you can always specify the StringFormat="null" for the column.

Just in case, show the full code of project:

public static class DataGridHeader
{
    #region Private Section

    private static string textColumnStringFormat = null;
    private static string templateColumnStringFormat = null;
    private static string currentStringFormat = null;
    private static DataTemplate dataTemplate = null;

    #endregion

    #region PropertyPath DependencyProperty

    public static readonly DependencyProperty PropertyPathProperty;

    public static void SetPropertyPath(DependencyObject DepObject, string value)
    {
        DepObject.SetValue(PropertyPathProperty, value);
    }

    public static string GetPropertyPath(DependencyObject DepObject)
    {
        return (string)DepObject.GetValue(PropertyPathProperty);
    }

    #endregion

    #region StringFormat DependencyProperty

    public static readonly DependencyProperty StringFormatProperty;

    public static void SetStringFormat(DependencyObject DepObject, string value)
    {
        DepObject.SetValue(StringFormatProperty, value);
    }

    public static string GetStringFormat(DependencyObject DepObject)
    {
        try
        {
            return (string)DepObject.GetValue(StringFormatProperty);
        }

        catch 
        {
            return String.Empty;
        }
    }

    #endregion

    #region Constructor

    static DataGridHeader()
    {
        PropertyPathProperty = DependencyProperty.RegisterAttached("PropertyPath",
                                                                   typeof(string),
                                                                   typeof(DataGridHeader),
                                                                   new UIPropertyMetadata(String.Empty, IsSetHeader));

        StringFormatProperty = DependencyProperty.RegisterAttached("StringFormat",
                                                                   typeof(string),
                                                                   typeof(DataGridHeader),
                                                                   new UIPropertyMetadata(String.Empty));  
    }

    #endregion

    #region IsSetHeader PropertyChanged Handler

    private static void IsSetHeader(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var textColumn = sender as DataGridTextColumn;
        var templateColumn = sender as DataGridTemplateColumn;
        string path = e.NewValue as string;

        if ((textColumn == null) & (templateColumn == null)) 
        {
            return;
        }

        if (String.IsNullOrEmpty(path) == false)
        {
            currentStringFormat = ReturnStringFormat(textColumn, templateColumn);
            dataTemplate = CreateDynamicDataTemplate(path, currentStringFormat);

            if (dataTemplate != null)
            {
                if (textColumn != null)
                    textColumn.HeaderTemplate = dataTemplate;

                if (templateColumn != null)
                    templateColumn.HeaderTemplate = dataTemplate;
            }
        }
    }

    #endregion

    #region ReturnStringFormat Helper

    private static string ReturnStringFormat(DependencyObject depObject1, DependencyObject depObject2) 
    {
        textColumnStringFormat = GetStringFormat(depObject1) as string;
        templateColumnStringFormat = GetStringFormat(depObject2) as string;

        if (String.IsNullOrEmpty(textColumnStringFormat) == false)
        {
            return textColumnStringFormat;
        }

        if (String.IsNullOrEmpty(templateColumnStringFormat) == false)
        {
            return templateColumnStringFormat;
        }

        return "null";
    }

    #endregion

    #region CreateDynamicDataTemplate Helper

    private static DataTemplate CreateDynamicDataTemplate(string propertyPath, string stringFormat)
    {
        var pc = new ParserContext();
        MemoryStream sr = null;

        string xaml = GetXamlString(propertyPath, stringFormat);            
        sr = new MemoryStream(Encoding.ASCII.GetBytes(xaml));

        pc.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
        pc.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");

        return XamlReader.Load(sr, pc) as DataTemplate;
    }

    #endregion

    #region GetXamlString Helper

    private static string GetXamlString(string propertyPath, string stringFormat)
    {
        #region Original PropertyPath for TextBlock

        // {Binding Path=DataContext.YourProperty, RelativeSource={RelativeSource AncestorType={x:Type DataGridCellsPanel}}}"
        // Thanks to Bruno (https://stackoverflow.com/users/248118/bruno) for this trick

        #endregion

        var sb = new StringBuilder();

        sb.Append("<DataTemplate><TextBlock Text=\"{Binding Path=DataContext.");
        sb.Append(propertyPath);
        sb.Append(", StringFormat=");
        sb.Append(stringFormat);
        sb.Append(", RelativeSource={RelativeSource AncestorType={x:Type DataGridCellsPanel}}}\" /></DataTemplate>");

        return sb.ToString();
    }

    #endregion
}

XAML

<Window x:Class="BindingHeaderInDataGrid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:this="clr-namespace:BindingHeaderInDataGrid"
        xmlns:Behaviors="clr-namespace:BindingHeaderInDataGrid.AttachedBehaviors"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="220" Width="600">

    <Window.DataContext>
        <this:TestData />
    </Window.DataContext>

    <Grid Name="TestGrid">
        <DataGrid Name="TestDataGrid" 
                  Width="550"
                  Height="100"
                  Margin="10"
                  VerticalAlignment="Top"
                  Background="AliceBlue">

            <DataGrid.Columns>
                <DataGridTextColumn Behaviors:DataGridHeader.StringFormat="StringFormat: {0:C}"
                                    Behaviors:DataGridHeader.PropertyPath="TestStringFormatValue"
                                    Width="100"
                                    IsReadOnly="False">

                    <DataGridTextColumn.HeaderStyle>
                        <Style TargetType="{x:Type DataGridColumnHeader}">
                            <Setter Property="Height" Value="20" />
                            <Setter Property="Background" Value="Pink" />
                            <Setter Property="Margin" Value="2,0,0,0" />
                        </Style>
                    </DataGridTextColumn.HeaderStyle>
                </DataGridTextColumn>

                <DataGridTextColumn Behaviors:DataGridHeader.StringFormat="{x:Static Member=this:TestData.TestStaticStringFormatValue}"
                                    Behaviors:DataGridHeader.PropertyPath="TestStringFormatValue"
                                    Width="2*"
                                    IsReadOnly="False">

                    <DataGridTextColumn.HeaderStyle>
                        <Style TargetType="{x:Type DataGridColumnHeader}">
                            <Setter Property="Height" Value="20" />
                            <Setter Property="Background" Value="CadetBlue" />
                            <Setter Property="Margin" Value="2,0,0,0" />
                        </Style>
                    </DataGridTextColumn.HeaderStyle>
                </DataGridTextColumn>

                <DataGridTextColumn Behaviors:DataGridHeader.PropertyPath="TestUsualHeaderValue"
                                    Width="1.5*" 
                                    IsReadOnly="False">

                    <DataGridTextColumn.HeaderStyle>
                        <Style TargetType="{x:Type DataGridColumnHeader}">
                            <Setter Property="Height" Value="20" />
                            <Setter Property="Background" Value="Gainsboro" />
                            <Setter Property="Margin" Value="2,0,0,0" />
                        </Style>
                    </DataGridTextColumn.HeaderStyle>
                </DataGridTextColumn>

                <DataGridTemplateColumn Behaviors:DataGridHeader.PropertyPath="TestTemplateColumnValue"
                                        Width="150"
                                        IsReadOnly="False">

                    <DataGridTemplateColumn.HeaderStyle>
                        <Style TargetType="{x:Type DataGridColumnHeader}">
                            <Setter Property="Height" Value="20" />
                            <Setter Property="Background" Value="Beige" />
                            <Setter Property="Margin" Value="2,0,0,0" />
                        </Style>
                    </DataGridTemplateColumn.HeaderStyle>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>

        <Button Name="ChangeHeader" 
                Width="100" 
                Height="30"
                VerticalAlignment="Bottom"
                Content="ChangeHeader" 
                Click="ChangeHeader_Click" />
    </Grid>
</Window>

Code-behind

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }        

    private void ChangeHeader_Click(object sender, RoutedEventArgs e)
    {
        TestData data = this.DataContext as TestData;

        data.TestStringFormatValue = "777";
        data.TestUsualHeaderValue = "DynamicUsualHeader";
        data.TestTemplateColumnValue = "DynamicTemplateColumn";
    }
}

public class TestData : NotificationObject
{
    #region TestStringFormatValue

    private string _testStringFormatValue = "1";

    public string TestStringFormatValue
    {
        get
        {
            return _testStringFormatValue;
        }

        set
        {
            _testStringFormatValue = value;
            NotifyPropertyChanged("TestStringFormatValue");
        }
    }

    #endregion

    #region TestStaticStringFormatValue

    public static string TestStaticStringFormatValue = "Static StringFormat: {0}$";

    #endregion

    #region TestUsualHeaderValue

    private string _testUsualHeaderValue = "UsualHeader";

    public string TestUsualHeaderValue
    {
        get
        {
            return _testUsualHeaderValue;
        }

        set
        {
            _testUsualHeaderValue = value;
            NotifyPropertyChanged("TestUsualHeaderValue");
        }
    }

    #endregion

    #region TestTemplateColumnValue

    private string _testTemplateColumnValue = "TemplateColumn";

    public string TestTemplateColumnValue
    {
        get
        {
            return _testTemplateColumnValue;
        }

        set
        {
            _testTemplateColumnValue = value;
            NotifyPropertyChanged("TestTemplateColumnValue");
        }
    }

    #endregion
}
7
votes

I know this post is old, but when I looked up how to do this, this is the first entry that came up. I did not like this answer because it seemed like overkill. After more searching, I ran across this link the showed how to do this in the markup, using a template column.

<DataGridTemplateColumn>
    <DataGridTemplateColumn.HeaderTemplate>
        <DataTemplate>
            **<TextBlock Text="{Binding DataContext.HeaderTitle, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />**
        </DataTemplate>
    </DataGridTemplateColumn.HeaderTemplate>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}" Width="200" />
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
7
votes

I solved this by using the HeaderTemplate and binding to the DataContext of the DataGrid, using RelativeSource.

<DataGrid ItemsSource="{Binding Items}">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Value1}">
            <DataGridTextColumn.HeaderTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding DataContext.ColumnTitel1, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
                </DataTemplate>
            </DataGridTextColumn.HeaderTemplate>
        </DataGridTextColumn>
    </DataGrid.Columns>
</DataGrid>

The same binding within the Header property did not work out.

4
votes

BTW, in Silverlight (tested with SL 3.0) you can simply use the Header property as the DataContext for the ControlTemplate set via HeaderStyle (see my related question on SO).

I just tried this solution in WPF 3.5 using the WPF Toolkit DataGrid and it works!

2
votes

**EDIT :-

You can style the DataGridColumnHeader and do some funky bindings. try here and download the ColumnHeaderBindings.zip, it has a little test project, that is a bit of a hack, but it works

**End Edit

The Binding on the column happens on a per row basis, the column is not part of the visual tree, the binding gets applied to each item in the grid, from the grids source code you can see that the property Binding has these comments

    /// <summary>
    ///     The binding that will be applied to the generated element.
    /// </summary>
    /// <remarks>
    ///     This isn't a DP because if it were getting the value would evaluate the binding.
    /// </remarks>

So binding to the columns does not make much sense, because as you have found out, when you are not part of the visual tree you have no data context.

The same problem exists with the ComboBoxColumn when you want to bind to the items source. You can bind to a StaticResource, but StaticResources dont have a data context either. You could use an object data provider or just instantiate directly in xaml.

but i would just create the columns in code, and set the header. this problem would just go away then.

there is a good article here on the visual layout.

2
votes

an even better solution would be to set the binding in the header's style and pass the column as the header's dataContext... (or even better: to set up an object representing the header's dataContext and pass it)

see there for a way to do this :

How to set the DataContext on a DataGrid Column Header

1
votes

@mmichtch's answer works well for me, you just have to create a local namespace(xmlns), which contains reference to your project as follows:

xmlns:local="clr-namespace:your_project_name"

and along with it don't forget to mention the property you want to bind:

                <DataGridTextColumn Width="Auto">
                <DataGridTextColumn.Header>
                    <TextBlock Text="{Binding DataContext.PropertyNameYouWantToBind, 
                                          RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}}"/>
                </DataGridTextColumn.Header>
            </DataGridTextColumn>

it works well with VS 2010 and .net version 4.

0
votes

I used it to populate the DataGrid Column Header.

<DataGridTextColumn Binding="{Binding RowData}" Header="{Binding Mode=OneWay, Source={StaticResource spy},Path=DataContext.HeaderText,  FallbackValue= header text}"/>

I used a lowercase fallback value, and the value from DataContext was capitalized to assure me that the resource was not null. Also, the value from DataContext was only showing up for me at run time, during design time it displayed the fallback value. Hope this helps.