Wednesday, 2 November 2011

Arithmetic Converter

So before we get started on some more of the data grid functionality just wanted to upload a converter I've been working on yesterday.

You'll have probably seen a number of attempts at this before so why have I bothered writing my own? Well there are three reasons:
  1. I haven't written many parsers before so thought I would give it a go. It's the best way to learn.
  2. Most of the ones out there are not easily extendable so wanted to see if I could make on that was.
  3. Most good arithmetic expression parsers available on the market cost money.
I'm really writing this because I need to use it for some of the deeper dives into styling within WPF and XAML that I'll be doing later. Although it is obviously possible to write a converter for each and every styling equation you come across (and when you get slightly more complex there are quite a few of them) it is a lot easier to understand if all in one place and a lot easier to write as well.

If you have style designers, they can focus completely on the XAML without any requirement of c# knowledge or having to be bogged down by the unit testing that comes with that as well.

Here you will find the code that incorporates the arithmetic converter codebase, plus the associated unit test and an example of it's usage which looks something like this:


All of the rows in this view are calculated via bindings. No magic here!

So what does the converter allow at the moment?
  • Constants: Currently supported are digits and Pi.
  • Bound Variables: The converter can be used with either a binding or multi-binding. There is nothing stopping these being referenced multiple times.
    • Binding - the variable can be referenced either by x, a or {0}
    • MultiBinding - The first four bindings can be referenced either by x,y,z,t or a,b,c,d. It is also possible to use {n} to reference the nth element. 
  • Unary Operators: Either a negative or positive sign in front of a variable or constant.
  • Binary Operators: Support for addition, subtraction, division and multiplication in standard precedence order.
  • Brackets: Brackets to control evalation order.
  • Trigonometric Functions: Currently can support the application of sine, cosine and tangent functions.
  • Absolute Function: Can be used to evaluate the absolute value of an expression.
This has grown slightly organically over the last few hours but here's the basic components:

ArithmeticConverter.cs
This is focused on the management of type conversion between the bound sources and the target. It uses the parser to evaluate the operator tree and calculate the result from this.

ArithmeticExpressionParser.cs
Firstly will maintain a cache of any operator trees for a given expression. This means that our performance of parsing the expression can take a slight hit as this will all be evaluated up front for us.

So lets take a look at the key part of this class, the parse method. This is a recursively called method which each of the tokens have access to call if they need to parse the expression at a certain precedence level (i.e, when evaluating the right hand side of multiplication, we don't want to be taking into account additions as they'll take control)

   1:  public IArithmeticOperator Parse(string parseExpression, ref int position, int precedence, ref bool needsClosed)
   2:  {
   3:      IArithmeticOperator currentOperator = null;
   4:   
   5:      while (position < parseExpression.Length)
   6:      {
   7:          SkipWhiteSpace(parseExpression, ref position);
   8:   
   9:          var applied = false;
  10:          var highPrecedenceTokens = from t in Tokens where t.Precedence() > precedence select t;
  11:          foreach (var token in highPrecedenceTokens)
  12:          {
  13:              if (token.Apply(this, parseExpression, ref currentOperator, ref position, ref needsClosed))
  14:              {
  15:                  applied = true;
  16:                  break;
  17:              }
  18:          }
  19:   
  20:          // Keep returning until we no longer need to be closed.
  21:          if (needsClosed)
  22:              return currentOperator;
  23:   
  24:          if (applied) 
  25:              continue;
  26:   
  27:          if (precedence == (int)ArithmeticTokenPrecedence.LowestPrecendence)
  28:              throw new ArgumentException(String.Format("Failed to parse expression {0}", parseExpression));
  29:   
  30:          return currentOperator;
  31:      }
  32:   
  33:      return currentOperator;
  34:  }
 
The key things of note here are the precedence settings which are used to control which tokens can take over control from the current one. So for example, if we're currently parsing an addition but then come across a multiplication, it will take precedence.

