WPF DataGrid (.NET 4) can be difficult to understand if you aren't used to thinking about objects and collections. This easy to follow introduction explains where the rows and columns have gone.
The big problem with most of the explanations of how the DataGrid and the examples that you will find is that they try to be realistic. They generally work with a database or some complex structured object and then explain a complicated set of steps but might be close to what you would really use the DataGrid for but they give little idea how it works. It can be difficult to separate the details that apply to the DataGrid generally and the technology it is being used with specifically.
This gives rise to questions, especially from programmers who have used other data grids, such as:
"Where are the rows?"
"How do I access a particular cell?"
"How do I add another column?"
and so on.
The biggest mystery to the WPF DataGrid beginner is:
"Where have all the rows gone?"
This article will show you exactly where they are and how the DataGrid works.
Look no binding!
Start a new Visual Studio/Express 2010 WPF project and place a DataGrid and a Button on the page.
The first problem we have to answer is where does the DataGrid get its data from? The standard answer is to use data binding and unless you understand how all this works it great detail it can obscure the simplicity of the mechanism. So while it might not be the commonest way of working let's start with a simple non-bound example.
The DataGrid will display any collection that implements IEnumerable and this means most of the collection classes that are supplied as part of the framework. When you create a DataGrid you get an empty collection object which you can access via the Items property. The Items property is read only as its purpose is to return a reference to the collection. You can however add objects to the collection using its Add method. For example:
dataGrid1.Items.Add("John");
dataGrid1.Items.Add("Jill");
If you try this out what you will see is a grid with some horizontal lines drawn. The lines clearly correspond to the two rows that you have just added but there is no sign of any data.
Whenever you see a grid with blank rows like this you can conclude that you have successfully given the grid some data to display but you so far have failed to set up the columns. In particular, you have failed to bind any columns to any of the data.
Columns
To see the data in the collection that we have set up we have to add some columns to the DataGrid's Columns collection. Columns do the work of actually displaying data and a DataGrid without any columns is a fairly useless object.
There are currently four types of column:
DataGridTextColumn displays text data
DataGridCheckBoxColumn displays boolean data
DataGridComboBoxColumn displays enumerations
DataGridHyperlinkColumn displays URIs
There is also the DataGridTemplateColumn which is completely customisable and will display any data for which there is a suitable display control.
The details of how some of these columns work is complicated - especially so the DataGridTemplateColumn - but the basic DataGridTextColumn is easy enough to use and given most simple data can be converted to text it is also very useful.
Let's add a DataGridTextColumn to the current example:
dataGrid1.Columns.Add(
new DataGridTextColumn());
Following this addition the DataGrid now shows two rows and a single column - but still no data.
Each of the objects that make up the Items collection roughly speaking provides the data for a row of the grid. How does a column work out how to get the data it has to display from each object? The answer is that you have to setup a binding between it and the objects of the collection.
Usually each column is bound to a property of each object so that each column shows different data - but a column can be bound directly to the object. In this case the binding defaults to calling the object's ToString method to retrieve the display data. This can be useful during debugging and general development just to check that you have the correct collection associated with the grid.
In the case of our simple example it is enough to get the data displayed:
((DataGridTextColumn)dataGrid1.
Columns[0]).Binding = new Binding(".");
Notice that you have to cast the column to a DataGridTextColumn because the base class doesn't support a Binding property. This is one good reason for keeping a reference to any columns that you are setting up in code. That is:
DataGridTextColumn col1 =
new DataGridTextColumn();
dataGrid1.Columns.Add(col1);
col1.Binding = new Binding(".");
The binding "." simply means the root of the bound object, i.e. the entire object, and this means that the binding engine will call the object's ToString method. SInce the objects in the collection are Strings this displays their value as required and at last the DataGrid displays the data:
The way the binding works
Notice that this is a little more subtle than simple binding and deserves a closer examination. If you are not sure you are happy about the way binding works skip this section until you are curious.
In simple binding you specify an object and a property of that object to bind to. The property is used to store and retrieve the data involved in the binding. In this case, however, we have not a single object but a collection.
If you lookup the details of binding to a collection you will discover that this always involves a CollectionView object which is used to determine which object in the collection is used in the binding - i.e. the CollectionView can be used to move through the collection and set the "current item".
In many cases it is the current item that is bound and then we are back in the realm of using a single object with a single property. However in many situations the CollectionView is used to supply the bound property for each object in the collection so that the data can be displayed as a list. This is exactly what happens when you use an ItemsControl and a DataGridColumn works in much the same way.
The collection being used is actualy an ItemCollection which has CollectionView as its base class. This means that it can be used to navigate the current object position through the collection and this is useful in more complicated situations.
The column object is also a collection - of textblocks. At the very lowest level what happens is that each textblock is bound to one of the objects in the collection, i.e. its DataContext is set to one of the objects and its Path is set to the path of the binding object assigned to the the column.
More Columns
Of course any real data worthy of being displayed in a DataGrid has to have multiple columns. Each column is bound to a different property of the objects making up the items collection. The objects have to have named properties and they have to be "get/set" properties - you can't just create a public data field.
For example:
public struct MyData
{
public string name { set; get; }
public int age { set; get; }
}
You could declare the same pair of properties within a class if you wanted to - but a struct works as well. Next you can add objects of this type to the DataGrid:
dataGrid1.Items.Add(
new MyData(){name="John",age=25});
dataGrid1.Items.Add(
new MyData(){name = "Jill",age = 29 });
To show the properties in the DataGrid we need two columns and each one has to be bound to one of the properties. The first column is bound to "name":
DataGridTextColumn col1 =
new DataGridTextColumn();
col1.Binding = new Binding("name");
The second is bound to "age":
DataGridTextColumn col2 =
new DataGridTextColumn();
col2.Binding = new Binding("age");
We also need to remember to add them to the DataGrid:
dataGrid1.Columns.Add(col1);
dataGrid1.Columns.Add(col2);
The result is a two-column display complete with data.
We can, of course improve the look of the grid but this is mostly a matter of setting properties on either the grid or the columns and isn't difficult. For example, you can set headers for each column:
col1.Header = "Name";
col2.Header = "Age";
One important thing to know about columns, and something that often causes problems for beginners, is that the order of the columns is not determined by the order they are stored but by the DisplayIndex property.
XAML
The above examples have all been code-based but we can achieve much of the same thing using XAML. The code has to create the data and add it to the items collection but we can use XAML to instantiate and initialise the two columns - after all that's all XAML does. For example, adding two columns bound to the same properties and complete with the same column headings can be acheived using:
<DataGrid Height="100"
HorizontalAlignment="Left"
Margin="7,9,0,0"
Name="dataGrid1"
VerticalAlignment="Top"
Width="200" >
<DataGrid.Columns>
<DataGridTextColumn
Binding="{Binding Path=name}"
Header="Name">
</DataGridTextColumn>
<DataGridTextColumn
Binding="{Binding Path=age}"
Header="Age">
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
Notice that in most real applications the DataGrid is bound to some source of data. In this case the data that the DataGrid uses is generated in code but the columns are still associated with the properties they display using binding objects.
External IEnumerables
It is easy enough to make use of the Items collection supplied along with the DataGrid but most of the time the data that is displayed comes from an external source - typically a database connection. It is easy to get a DataGrid to use an external collection object. All you have to do is set its ItemsSource property.
To see this in action we first create a suitable object type to hold the data:
public class Person
{
public string name { set; get; }
public int age { set; get; }
public bool member { set; get; }
}
Then we create a collection of these objects:
List<Person> myList =
new List<Person>();
myList.Add(new Person()
{name="john",age=25,member=true});
myList.Add(new Person()
{name="jill",age=25,member=false});
myList.Add(new Person()
{name="bill",age=15,member=true});
At the end of this code block MyList contains three Person objects suitably initialised. All we have to do to make the DataGrid display this collection is:
dataGrid1.ItemsSource=myList;
After this assignment the DataGrid's Items property references and hence uses myList.
We can also get the grid to generate the necessary columns with the correct bindings for us by setting its AutoGenerateColumns property to true. In fact as this is its default we generally get the columns autogenerated without having to do anything. The result is a three-column grid with two DataGridTextColumns and one DataGridCheckBoxColumn. You could have added and bound the three columns in code using the same sort of code used earlier.
There is a subtlety hidden here. The ItemSource property references the original list, i.e. MyList, but the Items property references an ItemCollection which the List has been converted to. It is always the derived ItemCollection which is used to display the data. This means that there is always a current item and you can use the ItemCollection methods to move the current position. Notice also that the original variable that references the collection you assign to the DataGrid can go out of scope without any problem as the DataSource property still provides a reference to the object.
Data editing
The easiest way of editing the data in the DataGrid is to simply set the properties that allow the user to do the job interactively. You can set or unset CanUserAddRows, CanUserDeleteRows,
CanUserReorderColumns and so on. Just look up properties that start with "Can". Notice that if the user adds a row or modifies the data then the modifications are made to the collection assigned to ItemsSource. This isn't an example of two way databinding, in fact databinding isn't being used - it is simply that the collection providing the data that the DataGrid shows is used to store the new or modified data.
Now we come to the subject of editing data from code and this is where the questions about rows, columns and even cells tend to crop up. If you have followed the discussion of how the DataGrid works then you will be quite clear that there is no need to think in terms of rows or columns or cells for that matter. The raw material of the DataGrid is a collection of objects which roughly correspond to the rows in the alternative model and each object has a collection of properties which correspond to the columns or the cells depending on how you want to look at it.
However there are equivalents of the row and column oriented access methods and they are sometimes useful.
To access a "cell" value
To access the data stored in the grid at a given row and column you would retrieve the item that represents the row, cast it to the correct type and use the property that corresponds to the column. For example, to retrieve the name of the first item in the DataGrid in the previous example:
string name =
((Person) dataGrid1.Items[0]).name;
To modify a "cell" value
To modify a cell value at a given row and column you simply use the recipe given above but assign to the property. For example to change the value of the name in the first item in the DataGrid in the previous example you would use:
((Person)dataGrid1.Items[0]).name = "Tom";
If you consider the situation for a moment then it is also clear that, as long as you still have a reference to it, you can modify the original collection just as easily as the grid. For example you could change MyList in the same way.
To access the currently selected row
This is just a matter of using the SelectedItem property:
string name =
((Person) dataGrid1.SelectedItem).name;
To access the currently select cell
This is perhaps the most tricky thing to do and it takes us into the use of the Cell and CellInfo objects which is perhaps the closest we get to the traditional rows, columns and cells approach.
First we have to switch the DataGrid into cell selection mode using its SelectionUnit property:
dataGrid1.SelectionUnit =
DataGridSelectionUnit.Cell;
Now the user can select a block of cells. To access the selected cells we have to use the SelectedCells collection. This returns a collection of DataGridCellInfo objects. To select the first we would use:
DataGridCellInfo cell =
dataGrid1.SelectedCells[0];
Now we have a DataGridCellInfo object we can actually access the cell itself. This isn't the cell value however but whatever control is used to display the cell contents. In the case of a text column this is a TextBlock control. To retrieve this we have to use a complicated multi-step approach. First get the column that the cell is in:
cell.Column
Now we can get the CellContent at the intersection of the column and the item that the selected cell is in:
cell.Column.GetCellContent(cell.Item)
This returns a TextBlock which we can finally access the text stored in the cell, but only with the help of a suitable cast. Putting all this together gives:
string value =
((TextBlock)cell.Column.
GetCellContent(cell.Item)).Text;
This is convoluted and I have to admit that there might be a better way but this is typical of the sort of thing that happens once you have to go back to thinking in terms or rows, columns and cells. Much better to think of collections of objects, columns and properties.
Source: iprogrammerinfo
The Tech Platform
Comments