Saturday 29 October 2011

Getting to grips with DataGrid (Styles Cont.)

So continuing on from the last post, here's a quick update on some more styling of your cells and grid. In this post we're going to show:
  • Changing the brush and hiding the vertical or horizontal grid lines.
  • Changing the foreground and background font brushes.
  • Playing around with text alignment (for the moment just left, center and right) in both vertical and horizontal.
So first of all, we're going to slightly expand on our test object to add some new values. Here's the one to use now:

namespace AllInTheXaml.DataGrid.BasicStyling
{
    public class TestObject
    {
        public string Test { get; set; }
        public double Value { get; set; }
        public double Bid { get; set; }
        public double Ask { get; set; }
    }
}
 
Here we're just introducing two new values which we can play around with.

Grid Lines

Lets start with the easy one. The DataGrid already exposes the brush for both the vertical and horizontal direction as VerticalGridLinesBrush and HorizontalGridLinesBrush respectively. Great thing is that you can set these to Transparent to make them invisible! It's as easy as that to hide them across the whole grid.


Here's a quick example:


<Window x:Class="AllInTheXaml.DataGrid.BasicStyling.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib" 
    xmlns:BasicStyling="clr-namespace:AllInTheXaml.DataGrid.BasicStyling" 
    xmlns:Controls="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit" 
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <Collections:ArrayList x:Key="data">
            <BasicStyling:TestObject Test="positive" Value="2.3" Bid="100" Ask="102"/>
            <BasicStyling:TestObject Test="negative" Value="-1.7" Bid="99" Ask="103"/>
            <BasicStyling:TestObject Test="zero" Value="0"/>
        </Collections:ArrayList>
        <BasicStyling:DoubleSignConverter x:Key="DoubleSignConverter"/>
    </Window.Resources>    
    <Controls:DataGrid CanUserAddRows="False" 
                       AutoGenerateColumns="False"                        
                       HeadersVisibility="Column" 
                       VerticalGridLinesBrush="Transparent"
                       ItemsSource="{Binding Source={StaticResource data}}">
        <Controls:DataGrid.Columns>
            <Controls:DataGridTextColumn Header="Test" Binding="{Binding Test}"/>
            <Controls:DataGridTemplateColumn Header="Change">
                <Controls:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <TextBlock Grid.Column="0" Text="{Binding Value}" HorizontalAlignment="Right"/> 
                            <Image Margin="2,0,0,0" Width="10" Height="10" x:Name="valueImage" Grid.Column="1">  
                                <Image.Style>
                                    <Style TargetType="{x:Type Image}">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="1">
                                                <Setter Property="Source" Value="greenUpArrow_16.png"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="-1">
                                                <Setter Property="Source" Value="redDownArrow_16.png"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="0">
                                                <Setter Property="Source" Value="{x:Null}"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </Image.Style>                                    
                            </Image>
                        </Grid>
                    </DataTemplate>
                </Controls:DataGridTemplateColumn.CellTemplate>
            </Controls:DataGridTemplateColumn>
        </Controls:DataGrid.Columns>
    </Controls:DataGrid>
</Window>
 
Which will produce a window like this with no vertical grid lines


Foreground Cell Brush

For this one we're going to keep playing around with the change column. Instead of having the negative sign, we'll show the absolute value of the change but add green or red colouring to indicate the direction.

First, you'll obviously need a new converter to get the absolute value, so lets put that together.

using System;
using System.Globalization;
using System.Windows.Data;

namespace AllInTheXaml.DataGrid.BasicStyling
{
    public class AbsConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if(value == null)
                return null;

            return Math.Abs((double) value);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException("Only able to support one way binding for AbsConverter");
        }
    }
}
And now all we need to do is add the right styling to our TextBlock to make sure it colours correctly. We can use the same sign converter as we did for the arrows for this.