If it was the other way round, then the current parse loop would be stopped (since addition wouldn't be a token to check) and the multiplication operator would be closed out. We would then go on to evaluate the addition afterwards.

The other main point is the needs closed parameter which is used for bracketing expressions. When a close bracket is found, we'll wriggle our way all the way back to the opening bracket and close out any open operators along the way. This is done by all tokens returning false when the expression needs closed until we get back to the bracket token, which will reset the variable so we can continue on our merry way.

The operators are relatively simple so I won't go into detail on them but what I will talk about is an example token so you can get a feel for how they work (and understand how to write your own if it so desires you). For our example we'll use a BinaryArithmeticToken (which all the basic operations like addition, subtraction, multiplication and division inherit from).

   1:  public abstract class BinaryArithmeticToken : BaseArithmeticToken
   2:  {
   3:      protected abstract char OperatorCharacter { get; }
   4:      protected abstract BinaryArithmeticOperator GetOperator(IArithmeticOperator leftOperator, IArithmeticOperator rightOperator);
   5:   
   6:      public override bool Apply(ArithmeticExpressionParser parser, string expression, ref IArithmeticOperator arithmeticOperator, ref int position)
   7:      {
   8:          if (arithmeticOperator == null)
   9:              return false;
  10:   
  11:          if (expression[position] != OperatorCharacter)
  12:              return false;
  13:   
  14:          position++;
  15:   
  16:          arithmeticOperator = GetOperator(arithmeticOperator, parser.Parse(expression, ref position, Precedence()));
  17:          return true;
  18:      }
  19:  }
 
So what is going on here? Well the operator character just refers to the grammer that we use in the expression like + or *. The key is in the application.

It first checks if arithmetic operator is null and doesn't process it if it is. The reason being that all binary operators (not functions if you're thinking of implementing it) have an expression on the left hand and on the right hand side. If the arithmetic operator is currently null, then no left hand side must exist so we'll pass this down to the unary tokens.

Then we obviously check the character matches up with the value in the expression. If so, we move the position forward passed this character and do our processing. All this means is that we set the arithmetic operator to a new BinaryOperator, where the left hand side is the currently evaluated operator, and the right hand side is left to the parser.

We set a precedence level so that no lowly operators can try to take the power from us.

So all of that is well and good, but lets be honest, most of the time you just want to hook up a new function. Well it's as simple as anything. say I want to write a new function which changes degrees to radians. What do I need to do?

Well given that there is a UnaryFunctionArithmeticToken already out there, you just need to inherit from that and fill in the pieces. Once you save it down, the parser will automatically pick it up and attempt to parse the expression with it! The code ain't too hard either.

   1:  public class DegsToRadsFunctionArithmeticToken : UnaryFunctionArithmeticToken
   2:  {
   3:      protected override IEnumerable<string> FunctionNames
   4:      {
   5:          get
   6:          {
   7:              yield return "radians";
   8:              yield return "rad";                
   9:          }
  10:      }
  11:   
  12:      protected override UnaryFunctionArithmeticOperator GetOperator(IArithmeticOperator arithmeticOperator)
  13:      {
  14:          return new UnaryFunctionArithmeticOperator(arithmeticOperator, x => (Math.PI / 180) * x);
  15:      }
  16:  }
 
All you need to know is how to use it. Well that isn't too much either. You can add the following XAML into the example provided to convert our entered value from degrees into radians

   1:  <Label Content="{Binding Text, ElementName=Text, Converter={ArithmeticConverter:ArithmeticConverter}, ConverterParameter=rad(x), UpdateSourceTrigger=PropertyChanged}"/>

And of course you can combine this in anyway you like with other operators or functions in your expression.

I want to stress one major thing here. I highly recommend NOT putting business logic into your XAML. It may seem very easy to do but should still focus on keeping the view seperate from the model using the MVVM pattern.

We will only be using this for styling purposes and as a visual helper in the later posts. Anyway, now that we have this, we can get on to some of the more cooler styling techniques. I'll also be looking to use the MediaFire site to host all the code written in this blog.

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!

Thursday, 27 October 2011

Getting to grips with DataGrid (Styles)

Due to work has been a while since I've posted here, but finally got a bit more free time. I've decided to invest a bit of time looking at the possibilities of the WPF Toolkit's DataGrid and really see how far we can push it.

