Add a class called NotifyPropertyChanged.
Public Class NotifyPropertyChanged
Inherits Window
Implements INotifyPropertyChanged
Public Event PropertyChanged As System.ComponentModel.PropertyChangedEventHandler Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
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)
PropertyName = PropertyName.Substring(4)
End If
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(PropertyName))
End Class
The Undo/Redo class
Now we define our Undo/Redo class that inherits from NotifyPropertyChanged. It hooks into the NotifyPropertyEvent to detect changes to properties and stores them in a stack. When the user clicks the Undo button the last change is popped off the stack, undone, and pushed onto the Redo stack. When the user clicks the Redo button the last change is popped off the Redo stack, reapplied, and pushed onto the Undo stack.
As we Undo and Redo we don't want to trigger changes that add to the Undo stack so we use the IsUndoingOrRedoing flag to suppress that.
We need the Undo and Redo commands to be defined in XAML so the Undo and Redo buttons can reference them, but the creation of the command bindings can be done in code as soon as we get a reference to the window they are defined in. The code is quite small, considering how powerful it is.
Public Class UndoRedo
Inherits NotifyPropertyChanged
Private Class cChange
Public PropertyName As String
Public OldValue As Object
Public NewValue As Object
End Class
Private UndoStack As New List(Of cChange)()
Private CommandBindingsResolved As Boolean = False
Public Sub New()
Private Sub TrackChange(sender As Object, e As PropertyChangedEventArgs)
If Not CommandBindingsResolved Then
ResolveCommandBindings()
End If
If IsUndoingOrRedoing Then Exit Sub
Dim PropertyName As String = e.PropertyName
Dim PriorChange As cChange = UndoStack.LastOrDefault(Function(c) c.PropertyName = PropertyName)
If PriorChange IsNot Nothing Then
OldValue = PriorChange.NewValue
End If
UndoStack.Add(New cChange() With {.PropertyName = PropertyName, .OldValue = OldValue, .NewValue = NewValue})
End Sub
Private Sub ResolveCommandBindings()
Dim UndoCommand As ICommand = TryFindResource("UndoCommand")
If UndoCommand IsNot Nothing Then
CommandBindings.Add(New CommandBinding(UndoCommand, AddressOf Undo_Executed, AddressOf Undo_CanExecute))
If RedoCommand IsNot Nothing Then
CommandBindings.Add(New CommandBinding(RedoCommand, AddressOf Redo_Executed, AddressOf Redo_CanExecute))
CommandBindingsResolved = True
End Sub
Friend Sub Undo_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
End Sub
Friend Sub Undo_Executed(sender As Object, e As ExecutedRoutedEventArgs)
Dim Change As cChange = UndoStack(UndoStack.Count - 1)
UndoStack.Remove(Change)
RedoStack.Add(Change)
Dim PropertyName As String = Change.PropertyName
IsUndoingOrRedoing = True
pi.SetValue(sender, NewValue)
IsUndoingOrRedoing = False
End Sub
Friend Sub Redo_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
End Sub
Friend Sub Redo_Executed(sender As Object, e As ExecutedRoutedEventArgs)
Dim Change As cChange = RedoStack(RedoStack.Count - 1)
RedoStack.Remove(Change)
UndoStack.Add(Change)
Dim PropertyName As String = Change.PropertyName
IsUndoingOrRedoing = True
pi.SetValue(sender, NewValue)
IsUndoingOrRedoing = False
End Sub
End Class
Using the Undo/Redo class.
We will create two text boxes, one bound on PropertyChanged and one with LostFocus. We will add an Undo and a Redo button. The Undo and Redo commands are defined in the XAML so the buttons can bind to them although it would also be possible to do the same thing by creating an UndoButton and a RedoButton control or giving them special names.
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="UndoCommand"/>
<RoutedCommand x:Key="RedoCommand"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<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}"/>
<Button Grid.Row="2" Grid.Column="0" Content="Undo" Command="{StaticResource UndoCommand}"/>
<Button Grid.Row="2" Grid.Column="1" Content="Redo" Command="{StaticResource RedoCommand}"/>
</Grid>
</local:UndoRedo>
The code-behind merely defines the two properties we are using. It inherits the UndoRedo class. Other than that, it does nothing special.
Inherits UndoRedo
Private _theText1 As String
Public Property theText1 As String
Get
Return _theText1
Set(value As String)
End Set
End Property
Private _theText2 As String
Public Property theText2 As String
Get
Return _theText2
Set(value As String)
End Set
End Property
End Class
Try typing in the two text boxes. You will see the [Undo] button becomes enabled as soon as there are entries in the Undo stack.
After typing |
After one Undo |
After Redo |
After two Undos |
Typing invalidates the Redo stack |
No comments:
Post a Comment