Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF ListView which can do - Sorting, Filtering, Totals, Cell-focus, Editing, and More

0.00/5 (No votes)
21 Apr 2010 5  
Enhanced WPF ListView that is almost a DataGrid
dsxDataGridIntro.gif

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 DataGrids 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 DataGrids. 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 DataGrids 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 ControlTemplates 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 CellTemplates. Behind the scenes, the CellTemplates are generated.

The Basic Layout of DsxDataGrid

BasicLayout.png

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 GridViewHeaderRowPresenters but with different Styles (ControlTemplates). 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,  // displays BulletChrome
    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 CellTemplates are generated for the different ViewTypes. 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 CellTemplates 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,  // not valid for Filter
}

Note: All EditTypes are templated to look nice if the ItemHeight is larger than the EditControl usually is:

EditControls.png

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 GroupCollections)
  • 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 ListViews 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here