3

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
Community
  • 1
  • 1
ElektroStudios
  • 19,105
  • 33
  • 200
  • 417
  • How will that handle more than one textbox? Will you have one of these for each text control? – Ňɏssa Pøngjǣrdenlarp Dec 24 '15 at 21:08
  • @Plutonix Yes I will have one instance per each TextBox, the class is not intended to handle more than one control like some kind of "undo/redo manager" that can handle a sort of different controls, not. – ElektroStudios Dec 25 '15 at 00:12
  • @Plutonix Your last comments hours ago were useful, could you please re-write at least the most important part? (I hope I don't forget anything from what you said in those comments). I cannot follow the sugerences that you did me before right now but I will do it once I can get into VS, and what you said seems pretty logic about adding the redo item only when undoing and do not intent anything more with the redo stack but I think I can't having it at all clear until I try it tomorrow. Thanks and Happy Christmas. – ElektroStudios Dec 25 '15 at 00:15
  • It looks like you are just trying to make the Undo pool for each TB "deeper"? Because one stack per control is not going to act like a normal document Undo. You wont know which stack has the "next" item so *most* pop/Undo will result in a document state that never was or is nonsensical: *Genie in a Bottle*; 1958; (by) Pink Floyd – Ňɏssa Pøngjǣrdenlarp Dec 25 '15 at 01:47
  • @Plutonix I'm not sure if totally understood your last comment, why it should not work?, If one class instance is handling one control with their undo/redo stacks then it will work as expected, seems that you are talking about one class with stacks for various controls, but as I said I will have one class per control, not any kind of undo/redo manager for various controls. – ElektroStudios Dec 26 '15 at 07:29
  • @Plutonix I solved it thanks to your firstlly removed comments (damn I was so ofuscated!, luckily I read you), you saved my day and I will publish my solution later, anyways, which is your GitHub?, its high time to promote it here in this way hehe, I've found one "Plutonix" but not sure if its you bcause doesn't has any repository. – ElektroStudios Dec 26 '15 at 07:34
  • `why it should not work?` If you have more than one TB using that, they are operating independently which means you can Undo each by varying amounts to result in nonsense combinations. The history for each TB would go back farther but be less useful. You also need a clear method for when a new "document" is started so data doesnt 'bleed' over – Ňɏssa Pøngjǣrdenlarp Dec 26 '15 at 15:06

1 Answers1

0

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
ElektroStudios
  • 19,105
  • 33
  • 200
  • 417