Emphasizing the Selection

All that is left for us to do now is to implement the selection rectangle and the style for the Select All button in the top left corner of the control. We can accomplish both tasks by adjusting the default ControlTemplate for the DataGrid class. Let's break this down into steps. First, we need to add a ControlTemplate for the Select All button into our Resources section:

<ControlTemplate x:Key="SelectAllButtonControlTemplate" 
TargetType="{x:Type Button}">
<Border BorderThickness="0,0,1,1" BorderBrush="{StaticResource
DiagonalBorderGradient}" Background="{StaticResource BackgroundBrush}">
<Polygon Fill="#FFB3B3B3" Points="0,12 12,12 12,0"
HorizontalAlignment="Right" VerticalAlignment="Bottom"
Stretch="Uniform" Margin="10,3,3,3" />
</Border>
</ControlTemplate>

Here, we have another very simple template, where we replace the default definition of a Button control with a basic triangle. It contains a Border element, that draws its right and bottom borders with the DiagonalBorderGradient brush that we added to the spreadsheet control's resources. It also paints the background of the Button control with our BackgroundBrush resource.

Within the Border element, we declare a Polygon shape, which we fill with a gray brush. Its shape is determined by the values declared in its Points property, so it begins at 0,12, continues to 12,12 , and 12,0, before returning to 0,12. Plotting these values on a graph would show a triangle and that is the shape that this Polygon element will render.

We align it to the bottom right of the Border element and set its Stretch property to Uniform to ensure that its aspect ratio is maintained throughout any changes of its size. Finally, we set its Margin property to space it away from the Border element's edge.

Next, we need to apply the SelectAllButtonControlTemplate template to the Select All button and add a transparent Canvas element into the ControlTemplate for the ScrollViewer object that appears inside the default ControlTemplate for the DataGrid class. Let's extract this from the default template and declare it in our Resources section too:

<ControlTemplate x:Key="ScrollViewerControlTemplate" 
TargetType="{x:Type ScrollViewer}">
<Grid>
...
<Button Command="ApplicationCommands.SelectAll"
Focusable="False" Width="26" Height="26"
Template="{StaticResource SelectAllButtonControlTemplate}" />
...
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter" ... />
<Border Grid.Row="1" Grid.Column="1" ClipToBounds="True"
BorderThickness="0" IsHitTestVisible="False" Margin="-2">
<Canvas Name="SelectionRectangleCanvas" Background="{x:Null}"
IsHitTestVisible="False" RenderTransformOrigin="0.5,0.5"
Margin="2" />
</Border>
<ScrollBar x:Name="PART_VerticalScrollBar" ... />
...
</Grid>
</ControlTemplate>

We first set a width and height of 26 pixels on the Select All button, in line with the dimensions of our row and column headers. We then apply our ControlTemplate from the Resources section to it. We also removed the Visibility binding from the default template, as we won't be needing that in our example. Note that this button has no action in our example and is purely decorative.

Next, we added the transparent Canvas control, that will display the selection rectangle, within a Border element. Note that we must add it after the required PART_ScrollContentPresenter named part, to ensure that the selection rectangle will appear above the cells in the Z plane. Also, notice that we must wrap it in an invisible Border element, so that we can clip its bounds. Try removing the ClipToBounds property and resize the control to be smaller as an experiment to see what happens.

We set the Margin property on the Border element to be -2 in all directions, so that it can display the selection rectangle over and just outside the bounds of each cell. We, therefore, need to set the Margin property on the Canvas that draws the rectangle to 2 in all directions, to compensate for the border's negative margin.

We name the Canvas element, so that we can access it from the code behind, and set its Background property to null, which is slightly cheaper than setting it to Transparent. We then set the IsHitTestVisible property to False, to make it invisible to the users and their mouse cursors and center the origin of the render transform, which we will use to update the position of the Canvas element each time the containing ScrollViewer object is moved.

Let's see our simplified ControlTemplate for the DataGrid class now:

<ControlTemplate x:Key="DataGridControlTemplate" 
TargetType="{x:Type DataGrid}">
<Border ... >
<ScrollViewer x:Name="DG_ScrollViewer" Focusable="False"
CanContentScroll="False"
Template="{StaticResource ScrollViewerControlTemplate}">
<ItemsPresenter
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
</ControlTemplate>

We made a few changes to the default ControlTemplate for the DataGrid control. The first was to set the CanContentScroll property to False on the ScrollViewer element named DG_ScrollViewer, to make it scroll in physical units (pixels) instead of logical units (rows). The only other change was to replace its inline ControlTemplate object with a reference to the custom template that we added into the Resources section.

