After my last blog I had some people ask me for details on how to implement some of my suggested enhancements. I also wanted to investigate how an editable data table could utilize the same logic. This is what I came up with. It supports...
- Undo/Redo functionality with minimal wire-up in the inherited classes
- Control of undo stack size
- Ability to exclude properties from undo/redo tracking
- Undo/redo properties in the DataContext
- Ability to undo and redo add, delete, and updates to data table and data set properties
- Grouping of consecutive changes to the same property into a single undo
- Grouping of changes to multiple properties due to same user action into a single undo
- Custom Undo and Redo buttons that are detected and wired-up by the base class
Start a new Visual Studio VB WPF (Framework) project and call it UndoClass.
Add a NotifyPropertyChanged class. This is a base class that implements INotifyPropertyChanged.
Imports
System.ComponentModel
Public Class NotifyPropertyChanged
Inherits Window
Implements INotifyPropertyChanged
Public Event PropertyChanged As System.ComponentModel.PropertyChangedEventHandler Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
' When we target 4.5 we can add <CallerMemberName>
here :-)
Public Function SetProperty(Of T)(ByRef storage As T, value As T,
<System.Runtime.CompilerServices.CallerMemberName> Optional PropertyName As String = Nothing) As Boolean
If Object.Equals(storage,
value) Then Return False
storage = value
NotifyPropertyChanged(PropertyName)
Return True
End Function
Public Sub
NotifyPropertyChanged(<System.Runtime.CompilerServices.CallerMemberName> Optional PropertyName As String = Nothing)
If PropertyName.StartsWith("set_", System.StringComparison.OrdinalIgnoreCase) Then
PropertyName =
PropertyName.Substring(4)
End If
RaiseEvent PropertyChanged(Me, New
System.ComponentModel.PropertyChangedEventArgs(PropertyName))
End Sub
End Class
Add the UndoButton and RedoButton classes. These custom buttons are automatically wired up by the base class. They have no special features other than their class.
Public Class UndoButton
Inherits Button
End Class
Public Class RedoButton
Inherits Button
End Class
Now for the fun bit. The UndoRedo class inherits NotifyPropertyChanged. All changes to properties are held in a stack of cChange. This class contains enough information to back-out a change or to re-apply it. The ChangeID is incremented whenever the user presses a key or mouse button. If multiple properties are created while ChangeID is the same, they need to be undone and redone as a group.
RowID, RowIndex, Table, and RowPriorState are all used for tracking changes to data tables. A data table must have a unique primary key defined for us to track changes. The value of this primary key is held in RowID. RowIndex is used to put deleted rows back where we found them. RowPriorState is used when we undelete a row so we can set it to Added or Modified as appropriate. For rows, the OldValue and NewValue are not the row objects but their ItemArrays.
Imports
System.ComponentModel
Imports
System.Data
Imports
System.Reflection
Public Class UndoRedo
Inherits NotifyPropertyChanged
Private Class cChange
Public ChangeID As Integer
Public PropertyName As String
Public RowID As Object
Public RowIndex As Integer
Public Table As DataTable
Public PriorRowState As DataRowState
Public OldValue As Object
Public NewValue As Object
End Class
End Class
Let's add a few fields. IsUndoingOrRedoing is used to prevent us from tracking changes due to an undo or redo operation. SetsAndProperty names is used to map from datasets to the properties they are bound to. Ditto TablesAndPropertyNames.
Private UndoStack As New List(Of cChange)()
Private RedoStack As New List(Of cChange)()
Private IsUndoingOrRedoing As Boolean = False
Private ChangeID As Integer = 0
Private InitialValues As New Dictionary(Of String, Object)
Private SetsAndPropertyNames As New Dictionary(Of DataSet, String)
Private TablesAndPropertyNames As New Dictionary(Of DataTable, String)
Private UndoCommand As ICommand
Private RedoCommand As ICommand
Public DontTrackProperties As String()
Public MaxUndoCount As Integer = 100
We have a default constructor that wires up some event handlers. Note how we attach an event handler to the PropertyChanged event to track all changes to properties. We remove these handlers in our finalize method.
Public Sub New()
AddHandler MyBase.PropertyChanged,
AddressOf TrackChange
AddHandler MyBase.PreviewKeyDown,
AddressOf TrackKeyDown
AddHandler MyBase.PreviewMouseDown,
AddressOf TrackMouseDown
AddHandler MyBase.Loaded, AddressOf InitializeProperties
End Sub
Protected Overrides Sub Finalize()
RemoveHandler MyBase.PropertyChanged, AddressOf TrackChange
RemoveHandler MyBase.PreviewKeyDown, AddressOf TrackKeyDown
RemoveHandler MyBase.PreviewMouseDown, AddressOf TrackMouseDown
RemoveHandler MyBase.Loaded, AddressOf InitializeProperties
MyBase.Finalize()
End Sub
The TrackKeyDown and TrackMouseDown event handlers simply increment ChangeID. Multiple property changes that occur while the value of ChangeID is the same will be undone and redone as a group.
Private Sub TrackMouseDown(sender As Object, e As MouseButtonEventArgs)
ChangeID += 1
End Sub
Private Sub TrackKeyDown(sender As Object, e As KeyEventArgs)
ChangeID += 1
End Sub
TrackChange is called whenever a bound property is changed. Nothing happens if the change was the result of an undo or redo operation, if the property is not being tracked, or if the property name is empty.
We call AddDataEventHandlers to see if the property is a data table or data set. If it is, we attach RowModified event handlers but we don't need to continue treating it as a simple property.
If it is a second change to the same property we consolidate the change otherwise we get the prior value and add a cChange to the undo stack.
Private Sub TrackChange(sender As Object, e As PropertyChangedEventArgs)
If IsUndoingOrRedoing Then Exit Sub
If DontTrackProperties.Contains(e.PropertyName) Then Exit Sub
If String.IsNullOrEmpty(e.PropertyName)
Then Exit Sub
Dim PropertyName As String =
e.PropertyName
Dim type As Type = sender.GetType()
Dim pi As PropertyInfo =
type.GetProperty(PropertyName, BindingFlags.Public Or BindingFlags.Instance)
Dim NewValue As Object = pi.GetValue(sender)
Dim OldValue As Object = Nothing
Dim PriorChange As cChange
' Did a dataset
or datatable property change?
If AddDataEventHandlers(sender, pi) Then Exit Sub
' No it was a
simple property. Is this a second change to the same property
If UndoStack.Count > 0 AndAlso UndoStack.Last().PropertyName = PropertyName Then
UndoStack.Last().NewValue =
NewValue
Else
' It needs to
be a new cChange - get the prior value
PriorChange = UndoStack.LastOrDefault(Function(c) c.PropertyName = PropertyName)
If PriorChange Is Nothing Then
OldValue =
InitialValues(PropertyName)
End If
' If something
really changed, add it to the stack
If Not
Object.Equals(OldValue, NewValue) Then
TrimUndoStack()
UndoStack.Add(New cChange() With
{.ChangeID = ChangeID, .PropertyName = PropertyName, .OldValue = OldValue,
.NewValue = NewValue})
End If
End If
' Any changes
invalidate the redo stack
RedoStack.Clear()
End Sub
Here is the undo and redo logic.
Friend Sub Undo_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
' We don't undo changes that were stored before the user started typing
e.CanExecute = (UndoStack.Any(Function(u) u.ChangeID > 0))
End Sub
Friend Sub Undo_Executed(sender As Object, e As ExecutedRoutedEventArgs)
Dim Change As cChange = UndoStack.Last()
Dim ChangeID As Integer = Change.ChangeID
If Change.ChangeID = 0 Then Exit Sub
IsUndoingOrRedoing = True
Do
UndoStack.Remove(Change)
RedoStack.Add(Change)
Dim PropertyName As String =
Change.PropertyName
Dim type As Type = sender.GetType()
Dim pi As PropertyInfo =
type.GetProperty(PropertyName, BindingFlags.Public Or BindingFlags.Instance)
Dim NewValue As Object = Change.OldValue
If Change.RowID Is Nothing Then
' It's a
simple property
pi.SetValue(sender, NewValue)
Else
Dim DT As DataTable = Change.Table
If Change.OldValue Is Nothing AndAlso Change.NewValue IsNot Nothing Then
' was added - need to delete
DT.Rows.Remove(DT.Rows.Find(Change.RowID))
End If
If Change.OldValue IsNot Nothing And Change.NewValue Is Nothing Then
' was deleted - need to put back. Was it removed or flagged for
deletion
Dim DR As DataRow = DT.Select(DT.PrimaryKey(0).ColumnName & "=" & Change.RowID, "", DataViewRowState.Deleted).FirstOrDefault()
If DR Is Nothing Then
' It was removed, need to add row
Dim row As DataRow = DT.NewRow()
row.ItemArray =
Change.OldValue
DT.Rows.InsertAt(row,
Change.RowIndex)
Else
' It was flagged for deletion, need to return to prior state
If Change.PriorRowState = DataRowState.Added Then
DR.SetAdded()
Else
DR.SetModified()
End If
End If
End If
If Change.OldValue IsNot Nothing AndAlso Change.NewValue IsNot Nothing Then
' Row was updated - undo the update
DT.Rows.Find(Change.RowID).ItemArray = Change.OldValue
End If
End If
Change = UndoStack.LastOrDefault()
' Keep undoing
changes until we run out or the next change has a different change id
Loop While Change IsNot Nothing AndAlso
Change.ChangeID = ChangeID
IsUndoingOrRedoing = False
End Sub
Friend Sub Redo_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
e.CanExecute = (RedoStack.Count > 0)
End Sub
Friend Sub Redo_Executed(sender As Object, e As ExecutedRoutedEventArgs)
Dim Change As cChange = RedoStack.Last()
Dim ChangeID As Integer = Change.ChangeID
IsUndoingOrRedoing = True
Do
TrimUndoStack()
RedoStack.Remove(Change)
UndoStack.Add(Change)
Dim PropertyName As String =
Change.PropertyName
Dim type As Type = sender.GetType()
Dim pi As PropertyInfo =
type.GetProperty(PropertyName, BindingFlags.Public Or BindingFlags.Instance)
Dim NewValue As Object = Change.NewValue
If Change.RowID Is Nothing Then
pi.SetValue(sender, NewValue)
Else
Dim DT As DataTable = Change.Table
If Change.OldValue Is Nothing And Change.NewValue IsNot Nothing Then
' Insert row
Dim row As DataRow = DT.NewRow()
row.ItemArray =
Change.NewValue
DT.Rows.InsertAt(row,
Change.RowIndex)
End If
If Change.OldValue IsNot Nothing And Change.NewValue Is Nothing Then
' Delete
DT.Rows.Remove(DT.Rows.Find(Change.RowID))
End If
If Change.OldValue IsNot Nothing And Change.NewValue IsNot Nothing Then
DT.Rows.Find(Change.RowID).ItemArray = Change.NewValue
End If
End If
Change = RedoStack.LastOrDefault()
Loop While Change IsNot Nothing AndAlso
Change.ChangeID = ChangeID
IsUndoingOrRedoing = False
End Sub
When we have loaded, we need to do our initial wire-up. This involves creating the Undo and Redo commands and wiring them up the Undo and Redo code above and to any UndoButtons and RedoButtons that may be on the page. Then we loop through all the properties looking for data sets and data tables that we need monitor for changes.
Private Sub InitializeProperties(sender As Object, e As RoutedEventArgs)
Dim type As Type = sender.GetType()
BindUndoAndRedoButtons(sender)
For Each PI As PropertyInfo In
type.GetProperties(BindingFlags.Public Or BindingFlags.Instance)
AddDataEventHandlers(sender, PI)
InitialValues.Add(PI.Name,
PI.GetValue(sender))
Next
End Sub
Here is how we detect data tables being added or removed from data sets, data rows being added, removed or modified, and also store the initial values in data rows.
Private Function AddDataEventHandlers(sender As Object, PI As PropertyInfo) As Boolean
Select Case
PI.PropertyType.Name
Case "DataSet"
Dim DS As DataSet = DirectCast(PI.GetValue(sender), DataSet)
If DS IsNot Nothing Then
SetsAndPropertyNames.Add(DS, PI.Name)
AddHandler DS.Tables.CollectionChanged, AddressOf DataSet_CollectionChanged
LoadDataSetHandlers(DS,
PI.Name)
End If
Return True
Case "DataTable"
Dim DT As DataTable = DirectCast(PI.GetValue(sender), DataTable)
LoadDataTableHandlers(DT,
PI.Name)
Return True
End Select
Return False
End Function
Private Sub DataSet_CollectionChanged(sender As Object, e As CollectionChangeEventArgs)
If e.Action = CollectionChangeAction.Add AndAlso TypeOf e.Element Is DataTable Then
Dim DT As DataTable = DirectCast(e.Element, DataTable)
Dim PropertyName As String =
SetsAndPropertyNames(DT.DataSet)
LoadDataTableHandlers(DT,
PropertyName)
End If
End Sub
Private Sub LoadDataSetHandlers(DS As DataSet, PropertyName As String)
If DS Is Nothing Then Exit Sub
For Each DT As DataTable In DS.Tables
LoadDataTableHandlers(DT,
PropertyName)
Next
End Sub
Private Sub LoadDataTableHandlers(DT As DataTable, PropertyName As String)
If DT Is Nothing Then Exit Sub
TablesAndPropertyNames.Add(DT,
PropertyName)
AddHandler DT.RowChanged, AddressOf RowChanged
AddHandler DT.RowDeleting, AddressOf RowDeleting
SaveRowValues(DT, PropertyName)
End Sub
Private Sub SaveRowValues(DT As DataTable, PropertyName As String)
Dim ID As Object
Dim Index As Integer
For Each row As DataRow In DT.Select()
ID = row(DT.PrimaryKey(0))
Index = DT.Select().ToList().IndexOf(row)
TrimUndoStack()
UndoStack.Add(New cChange() With
{.ChangeID = ChangeID, .PropertyName = PropertyName, .PriorRowState =
row.RowState, .RowID = ID, .RowIndex = Index, .Table = DT, .OldValue = Nothing, .NewValue = row.ItemArray})
Next
End Sub
Here is how we track data table add, modify, and delete changes.
Private Sub RowDeleting(sender As Object, e As DataRowChangeEventArgs)
Dim DT As DataTable = e.Row.Table
If IsUndoingOrRedoing Then Exit Sub
If DT.PrimaryKey Is Nothing OrElse DT.PrimaryKey.Count = 0 Then Exit Sub
Dim PropertyName As String =
TablesAndPropertyNames(DT)
Dim ID As Object = e.Row(e.Row.Table.PrimaryKey(0))
Dim Index As Integer = e.Row.Table.Rows.IndexOf(e.Row)
Dim priorChange As cChange = UndoStack.LastOrDefault(Function(c) DT.Equals(c.Table) AndAlso TypeOf c.NewValue Is Object() AndAlso c.RowID = ID)
Dim priorItemArray As Object() = Nothing
If priorChange IsNot Nothing Then
priorItemArray =
priorChange.NewValue
End If
TrimUndoStack()
UndoStack.Add(New cChange() With
{.ChangeID = ChangeID, .PropertyName = TablesAndPropertyNames(e.Row.Table),
.RowID = ID, .RowIndex = Index, .Table = DT, .PriorRowState = e.Row.RowState,
.OldValue = priorItemArray, .NewValue = Nothing})
End Sub
Private Sub RowChanged(sender As Object, e As DataRowChangeEventArgs)
Dim DT As DataTable = e.Row.Table
If IsUndoingOrRedoing Then Exit Sub
If DT.PrimaryKey Is Nothing OrElse DT.PrimaryKey.Count = 0 Then Exit Sub
Dim PropertyName As String =
TablesAndPropertyNames(DT)
Dim ID As Object = e.Row(e.Row.Table.PrimaryKey(0))
Dim Index As Integer = DT.Rows.IndexOf(e.Row)
Dim priorChange As cChange = UndoStack.LastOrDefault(Function(c) c.PropertyName = PropertyName AndAlso TypeOf c.NewValue Is Object() AndAlso c.RowID = ID)
Dim priorItemArray As Object() = Nothing
If priorChange IsNot Nothing Then
priorItemArray =
priorChange.NewValue
End If
TrimUndoStack()
Select Case e.Action
Case DataRowAction.Add
UndoStack.Add(New cChange() With
{.ChangeID = ChangeID, .PropertyName = TablesAndPropertyNames(e.Row.Table),
.RowID = ID, .RowIndex = Index, .Table = DT, .OldValue = Nothing, .NewValue = e.Row.ItemArray})
Case DataRowAction.Change
UndoStack.Add(New cChange() With
{.ChangeID = ChangeID, .PropertyName = TablesAndPropertyNames(e.Row.Table),
.RowID = ID, .RowIndex = Index, .Table = DT, .OldValue = priorItemArray,
.NewValue = e.Row.ItemArray})
End Select
End Sub
We expose a property called MaxUndoCount that limits the size of the Undo stack. Before we add a cChange to the undo stack, we call this method to make sure there is enough room.
Private Sub TrimUndoStack()
'Make room for
new change
Dim ChangeID As Integer = -1
Do While
UndoStack.Count > 0 AndAlso
UndoStack.Count > MaxUndoCount - 1
ChangeID =
UndoStack.First().ChangeID
UndoStack.Remove(UndoStack.First())
Loop
' Don't leave
partial changeid on stack
Do While
UndoStack.Count > 0 AndAlso
UndoStack.First().ChangeID = ChangeID
UndoStack.Remove(UndoStack.First())
Loop
End Sub
We also expose a method that allows the inherited window to explicitly clear the stack, such as when the page has been refreshed from the database.
Public Sub ClearUndo()
UndoStack.Clear()
RedoStack.Clear()
End Sub
This method creates the Undo and Redo commands, wires them up to the Undo and Redo methods and binds them to the Undo and Redo buttons. It needs a helper function called FindChildrenOfType.
Private Sub BindUndoAndRedoButtons(sender As Object)
UndoCommand = New RoutedCommand("UndoCommand", sender.GetType())
RedoCommand = New RoutedCommand("RedoCommand", sender.GetType())
CommandBindings.Add(New CommandBinding(UndoCommand, AddressOf Undo_Executed, AddressOf Undo_CanExecute))
CommandBindings.Add(New CommandBinding(RedoCommand, AddressOf Redo_Executed, AddressOf Redo_CanExecute))
For Each
UndoButton As UndoButton In
FindChildrenOfType(Of UndoButton)(sender)
UndoButton.Command = UndoCommand
Next
For Each
RedoButton As RedoButton In
FindChildrenOfType(Of RedoButton)(sender)
RedoButton.Command = RedoCommand
Next
End Sub
Public Shared Function FindChildrenOfType(Of T As Windows.DependencyObject)(depObj As Windows.DependencyObject) As Collections.Generic.List(Of T)
Dim Children As New Collections.Generic.List(Of T)
If depObj IsNot Nothing Then
For i As Integer = 0 To Windows.Media.VisualTreeHelper.GetChildrenCount(depObj) - 1
Dim child As Windows.DependencyObject = Windows.Media.VisualTreeHelper.GetChild(depObj, i)
If child IsNot Nothing AndAlso TypeOf child Is T Then
Children.Add(DirectCast(child, T))
End If
Children.AddRange(FindChildrenOfType(Of T)(child))
Next
End If
Return Children
End Function
Now we have to consume this cool new feature. Change MainWindow.xaml to look like this. We are defining some text boxes, a checkbox, and a data grid. We have buttons to add and remove rows from the grid, an Undo button, and a Redo button. Other than inheriting UndoRedo and adding the last two buttons, we don't have to modify our window at all.
<local:UndoRedo x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:UndoClass"
mc:Ignorable="d"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<RoutedCommand x:Key="DeleteRowCommand"/>
<RoutedCommand x:Key="InsertRowCommand"/>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding Command="{StaticResource DeleteRowCommand}" Executed="DeleteRow_Executed"/>
<CommandBinding Command="{StaticResource InsertRowCommand}" Executed="InsertRow_Executed"/>
</Window.CommandBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Field
1: "/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding theText1, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Field
2: "/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding theText2, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Checkbox"/>
<CheckBox Grid.Row="2" Grid.Column="1" IsChecked="{Binding theBoolean}"/>
<Button Grid.Row="3" Grid.Column="0" Content="Delete
row" Command="{StaticResource DeleteRowCommand}"/>
<Button Grid.Row="3" Grid.Column="1" Content="Insert
row" Command="{StaticResource InsertRowCommand}"/>
<DataGrid Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding DS.Tables[Values]}" AutoGenerateColumns="False" SelectedItem="{Binding SelectedDRV}" CanUserAddRows="False" CanUserDeleteRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding ID}"/>
<DataGridTextColumn Header="Value" Binding="{Binding Value}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
<local:UndoButton Grid.Row="6" Grid.Column="0" Content="Undo"/>
<local:RedoButton Grid.Row="6" Grid.Column="1" Content="Redo"/>
</Grid>
</local:UndoRedo>
The code behind looks like this. Again, other than inherting UndoRedo and specifying DontTrackProperties, we haven't done anything special.
Imports
System.Data
Class MainWindow
Inherits UndoRedo
Private _theText1 As String = "ABC"
Public Property theText1 As String
Get
Return _theText1
End Get
Set(value As String)
SetProperty(_theText1, value)
End Set
End Property
Private _theText2 As String
Public Property theText2 As String
Get
Return _theText2
End Get
Set(value As String)
SetProperty(_theText2, value)
End Set
End Property
Private _theBoolean As Boolean = False
Public Property theBoolean As Boolean
Get
Return _theBoolean
End Get
Set(value As Boolean)
SetProperty(_theBoolean, value)
End Set
End Property
Private _DS As New DataSet()
Public Property DS As DataSet
Get
Return _DS
End Get
Set(value As DataSet)
SetProperty(_DS, value)
End Set
End Property
Private _SelectedDRV As DataRowView
Public Property SelectedDRV As DataRowView
Get
Return _SelectedDRV
End Get
Set(value As DataRowView)
SetProperty(_SelectedDRV,
value)
End Set
End Property
Public Sub New()
DontTrackProperties = {NameOf(SelectedDRV)}
InitializeComponent()
End Sub
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
Dim DT As New DataTable("Values")
DT.Columns.Add(New DataColumn("ID", GetType(Integer)))
DT.Columns.Add(New DataColumn("Value", GetType(String)))
DT.PrimaryKey = {DT.Columns(0)}
For i As Integer = 1 To 5
Dim row As DataRow = DT.NewRow()
row("ID") = i
row("Value") = "Value " & i
DT.Rows.Add(row)
Next
DS.Tables.Add(DT)
NotifyPropertyChanged("")
SelectedDRV =
DS.Tables(0).DefaultView.Item(0)
End Sub
Private Sub InsertRow_Executed(sender As Object, e As ExecutedRoutedEventArgs)
Dim DT As DataTable = DS.Tables("Values")
Dim row As DataRow = DT.NewRow()
row("ID") = DT.Select().Max(Function(r) r(DT.PrimaryKey(0))) + 1
DT.Rows.Add(row)
End Sub
Private Sub DeleteRow_Executed(sender As Object, e As ExecutedRoutedEventArgs)
SelectedDRV.Row.Delete()
End Sub
End Class
Let's try this out. Run the program, alter a textbox, check the checkbox, delete a row, add a row and enter some data in it. Like this.
|
Make some changes |
Now click [Undo] three times. You see the individual changes you made to the grid being undone.
Click [Undo] two more times to see the checkbox and the textbox being undone too.