Introduction
It was one of those days. In a solution, the bottleneck was figured out. Once again, it was the third party DataGrid
I used. After having tried three different vendors, I was frustrated, and went back to the WPF ListView
, and the performance was better again. I wanted the DataGrid
to present the data fast, specially when I reset the ItemsSource
to a new collection.
Without a doubt, the commercial DataGrid
s are nice looking and feature rich. But the overhead on all that slows them down. The DataGrid
from Microsoft never could win my interest, it seems somewhere in between the ListView
and the commercial DataGrid
s. Anyways, I started to tweak the ListView
, and here is what I've managed to do.
The Basic Usage Should be Easier
Some thing always annoyed me when using commercial DataGrid
s or the ListView
itself. I was tired of writing all those CellTemplates
I would have to use. WPF has its own paradigm, but a DataGrid
is a DataGrid
, at least most of the time.
Wanted Features
- Common configuration scenarios available through properties
- Default style for the whole grid and optional styles for columns
- Lightweight styling and customizing without re-templating
- Sorting any column, and SortName property for first-run
- Filtering any column (AutoFilter row)
- Totals/Summaries for any column
- Frozen column left/right
- GridLines vertical and horizontal
- Cell-focus (cursor)
- Cell-editing
Using the Code
The basic usage of the DsxDataGrid
is pretty straightforward. You can think of it as a 'ListView
'. It is a custom control derived from Selector
. The ControlTemplate
s are mainly based on the ListView
. The following snippet shows most of the properties for the additional features:
<dsx:DsxDataGrid x:Name="dataGrid1"
Grid.Row="1"
Margin="5"
AllowCheckAnyTime="True"
SortField="CompanyName"
HorizontalGridLinesIsVisible="True"
VerticalGridLinesIsVisible="True"
CellAdornerIsVisible="True"
CellEditingIsEnabled="True"
HeaderVisibility="Visible"
FilterVisibility="Auto"
FooterVisibility="Auto"
ItemFixHeight="20"
AreaLeftWidth="212"
AreaRightWidth="250"
SplitterLeftWidth="3"
SplitterLeftIsSizing="False"
SplitterRightWidth="3"
SplitterRightIsSizing="False"
IsVirtualizing="False">
<dsx:DsxDataGrid.Columns>
<dsx:DsxColumn FieldName="MarkFlag" ColumnArea="Left"
Header="" Width="25"
IsSizable="False" IsSortable="False"
ViewType="CheckBox" FilterType="CheckBox"
FooterType="None" EditType="None"
CellHAlign="Center"/>
<dsx:DsxColumn FieldName="CompanyName" ColumnArea="Left"
Header="Company" Width="120"
IsSizable="False" ViewType="Text"
FilterType="TextBox" FooterType="Count"
EditType="TextBox" CellHAlign="Left"
CellForeground="Blue"
CellBackground="#33C3E8D1"/>
<dsx:DsxColumn FieldName="ContactTitle" ColumnArea="Center"
Header="Title" Width="120"
ViewType="Text" FilterType="ComboBox"
FooterType="None" EditType="ComboBox"
CellHAlign="Left"
CellContentItemsSource="{x:Static local:ContactTitles.ComboSource}"/>
</dsx:DsxDataGrid.Columns>
</dsx:DsxDataGrid>
Most basic columns are ready to use without CellTemplate
s. Behind the scenes, the CellTemplate
s are generated.
The Basic Layout of DsxDataGrid
The DataGrid
is divided in three areas. Each area contains a ListView
with the same style. The right area is configured to show the vertical scrollbar, and contains the additional areas that fill the special rows. The header (blue), filter (green), and footer (purple) are all GridViewHeaderRowPresenter
s but with different Style
s (ControlTemplate
s). The synchronization of the vertical scrolling is done by the very useful ScrollSynchronizer
: Scroll Synchronization by Karin Huber
Important: If the left and center areas are not needed, only the area on the right is shown.
Column ViewTypes
Each column has a ViewType
(default is Text
). This injects a CellTemplate
to be created that does everything needed.
public enum EViewType
{
Text = 1,
Integer = 2,
Decimal = 3,
Currency = 4,
Date = 5,
Boolean = 6, CheckBox = 7,
Image = 8,
Progress = 9,
}
In order to serve the different ListView
areas, the columns are recreated inside the contained ListView
Columns
collections. In this step, the column is examined, and depending on the settings, the CellTemplate
s are generated for the different ViewType
s. In this scenario, I prefer code over XAML, because in code, you can do nasty things like reuse the existing DisplayMemberBinding
, and you have full control to minimize the CellTemplate
s to a minimum.
Column FilterTypes / EditTypes
Each column can have a FilterType
and or an EditType
, both of type EEditType
. If a FilterType
is set, the FilterRow
displays the FilterCell
. The editing is done by placing an Adorner over the current cell. All controls used in the Adorner are configured to look nice even if the cell is of variable height. This approach is much lighter than placing full controls inside column-cells.
public enum EEditType
{
None = 0,
TextBox = 1,
CheckBox = 2,
DatePicker = 3,
ComboBox = 4,
Slider = 5,
CellTemplate = 6, }
Note: All EditType
s are templated to look nice if the ItemHeight
is larger than the EditControl
usually is:
Column FooterTypes
A FooterType
can be set on a column to display a result on the displayed data. If the displayed data is filtered, the result is narrowed to the filtered data.
public enum EFooterType
{
None = 0,
Sum = 1,
Avg = 2,
Min = 3,
Max = 4,
Count = 5,
}
How Sorting/Filtering/Summarize Work
Sorting can be invoked either by setting the SortField
property on the DataGrid
, or by clicking the column-header. Both check first if the referring column allows sorting. The sorting triggers the DisplaySource
(result of displayed data) to be re-evaluated. In there, the ICollectionView
will do the sorting.
Filtering is invoked by the Adorner and is not accessible by property. The filtering acts on PropertyChanged
, but has a short timer to wait for some time before invoking the routine. The filtering triggers the DisplaySource
to be re-evaluated too. A callback handles all filter-columns to be checked.
Summarizing the totals is (like you might guess) also done while rebuilding the DisplaySource
. Since there is no way to build a sum without enumerating all relevant items at least once, this is done by a simple loop before presenting the data.
Custom CellTemplates
In the sample, the column 'Address
' uses a custom CellTemplate
, triggering the EditMode
property that takes control over the editing (entering/exiting). It would probably easier to enhance the existing types in code, so one could as well use a CellTemplate
.
How to Define Alternating RowColors
AlternationCount
/-Index
is already prepared in the ItemsControl
. DsxDataGrid
just has the collection for the brushes all ready for use, so you just do this, and it will work:
<dsx:DsxDataGrid.AlternatingRowBrushes>
<SolidColorBrush Color="Yellow"/>
<SolidColorBrush Color="Red"/>
<SolidColorBrush Color="OliveDrab"/>
</dsx:DsxDataGrid.AlternatingRowBrushes>
How to Use Styles with DsxDataGrid
My approach is to have a default style for header/filter/footer on the DataGrid
that is used for every column, but some columns override the style. The GridViewHeaderRowPresenter
gets the style from the custom Control-Template, which refers (if not replaced) to, e.g., the dsxFilterStyle
(for FilterRow
). In this style, you can see this:
<Style x:Key="dsxFilterStyle" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource dsxFilterStyleConverter},
ConverterParameter=Background}"/>
<Setter Property="BorderBrush"
Value="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource dsxFilterStyleConverter},
ConverterParameter=BorderBrush}"/>
<Setter Property="Foreground"
Value="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource dsxFilterStyleConverter},
ConverterParameter=Foreground}"/>
<Setter Property="BorderThickness"
Value="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource dsxFilterStyleConverter},
ConverterParameter=BorderThickness}"/>
<Setter Property="Padding"
Value="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource dsxFilterStyleConverter},
ConverterParameter=Padding}"/>
<Setter Property="Margin"
Value="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource dsxFilterStyleConverter},
ConverterParameter=Margin}"/>
<Setter Property="Template"
Value="{StaticResource dsxFilterStyleControlTemplate}"/>
</Style>
All used properties to style the filters refer to a converter. The converter itself evaluates if there is a style on the column overriding the style on the DataGrid
, or maybe none at all.
More Details, Sorry So Much!
Unfortunately, I cannot explain all the little tweaks I built in, there are many, and my time is not sufficient for that. Some solutions maybe no rocket science, but they do the job. And yes, I suppose some could be done much more elegantly. But for now, I hope there is some usage for those who come along here because they need ideas.
Limitations
DsxDataGrid
must use ItemFixHeight
if IsVirtualizing
is set or there are multiple areas (frozen column on left and/or right side).
The filters do not listen to changes in the filtered fields of the underlying source.
What is Missing/Needs Redesign?
- Grouping, best if
IsVirtualizing
could be kept (flattened GroupCollection
s)
- Make the filter listen to changes
- Redesign frozen columns (WPFToolKit
DataGrid
has an implementation)
What I Have Learned?
Having different areas or frozen columns organized in different independent ListView
s is not the best practice. I rarely use that so I can go with the current solution for now. If I knew from the start, I would have invested my time in writing my own GridViewRowPresenter
. But I did not want to go down that deep when I started. Well, like the saying: all people are smart - some before, some after.
History
- 28-Jan-2010 - Initial version
- 17-Apr-2010 - Updated .NET 4 RTM (
StaticResource
order problem)
- 20-Apr-2010 - Updated both source and sample code