We must also remember to assign this custom ControlTemplate object to our spreadsheet control. This can be achieved in the class declaration:

<DataGrid 
x:Class="CompanyName.ApplicationName.Views.Controls.Spreadsheet" ...
Template="{DynamicResource DataGridControlTemplate}">
...
</DataGrid>

Now, let's see our Spreadsheet control again, with all the latest changes:

We can see that the job is nearly complete. We now have the XAML all set up to display the selection rectangle, but we still need to programmatically position and animate it. First, we'll need to attain a reference to the Scrollviewer from our custom DataGrid template. 

We can achieve this by overriding another method from the DataGrid base class. The OnApplyTemplate method is called whenever a ControlTemplate is applied, so it's an ideal location to access the elements contained within it:

private ScrollViewer scrollViewer; 
 
... 
 
public override void OnApplyTemplate() 
{ 
  scrollViewer = Template.FindName("DG_ScrollViewer", this) as ScrollViewer; 
} 

In this method, we call the FindName method on the Spreadsheet control's template, passing in the name of our ScrollViewer object and a reference to the spreadsheet, as the templated parent. We then cast the returned object to a ScrollViewer, using the as operator keyword, to avoid exceptions being thrown.

Note that as this spreadsheet example is quite long, we have omitted the usual null checks, with regards to accessing the internal controls from the ControlTemplate elements. In a real-world application, these checks should always be implemented, as we can never be sure that our required elements will be in the template, because it may have been changed.

Next, we need a reference to the Canvas panel that we will draw our selection rectangle on:

private Canvas selectionRectangleCanvas; 
 
... 
 
private void SpreadsheetScrollViewer_ScrollChanged(object sender, 
ScrollChangedEventArgs e)
{
if (selectionRectangleCanvas == null) GetCanvasReference();
}

private void GetCanvasReference()
{
ControlTemplate scrollViewerControlTemplate = scrollViewer.Template;
selectionRectangleCanvas = scrollViewerControlTemplate.
FindName("SelectionRectangleCanvas", scrollViewer) as Canvas;
selectionRectangleCanvas.RenderTransform = new TranslateTransform();
}

In the SpreadsheetScrollViewer_ScrollChanged event handler, we start by checking if the selectionRectangleCanvas private variable is null. If it is, we call the GetCanvasReference method, to attain a reference to it and to assign it to a private member variable.

In the GetCanvasReference method, we access the ControlTemplate object from the Template property of the ScrollViewer element that we previously stored a reference to. We call the FindName method on it, passing in the name of our Canvas object and a reference to the ScrollViewer element, as its templated parent.

We then assign the returned object, cast to the Canvas type, to the private selectionRectangleCanvas member variable and set a new TranslateTransform object to its RenderTransform property. We will use this to update the position of the Canvas element each time the containing ScrollViewer object's viewport is moved, and this will ensure that the selection rectangle will be scrolled, along with the spreadsheet.

Note that we attain a reference to the Canvas element from this event handler only in an attempt to shorten this example. A far better solution would be to extend the ScrollViewer class and declare a TemplateChanged event, that passed a reference of the new template in a custom EventArgs class.

We could raise it from an overridden OnApplyTemplate method, as we did to access our ScrollViewer reference, and subscribe to it from our Spreadsheet class. The problem with our current implementation is that the ScrollChanged event is raised many times and each time, we check if we already have the reference and so a lot of CPU cycles will be wasted when scrolling.

Returning to the current implementation now, let's assign our event handler for the ScrollChanged event to the ScrollViewer in our custom template for the DataGrid class:

<ScrollViewer x:Name="DG_ScrollViewer" ... 
ScrollChanged="SpreadsheetScrollViewer_ScrollChanged">

Let's now investigate the code that is used to draw and animate the selection rectangle:

using System; 
using System.Windows.Media; 
using System.Windows.Media.Animation; 
using System.Windows.Shapes; 
 
... 
 
private Rectangle selectionRectangle; 
private bool isSelectionRectangleInitialized = false; 
 
... 
 
private void UpdateSelectionRectangle(Point startPosition, 
Point endPosition)
{
TimeSpan duration = TimeSpan.FromMilliseconds(150);
if (!isSelectionRectangleInitialized)
InitializeSelectionRectangle(startPosition, endPosition);
else
{
selectionRectangle.BeginAnimation(WidthProperty, new DoubleAnimation(
endPosition.X - startPosition.X, duration), HandoffBehavior.Compose);
selectionRectangle.BeginAnimation(HeightProperty, new DoubleAnimation(
endPosition.Y - startPosition.Y, duration), HandoffBehavior.Compose);
}
TranslateTransform translateTransform =
selectionRectangle.RenderTransform as TranslateTransform;
translateTransform.BeginAnimation(TranslateTransform.XProperty,
new DoubleAnimation(startPosition.X - RowHeaderWidth +
scrollViewer.HorizontalOffset, duration), HandoffBehavior.Compose);
translateTransform.BeginAnimation(TranslateTransform.YProperty,
new DoubleAnimation(startPosition.Y - ColumnHeaderHeight +
scrollViewer.VerticalOffset, duration), HandoffBehavior.Compose);
}

