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

A C# WPF .NET 4.0 DataGrid with Persistent Controls in Cells

0.00/5 (No votes)
29 Aug 2011 1  
A DataGrid lookalike that has persistent controls in cells

Introduction

The official DataGrid in WPF .NET 4.0 does not behave in a satisfactory manner in my mind. You have to click twice on a cell: once to activate it, and once to edit or set focus on control. I think this is completely impractical from a user point of view.

After some research, I found some articles on the web about how to check a check box with one click, instead of two. After looking at the code, I realised these were only patch ups riddled with bugs, and only solved the issue for a checkbox.

This article is about a pseudo DataGrid that has cells filled with controls that are and behave like normal controls. It does have some pitfalls: height of cells are fixed and cannot be adjusted through scrolling on left hand side as with DataGrid, selection mode not implemented… I am sure users will find other issues. I feel none of these problems are critical, and it is always possible to improve and fix code at a later date.

However, most importantly, it is practical to use for an application user as the controls have immediate focus when you click on them. Furthermore, because the inserted controls are essentially standard controls, these have the same look and feel when applying themes.

screenshot.png

Brief Description of Code

There were some areas I needed to research before doing any development:

  • I needed to know how to have a header that was synchronised with table containing controls when scrolling horizontally, but which remained fixed when scrolling vertically. This can be done with 2 ScrollViewers with one of these using a style "ExtScrollView". See file DataGridPCtrls.xaml.
  • The other area that I felt was needed is it should be possible to adjust cell width. A special UserControlSplitterGrid’ had been created for that purpose. This control just adds a GridSplitter between elements of a grid. As the GridSplitter must not be too wide, I extended its capture area. See SplitGrid_MouseMove. If you don’t extend capture area, the user will have some difficulties putting the mouse cursor on the GridSplitter.

Once this research had been done, development went very smoothly. The headers consist of a SplitterGrid filled with Header user controls. The main grid is just a StackPanel of Column user controls. The Column user control is itself another StackPanel, but with different orientation.

Each ‘cell’ consist of a Border with individual controls in these. Only 2 sides of the Border are visible, so you have the grid aspect. The controls in cells have borders thickness set to 0 for better look.

When a Header width changes, corresponding Column’s width needs to also change. This is done by raising the WidthChangedEventArgs event in SplitterGrid. Handling is then done in Headers_WidthChanged function in DataGridPCtrls. (The addition of 1 is to take account of the GridSplitter width.)

There is code for handling tab, up, down, etc… keys. Handling is done in ‘Column’ via ctrl_PreviewKeyDown handler. A delegate ‘KeyDownDelegate’ is passed to Column, and handling is transferred to OnKeyDown using callback feature of delegates.

The header is usually scrolled from the main horizontal scroll. However if you tried Shift + Tab or Tab, it was possible to scroll header, without main horizontal scroll having interfered. This resulted in header and main part no longer being synchronised. To correct this, a RangeBase.ValueChanged handler 'HeaderScroll_ValueChanged' has been added. This will just synchronise the main part to header on rare occasions this is needed.

In the second version, I have implemented sorting. To do this, I have created a SortStruct struct with 2 members: a list of controls ‘RowList’ and an object ‘SortingObj’. All controls of each row are stored into RowList, and value of sorting column is stored into SortingObj. Once this is done for all rows, the sorting is performed by running a LINQ query using the orderby ascending/descending feature on ‘SortingObj’. Table has to be emptied of all controls before it can be filled again. This is because otherwise a control will be the logical child of 2 other controls and an exception will get thrown. Sorting is performed on the controls and not their value. This is because a control may have other information apart from its value that distinguishes it from other controls on the same column. This occurs for instance for ComboBoxes, where each ComboBox may contain different elements in its drop down list.

In third version of this control, I have implemented style-configurable properties and theme support. No doing so is nearly heretic, since these features are almost the raison d’être of WPF. On the other hand, I confess to being a C#/WPF beginner, but learning fast.

Only DataGridPCtrls is meant to be used by a client application, so only made DataGridPCtrls properties configurable in an XAML style – that is, changed these into dependency properties.

The 4 properties of concern are: GridLinesColor, GridBackColor, CanUserResizeColumns, CanUserSortColumns. When set, all of these properties need to change internal data. There are 2 ways I know of doing this:

  • Registering the dependency property with a xxxChangedCallBack static function
  • Overriding the property so it calls a xxxCoerceValueCallBack static function

I would have thought that only the first method is applicable here, since the second method appears to be only used when the DependencyProperty is inherited, as with BorderThicknessProperty. In this case, we want to change the internal border thickness, and not the outside one, which is why the property needs to be overridden here.

Strangely, only the second method works for CanUserResizeColumns and CanUserSortColumns, even though these properties are not inherited.

Apart from dependency properties, I have also implemented theme support in this version. To do this, I have made the usual change in AssemblyInfo.cs file from:

[assembly:ThemeInfo(ResourceDictionaryLocation.None, 
	ResourceDictionaryLocation. SourceAssembly)]

to:

[assembly:ThemeInfo(ResourceDictionaryLocation.SourceAssembly, 
	ResourceDictionaryLocation.SourceAssembly)]

The next stage is to create the usual .xaml files in Theme folder. I have tried to keep content of these files to exclusively theme specific XAML code. It made sense to change ‘Header’ class from a UserControl into a WPF Custom Control, so the theme files have a bit of extra code duplicated all over. I would like to know a way of avoiding this. Putting duplicated code in a separate XAML file and calling MergeDictionary doesn’t work.

I needed calls to TryFindResource. For instance, the SplitterGrid splitter color needs to be set to same as border of Header. In order for TryFindResource to work, we need to add following code in App.xaml file:

<ResourceDictionary>
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="{ThemeDictionary PersistDataGrid}"/>     
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> 

Once done all this, I still had trouble with layout when changed the theme with the application open. Changing all theme-specific resources from StaticResource into DynamicResource in .XAML files went a long way into resolving issues.

Remaining problems needed to be fixed with a Loaded RoutedEventHandler function, which is called every time the theme changes: SplitterGrid splitter, sort arrows and row height.

I am not aware of any theme issues and have tried theme changes in Windows 7, XP and Vista. Aero, Classic and all Luna types all work fine.

I feel everything else in the code is very straightforward, and no extra discussion is necessary.

Using the Code

Use of DataGridPCtrls can be seen in MainWindow.xaml and MainWindow.xaml.cs. The functions I have used for manipulation are:

  • AddColumn
  • AddRow
  • GetRowDetails
  • SetRowDetails
  • GetRowCount
  • GetControl

These are described in more detail below:

public void AddColumn(string strHeader,
ColumnType ct = ColumnType.TextBox, double width = 145.0);

where:

  • strHeader: The text that will be displayed in header column
  • ColumnType: This is an enum and can be one of TextBox, CheckBox, ComboBox or DatePicker. Defaults to TextBox.
  • width: Width of the column. Defaults to 145.0
public void AddRow();
public void AddRow(object[] RowDetails);

If no parameter is specified, a simple blank row is inserted. Otherwise, specify an array of objects for adding a row with filled fields. The object must be:

  • A string for a TextBox or ComboBox column
  • A bool for a CheckBox column
  • A DateTime for a DatePicker column
public void GetRowDetails(int row, out object[] RowDetails);

row indicates the row returned. 0 based. RowDetails is an array of objects, which follows the same rules as RowDetails in AddRow.

public void SetRowDetails(int row, object[] RowDetails);

row sets details of relevant row. 0 based. RowDetails is an array of objects, which follows the same rules as RowDetails in AddRow.

public int GetRowCount(int iCol = 0);

iCol indicates the number of rows for the column (0 based). By default, selects number of rows of first column. Currently, all columns have same numbers of rows, but alterations could be made in code so this is no longer the case.

public Control GetControl(int iRow, int iCol);

iRow and iCol indicate the row and column. These are 0 based. Returns the Control in particular cell (a TextBox, ComboBox, CheckBox or DatePicker).

In addition, there are 4 extra properties:

  • GridBackColor and GridLinesColor, which can be used for configuring grid line and background colour
  • CanUserResizeColumns - If set to False, columns can no longer be resized.
  • CanUserSortColumns - If set to False, sorting and highlighting of columns is no longer possible.
<loc:DataGridPCtrls x:Name="MyPerDataGrid" Margin="22,56,22,0"
Height="192" GridBackColor="White" GridLinesColor="Blue" 
CanUserResizeColumns="True" CanUserSortColumns="True" VerticalAlignment="Top"/>

Points of Interest

I originally tried working with the standard DataGrid, but spent far too much time trying to get this to behave differently. This has been a waste of time, as DataGrid is essentially not designed to allow controls in cells to be activated on a single click.

Design and coding of DataGridPCtrls went very rapidly. I spent one day doing the research before the development part and 3.5 days coding and bug fixing. In all, 4.5 days for the initial version, which I think is very reasonable, particularly as I am still pretty much a novice in C#/WPF (but then, most people probably are at this stage, as it is so new).

Due to the great interest the initial article has received, I have spent an extra 2 days on improvements, mostly spent on implementing the sorting of columns.

The behaviour of scrolling when changing column width is not quite satisfactory to me. Sometimes, it is the left hand side of resized column that moves, when it should be the right hand side instead. It may be possible to fix this, and I have not investigated this yet. In all circumstances, when a user scrolls towards the left, behaviour is OK. I can hardly imagine users spending vast amounts of time entertaining themselves at resizing columns width anyway. In any case, it is now possible to prevent column resizing using CanUserResizeColumns property.

In the third version, I have learnt a lot about themes: when to use a WPF UserControl or a WPF Custom Control, for instance. A Custom Control is not practical to use if require saving of data into XAML-defined fields. For example, such a control is not appropriate for DataGridPCtrls, because you have to save data into a StackPanel and this data get lost every time change theme. I have learnt the hard (and probably best) way: by doing it!

Otherwise, I am not aware of any other significant issues, and lots of improvements to this code are easily feasible: row height adjustment…

Please note: I am well aware of these improvements, which are for the most part very straightforward. At this stage, I want to get a feel of how useful this project actually is.

A Few Themed Screenshots

I have forced theme for the purpose of displaying a few screenshots. These are shown below:

Aero

Aero.png

Classic

Classic.png

Luna

LunaNormal.png

Luna Homestead

LunaHomestead.png

Luna metallic

LunaMetallic.png

Royale

Royale.png

Acknowledgements

The following links were very helpful:

For re-styled ScrollViewer:

The first of these links shows how to work with style of ScrollViewer. The second gave hints on how to get a ScrollViewer with a fixed header (but I found the way myself).

For header with GridSplitter separators:

My SplitterGrid user control is inspired by this, but has completely rewritten code.

History

  • 3rd May, 2011: Initial version
  • 12th May, 2011: Due to great interest received in the initial version, I have now implemented sorting of columns, as well as SetRowDetails function and CanUserResizeColumns and CanUserSortColumns properties.
  • 15th May, 2011: Attached a new version of the project source code. This has no other changes except a bug fix in file DataGridPCtrls.xaml.cs. With this change, the right vertical scrollbar gets mouse capture and is highlighted when cursor is on header and user moves it right onto scrollbar.
  • 28th August, 2011: Made changes - supports themes and XAML style properties

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