Let's imagine that we want to create an application that displays tabular data. This doesn't initially sound very complicated, but it is actually a very good example with which to demonstrate how to adapt the built-in .NET controls to fulfill our requirements. As we progress through this example, we will come across several potential problems and find out how to overcome each one in turn.
For this extended example, we will create a Spreadsheet control. As always, when creating new controls, we look at the existing controls, to see if any of them can provide us with a good starting point. The first control that springs to mind is the Grid panel, as it has rows, columns and therefore also cells, but the creation of all of the RowDefinition and ColumnDefinition objects could be cumbersome or problematic.
There is also the UniformGrid panel, but as its name suggests, all of its cells are uniform, or the same size as each other, but this is not always the case in spreadsheets. We could potentially use an ItemsControl object and a custom DataTemplate to draw the borders and contents of each cell manually, but could there be a better starting point?
How about the DataGrid control? It has rows, columns, and cells, and even draws the grid lines between the cells for us. It also has the concept of a selected cell, which could be useful if we wanted users to interact with our spreadsheet control. It has no numbers, letters, or selected cell markers in the grid axes, but we can extend the control to add these, so it seems like the best candidate for the job.
The first thing that we need to do is to create a new class that extends the DataGrid class. As we saw earlier in this chapter, we can do this by adding a UserControl to our project and replacing the word UserControl with the word DataGrid in both the XAML file and its code behind file. Failure to change both will result in a design-time error that complains about mismatched classes.
Let's take a look at our new Spreadsheet class:
using System.Windows.Controls; namespace CompanyName.ApplicationName.Views.Controls { public partial class Spreadsheet : DataGrid { public Spreadsheet() { InitializeComponent(); } } }
The code behind is a simple affair, currently with no custom code in it. The XAML however, has a number of important properties set in it, so let's see that now:
<DataGrid x:Class="CompanyName.ApplicationName.Views.Controls.Spreadsheet" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" AutoGenerateColumns="False" SelectionUnit="Cell" SelectionMode="Single"
IsReadOnly="True" RowHeight="20" RowHeaderWidth="26" ColumnHeaderHeight="26"
CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False"
CanUserResizeColumns="False" CanUserResizeRows="False"
HorizontalGridLinesBrush="{DynamicResource GridlinesBrush}"
VerticalGridLinesBrush="{DynamicResource GridlinesBrush}"
BorderBrush="{DynamicResource BorderBrush}"> <DataGrid.Resources> <Color x:Key="BackgroundColor">#FFE6E6E6</Color> <Color x:Key="BorderColor">#FF999999</Color> <SolidColorBrush x:Key="BackgroundBrush" Color="{StaticResource BackgroundColor}" /> <SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}" /> <SolidColorBrush x:Key="SelectedBackgroundBrush" Color="#FFD2D2D2" /> <SolidColorBrush x:Key="GridlinesBrush" Color="#FFD4D4D4" /> <SolidColorBrush x:Key="SelectionBrush" Color="#FF217346" /> </DataGrid.Resources> </DataGrid>
For this implementation, we set the AutoGenerateColumns property to False, because we will be programmatically creating the columns of our spreadsheet control. In order to approximate a spreadsheet control, we also need to restrict the selection possibilities of our custom DataGrid.
As such, we set the SelectionUnit property to Cell, so that users select just the cell that they click on, rather than the whole row, which is the default selection behavior. In addition, to simplify this example, we also set the SelectionMode property to Single, the IsReadOnly property to True and the RowHeight property to 20.
Our row and column headers will both be 26 pixels each, so we set the RowHeaderWidth and ColumnHeaderHeight properties to 26. Note that we could set the row and column header dimensions in their relative styles instead, but we will need to reference these properties later, so it is important that we set them here. The next five properties, prefixed with CanUser, have also been set to False, to further shorten this example.
We then set both of the HorizontalGridLinesBrush and VerticalGridLinesBrush properties to the GridlinesBrush brush from the Resources section and the BorderBrush property to the BorderBrush brush. Note that we need to use a DynamicResource markup extension in these cases, because these brushes are defined after the DataGrid declaration, along with the rest of the resources, and the XAML parser would not be able to locate them with a standard StaticResource markup extension.
Also, note that it is essential that we remove the empty Grid panel that Visual Studio adds into each new UserControl. The reason is that any elements declared inside the DataGrid control are determined to be its items and we cannot simultaneously use both its Items property and its ItemsSource property, which we intend on using. If we use them both, we'll see this exception being thrown at runtime:
System.InvalidOperationException: 'Items collection must be empty before using ItemsSource.'
Let's move on now, to investigate how we can display data in our spreadsheet.