Published articles on other web sites*

Published articles on other web sites*

Composing WPF DataGrid Column Templates

Composing WPF DataGrid Column Templates for a Better User Experience

Julie Lerman

 Recently I’ve been doing some work in Windows Presentation Foundation (WPF) for a client. Although I’m a big believer in using third-party tools, I sometimes avoid them in order to find out what challenges lay in wait for developers who, for one reason or another, stick to using only those tools that are part of the Visual Studio installation.
So I crossed my fingers and jumped into the WPF DataGrid. There were some user-experience issues that took me days to solve, even with the aid of Web searches and suggestions in online forums. Breaking my DataGrid columns into pairs of complementary templates turned out to play a big role in solving these problems. Because the solutions weren’t obvious, I’ll share them here.
The focus of this column will be working with the WPF ComboBox and DatePicker controls that are inside a WPF DataGrid.

The DatePicker and New DataGrid Rows

One challenge that caused me frustration was user interaction with the date columns in my DataGrid. I had created a DataGrid by dragging an object Data Source onto the WPF window. The designer’s default behavior is to create a DatePicker for each DateTime value in the object. For example, here’s the column created for a DateScheduled field:
  2.           <DataGridTemplateColumn x:Name=" dateScheduledColumn"  
  3.   Header="DateScheduled" Width="100">
  4.   <DataGridTemplateColumn.CellTemplate>
  5.     <DataTemplate>
  6.       <DatePicker
  7.         SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay,
  8.           ValidatesOnExceptions=true, NotifyOnValidationError=true}" />
  9.     </DataTemplate>
  10.   </DataGridTemplateColumn.CellTemplate>
  11. </DataGridTemplateColumn>
This default isn’t conducive to editing. Existing rows weren’t updating when edited. The DatePicker doesn’t trigger editing in the DataGr
id, which means that the data-binding feature won’t push the change through to the underlying object. Adding the UpdateSourceTrigger attribute to the Binding element and setting its value to PropertyChanged solved this particular problem:
  2.           <DatePicker
  3.    SelectedDate="{Binding Path= DateScheduled, Mode=TwoWay,
  4.      ValidatesOnExceptions=true, NotifyOnValidationError=true,
  5.      UpdateSourceTrigger=PropertyChanged}" />
However, with new rows, there’s a worse implication of the 
inability of the DatePicker to trigger the DataGrid edit mode. In a DataGrid, a new row is represented by a NewRowPlaceHolder. When you first edit a cell in a new row, the edit mode triggers an insert in the data source (again, not in the database, but in the underlying in-memory source). Because DatePicker isn’t triggering edit mode, this doesn’t happen.
I discovered this problem because, coincidentally, a date column was the first column in my row. I was depending on it to trigger the row’s edit mode.
Figure 1 shows a new row where the date in the first editable column has been entered. 
Figure 1 Entering a Date Value into a New Row Placeholder
But after editing the value in the next column, the previous edit value has been lost, as you can see in Figure 2.
Figure 2 Date Value Is Lost After the Value of the Task Column in the New Row Is Modified
The key value in the first column has become 0 and the date that was just entered has changed to 1/1/0001. Editing the Task column finally triggered the DataGrid to add a new entity in the source. The ID value becomes an integer—default, 0—and the date value becomes the .NET default minimum date, 1/1/0001. If I had a default date specified for this class, the user’s entered date would have changed to the class default rather than the .NET default. Notice that the date in the Date Performed column didn’t change to its default. That’s because DatePerformed is a nullable property.
So now the user has to go back and fix the Scheduled Date again? I’m sure the user won’t be happy with that. I struggled with this problem for a while. I even changed the column to a DataTextBoxColumn instead, but then I had to deal with validation issues that the DatePicker had protected me from.
Finally, Varsha Mahadevan on the WPF team set me on the right path.
By leveraging the compositional nature of WPF, you can use two elements for the column. Not only does the DataGridTemplateColumn have a CellTemplate element, but there’s a CellEditingTemplate as well. Rather than ask the DatePicker control to trigger edit mode, I use the DatePicker only when I’m already editing. For displaying the date in the CellTemplate, I switched to a TextBlock. Here’s the new XAML for dateScheduledCoumn:
  2.           <DataGridTemplateColumn x:Name="dateScheduledColumn" 
  3.   Header="Date Scheduled" Width="125">
  4.   <DataGridTemplateColumn.CellTemplate>
  5.     <DataTemplate>
  6.       <TextBlock Text="{Binding Path= DateScheduled, StringFormat=\{0:d\}}" />
  7.     </DataTemplate>
  8.   </DataGridTemplateColumn.CellTemplate>
  9.   <DataGridTemplateColumn.CellEditingTemplate>
  10.     <DataTemplate>
  11.       <DatePicker SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay,
  12.                   ValidatesOnExceptions=true, NotifyOnValidationError=true}" />
  13.     </DataTemplate>
  14.   </DataGridTemplateColumn.CellEditingTemplate>
  15. </DataGridTemplateColumn>
