So, what changes do we need to make to the turn this DataGrid control into something that looks more like a spreadsheet? We need to style it accordingly and to populate the row headers with numbers that identify each row. We also need to highlight the relevant row and column headers when a cell is selected and can implement an animated selection rectangle to highlight the selected cell, instead of using the default highlighting shown in the image.
First, let's populate the row headers with numbers. There are several ways to achieve this, but I prefer to simply ask each row what its index is in a converter class and connect it to the row header via a data binding. Let's see this converter now:
using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace CompanyName.ApplicationName.Converters { [ValueConversion(typeof(DataGridRow), typeof(int))] public class DataGridRowToRowNumberConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter,
CultureInfo culture) { if (value is DataGridRow dataGridRow)
return dataGridRow.GetIndex() + 1; return DependencyProperty.UnsetValue; } public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
This is another simple class and, as usual, we start by specifying the data types involved in the converter in the ValueConversion attribute. In this case, our input will be DataRow objects and our output will be their integer row numbers. In the Convert method, we use Pattern Matching from C# 6.0 as a shortcut to validate that our input value is not null and is of the appropriate type and if suitable, to cast it to that type.
If the input is valid, we call the GetIndex method on the pre-cast dataGridRow variable, remembering to add 1 to the zero-based method result, before returning it from the converter. For all other input values, we return the DependencyProperty.UnsetValue value. As we will not need to convert any values in the other direction, we leave the ConvertBack method unimplemented.
Let's see how we use this converter class now. First, we need to set up a XAML Namespace for our Converters CLR Namespace and create an instance of it in the control's Resources section:
xmlns:Converters="clr-namespace:CompanyName.ApplicationName.Converters;
assembly=CompanyName.ApplicationName.Converters"
... <Converters:DataGridRowToRowNumberConverter
x:Key="DataGridRowToRowNumberConverter" />
We are then able to use it in a data binding on the Text property of a TextBlock element in the DataTemplate, that is applied to the RowHeaderTemplate property in our custom DataGrid:
<DataGrid.RowHeaderTemplate> <DataTemplate> <TextBlock Text="{Binding Path = .,
RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}},
Converter={StaticResource DataGridRowToRowNumberConverter}}" /> </DataTemplate> </DataGrid.RowHeaderTemplate>
Note that the binding path is set to ., which as you may remember, sets it to the whole binding object. The RelativeSource binding sets the binding source to the first ancestor of the TextBlock of type DataGridRow, and so we pass the whole DataGridRow object through to the binding and therefore, also to the converter, as required.
Also, note that we must declare this RowHeaderTemplate property below the Resources section in the XAML file. Failure to do this will result in the following runtime error:
Cannot find resource named 'DataGridRowToRowNumberConverter'. Resource names are case sensitive.
Whereas sometimes we can fix these "reference not found" errors by using a DynamicResource markup extension instead of a StaticResource markup extension, it won't work in this case. This is because we can only use them on a DependencyProperty of a DependencyObject and the Converter property is not a DependencyProperty and the Binding class is not a DependencyObject.
Let's see what our spreadsheet looks like now:
As can be seen from the preceding image, we clearly need to add some styling to fix some issues and make it look more like a typical spreadsheet:
<!--Default Selection Colors--> <SolidColorBrush
x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" /> <SolidColorBrush
x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" /> <SolidColorBrush
x:Key="{x:Static DataGrid.FocusBorderBrushKey}" Color="Transparent" /> <SolidColorBrush x:Key="{x:Static
SystemColors.InactiveSelectionHighlightBrushKey}" Color="Transparent" /> <LinearGradientBrush x:Key="HorizontalBorderGradient" StartPoint="0,0"
EndPoint="0,1"> <GradientStop Color="{StaticResource BackgroundColor}" /> <GradientStop Color="{StaticResource BorderColor}" Offset="1" /> </LinearGradientBrush> <LinearGradientBrush x:Key="VerticalBorderGradient" StartPoint="0,0"
EndPoint="1,0"> <GradientStop Color="{StaticResource BackgroundColor}" /> <GradientStop Color="{StaticResource BorderColor}" Offset="1" /> </LinearGradientBrush> <LinearGradientBrush x:Key="DiagonalBorderGradient" StartPoint="0.2,0"
EndPoint="1,1"> <GradientStop Color="{StaticResource BackgroundColor}" Offset="0.45" /> <GradientStop Color="{StaticResource BorderColor}" Offset="1" /> </LinearGradientBrush> ... <Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Background"
Value="{StaticResource BackgroundBrush}" />
<Setter Property="BorderThickness" Value="0,0,1,1" />
<Setter Property="BorderBrush"
Value="{StaticResource VerticalBorderGradient}" />
<Setter Property="Padding" Value="4,0" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background"
Value="{StaticResource BackgroundBrush}" />
<Setter Property="BorderThickness" Value="0,0,1,1" />
<Setter Property="BorderBrush"
Value="{StaticResource HorizontalBorderGradient}" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="13" />
</Style>
The only thing to note here are the first four SolidColorBrush objects that we declared. They are used by the .NET Framework to set the default selection colors for a number of the built-in controls. We can use them to change the default blue background and white text shown in the previous image. There are many more of these default colors to be found in the SystemColors class, so it's worth familiarizing yourself with them.
Let's see what our spreadsheet looks like now:
Now, our Spreadsheet control is starting to look more like a typical spreadsheet application, but we have no highlighting for our selected cell anymore. You may also notice that the row headers are not center-aligned horizontally, as our style suggests they should be.
This happens because, unlike the default ControlTemplate for the DataGridColumnHeader class, the default ControlTemplate for the DataGridRowHeader class does not map the HorizontalContentAlignment property to the HorizontalAlignment property on any internal elements within the template.
This might at first seem like an oversight on Microsoft's part, but it is actually because, in the default ControlTemplate, each DataGridRowHeader object has an additional control that displays the validation error to the right of the header content. With this extra control taking up the limited space, there is not enough space to horizontally center the row header.
To fix this problem, we will need to alter the default ControlTemplate, to remove the control that displays the error template. Co-incidentally, we will also need to alter this template to be able to highlight the selected cell in the row header. Likewise, to highlight the selected cell in the column header, we will need to adjust the default ControlTemplate for the DataGridColumnHeader class.