<Window x:Class="AllInTheXaml.DataGrid.BasicStyling.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib" 
    xmlns:BasicStyling="clr-namespace:AllInTheXaml.DataGrid.BasicStyling" 
    xmlns:Controls="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit" 
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <Collections:ArrayList x:Key="data">
            <BasicStyling:TestObject Test="positive" Value="2.3" Bid="100" Ask="102"/>
            <BasicStyling:TestObject Test="negative" Value="-1.7" Bid="99" Ask="103"/>
            <BasicStyling:TestObject Test="zero" Value="0"/>
        </Collections:ArrayList>
        <BasicStyling:DoubleSignConverter x:Key="DoubleSignConverter"/>
        <BasicStyling:AbsConverter x:Key="AbsConverter"/>
    </Window.Resources>    
    <Controls:DataGrid CanUserAddRows="False" 
                       AutoGenerateColumns="False"                        
                       HeadersVisibility="Column" 
                       VerticalGridLinesBrush="Transparent"
                       ItemsSource="{Binding Source={StaticResource data}}">
        <Controls:DataGrid.Columns>
            <Controls:DataGridTextColumn Header="Test" Binding="{Binding Test}"/>
            <Controls:DataGridTemplateColumn Header="Change">
                <Controls:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <TextBlock Grid.Column="0" Text="{Binding Value, Converter={StaticResource AbsConverter}}" HorizontalAlignment="Right">
                                <TextBlock.Style>
                                    <Style TargetType="{x:Type TextBlock}">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="1">
                                                <Setter Property="Foreground" Value="#FF66C872"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="-1">
                                                <Setter Property="Foreground" Value="#FFBD182C"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="0">
                                                <Setter Property="Foreground" Value="#FF000000"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </TextBlock.Style>
                            </TextBlock>
                            <Image Margin="2,0,0,0" Width="10" Height="10" x:Name="valueImage" Grid.Column="1">  
                                <Image.Style>
                                    <Style TargetType="{x:Type Image}">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="1">
                                                <Setter Property="Source" Value="greenUpArrow_16.png"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="-1">
                                                <Setter Property="Source" Value="redDownArrow_16.png"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="0">
                                                <Setter Property="Source" Value="{x:Null}"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </Image.Style>                                    
                            </Image>
                        </Grid>
                    </DataTemplate>
                </Controls:DataGridTemplateColumn.CellTemplate>
            </Controls:DataGridTemplateColumn>
        </Controls:DataGrid.Columns>
    </Controls:DataGrid>
</Window> 
 
Just look how easy this is. We get to actually work with the value itself and format it exactly how we want with touching code and it's so easy to plug and play our converters and different controls to create really custom styles to the grid. This just shows one of the real great powers of WPF in action. Here's what we get


Background Cell Brush and Text Alignment Together!


There is a reason I want to show these together and it's mainly to show the limitation of some of the controls in the DataGrid. This actually is not possible to do with the DataGridTextColumn. The default control it seems that underlines this is a TextBlock (which makes sense) but if you've worked much with it you'll know that it doesn't have a HorizontalContentAlignment property. This means you can't have the control itself stretch the whole cell and align the text.

I must admit, I found this quite a bad oversight from the DataGrid developers as it seems like reasonably obvious functionality. However, given we have the DataGridTemplateColumn all is not lost! We can design it ourselves. Hoozah!

All we really need to do is add a Border around the TextBlock. We can align the TextBlock as we want with a transparent background and then use the Border (which stretches by default) to control our background brush.

Before we put that all in motion just going to introduce one last converter so that if the bid or ask values are zero they don't show up (this will also show how to hide error or null values in the grid)
using System;
using System.Globalization;
using System.Windows.Data;

namespace AllInTheXaml.DataGrid.BasicStyling
{
    public class IsZeroOrNullConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if(value == null)
                return null;

            return ((double) value) == 0;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException("Only able to support one way binding for IsZeroOrNullConverter");
        }
    }
}
And now the XAML

