Friday, May 7, 2021

Fully Functional Undo/Redo base class

 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.






No comments:

Post a Comment