private void InitializeSelectionRectangle(Point startPosition,
Point endPosition)
{
selectionRectangle = new Rectangle();
selectionRectangle.Width = endPosition.X - startPosition.X;
selectionRectangle.Height = endPosition.Y - startPosition.Y;
selectionRectangle.Stroke =
new SolidColorBrush(Color.FromRgb(33, 115, 70));
selectionRectangle.StrokeThickness = 2;
selectionRectangle.RenderTransform = new TranslateTransform();
Canvas.SetTop(selectionRectangle, 0); // row and column header
Canvas.SetLeft(selectionRectangle, 0);
selectionRectangleCanvas.Children.Add(selectionRectangle);
isSelectionRectangleInitialized = true;
}

In the UpdateSelectionRectangle method, we first declare a duration of 150 ms to use in our animations and check if the selection rectangle has been initialized or not. If it hasn't, we call the InitializeSelectionRectangle method, passing the startPosition and endPosition input parameters through. Let's examine this method before continuing.

In the InitializeSelectionRectangle method, we initialize the SelectionRectangle element, with dimensions calculated from the two Point input parameters and default values for its stroke. We assign a new TranslateTransform object to its RenderTransform property, to enable its position to be manipulated in code.

We then use the SetTop and SetLeft Attached Properties of the Canvas class to position the rectangle in the top left corner of the Canvas panel, that we added into our custom ControlTemplate for the ScrollViewer class.

We end by adding the SelectionRectangle element into the Children collection of the selectionRectangleCanvas panel and setting the isSelectionRectangleInitialized variable to true, to ensure that this initialization code is only called once.

Returning to the UpdateSelectionRectangle method now, if the selection rectangle has already been initialized, then we animate its size, from the size of the previous cell to the size of the newly selected cell, using the startPosition and endPosition input parameters.

We call the BeginAnimation method on the SelectionRectangle element for both its WidthProperty and HeightProperty dependency properties, so that the dimensions of the rectangle will smoothly animate from the size of the previously selected cell to the size of the new one.

Next, we access the TranslateTransform instance from the RenderTransform property of the SelectionRectangle element and call the BeginAnimation method on it, for both the Xproperty and Yproperty Dependency Properties. This is what animates the position of the selection rectangle on the Canvas that we added into the ScrollViewer element's template.

To calculate the horizontal position, we subtract the value of the RowHeaderWidth property, that we set earlier in the XAML class declaration, from the X property value of the startPosition input parameter and then add the value of the HorizontalOffset property of the ScrollViewer element.

Likewise, the vertical position is calculated from the Y property value of the startPosition input parameter, with the value of the ColumnHeaderHeight property subtracted from it and the value of the VerticalOffset property of the ScrollViewer element added to it.

All four animations share the same duration, that we declared at the start, so that they morph the dimensions and position of our selection rectangle in unison. They also all set a HandoffBehavior value of Compose, which basically provides smoother joins between consecutive animations. We'll discover more about this in Chapter 7, Mastering Practical Animations, but for now, we'll keep it simple.

So, our UpdateSelectionRectangle method is responsible for animating the selection rectangle between the previous and current cell selections, but where is it called from? That's right... we're going to call it from yet another overridden protected base class method.

Looking through the protected base class methods of the DataGrid class, we find the OnSelectedCellsChanged method, which is called each time a user selects a new cell in our spreadsheet control, so it's the perfect candidate. Let's take a look at its implementation now:

protected override void 
OnSelectedCellsChanged(SelectedCellsChangedEventArgs e)
{
// base.OnSelectedCellsChanged(e);
if (e.AddedCells != null && e.AddedCells.Count == 1)
{
DataGridCellInfo cellInfo = e.AddedCells[0];
if (!cellInfo.IsValid) return;
FrameworkElement cellContent =
cellInfo.Column.GetCellContent(cellInfo.Item);
if (cellContent == null) return;
DataGridCell dataGridCell = (DataGridCell)cellContent.Parent;
if (dataGridCell == null) return;
Point relativePoint =
dataGridCell.TransformToAncestor(this).Transform(new Point(0, 0));
Point startPosition =
new Point(relativePoint.X - 3, relativePoint.Y - 3);
Point endPosition =
new Point(relativePoint.X + dataGridCell.ActualWidth,
relativePoint.Y + dataGridCell.ActualHeight);
UpdateSelectionRectangle(startPosition, endPosition);
}
}

