Friday, August 29, 2014

Issue using WPF DataGrid ColumnHeader with a DataTemplate & ClipboardCopyMode="IncludeHeader"

While working on a project recently, I ran into a small hitch with the WPF data grid.  The software I spend most of my time working on relies heavily on units of measure.  A common requirement is to have units of measure displayed in the data grid column headers.  Something like this...


However, you can't directly bind to the Header property of the data grid in XAML.  There are probably several ways this could be worked around, but a common solution is to use a HeaderTemplate.  The XAML to create a data template for the data grid column header would look like this.

 <UserControl.Resources>  
     <DataTemplate x:Key="FlowRate" DataType="DataGridColumnHeader">  
       <TextBlock Text="{Binding Source={x:Static units:UnitsContext.CurrentSymbols}, Path=FlowUnit, StringFormat=Flow-Rate ({0})}" />  
     </DataTemplate>  
     <DataTemplate x:Key="Pressure" DataType="DataGridColumnHeader">  
       <TextBlock Text="{Binding Source={x:Static units:UnitsContext.CurrentSymbols}, Path=PressureUnit, StringFormat=Pressure ({0})}" />  
     </DataTemplate>  
   </UserControl.Resources>  

All is well and good until the next requirement comes along.  Users need to be able to copy the data from the data grid into Excel, including the column headers.  The data grid has a clipboard copying setting, ClipboardCopyMode="IncludeHeader", if the column uses a HeaderTemplate, it will show as an empty header in Excel.  A HeaderTemplate could include almost anything, and it isn't guaranteed to be text.  Therefore, I had to admit to myself that, while annoying, this data grid issue does make sense.

I arrived at a simple fix by using an attached property to move the the text from the header template, into the data grids header.

  /// <summary>  
   /// WPF Data grid does not know what is in a header template, so it can't copy it to the clipboard when using ClipboardCopyMode="IncludeHeader".  
   /// This attached property works with a header template that includes one TextBlock. Text content from the templates TextBlock is copied to the  
   /// column header for the clipboard to pick up.  
   /// </summary>  
   public static class TemplatedDataGridHeaderText  
   {  
     private static readonly Type OwnerType = typeof(TemplatedDataGridHeaderText);  
     public static readonly DependencyProperty UseTextFromTemplateProperty = DependencyProperty.RegisterAttached("UseTextFromTemplate", typeof(bool), OwnerType, new PropertyMetadata(false, OnHeaderTextChanged));  
     public static bool GetUseTextFromTemplate(DependencyObject obj)  
     {  
       return (bool)obj.GetValue(UseTextFromTemplateProperty);  
     }  
     public static void SetUseTextFromTemplate(DependencyObject obj, bool value)  
     {  
       obj.SetValue(UseTextFromTemplateProperty, value);  
     }  
     private static void OnHeaderTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
     {  
       var textColumn = d as DataGridTextColumn;  
       if (textColumn == null) return;  
       if (textColumn.HeaderTemplate == null) return;  
       var headerTemplateTexblockText = textColumn.HeaderTemplate.LoadContent().GetValue(TextBlock.TextProperty).ToString();  
       textColumn.Header = headerTemplateTexblockText;  
     }  
   }  

An alternative approach might be to directly set the header text through an attached property...

  /// <summary>  
   /// Allows binding a property to the header text. Works with the clipboard copy mode - IncludeHeaders.  
   /// </summary>  
   public static class DataGridHeaderTextAttachedProperty  
   {  
     private static readonly Type OwnerType = typeof(DataGridHeaderTextAttachedProperty);  
     public static readonly DependencyProperty HeaderTextProperty = DependencyProperty.RegisterAttached("HeaderText", typeof(string), OwnerType, new PropertyMetadata(OnHeaderTextChanged));  
     public static string GetHeaderText(DependencyObject obj)  
     {  
       return (string)obj.GetValue(HeaderTextProperty);  
     }  
     public static void SetHeaderText(DependencyObject obj, string value)  
     {  
       obj.SetValue(HeaderTextProperty, value);  
     }  
     private static void OnHeaderTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)  
     {  
       var textColumn = d as DataGridTextColumn;  
       if (textColumn == null) return;  
       textColumn.Header = GetHeaderText(textColumn);  
     }  
   }  

In XAML, the attached property can be used on each data grid column...

 <DataGrid ItemsSource="{Binding }" AutoGenerateColumns="False" IsReadOnly="True" VerticalScrollBarVisibility="Auto" VerticalAlignment="Stretch">  
     <DataGrid.Columns>  
       <DataGridTextColumn Binding="{Binding FlowRate.UserValue, StringFormat=N3}" HeaderTemplate="{StaticResource FlowRate}"  
                 attachedProperties:TemplatedDataGridHeaderText.UseTextFromTemplate="True"/>  
       <DataGridTextColumn Binding="{Binding Pressure.UserValue, StringFormat=N3}" HeaderTemplate="{StaticResource Pressure}"  
                 attachedProperties:TemplatedDataGridHeaderText.UseTextFromTemplate="True"/>  
     </DataGrid.Columns>  
   </DataGrid>  

Thus far, this has proved to be a simple solution for this particular data grid issue :)




1 comment:

  1. Thanks ... your attached property route worked perfectly for me!

    ReplyDelete