Notice that I no longer need to specify UpdateSourceTrigger. I’ve made the same changes to the DatePerformed column.
Now the date columns start out as simple text until you enter the cell and it switches to the DatePicker, as you can see in Figure 3.
Figure 3 DateScheduled Column Using Both a TextBlock and a DatePicker
In the rows above the new row, you don’t have the DatePicker calendar icon.
But it’s still not quite right. We’re still getting the default .NET value as we begin editing the row. Now you can benefit from defining a default in the underlying class. I’ve modified the constructor of the ScheduleItem class to initialize new objects with today’s date. If data is retrieved from the database, it will overwrite that default. In my project, I’m using the Enity Framework, therefore my classes are generated automatically. However, the generated classes are partial classes, which allow me to add the constructor in an additional partial class:
  2.           public partial class ScheduleItem
  3.     {
  4.       public ScheduleItem()
  5.       {
  6.         DateScheduled = DateTime.Today;
  7.       }
  8.     }
Now when I begin entering data into the new row placeholder by modifying the DateScheduled column, the DataGrid will create a new ScheduleItem for me and the default (today’s date) will be displayed in the DatePicker control. As the user continues to edit the row, the value entered will remain in place this time.

Reducing User Clicks to Allow Editing

One downside to the two-part template is that you have to click on the cell twice to trigger the DatePicker. This is a frustration to anyone doing data entry, especially if they’re used to using the keyboard to enter data without touching the mouse. Because the DatePicker is in the editing template, it won’t get focus until you’ve triggered the edit mode—by default, that is. The design was geared for TextBoxes and with those it works just right. But it doesn’t work as well with the DatePicker. You can use a  combination of XAML and code to force the DatePicker to be ready for typing as soon as a user tabs into that cell.
First you’ll need to add a Grid container into the CellEditingTemplate so that it becomes a container of the DatePicker. Then, using the WPF FocusManager, you can force this Grid to be the focal point of the cell when the user enters the cell. Here’s the new Grid element surrounding the DatePicker:
  2.           <Grid FocusManager.FocusedElement="{Binding ElementName= dateScheduledPicker}">
  3.   <DatePicker x:Name=" dateScheduledPicker" 
  4.     SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay,
  5.     ValidatesOnExceptions=true, NotifyOnValidationError=true}"  />
  6. </Grid>
Notice I’ve provided a name for the DatePicker control and I’m pointing to that name using the FocusedElement Binding ElementName.
Moving your attention to the DataGrid that contains this Date-Picker, notice that I’ve added three new properties (RowDetailsVisibilityMode, SelectionMode and SelectionUnit), as well as a new event handler (SelectedCellsChanged):
  2.           <DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True" 
  3.           ItemsSource="{Binding}" Margin="12,12,22,31" 
  4.           Name="scheduleItemsDataGrid" 
  5.           RowDetailsVisibilityMode="VisibleWhenSelected" 
  6.           SelectionMode="Extended" SelectionUnit="Cell"
  7.           SelectedCellsChanged="scheduleItemsDataGrid_SelectedCellsChanged">
