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.
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:
<DataGridTemplateColumn x:Name=" dateScheduledColumn" Header="DateScheduled" Width="100"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <DatePicker SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn>
<DataGridTemplateColumn x:Name=" dateScheduledColumn" Header="DateScheduled" Width="100"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <DatePicker SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn>
<DatePicker SelectedDate="{Binding Path= DateScheduled, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true, UpdateSourceTrigger=PropertyChanged}" />
<DatePicker SelectedDate="{Binding Path= DateScheduled, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true, UpdateSourceTrigger=PropertyChanged}" />
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 inFigure 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:
<DataGridTemplateColumn x:Name="dateScheduledColumn" Header="Date Scheduled" Width="125"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <TextBlock Text="{Binding Path= DateScheduled, StringFormat=\{0:d\}}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellEditingTemplate> <DataTemplate> <DatePicker SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" /> </DataTemplate> </DataGridTemplateColumn.CellEditingTemplate> </DataGridTemplateColumn>
<DataGridTemplateColumn x:Name="dateScheduledColumn" Header="Date Scheduled" Width="125"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <TextBlock Text="{Binding Path= DateScheduled, StringFormat=\{0:d\}}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellEditingTemplate> <DataTemplate> <DatePicker SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" /> </DataTemplate> </DataGridTemplateColumn.CellEditingTemplate> </DataGridTemplateColumn>
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:
public partial class ScheduleItem { public ScheduleItem() { DateScheduled = DateTime.Today; } }
public partial class ScheduleItem { public ScheduleItem() { DateScheduled = DateTime.Today; } }
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:
<Grid FocusManager.FocusedElement="{Binding ElementName= dateScheduledPicker}"> <DatePicker x:Name=" dateScheduledPicker" SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" /> </Grid>
<Grid FocusManager.FocusedElement="{Binding ElementName= dateScheduledPicker}"> <DatePicker x:Name=" dateScheduledPicker" SelectedDate="{Binding Path=DateScheduled, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" /> </Grid>
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):
<DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True" ItemsSource="{Binding}" Margin="12,12,22,31" Name="scheduleItemsDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" SelectionMode="Extended" SelectionUnit="Cell" SelectedCellsChanged="scheduleItemsDataGrid_SelectedCellsChanged">
<DataGrid AutoGenerateColumns="False" EnableRowVirtualization="True" ItemsSource="{Binding}" Margin="12,12,22,31" Name="scheduleItemsDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" SelectionMode="Extended" SelectionUnit="Cell" SelectedCellsChanged="scheduleItemsDataGrid_SelectedCellsChanged">
private void scheduleItemsDataGrid_SelectedCellsChanged (object sender, System.Windows.Controls.SelectedCellsChangedEventArgs e) { if (e.AddedCells.Count == 0) return; var currentCell = e.AddedCells[0]; string header = (string)currentCell.Column.Header; var currentCell = e.AddedCells[0]; if (currentCell.Column == scheduleItemsDataGrid.Columns[DateScheduledColumnIndex]) { scheduleItemsDataGrid.BeginEdit(); } }
private void scheduleItemsDataGrid_SelectedCellsChanged (object sender, System.Windows.Controls.SelectedCellsChangedEventArgs e) { if (e.AddedCells.Count == 0) return; var currentCell = e.AddedCells[0]; string header = (string)currentCell.Column.Header; var currentCell = e.AddedCells[0]; if (currentCell.Column == scheduleItemsDataGrid.Columns[DateScheduledColumnIndex]) { scheduleItemsDataGrid.BeginEdit(); } }
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.
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:
<DataGridComboBoxColumn x:Name="frequencyCombo" MinWidth="100" Header="Frequency" ItemsSource="{Binding Source={StaticResource frequencyViewSource}}" SelectedValueBinding= "{Binding Path=Frequency, UpdateSourceTrigger=PropertyChanged}"> </DataGridComboBoxColumn>
<DataGridComboBoxColumn x:Name="frequencyCombo" MinWidth="100" Header="Frequency" ItemsSource="{Binding Source={StaticResource frequencyViewSource}}" SelectedValueBinding= "{Binding Path=Frequency, UpdateSourceTrigger=PropertyChanged}"> </DataGridComboBoxColumn>
private void PopulateTrueFrequencyList() { _frequencyList = new List<String>{"", "Initial","2 Weeks", "1 Month", "2 Months", "3 Months", "4 Months", "5 Months", "6 Months", "7 Months", "8 Months", "9 Months", "10 Months", "11 Months", "12 Months" }; }
private void PopulateTrueFrequencyList() { _frequencyList = new List<String>{"", "Initial","2 Weeks", "1 Month", "2 Months", "3 Months", "4 Months", "5 Months", "6 Months", "7 Months", "8 Months", "9 Months", "10 Months", "11 Months", "12 Months" }; }
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
<DataGridTemplateColumn x:Name="taskColumnFaster" Header="Task" Width="100" > <DataGridTemplateColumn.CellTemplate> <DataTemplate> <TextBlock Text="{Binding Path=Task}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellEditingTemplate> <DataTemplate> <Grid FocusManager.FocusedElement= "{Binding ElementName= taskCombo}" > <ComboBox x:Name="taskCombo" ItemsSource="{Binding Source={StaticResource taskViewSource}}" SelectedItem ="{Binding Path=Task}" IsSynchronizedWithCurrentItem="False"/> </Grid> </DataTemplate> </DataGridTemplateColumn.CellEditingTemplate> </DataGridTemplateColumn>
<DataGridTemplateColumn x:Name="taskColumnFaster" Header="Task" Width="100" > <DataGridTemplateColumn.CellTemplate> <DataTemplate> <TextBlock Text="{Binding Path=Task}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellEditingTemplate> <DataTemplate> <Grid FocusManager.FocusedElement= "{Binding ElementName= taskCombo}" > <ComboBox x:Name="taskCombo" ItemsSource="{Binding Source={StaticResource taskViewSource}}" SelectedItem ="{Binding Path=Task}" IsSynchronizedWithCurrentItem="False"/> </Grid> </DataTemplate> </DataGridTemplateColumn.CellEditingTemplate> </DataGridTemplateColumn>
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:
private void scheduleItemsDataGrid_SelectedCellsChanged(object sender, System.Windows.Controls.SelectedCellsChangedEventArgs e) { if (e.AddedCells.Count == 0) return; var currentCell = e.AddedCells[0]; string header = (string)currentCell.Column.Header; if (currentCell.Column == scheduleItemsDataGrid.Columns[DateScheduledColumnIndex] || currentCell.Column == scheduleItemsDataGrid.Columns[TaskColumnIndex]) { scheduleItemsDataGrid.BeginEdit(); } }
private void scheduleItemsDataGrid_SelectedCellsChanged(object sender, System.Windows.Controls.SelectedCellsChangedEventArgs e) { if (e.AddedCells.Count == 0) return; var currentCell = e.AddedCells[0]; string header = (string)currentCell.Column.Header; if (currentCell.Column == scheduleItemsDataGrid.Columns[DateScheduledColumnIndex] || currentCell.Column == scheduleItemsDataGrid.Columns[TaskColumnIndex]) { scheduleItemsDataGrid.BeginEdit(); } }
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.
Julie Lerman is a Microsoft MVP, .NET mentor and consultant who lives in the hills of Vermont. You can find her presenting on data access and other Microsoft .NET topics at user groups and conferences around the world. She blogs at thedatafarm.com/blog and is the author of the highly acclaimed book, “Programming Entity Framework” (O’Reilly Media, 2010). Follow her on Twitter at twitter.com/julielerman.
Thanks to the following technical expert for reviewing this article: Varsha Mahadevan
Learn about Windows Azure Caching Services and Command Query Responsibility Segregation (CQRS).