You already know how easy it is to implement databinding in the WPF DataGrid from a previous article. Let's dive just a little bit deeper, and decorate this application with the code to trigger insert-, update- and delete-calls against the underlying Model and/or Data Access Layer. I'll stick to the M-V-VM approach where the View decides when to update the Model (usually via executing a Command), but the ViewModel decides how to update the Model (by implementing that same Command). For starters, decorate the Formula 1 Driver class with the corresponding -dummy- Save() and Delete() methods:
/// <summary>
/// Persists a Formula 1 Driver
/// </summary>
public void Save()
{
// Ask the Model or the DAL to persist me...
}
/// <summary>
/// Deletes a Formula 1 Driver.
/// </summary>
public void Delete()
{
// Ask the Model or the DAL to delete me...
}
Insert and Update
Objects are persisted (inserted or updated) when the user leaves a row that he was editing. Moving to another cell in the same row updates the corresponding property through data binding, but doesn't signal the Model (or the Data Access Layer) yet. The only useful event is DataGrid.RowEditEnding. This is fired just before committing the modified row. Here's the registration of the event handler, in XAML:
<toolkit:DataGrid
x:Name="DriversDataGrid"
ItemsSource="{Binding Source={x:Static local:FormulaOneDriver.GetAll}}"
RowEditEnding="DriversDataGrid_RowEditEnding"
AutoGenerateColumns="False"
CanUserAddRows="True"
The RowEditEnding event is called when the user leaves Edit-mode, and can be the result of Cancelling, or Committing. Of course you only need to react to a Commit. This is the even handler:
/// <summary>
/// Persists a row after editing (update or insert).
/// </summary>
/// <param name="sender">Sender of the event: the DataGrid.</param>
/// <param name="e">Event arguments.</param>
private void DriversDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
// Only act on Commit
if (e.EditAction == DataGridEditAction.Commit)
{
FormulaOneDriver driver = e.Row.DataContext as FormulaOneDriver;
driver.Save();
}
}
Important remark
In its current implementation, a DataGridTemplateColumn will NOT trigger any of the DataGrid's events. There seems to be no elegant work-around for this. In this sample, if you only change the LatestVictory date of a Formula 1 Driver, the RowEditEnding will not fire, hence the Save() method on the Formula 1 Driver will NOT be called. Needless to say that this may be a showstopper !
Delete
Objects are deleted if the user selects one or multiple rows in the DataGrid and hits the Delete button. In most cases we would like to ask the user for a confirmation. We can do this by intercepting the Execute call via tunelling, and allow the user to cancel the delete command before it actually executes. This sounds pretty impressive, but it just involves writing an event handler for CommandManger.PreviewExecute. You can register this event handler in XAML:
<toolkit:DataGrid
x:Name="DriversDataGrid"
ItemsSource="{Binding Source={x:Static local:FormulaOneDriver.GetAll}}"
CommandManager.PreviewExecuted="DriversDataGrid_PreviewDeleteCommandHandler"
RowEditEnding="DriversDataGrid_RowEditEnding"
AutoGenerateColumns="False"
CanUserAddRows="True"
This is the actual eventhandler code, in which we can cancel the event:
/// <summary>
/// Asks for confirmation of a delete.
/// </summary>
/// <param name="sender">Sender of the event: the DataGrid.</param>
/// <param name="e">Event arguments.</param>
private void DriversDataGrid_PreviewDeleteCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
if (e.Command == DataGrid.DeleteCommand)
{
if (!(MessageBox.Show("Are you sure you want to delete?", "Please confirm.", MessageBoxButton.YesNo) == MessageBoxResult.Yes))
{
// Cancel Delete.
e.Handled = true;
}
}
}
Here's how it looks like in action:
If the Delete action is not cancelled, then the underlying collection is updated. This triggers its CollectionChanged event, in which you can call the Formula 1 Driver's Delete() method:
/// <summary>
/// The collection of drivers just changed: add or remove
/// </summary>
/// <param name="sender">Sernder of the Event.</param>
/// <param name="e">Event Arguments.</param>
private void Drivers_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Only Delete
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (FormulaOneDriver driver in e.OldItems)
{
driver.Delete();
}
}
}
Here's the registration of the event handler, put it e.g. in the constructor of the form:
ObservableCollection<FormulaOneDriver> drivers = this.DriversDataGrid.ItemsSource as ObservableCollection<FormulaOneDriver>;
drivers.CollectionChanged += new NotifyCollectionChangedEventHandler(this.Drivers_CollectionChanged);
For completeness, here's the XAML for the sample form:
<Window
x:Class="U2UConsult.DockOfTheBay.DataGridSampleWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:U2UConsult.DockOfTheBay"
xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="WPF DataGrid CRUD Sample"
SizeToContent="WidthAndHeight"
Icon="/DataGridSample;component/dotbay.png" >
<Window.Resources>
<Style TargetType="{x:Type toolkit:DatePickerTextBox}">
<Setter Property="Text" Value="None" />
<Setter Property="Background" Value="#00000000" />
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<toolkit:DataGrid
x:Name="DriversDataGrid"
ItemsSource="{Binding Source={x:Static local:FormulaOneDriver.GetAll}}"
CommandManager.PreviewExecuted="DriversDataGrid_PreviewDeleteCommandHandler"
RowEditEnding="DriversDataGrid_RowEditEnding"
AutoGenerateColumns="False"
CanUserAddRows="True"
CanUserDeleteRows="True"
CanUserSortColumns="True"
CanUserReorderColumns="True"
AlternatingRowBackground="WhiteSmoke"
RowHeaderWidth="16"
Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<toolkit:DataGrid.Columns>
<toolkit:DataGridTextColumn
Header="Name"
Binding="{Binding Name}"
CanUserReorder="True"
IsReadOnly="False"
CanUserSort="True"
SortMemberPath="Name"/>
<toolkit:DataGridComboBoxColumn
x:Name="TeamsCombo"
Header="Team"
ItemsSource="{Binding Source={x:Static local:FormulaOneTeam.GetAll}}"
SelectedValueBinding="{Binding TeamId}"
SelectedValuePath="Key"
DisplayMemberPath="Value.Name"
SortMemberPath="Team.Name" />
<toolkit:DataGridTextColumn
Header="Pole Positions"
Binding="{Binding PolePositions, NotifyOnSourceUpdated=True}"
CanUserSort="True" />
<toolkit:DataGridTemplateColumn
Header="Latest Victory"
CanUserSort="True"
SortMemberPath="LatestVictory">
<toolkit:DataGridTemplateColumn.CellTemplate >
<DataTemplate >
<toolkit:DatePicker
SelectedDate="{Binding LatestVictory}"
BorderThickness="0" />
</DataTemplate >
</toolkit:DataGridTemplateColumn.CellTemplate >
<toolkit:DataGridTemplateColumn.CellEditingTemplate >
<DataTemplate >
<toolkit:DatePicker
SelectedDate="{Binding LatestVictory, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
BorderThickness="0"/>
</DataTemplate >
</toolkit:DataGridTemplateColumn.CellEditingTemplate >
</toolkit:DataGridTemplateColumn>
</toolkit:DataGrid.Columns>
</toolkit:DataGrid>
</Grid>
</Window>
And the C# as well:
namespace U2UConsult.DockOfTheBay
{
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Input;
using Microsoft.Windows.Controls;
/// <summary>
/// WPF DataGrid CRUD Sample.
/// </summary>
public partial class DataGridSampleWindow : Window
{
/// <summary>
/// Initializes a new instance of the DataGridSampleWindow class.
/// </summary>
public DataGridSampleWindow()
{
InitializeComponent();
ObservableCollection<FormulaOneDriver> drivers = this.DriversDataGrid.ItemsSource as ObservableCollection<FormulaOneDriver>;
drivers.CollectionChanged += new NotifyCollectionChangedEventHandler(this.Drivers_CollectionChanged);
}
/// <summary>
/// Persists a row after editing (update or insert).
/// </summary>
/// <param name="sender">Sender of the event: the DataGrid.</param>
/// <param name="e">Event arguments.</param>
private void DriversDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
// Only act on Commit
if (e.EditAction == DataGridEditAction.Commit)
{
FormulaOneDriver driver = e.Row.DataContext as FormulaOneDriver;
driver.Save();
}
}
/// <summary>
/// Asks for confirmation of a delete.
/// </summary>
/// <param name="sender">Sender of the event: the DataGrid.</param>
/// <param name="e">Event arguments.</param>
private void DriversDataGrid_PreviewDeleteCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
if (e.Command == DataGrid.DeleteCommand)
{
if (!(MessageBox.Show("Are you sure you want to delete?", "Please confirm.", MessageBoxButton.YesNo) == MessageBoxResult.Yes))
{
// Cancel Delete.
e.Handled = true;
}
}
}
/// <summary>
/// The collection of drivers just changed: add or remove
/// </summary>
/// <param name="sender">Sernder of the Event.</param>
/// <param name="e">Event Arguments.</param>
private void Drivers_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Only Delete
if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (FormulaOneDriver driver in e.OldItems)
{
driver.Delete();
}
}
}
}
}
The Formula 1 Driver and Formula 1 Team classes are still the same from the previous article -except for the dummy Save() and Delete() methods.