WPF DataGrid: Dissecting the Visual Layout
I’m going to be dissecting and discussing the DataGrid visuals and how they are all assembled together to form the overall DataGrid. Note: This is really more of a post for people who want to understand how the DataGrid works internally. I’m not going to be going over how to use some of the APIs in this post.
If you haven’t already, get the binaries and source for it here. For more related material take a look at these posts:
Working with DataGridComboBoxColumn (Part1)
Working with DataGridComboBoxColumn (Part2)
Understanding the visual tree isn’t the most important thing to know about basic DataGrid usage, but it is very helpful for such things like customizing its appearance through the different styling templates available, replacing the visual tree altogether, accessing visual properties, understanding routing events, or when you want to learn how the data maps to the visual tree. Here is the big picture view of the DataGrid:
You can view the equivalent in Generic.xaml in the source for DataGrid. The DataGrid, like a generic spreadsheet, is made of rows and cells. The rows are generated from the DataGrid’s GetContainerForItemOverride. That should sound familiar as DataGrid ultimately derives from ItemsControl. If you need a brush up on that, please check out Dr. WPF’s awesome series on ItemsControl. The cells are generated from DataGridCellsPresenter’s GetContainerForItemOverride. The important containers are DataGridRowsPresenter and DataGridCellsPanel. DataGridRowsPresenter derives from VSP and is a really small class that does some clean up on the ItemsHost and some scrolling work. If you plan to replace this container, be sure to handle scrolling. The DataGridCellsPanel on the other hand is a very specific implementation to the default DataGrid which handles sizing of each DataGridCell. Replacing this container will need a good understanding of the DataGrid internals which I will not go into here.
In my previous post I did a really brief intro of the DataGrid and just got it up and running. Well, now I want to break down how it actually works. Whether columns are auto-generated or built manually, the DataGrid will use those columns to map the data source to each cell. Notice that the list of DataGridColumns is not in the diagram as it is not really a visual. DataGridColumn is used as the glue between the data and the DataGridCells. So given this column implementation as an example,
<dg:DataGrid AutoGenerateColumns="False">
<dg:DataGrid.Columns>
<dg:DataGridTextColumn Binding="{Binding Path=FirstName}" />
<dg:DataGridTextColumn Binding="{Binding Path=LastName}" />
<dg:DataGridCheckBoxColumn Binding="{Binding Path=LikesCake}"/>
<dg:DataGridHyperlinkColumn Binding="{Binding Path=Homepage}" />
</dg:DataGrid.Columns>
</dg:DataGrid>
here is the sequence that follows in creating the DataGrid when the ItemsSource is set (assuming all the Path values are properties on the data source),
1. When ItemsSource is set on the DataGrid, PrepareContainerForItemOverride is called on each item of the data source collection which prepares a DataGridRow for each.
2. When a DataGridRow is prepared, it passes the item to its DataGridCellsPresenter which internally creates a copied collection of that item and sets its ItemsSource to that collection.
3. When DataGridCellsPresenter.ItemsSource is set, PrepareContainerForItemOverride is called on each copied item which prepares a DataGridCell for each.
4. When a DataGridCell is prepared (which is a ContentControl), it gets its corresponding column from the DataGrid and asks that column to generate the visual tree for DataGridCell’s Content property.
It is in the generation of the visual tree where the binding is hooked up from the data source to the UI. Recall in the column implementation, the Binding property was set to a property on the data source. In the visual tree generation it takes that Binding and applies it to the generated UIElement (which in step 4 is set to DataGridCell’s Content property). That is basically it. You should also know that different column types will generate different UIElements. DataGridTextColumn will generate a textbox; DataGridCheckBoxColumn will generate a checkbox, etc. So hopefully some of the magic has been revealed on how the DataGrid populated itself.
In addition to understanding the mapping of the data to the UI, you should have a good grasp of the visuals involved from looking at the diagram. This helps when you want to style the DataGrid. Convenience properties for styling have also been added the DataGrid class itself so you can look there for the available styles as well, but let’s stop here for now as I will get more into styling and templating in an upcoming post.
Comments
Anonymous
August 14, 2008
PingBack from http://housesfunnywallpaper.cn/?p=739Anonymous
August 14, 2008
Link Listing - August 14, 2008Anonymous
August 14, 2008
Great stuff Vincent... Keep it up!!! Regards, Rudi GroblerAnonymous
August 19, 2008
Overview The DataGrid uses a set of DataGridColumns to describe how to display its data just like a GridViewAnonymous
August 29, 2008
What I am really struggling to figure out is how to do some simple calculations that update one cell based on the input of another: eg: price qty total 100 2 (calculates to 200) I understand that it has to be done in the CommitingChanged event, but how do I go about finding the cell I need to update?Anonymous
August 29, 2008
You could leave the calculation to your data object. Then you won't have to worry about the logic in the UI. So for exmaple you could have a class like this, class Product { public double Price {get; set;} public double Quantity {get; set;} public double Total { get { return Price * Quantity; } } } If you want to do it in the UI though, you can listen to the CommittingEdit event where you will have access to the row being committed. With that you have row.Item which is your data item and you can just set the Total there if you wanted and the cell will update.Anonymous
September 16, 2008
There have been several questions on the WPF CodePlex discussion list relating to styling rows and columnsAnonymous
October 01, 2008
Introduction I’m going to talk a little on the editing features of the DataGrid. I will dive deep intoAnonymous
November 01, 2008
Hi where is the DataGridCellsPresenter in release of WPF toolkit? :SAnonymous
November 02, 2008
Keoz, It is under the namespace, Microsoft.Windows.Controls.Primitives.Anonymous
January 21, 2009
Hi, First off I have to say fantastic work Vincent. Anyway I have run into a little problem. I have styled my DataGridCheckBoxColumn (See Below). This works fine except the dataGrids_BeginningEdit event is not being fired. It seems the Combobox is swallowing the event. Any ideas what I need to do to get the event to fire? Tks Kevin <tk:DataGridCheckBoxColumn Binding="{Binding Path=Selected}" Header=" Action " CanUserResize="False" ElementStyle="{StaticResource DataCheckBoxGridCellStyle}" /> <Style x:Key="DataCheckBoxGridCellStyle" TargetType="{x:Type CheckBox}"> <Setter Property="Foreground" Value="White"/> <Setter Property="HorizontalAlignment" Value="Center"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="CheckBox"> <BulletDecorator Background="Transparent" VerticalAlignment="Center" HorizontalAlignment="Left"> <BulletDecorator.Bullet> <Border x:Name="Border" Width="13" Height="13" CornerRadius="0" Background="White" BorderThickness="1" BorderBrush="Black"> <Path Width="7" Height="7" x:Name="CheckMark" SnapsToDevicePixels="False" Stroke="Green" StrokeThickness="2" Data="M 0 0 L 7 7 M 0 7 L 7 0" /> </Border> </BulletDecorator.Bullet> </BulletDecorator> <!--Ihave excluded the triggers--> </ControlTemplate> </Setter.Value> </Setter> </Style>Anonymous
January 22, 2009
Kevin, It's hard to say from just that code that you provided. So it does fires for all the other columns?Anonymous
January 22, 2009
The comment has been removedAnonymous
January 23, 2009
Kevin, Could you send me a repro app. You can first contact me through the automated email through this blog and I can contact you from there. ThanksAnonymous
March 03, 2009
Great post Vincent.I am facing a problem.I am using the datagrid with a checkbox column. Now I have to select the checkbox on the basis of a recordset column value(i.e. if the value for one row is 'Y' then the checkbox should be checked andd if it is 'N' then it should remain unchecked). Could you please help me out.Anonymous
March 04, 2009
Avijit, You could use a converter for the binding in your DataGridCheckBoxColumn. For example, <DataGridCheckBoxColumn Binding="{Binding Path=SomeRecordColumn, Converter={StaticResource RecordColumnConverter}}" /> public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var strVal = (string)value; if (strVal == "Y") return true; else return false; } Of course, this Convert method is a very simplistic version. Make the necessary robust changes that you need.Anonymous
March 04, 2009
I need to access textblock controls defined in a column header template for a datagrid. Any suggestions on how I can do this? thanks much.Anonymous
March 08, 2009
Because all of this DataGrid stuff is fairly new, I'm having a hard time figuring out what is possible versus impossible. I have a need to be able to load a bunch of templates at runtime from a file. The problem is that I don't know the names of the columns beforehand. My interim solution is to have some wildcard string in the file that I replace with the current column name before calling XamlReader.Load() on the read in xaml, so I get a customized template on demand. What I want to know is if there is some way to do the binding in the inner controls of the DataTemplate for the DataGridTemplateColumn so that it somehow pulls the current column header (and thus the column name from the dataset) from somewhere. I am setting the header name in code once the template object is constructed. I guess I would just need to bind the inner controls to the header on the template. Any ideas?Anonymous
March 15, 2009
Brian, If your DataGridTemplateColumn does not requre a very flexible DataTemplate that dynamically changes all the time, you can consider using a solution similar to what I did in the sample on this post, http://blogs.msdn.com/vinsibal/archive/2008/10/22/wpf-datagrid-and-the-wpftoolkit-have-released.aspx. Take a look at the DataGridCustomTemplateColumn class. In that class I added a Binding DependencyProperty so you can set the binding directly on the column. In the GenerateElement and GenerateEditingElement you can see I walk the visual tree and find the inner control that I want to set the binding to. Hope that can give you some ideas.Anonymous
March 31, 2009
Hi Vinsibal, Hope you are doing fine. Am fed up with an issue in wpf datagrid . I have a data grid which is having check boxes in first column . Also i have a checkbox outside the Datagrid . When am cheking the checkbox outside the datagrid, i want all the checkboxes inside the grid . I tried implementing this in the following way(Code attached along with this) , but it is showing strange behaviour . Some checkboxes in rows randomly getting checked(if some more clearly says first 10 rows are getting checked after that say 5 not checked again next 10 getting checked and this process continues till the end of records). I have tried solvint this issue using the helper class in the demo application you have published, but still it is showing same behaviour. I dont know why this happening . Am attaching my code here . Please suggest a solution Regards sreeraj -----------XAML For Grid------------------ <dg:DataGrid Grid.Row="1" AutoGenerateColumns="False" AlternatingRowBackground="SkyBlue" RowDetailsVisibilityMode="Collapsed" AllowDrop="False" HeadersVisibility="All" ColumnHeaderHeight="30" FontFamily="Verdana" FontSize="12" Background="#FFFFFFFF" OpacityMask="#FFF1E3E3" Foreground="#FF000000" BorderThickness="1" SnapsToDevicePixels="False" CanUserDeleteRows="False" CanUserAddRows="False" ClipToBounds="False" RowHeight="25" ScrollViewer.CanContentScroll="True" Padding="0" VerticalContentAlignment="Center" MinWidth="0" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Visible" GridLinesVisibility="All" Name="grdWorkOrders" Margin="0,0,0,52" Height="150" VerticalAlignment="Top" EnableColumnVirtualization="False"> <dg:DataGrid.Columns> <dg:DataGridTemplateColumn MinWidth="30"> <dg:DataGridTemplateColumn.CellTemplate> <DataTemplate> <CheckBox x:Name="chkMain"></CheckBox> </DataTemplate> </dg:DataGridTemplateColumn.CellTemplate> </dg:DataGridTemplateColumn> <!--<dg:DataGridCheckBoxColumn Header="Access"></dg:DataGridCheckBoxColumn>--> <dg:DataGridTextColumn Binding="{Binding Path=ActivityName}" Header="AIRCRAFT" FontSize="10" MinWidth="110" /> <dg:DataGridTextColumn Binding="{Binding Path=Description}" Header="FLEET" FontSize="10" MinWidth="40" /> <dg:DataGridTextColumn Binding="{Binding Path=Description}" Header="MAINTENANCE FLEET" FontSize="10" MinWidth="40" /> <dg:DataGridCheckBoxColumn Header="RESPONSABILITY"></dg:DataGridCheckBoxColumn> </dg:DataGrid.Columns> </dg:DataGrid> -----------Code Behind -------------------- for (int nCntr = 0; nCntr < grdWorkOrders.Items.Count; nCntr++) { var cntr = MyDataGrid.ItemContainerGenerator.ContainerFromIndex(nCntr); DataGridRow ObjROw = (DataGridRow)cntr; if (ObjROw == null) { ObjROw = (DataGridRow)MyDataGrid.ItemContainerGenerator.ContainerFromIndex(nCntr); } else { FrameworkElement objElement = MyDataGrid.Columns[0].GetCellContent(ObjROw); if (objElement != null) { if (objElement.GetType().ToString().EndsWith("CheckBox")) { CheckBox objChk = (CheckBox)objElement; objChk.IsChecked = true; } } } }Anonymous
April 01, 2009
sreeraj, See this thread, http://wpf.codeplex.com/Thread/View.aspx?ThreadId=51923.Anonymous
April 28, 2009
For DataGridCheckBoxColumn, I was hoping you have sth like CheckedValue = "Y", CheckedValue = "", UncheckedValue = "Whatever", UncheckedValue = "".Anonymous
April 28, 2009
Hi Vin, thank you for doing all this work. I read your artictle and one thing, that is still missing in the picure to me is the DataGridRowHeader. Since I am developing applications for mathematical use, I am very much interested on being able to build a x/Y axis representation of a grid. Getting to it: In the four steps above you descibed how each Column gets its data bound and of course I can imagine how to use templates for that. But where does the rowheader get it's data from? If I make an Observable Collection, that contains objecs that have a field for the header this field will also be generated as a column. which really is not what I want :-). So how can I distinguish the Itemsource for the coumn from the Source for the rowheader preventing my rowheader also becoming generated as a colum? Many regs, KerryAnonymous
May 02, 2009
Kerry, There are a couple things you can do here. You can listen to the AutoGeneratingColumn event and cancel the generation of particular columns or just not do auto-generated columns. Hope that helps.Anonymous
May 28, 2009
I have grouped my data in a datagrid and used a customized expander to hide the toggle button. I have expanded it to the right. The result is shown below. <img src"http://i629.photobucket.com/albums/uu13/cheapsaket/datagrid2.png">">http://i629.photobucket.com/albums/uu13/cheapsaket/datagrid2.png"> http://i629.photobucket.com/albums/uu13/cheapsaket/datagrid2.png My question is how to access/set the value of the column header above group names? The layout diagram in the post does not address this area so if you can specify what goes there, it would be helpful.Anonymous
August 05, 2009
Thanks Vinsibal for excellent article series. This is what I was looking for. I was trying to change grid orientation to verticle using control template/style triggers etc. No success. pl. suggest some way to achieve this. Thanks again ShailendraAnonymous
September 05, 2009
Hi Vin, How can I create a multirow column header? Something like this: | Alphabet | Number| |A | B | C | 1 | 2 |Anonymous
February 17, 2010
Vince, When I set the DataGrid property for HeadersVisibility to 'None' I get the following binding errors during runtime (listed below). I have a grid that I want no headers shown. If I set the property to 'Column' or 'Row' (or anything but 'None') the binding errors go away. Is there a fix or a workaround? I know the warning is harmless, but I want to fix all errors or warnings that appear in the output window of Visual Studio to be certain the xaml is completely correct. I think this is a bug. Thanks, Steve
System.Windows.Data Error: 39 : BindingExpression path error: 'IsSelected' property not found on 'object' ''TotalsDataItem' (HashCode=50423602)'. BindingExpression:Path=IsSelected; DataItem='TotalsDataItem' (HashCode=50423602); target element is 'DataGridRow' (Name=''); target property is 'NoTarget' (type 'Object') System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='Microsoft.Windows.Controls.DataGrid', AncestorLevel='1''. BindingExpression:Path=AreRowDetailsFrozen; DataItem=null; target element is 'DataGridDetailsPresenter' (Name=''); target property is 'SelectiveScrollingOrientation' (type 'SelectiveScrollingOrientation')