<Window x:Class="AllInTheXaml.DataGrid.BasicStyling.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:Collections="clr-namespace:System.Collections;assembly=mscorlib" 
    xmlns:BasicStyling="clr-namespace:AllInTheXaml.DataGrid.BasicStyling" 
    xmlns:Controls="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit" 
    Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <Collections:ArrayList x:Key="data">
            <BasicStyling:TestObject Test="positive" Value="2.3" Bid="100" Ask="102"/>
            <BasicStyling:TestObject Test="negative" Value="-1.7" Bid="99" Ask="103"/>
            <BasicStyling:TestObject Test="zero" Value="0"/>
        </Collections:ArrayList>
        <BasicStyling:DoubleSignConverter x:Key="DoubleSignConverter"/>
        <BasicStyling:AbsConverter x:Key="AbsConverter"/>
        <BasicStyling:IsZeroOrNullConverter x:Key="IsZeroOrNullConverter"/>
    </Window.Resources>    
    <Controls:DataGrid CanUserAddRows="False" 
                       AutoGenerateColumns="False"                        
                       HeadersVisibility="Column" 
                       VerticalGridLinesBrush="Transparent"
                       ItemsSource="{Binding Source={StaticResource data}}">
        <Controls:DataGrid.Columns>
            <Controls:DataGridTextColumn Header="Test" Binding="{Binding Test}"/>
            <Controls:DataGridTemplateColumn Header="Change">
                <Controls:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <TextBlock Grid.Column="0" Text="{Binding Value, Converter={StaticResource AbsConverter}}" HorizontalAlignment="Right">
                                <TextBlock.Style>
                                    <Style TargetType="{x:Type TextBlock}">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="1">
                                                <Setter Property="Foreground" Value="#FF66C872"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="-1">
                                                <Setter Property="Foreground" Value="#FFBD182C"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="0">
                                                <Setter Property="Foreground" Value="#FF000000"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </TextBlock.Style>
                            </TextBlock>
                            <Image Margin="2,0,0,0" Width="10" Height="10" x:Name="valueImage" Grid.Column="1">  
                                <Image.Style>
                                    <Style TargetType="{x:Type Image}">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="1">
                                                <Setter Property="Source" Value="greenUpArrow_16.png"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="-1">
                                                <Setter Property="Source" Value="redDownArrow_16.png"/>
                                            </DataTrigger>
                                            <DataTrigger Binding="{Binding Value, Converter={StaticResource DoubleSignConverter}}" Value="0">
                                                <Setter Property="Source" Value="{x:Null}"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </Image.Style>                                    
                            </Image>
                        </Grid>
                    </DataTemplate>
                </Controls:DataGridTemplateColumn.CellTemplate>
            </Controls:DataGridTemplateColumn>
            <Controls:DataGridTemplateColumn Width="40" Header="Bid">
                <Controls:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Border Margin="-1">
                            <TextBlock Text="{Binding Bid}" HorizontalAlignment="Center">
                            <TextBlock.Style>
                                <Style TargetType="{x:Type TextBlock}">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Bid, Converter={StaticResource IsZeroOrNullConverter}}" Value="true">
                                            <Setter Property="Foreground" Value="Transparent"/>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBlock.Style>
                            </TextBlock>
                            <Border.Style>
                                <Style TargetType="{x:Type Border}">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Bid, Converter={StaticResource IsZeroOrNullConverter}}" Value="false">
                                            <Setter Property="Background" Value="#FFAAC9E9"/>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Border.Style>
                        </Border>
                    </DataTemplate>
                </Controls:DataGridTemplateColumn.CellTemplate>
            </Controls:DataGridTemplateColumn>
            <Controls:DataGridTemplateColumn Width="40" Header="Ask">
                <Controls:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <Border Margin="-1">
                            <TextBlock Text="{Binding Ask}" HorizontalAlignment="Center">
                            <TextBlock.Style>
                                <Style TargetType="{x:Type TextBlock}">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Ask, Converter={StaticResource IsZeroOrNullConverter}}" Value="true">
                                            <Setter Property="Foreground" Value="Transparent"/>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </TextBlock.Style>
                            </TextBlock>
                            <Border.Style>
                                <Style TargetType="{x:Type Border}">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Ask, Converter={StaticResource IsZeroOrNullConverter}}" Value="false">
                                            <Setter Property="Background" Value="#FFFFA4A4"/>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Border.Style>
                        </Border>
                    </DataTemplate>
                </Controls:DataGridTemplateColumn.CellTemplate>
            </Controls:DataGridTemplateColumn>
        </Controls:DataGrid.Columns>
    </Controls:DataGrid>
</Window> 
 
Those with the observant eye will have noticed the little introduction of margins on the border. These are due to the whitespace that the data grid cell automatically adds around your template. I don't particularly like this so using margins to override them. I'm using a constant margin around the whole border so that this wouldn't look odd if you started moving the columns around.

Here's how it looks

So that's pretty much it for me today. Look forward to some more data grids tomorrow!

2 comments:

  1. Nice post, I just wonder if there is a way to totally hide the vertical border, like: VerticalBorderThickness="0" ?

    Regards,
    Peter Larsson

    ReplyDelete
  2. Thanks Peter. At the moment the release build of WpfToolkit's DataGrid doesn't actually support border thickness. However, if you download the toolkit from here you'll find that the code is actually there (though not available in the release build).

    The easiest way to get this working is to build the code yourself using a conditional compilation symbol of GridLineThickness. I've not tried this myself but looking at the codebase that should get it working.

    ReplyDelete