I'm trying to implement a simple undo/redo mechanism (based on stacks) for some events of a textbox.
Before asking this, I've seen a lot of undo/redo implementations like these, but more or less they are incomplete and showing things that I already knew (on the other hand, the profesional way using rare interfaces scapes from my compehenssion, so I want to follow this stack-based way), because those examples more than a undo/redo example for edit-controls, are a push/pop examples for stacks, but a undo/redo is a little more than writting a method to pop the last item of a "undo stack" and another method to pop the last item of a "redo stack", because in some point while the user is interacting with the control, the stacks should be cleared/resetted.
I mean that in a real undo/redo mechanism for edit-controls, the "redo stack" should be cleared when the user undoes and the user makes a text modification in the control while the "undo stack" still contain items, so in that point there is nothing to redo because a change occured while undoing. I didn't seen any complete example of a undo-redo mechanism in this way bearing in mind how must act the undo/redo stacks when changes occur in the control.
I need help to properlly implement the logic of my undo/redo stacks, I started trying it by my own for days with a couple of trial and erros, but always escapes me some detail, because when I get one (undo or redo)stack to work properly, the other one stops working as expected undoing what it should not undo or redoing what it should not redo, so I discarded out (again) all the conditional logic that I written because my logic always is wrong, I should start from zero again with a proper conditional algorithm, I mean the proper conditionals to push or pop the stack items at the right moment.
Then, more than words or suggestions, I need a working code that can solve the problem with my algorithm, I need to complete the algorithm logic of the AddUndoRedoItem
method in the code below, this is a specific question about this.
If I am missing a simpler solution following the same principles (a undo and redo stacks), I will accept that solution too.
In C# or Vb.Net, no matter.
PD: If because my poor English I didin't explained some thing prroperly and you are not totally sure of what kind of undo/redo I'am asking for, just I'm asking for a undo/redo as it sounds, just test the Ctrl+Z(undo) and Ctrl+Y(redo) keys in the Notepad while performing changes in the text undone or redone, see how it acts, that is a real undo/redo implementation, what I'm trying to reproduce with the stacks.
This is the current code:
Public Enum UndoRedoCommand As Integer
Undo
Redo
End Enum
Public Enum UndoRedoTextBoxEvent As Integer
TextChanged
End Enum
Public NotInheritable Class UndoRedoTextBox
Private ReadOnly undoStack As Stack(Of KeyValuePair(Of UndoRedoTextBoxEvent, Object))
Private ReadOnly redoStack As Stack(Of KeyValuePair(Of UndoRedoTextBoxEvent, Object))
Private lastCommand As UndoRedoCommand
Private lastText As String
Public ReadOnly Property Control As TextBox
Get
Return Me.controlB
End Get
End Property
Private WithEvents controlB As TextBox
Public ReadOnly Property CanUndo As Boolean
Get
Return (Me.undoStack.Count <> 0)
End Get
End Property
Public ReadOnly Property CanRedo As Boolean
Get
Return (Me.redoStack.Count <> 0)
End Get
End Property
Public ReadOnly Property IsUndoing As Boolean
Get
Return Me.isUndoingB
End Get
End Property
Private isUndoingB As Boolean
Public ReadOnly Property IsRedoing As Boolean
Get
Return Me.isRedoingB
End Get
End Property
Private isRedoingB As Boolean
Public Sub New(ByVal tb As TextBox)
Me.undoStack = New Stack(Of KeyValuePair(Of UndoRedoTextBoxEvent, Object))
Me.redoStack = New Stack(Of KeyValuePair(Of UndoRedoTextBoxEvent, Object))
Me.controlB = tb
Me.lastText = tb.Text
End Sub
Public Sub Undo()
If (Me.CanUndo) Then
Me.InternalUndoRedo(UndoRedoCommand.Undo)
End If
End Sub
Public Sub Redo()
If (Me.CanRedo) Then
Me.InternalUndoRedo(UndoRedoCommand.Redo)
End If
End Sub
' Undoes or redoues.
Private Sub InternalUndoRedo(ByVal command As UndoRedoCommand)
Dim undoRedoItem As KeyValuePair(Of UndoRedoTextBoxEvent, Object) = Nothing
Dim undoRedoEvent As UndoRedoTextBoxEvent
Dim undoRedoValue As Object = Nothing
Select Case command
Case UndoRedoCommand.Undo
Me.isUndoingB = True
undoRedoItem = Me.undoStack.Pop
Me.AddUndoRedoItem(UndoRedoCommand.Redo, UndoRedoTextBoxEvent.TextChanged, Me.lastText, undoRedoItem.Value)
Case UndoRedoCommand.Redo
Me.isRedoingB = True
undoRedoItem = Me.redoStack.Pop
Me.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.TextChanged, undoRedoItem.Value, Me.lastText)
End Select
undoRedoEvent = undoRedoItem.Key
undoRedoValue = undoRedoItem.Value
Select Case undoRedoEvent
Case UndoRedoTextBoxEvent.TextChanged
Me.controlB.Text = CStr(undoRedoValue)
End Select
Me.isUndoingB = False
Me.isRedoingB = False
End Sub
Private Sub AddUndoRedoItem(ByVal command As UndoRedoCommand, ByVal [event] As UndoRedoTextBoxEvent,
ByVal data As Object, ByVal lastData As Object)
Console.WriteLine()
Console.WriteLine("command :" & command.ToString)
Console.WriteLine("last command:" & lastCommand.ToString)
Console.WriteLine("can undo :" & Me.CanUndo)
Console.WriteLine("can redo :" & Me.CanRedo)
Console.WriteLine("is undoing :" & Me.isUndoingB)
Console.WriteLine("is redoing :" & Me.isRedoingB)
Console.WriteLine("data :" & data.ToString)
Console.WriteLine("last data :" & lastData.ToString)
Dim undoRedoData As Object = Nothing
Me.lastCommand = command
Select Case command
Case UndoRedoCommand.Undo
If (Me.isUndoingB) Then
Exit Select
End If
undoRedoData = lastData
Me.undoStack.Push(New KeyValuePair(Of UndoRedoTextBoxEvent, Object)([event], undoRedoData))
Case UndoRedoCommand.Redo
If (Me.isRedoingB) Then
Exit Select
End If
undoRedoData = lastData
Me.redoStack.Push(New KeyValuePair(Of UndoRedoTextBoxEvent, Object)([event], undoRedoData))
End Select
End Sub
Private Sub TextBox_TextChanged(ByVal sender As Object, ByVal e As EventArgs) _
Handles controlB.TextChanged
Dim currentText As String = Me.controlB.Text
If Not String.Equals(Me.lastText, currentText, StringComparison.Ordinal) Then
Select Case Me.lastCommand
Case UndoRedoCommand.Undo
Me.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.TextChanged, currentText, Me.lastText)
Case UndoRedoCommand.Redo
Me.AddUndoRedoItem(UndoRedoCommand.Redo, UndoRedoTextBoxEvent.TextChanged, Me.lastText, currentText)
End Select
Me.lastText = currentText
End If
End Sub
End Class
Thanks to @Plutonix I solved it, I really didn't believe that a simple comment could help me to solve logic, but yes, I was making things more complicated than they really are.
I still need to think about how to manage disposable objects, but more or less the base of the idea is finished and the code below is working as expected (at least for what I expect).
These are the parts of the base undo/redo class for controls:
Public Enum UndoRedoCommand As Integer
Undo
Redo
End Enum
Public Class UndoRedoItem
Public Property [Event] As Integer
Public Property LastValue As Object
Public Property CurrentValue As Object
End Class
Public MustInherit Class UndoRedo(Of T As Control)
#Region " Private Fields "
Private ReadOnly undoStack As Stack(Of UndoRedoItem)
Private ReadOnly redoStack As Stack(Of UndoRedoItem)
#End Region
#Region " Properties "
Public ReadOnly Property Control As T
Get
Return Me.controlB
End Get
End Property
Protected WithEvents controlB As T
Public ReadOnly Property CanUndo As Boolean
Get
Return (Me.undoStack.Count <> 0)
End Get
End Property
Public ReadOnly Property CanRedo As Boolean
Get
Return (Me.redoStack.Count <> 0)
End Get
End Property
Public ReadOnly Property IsUndoing As Boolean
Get
Return Me.isUndoingB
End Get
End Property
Private isUndoingB As Boolean
Public ReadOnly Property IsRedoing As Boolean
Get
Return Me.isRedoingB
End Get
End Property
Private isRedoingB As Boolean
#End Region
#Region " Constructors "
Private Sub New()
End Sub
Public Sub New(ByVal ctrl As T)
Me.undoStack = New Stack(Of UndoRedoItem)
Me.redoStack = New Stack(Of UndoRedoItem)
Me.controlB = ctrl
End Sub
#End Region
#Region " Public Methods "
Public Sub Undo()
If (Me.CanUndo) Then
Me.InternalUndoRedo(UndoRedoCommand.Undo)
End If
End Sub
Public Sub Redo()
If (Me.CanRedo) Then
Me.InternalUndoRedo(UndoRedoCommand.Redo)
End If
End Sub
#End Region
#Region " Private Methods "
Private Sub InternalUndoRedo(ByVal command As UndoRedoCommand)
Dim undoRedoItem As UndoRedoItem = Nothing
Select Case command
Case UndoRedoCommand.Undo
Me.isUndoingB = True
undoRedoItem = Me.undoStack.Pop
Me.AddUndoRedoItem(UndoRedoCommand.Redo, undoRedoItem.Event, undoRedoItem.LastValue, undoRedoItem.CurrentValue)
Case UndoRedoCommand.Redo
Me.isRedoingB = True
undoRedoItem = Me.redoStack.Pop
End Select
Me.DoUndo(undoRedoItem.Event, undoRedoItem.CurrentValue)
Me.isUndoingB = False
Me.isRedoingB = False
End Sub
Protected MustOverride Sub DoUndo(ByVal [event] As Integer, ByVal data As Object)
Protected Sub AddUndoRedoItem(ByVal command As UndoRedoCommand,
ByVal [event] As Integer,
ByVal currentData As Object,
ByVal lastData As Object)
Dim undoRedoItem As New UndoRedoItem
undoRedoItem.Event = [event]
Select Case command
Case UndoRedoCommand.Undo
If (Me.isUndoingB) Then
Exit Select
End If
If (Me.CanUndo) AndAlso (Me.CanRedo) AndAlso Not (Me.IsRedoing) Then
Me.redoStack.Clear()
End If
undoRedoItem.CurrentValue = lastData
undoRedoItem.LastValue = currentData
Me.undoStack.Push(undoRedoItem)
Case UndoRedoCommand.Redo
If (Me.isRedoingB) Then
Exit Select
End If
undoRedoItem.CurrentValue = currentData
undoRedoItem.LastValue = lastData
Me.redoStack.Push(undoRedoItem)
End Select
End Sub
#End Region
End Class
And this the implementation for a undo/redo on a textbox:
Public Enum UndoRedoTextBoxEvent As Integer
TextChanged
FontChanged
BackColorChanged
ForeColorChanged
End Enum
Public NotInheritable Class UndoRedoTextBox : Inherits UndoRedo(Of TextBox)
Private lastText As String
Private lastFont As Font
Private lastBackColor As Color
Private lastForeColor As Color
Public Sub New(ByVal tb As TextBox)
MyBase.New(tb)
End Sub
Protected Overrides Sub DoUndo([event] As Integer, data As Object)
Select Case DirectCast([event], UndoRedoTextBoxEvent)
Case UndoRedoTextBoxEvent.TextChanged
MyBase.controlB.Text = CStr(data)
Case UndoRedoTextBoxEvent.FontChanged
MyBase.controlB.Font = DirectCast(data, Font)
Case UndoRedoTextBoxEvent.BackColorChanged
MyBase.controlB.BackColor = DirectCast(data, Color)
Case UndoRedoTextBoxEvent.ForeColorChanged
MyBase.controlB.ForeColor = DirectCast(data, Color)
End Select
End Sub
Private Sub TextBox_TextChanged(ByVal sender As Object, ByVal e As EventArgs) _
Handles controlB.TextChanged
Dim currentText As String = MyBase.controlB.Text
If Not String.Equals(Me.lastText, currentText, StringComparison.Ordinal) Then
MyBase.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.TextChanged, currentText, Me.lastText)
Me.lastText = currentText
End If
End Sub
Private Sub TextBox_FontChanged(sender As Object, e As EventArgs) _
Handles controlB.FontChanged
Dim currentFont As Font = MyBase.controlB.Font
If (Me.lastFont IsNot currentFont) Then
MyBase.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.FontChanged, currentFont, Me.lastFont)
Me.lastFont = currentFont
End If
End Sub
Private Sub TextBox_BackColorChanged(sender As Object, e As EventArgs) _
Handles controlB.BackColorChanged
Dim currentBackColor As Color = MyBase.controlB.BackColor
If (Me.lastBackColor <> currentBackColor) Then
MyBase.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.BackColorChanged, currentBackColor, Me.lastBackColor)
Me.lastBackColor = currentBackColor
End If
End Sub
Private Sub TextBox_ForeColorChanged(sender As Object, e As EventArgs) _
Handles controlB.ForeColorChanged
Dim currentForeColor As Color = MyBase.controlB.ForeColor
If (Me.lastForeColor <> currentForeColor) Then
MyBase.AddUndoRedoItem(UndoRedoCommand.Undo, UndoRedoTextBoxEvent.ForeColorChanged, currentForeColor, Me.lastForeColor)
Me.lastForeColor = currentForeColor
End If
End Sub
End Class