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.
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
UserControl
‘SplitterGrid
’ 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 ComboBoxe
s, 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 DataGridPCtrl
s 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
Classic
Luna
Luna Homestead
Luna metallic
Royale
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