So first thing is first, basic stylings. I want to be able to show an arrow that indicates the direction a value is going in. A simple premise that was painful to do in WinForms.

All ready? Then lets get started! First will create a mock object that I'll look to use throughout this series so it's easy for you to recreate the data. I'll also upload the code at some point as well.

namespace AllInTheXaml.DataGrid.BasicStyling
{
    public class TestObject
    {
        public string Test { get; set; }
        public double Value { get; set; }
    }
}
 
Great, now we'll start with the most basic implementation of the data grid and see what we get.
<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"/>
            <BasicStyling:TestObject Test="negative" Value="-1.7"/>
            <BasicStyling:TestObject Test="zero" Value="0"/>
        </Collections:ArrayList>        
    </Window.Resources>
    <Controls:DataGrid CanUserAddRows="False" HeadersVisibility="Column" 
        ItemsSource="{Binding Source={StaticResource data}}"/>        
</Window>
Well that seemed simple enough, so how does that look?

 Not too bad at all. 

Now lets come to styling. As you would imagine for the Microsoft provided grid model, it is very WPF focused. That means we can achieve a lot of what we want to do in XAML. In fact, nearly everything can be done without a single piece of code. Lets take a look at some of this XAML then.
 
<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"/>
            <BasicStyling:TestObject Test="negative" Value="-1.7"/>
            <BasicStyling:TestObject Test="zero" Value="0"/>
        </Collections:ArrayList>        
    </Window.Resources>    
        <Controls:DataGrid Margin="0,10,0,0" CanUserAddRows="False"  
            AutoGenerateColumns="False" HeadersVisibility="Column"  
            ItemsSource="{Binding Source={StaticResource data}}">
            <Controls:DataGrid.Columns>
                <Controls:DataGridTextColumn Header="Test" Binding="{Binding Test}"/>
                <Controls:DataGridTemplateColumn Header="Value">
                    <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" Source="greenUpArrow_16.png"/>
                        </Grid>
                        </DataTemplate>
                    </Controls:DataGridTemplateColumn.CellTemplate>
                </Controls:DataGridTemplateColumn>
                </Controls:DataGrid.Columns>
        </Controls:DataGrid>
</Window> 
 
The DataGrid provides a number of different types of template columns that you can use for rendering your cell. As you can see here, we have used a basic text one to show the header (given that we don't really want this to change from the default) but using the DataGridTemplateColumn for the value. This control gives us the ability to set exactly how the cell should look when being read. It also can be defined to show what it looks like when it is being edited as well! But that's for another time. Here we now show a text block (right aligned like all numbers should be) and an image of our arrow. So what does this look like then?


Ahh yes, well I've only used a single image. Well the reason was I wanted you to understand the templating first before we actually went to complete our goal. So I did say that almost all of it could be achieved in XAML, well here comes the little bit of code. We need to actually write a converter which will return us the sign of the value so we can predicate which arrow to use. Positive will mean an up green arrow, negative will mean a down red arrow and zero will mean set it to null. Simples! So here's the code.
using System;
using System.Globalization;
using System.Windows.Data;

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

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

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

And the XAML to go with 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"/>
            <BasicStyling:TestObject Test="negative" Value="-1.7"/>
            <BasicStyling:TestObject Test="zero" Value="0"/>
        </Collections:ArrayList>
        <BasicStyling:DoubleSignConverter x:Key="DoubleSignConverter"/>
    </Window.Resources>    
    <Controls:DataGrid Margin="0,10,0,0" CanUserAddRows="False" 
        AutoGenerateColumns="False" HeadersVisibility="Column"  
        ItemsSource="{Binding Source={StaticResource data}}">
        <Controls:DataGrid.Columns>
            <Controls:DataGridTextColumn Header="Test" Binding="{Binding Test}"/>
            <Controls:DataGridTemplateColumn Header="Value">
                <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> 
 

And we get a lovely column looking like this
   
So I hope that this shows just how much can be achieved with the DataGrid from a styling perspective very easily from just a few basic pieces of XAML.