Note that the base class version of this method is responsible for raising the SelectedCellsChanged event, so if we need that to happen, we should call it from this method. If we are ever in doubt if to call the base class version of a method that we're overriding, it's generally safer to do so, as we might lose some required functionality that it provides otherwise. As we do not require this event in this example however, we can safely omit the call to the base class method.

In our overridden OnSelectedCellsChanged method, we check that the AddedCells property of the SelectedCellsChangedEventArgs input parameter contains exactly one item. Note that in this example, it should only ever contain a single item, because we set the SelectionMode property to Single on our spreadsheet control, but it is always good practice to validate these things.

We then extract the single DataGridCellInfo object from the AddedCells property and return execution from the method if it is invalid. If it is valid, we call the GetCellContent method on its Column property, passing in its Item property, to access the cell content as a FrameworkElement object. This could benefit from a little more explanation.

The Column property contains the DataGridBoundTemplateColumn element that relates to the selected cell and likewise, the Item property holds the DataRow object that contains the selected cell. The returned FrameworkElement object represents the content of the DataGridCell element, which in our case is a ContentPresenter object.

Any UI elements that we declare in the DataTemplate element that is applied to the DataGridBoundTemplateColumn. The CellTemplate property can be accessed through this ContentPresenter object, by walking the visual tree. In our case, that is a simple TextBlock element. Returning to our code now, if this cell content is null, we return execution from the method.

If the cell content is valid, we cast its Parent property value to its actual type of DataGridCell. If this DataGridCell object is null, we also return execution from the method. If it is valid, we call its TransformToAncestor method, followed by the Transform method, to find its onscreen position, relative to the spreadsheet control.

We then use the relative position to create the start point, or the top left corner, of the rectangle, by subtracting 3 pixels in each axis. This ensures that the rectangle will sit just outside the cell contents, overlapping it slightly.

Similarly, we also use the relative position to create the endpoint, or the bottom right corner, of the rectangle, by adding the actual dimensions of the DataGridCell object to it. Finally, we call the UpdateSelectionRectangle method, to draw the selection rectangle, passing the calculated start and endpoints through.

Now, our selection rectangle is working and smoothly animates from one selected cell to the next. However, on a bigger spreadsheet, you might notice that it won't scroll in line with the spreadsheet itself. This is because there is not yet a connection between its position and the horizontal and vertical offsets of the ScrollViewer that it is defined inside.

To address this issue, we will need to update the positional information on the TranslateTransform object, from the Canvas element that the selection rectangle is drawn on, each time the spreadsheet control is scrolled. Let's see how we do this, by adding further code into our SpreadsheetScrollViewer_ScrollChanged event handler now:

private void SpreadsheetScrollViewer_ScrollChanged(object sender, 
ScrollChangedEventArgs e)
{
if (selectionRectangleCanvas == null) GetCanvasReference();
TranslateTransform selectionRectangleCanvasTransform =
selectionRectangleCanvas.RenderTransform as TranslateTransform;
selectionRectangleCanvas.RenderTransform = new TranslateTransform(
selectionRectangleCanvasTransform.X - e.HorizontalChange,
selectionRectangleCanvasTransform.Y - e.VerticalChange);
}

Skipping over the existing code that attained the reference to our selection rectangle Canvas panel, we access the TranslateTransform element, that we declared in the GetCanvasReference method, from its RenderTransform property. We then create a new TranslateTransform object, with the values coming from the original one, plus the distance scrolled in either direction, and set it back to the RenderTransform property.

Note that we have to do this because the TranslateTransform element is immutable and cannot be altered. Therefore, we need to replace it with a new element instead of just updating its property values. Any attempts to modify it will result in a runtime exception being thrown:

System.InvalidOperationException: 'Cannot set a property on object 'System.Windows.Media.TranslateTransform' because it is in a read-only state.'

Let's take a final look at the visual output of our spreadsheet control now:

Of course, we could continue to improve our spreadsheet control, perhaps by adding event handlers to detect changes to the size of the rows and columns when users resize them and update the selection rectangle accordingly. We could extend the Cell class, to add style and format properties, to style each cell and format the content.

We could add a formula bar or an alternative information panel to display formulas or further information from the cells when clicked on. We could implement multi-cell selection, or enable users to edit cell contents. But either way, hopefully, this extended example has now provided you with enough understanding to be able to undertake these kinds of advanced projects successfully yourself.