Angled headers for DataGrid columns…not just angled text…
My last post demonstrated one way that you could angle the text in a DataGrid column header. I received a comment asking for how to angle the entire headers, along the lines of what you can do very easily with Excel. Here’s an example from Excel:
My friend Margaret on the WPF team saw this comment as well and set about solving this problem for WPF. I worked with her for a bit and we came up with what we thought was a pretty good solution. Here's a link to Margaret's post…I took the WPF XAML and set about porting it to Silverlight. I had a couple of obstacles:
1) Layout works just a bit different in Silverlight than WPF
2) I don't know as much as I should about layout in Silverlight
We created a style for the ColumnHeaderStyle property that contains a new template for the header. The basic summary of our template is that we drew a rectangle, skewed it 45 degrees and moved it to the right, then added a content presenter bound to the content of the column header, which we rotated 45 degrees and also moved to the right, to align with the column the header is associated with.
In our first WPF sample, we used a combination of render and layout transforms. I couldn’t get this code to work properly in Silverlight so I reworked the code to use just render transforms. The resulting code works pretty well, and when I ported it back to WPF, it worked there too. The render transform isn't perfect because as the column header content gets longer, so does the width of the column, but it's not 1 to 1, so I can live with this for now. It's something I'll continue to investigate.
Here’s what the running code looks like when I’ve bound the DataGrid to a collection that contains 3 writers with the same data as in the image above:
Not bad..huh?
Here’s the Silverlight version of the XAML:
<UserControl xmlns:my="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
x:Class="DataGridAngledHeaders.MainPage"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:primitives="clr-namespace:System.Windows.Controls.Primitives;assembly=System.Windows.Controls.Data"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DataGridAngledHeaders"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.Resources>
<local:WidthToTranslate x:Key="WidthConverter" />
<local:RectangleHeight x:Key="HeightConverter" />
</Grid.Resources>
<my:DataGrid Name="DG" ItemsSource="{Binding}" >
<my:DataGrid.ColumnHeaderStyle>
<Style TargetType="primitives:DataGridColumnHeader">
<Setter Property="Template" >
<Setter.Value>
<ControlTemplate >
<Grid ShowGridLines="True">
<Rectangle Name="Angle" Width="{TemplateBinding Width}"
Height="{Binding Path=[0], Converter={StaticResource HeightConverter}}"
Fill="LightBlue" Stroke="Black"
StrokeThickness="1" >
<Rectangle.RenderTransform>
<SkewTransform CenterX="0"
CenterY="{Binding ElementName=Angle, Path=Height}" AngleX="-45" AngleY="0" />
</Rectangle.RenderTransform>
</Rectangle>
<ContentPresenter VerticalAlignment="Bottom"
HorizontalAlignment="Left" >
<ContentPresenter.RenderTransform>
<TransformGroup>
<RotateTransform Angle="-45"/>
<TranslateTransform X="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=ActualWidth, Mode=OneWay, Converter={StaticResource WidthConverter}}" />
</TransformGroup>
</ContentPresenter.RenderTransform>
</ContentPresenter>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</my:DataGrid.ColumnHeaderStyle>
</my:DataGrid>
</Grid>
</UserControl>
Of course you probably want the code behind, as well. The most interesting items in this code are the two converters. WidthToTranslate calculates where the text content should be moved to to align with its column, and RectangleHeight calculates the height of the rectangle that contains the header name.
Originally we set a static height for the rectangle, but we both became sort of obsessed with setting this height dynamically, based on the length of property names for type contained in the collection you’ve bound to. I sort of solved this problem in Silverlight, but it’s still not a perfect solution. I bind to one of the items in the collection displayed by the DataGrid and use the RectangleHeight converter to calculate a value based on the longest property name of the item type. This converter gets called for every column so I put in some code to circumvent all the calculating after the first column so this converter is a bit complex. Also the converter works well until you change the default font size or put in your own header name or something like that…then you won’t get the correct rectangle height. So the RectangleHeight converter feels a bit like using an expensive sledgehammer when you really need a specific wrench. It’ll work, but not all the time, and you’ve wasted a lot of money for the times when it doesn’t work. Margaret can make this work in WPF because of WPF’s multi-binding capability. Check out her post here. There might be some other approaches for Silverlight here, but I haven’t come up with them yet.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Reflection;namespace DataGridAngledHeaders
{
public partial class MainPage : UserControl
{
// Create a collection of writer objects.
ObservableCollection<Writer> writers = new ObservableCollection<Writer>() {new Writer("Chris Sells", 114, "Writer II"),
new Writer("Luka Albrous",225, "Senior Writer"), new Writer("Jim Hance", 140, "Writer I")};
public MainPage()
{
InitializeComponent();
DG.DataContext = writers;
}}
//Simple class to bind to.
public class Writer
{
public Writer() { }
public Writer(string writerName, int numOfTypes, string writerTitle)
{
Name = writerName;
Types = numOfTypes;
Title = writerTitle;
}public string Name { get; set; }
public int Types { get; set; }
public string Title { get; set; }
}
// Converter to figure out how much the text needs to be moved.
public class WidthToTranslate : IValueConverter
{
#region IValueConverter Memberspublic object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
Double width = (Double)value;
return width / 2 ;
}public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}#endregion
}
// Converter to calculate the height of the rectangle that contains the header text.
// This converter should be passed an item in the collection.
public class RectangleHeight : IValueConverter
{
#region IValueConverter Members
// Static to check if header height has been calculated.
static double headerHeight = 0;public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
// If header height has not been calculated, go ahead and calculate,
// otherwise just return the previously calculated value.
if (headerHeight == 0)
{
// Get the type of the item passed in.
Type valueType = value.GetType();// Get the properties of the type.
PropertyInfo[] properties = valueType.GetProperties();TextBlock tb = new TextBlock();
int propLength = 0;
// Get the longest property name and set the TextBlock text to the name.
foreach (PropertyInfo p in properties)
if (p.Name.Length > propLength)
{
tb.Text = p.Name;
propLength = p.Name.Length;
}//Return the width of the textblock plus some padding.
headerHeight = tb.ActualWidth + 5;
return headerHeight;
}
// If the value has already been calulated, then return it.
return headerHeight;
}public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}#endregion
}
}
I also need to mention that in this solution the column header keep their original hit region, so in order to sort or move the columns, you have to click very low in the header. I am sure there are ways to change the hit regions to reflect the actual area of the column header, but at this point, I need to move on to something else. Margaret is working on the hit region problem and I’ll make sure and link to her post if she solves it.
Enjoy!
--Cheryl
Comments
Anonymous
January 07, 2010
It's not working for me. Could you please provide source code? Thanks.Anonymous
January 18, 2010
This isn't working for me either. Does this require Silverlight 4? It blows up when binding the Rectangle's SkewTransform CenterY property and when binding the ContentPresenter's TranslateTransform X property.Anonymous
February 04, 2010
Yes, this is a Silverlight 4 sample. I'll get this posted to Code Gallery and add a link.Anonymous
April 07, 2010
The comment has been removedAnonymous
April 12, 2010
i've got the same issue on ie8. any feedback?Anonymous
April 13, 2010
I've fixed the issue with this sample for SL4. For some reason the template binding on the ContentPresenter creates a parse error. I'm investigating why..meanwhile, without this template binding the sample should work just fine on SL4. CherylAnonymous
April 13, 2010
Nice, Thank's for this code review, it work fine now For the new lector of this page: the code up in this page is the updated code and work fine. thank you CherylAnonymous
June 29, 2010
This is a fantastic article. I have been testing it out and I have one little issue with it. Inside of Excel you are able to shrink the columns width so there is no padding between each column header. This is nice if the content you have under the column is small like a combobox for instance. I really want it to look like this (this is what it looks like inside of Excel): docs.google.com/leaf However with a slight modification I made it look like this in silverlight: docs.google.com/leaf As you can see we have an issue on the column headers. In the XAML I changed <ContentPresenter VerticalAlignment="Bottom" HorizontalAlignment="Left" > to this <ContentPresenter VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="30"> Now the size of the additional headers is correct, but the text is all chopped off since I made the width set to 30. Am I thinking about this all wrong? Is there a way to make it look like excel? Thank you so much.Anonymous
July 27, 2010
This is not at all working for me. First tell me how would I add the namespace/assembly DataGridAngledHeaders which you have mentioned. I need it quickly if you can help. ThanksAnonymous
July 27, 2010
The comment has been removed