The changes to the DataGrid will enable notification when a user selects a new cell in the DataGrid. Finally, when this happens, you need to ensure that the DataGrid does indeed go into edit mode, which will then provide the user with the necessary cursor in the DatePicker. The scheduleItemsDataGrid_SelectedCellsChanged method will provide this last bit of logic:
  2.           private void scheduleItemsDataGrid_SelectedCellsChanged
  3.   (object sender, 
  4.    System.Windows.Controls.SelectedCellsChangedEventArgs e)
  5. {
  6.   if (e.AddedCells.Count == 0return;
  7.   var currentCell = e.AddedCells[0];
  8.   string header = (string)currentCell.Column.Header;
  10.   var currentCell = e.AddedCells[0];
  12.   if (currentCell.Column == 
  13.     scheduleItemsDataGrid.Columns[DateScheduledColumnIndex])
  14.   {
  15.     scheduleItemsDataGrid.BeginEdit();
  16.   }
  17. }
Note that in the class declarations, I’ve defined the constant, DateScheduledColumnIndex as 1, the position of the column in the grid.
With all of these changes in place, I now have happy end users. It took a bit of poking around to find the right combination of XAML and code elements to make the DatePicker work nicely inside of a DataGrid, and I hope to have helped you avoid making that same effort. The UI now works in a way that feels natural to the user.

Enabling a Restricted ComboBox to Display Legacy Values

Having grasped the value of layering the elements inside the DataGridTemplateColumn, I revisited another problem that I’d nearly given up on with a DataGrid-ComboBox column.
This particular application was being written to replace a legacy application with legacy data. The legacy application had allowed users to enter data without a lot of control. In the new application, the client requested that some of the data entry be restricted through the use of drop-down lists.The contents of the drop-down list were provided easily enough using a collection of strings. The challenge was that the legacy data still needed to be displayed even if it wasn’t contained in the new restricted list.
My first attempt was to use the DataGridComboBoxColumn:
  2.           <DataGridComboBoxColumn x:Name="frequencyCombo"   
  3.  MinWidth="100" Header="Frequency"
  4.  ItemsSource="{Binding Source={StaticResource frequencyViewSource}}"
  5.  SelectedValueBinding=
  6.  "{Binding Path=Frequency, UpdateSourceTrigger=PropertyChanged}">
  7. </DataGridComboBoxColumn>
The source items are defined in codebehind:
  2.           private void PopulateTrueFrequencyList()
  3. {
  4.   _frequencyList =
  5.                  new List<String>{"",
  6.                    "Initial","2 Weeks",
  7.                    "1 Month""2 Months",
  8.                    "3 Months""4 Months",
  9.                    "5 Months""6 Months",
  10.                    "7 Months""8 Months",
  11.                    "9 Months""10 Months",
  12.                    "11 Months""12 Months"
  13.                  };
  14.     }
This _frequencyList is bound to frequencyViewSource.Source in another method.
In the myriad possible configurations of the DataGridCombo-BoxColumn, I could find no way to display disparate values that may have already been stored in the Frequency field of the database table. I won’t bother listing all of the solutions I attempted, including one that involved dynamically adding those extra values to the bottom of the _frequencyList and then removing them as needed. That was a solution I disliked but was afraid that I might have to live with.
I knew that the layered approach of WPF to composing a UI had to provide a mechanism for this, and having solved the Date-Picker problem, I realized I could use a similar approach for the ComboBox. The first part of the trick is to avoid the slick DataGridComboBoxColumn and use the more classic approach of embedding a ComboBox inside of a DataGridTemplateColumn. Then, leveraging the compositional nature of WPF, you can use two elements for the column just as with the DateScheduled column. The first is a TextBlock to display values and the second is a ComboBox for editing purposes.
Figure 4 shows how I’ve used them together.
Figure 4 Combining a TextBlock to Display Values and a ComboBox for Editing
  2.           <DataGridTemplateColumn x:Name="taskColumnFaster" 
  3.   Header="Task" Width="100" >
  4.   <DataGridTemplateColumn.CellTemplate>
  5.     <DataTemplate>
  6.       <TextBlock Text="{Binding Path=Task}" />
  7.     </DataTemplate>
  8.   </DataGridTemplateColumn.CellTemplate>
  10.   <DataGridTemplateColumn.CellEditingTemplate>
  11.     <DataTemplate>
  12.       <Grid FocusManager.FocusedElement=
  13.        "{Binding ElementName= taskCombo}" >
  14.         <ComboBox x:Name="taskCombo"
  15.           ItemsSource="{Binding Source={StaticResource taskViewSource}}" 
  16.           SelectedItem ="{Binding Path=Task}" 
  17.             IsSynchronizedWithCurrentItem="False"/>
  18.       </Grid>
  19.     </DataTemplate>
  20.   </DataGridTemplateColumn.CellEditingTemplate>
  21. </DataGridTemplateColumn>
The TextBlock has no dependency on the restricted list, so it’s able to display whatever value is stored in the database. However, when it’s time to edit, the ComboBox will be used and entry is limited to the values in the frequencyViewSource.

Allowing Users to Edit the ComboBox When the Cell Gets Focused

Again, because the ComboBox won’t be available until the user clicks twice in the cell, notice that I wrapped the ComboBox in a Grid to leverage the FocusManager.
I’ve modified the SelectedCellsChanged method in case the user starts his new row data entry by clicking the Task cell, not by moving to the first column. The only change is that the code also checks to see if the current cell is in the Task column:
  2.           private void scheduleItemsDataGrid_SelectedCellsChanged(object sender,  
  3.   System.Windows.Controls.SelectedCellsChangedEventArgs e)
  4. {
  5.   if (e.AddedCells.Count == 0return;
  6.   var currentCell = e.AddedCells[0];
  7.   string header = (string)currentCell.Column.Header;
  9.   if (currentCell.Column == 
  10.     scheduleItemsDataGrid.Columns[DateScheduledColumnIndex] 
  11.     || currentCell.Column == scheduleItemsDataGrid.Columns[TaskColumnIndex])
  12.   {
  13.     scheduleItemsDataGrid.BeginEdit();
  14.   }
  15. }

Don’t Neglect User Experience

While we developers are building solutions, it’s common to focus on making sure data is valid, that it’s getting where it needs to go and other concerns. We may not even notice that we had to click twice to edit a date. But your users will quickly let you know if the application you’ve written to help them get their jobs done more effectively is actually holding them back because they have to keep going back and forth from the mouse to the keyboard.
While the WPF data-binding features of Visual Studio 2010 are fantastic development time savers, fine-tuning the user experience for the complex data grid—especially when combining it with the equally complex DatePicker and ComboBoxes—will be greatly appreciated by your end users.Chances are, they won’t even notice the extra thought you put in because it works the way they expect it—but that’s part of the fun of our job.

1 comment:

  1. Hi Julie

    I have not been able to find anything to help me with validation of WPF 4.5 datagrid cells. I know how to validate textboxes, but I am having a tough time validating datagrid cells (e.g. null values) where the columns of the grid are bound to a dataset. I tried using CellTemplate and CellEditingTemplate, but I still do not get the desired result.

    Any advice would be really appreciated.



Related Posts Plugin for WordPress, Blogger...