Undo/Redo Capable TextBox (winforms)

This is a continuation of my previous articles:

If you haven’t read the first part, I recommend you read that before continuing further, since we will be using the class we built there to our use in this demo.

In the first part we saw how to construct a class that can support undo/redo capabilities. In this article we will demonstrate its use by using a textbox. The normal windows forms TextBox has only one level of undo/redo capability which is no good for most situations. So let’s see what it would take to add proper undo/redo capabilities to the TextBox control.

The links to full source code and a demo executable can be found at the end of this article.

Building the Basic Setup

1. Start Visual Studio and create a new Windows Forms application. A form (Form1) will be added by default.

2. Add a new class to the project and name it TextBoxEx.

3. Modify the code as follows so that it inherits from the TextBox

Public Class TextBoxEx
    Inherits TextBox

End Class

We will add more code to it as we move further, but let’s first setup our project.

4. Add a new class named UndoRedoClass and add the code from our first part of the article into it.

5. Compile the project.

6. You should see an Item named TextBoxEx in your toolbox. Drag it to your Form and set its Multiline property to True and Scrollbars to Vertical.

7. Add two Buttons to the Form. Name them UndoButton and RedoButton respectively, and set their Text property to “Undo” and “Redo”.

8. Run the project to make sure that everything is OK. It should behave like a normal TextBox.

undo redo capable textbox

This completes our basic rig. We now have the class TextBoxEx we will build with Undo/Redo capabilities, and a form Form1 to test our code.

The TextBoxEx Class

Modify the code in the TextBoxEx class as follows:

Option Strict On

Public Class TextBoxEx
    Inherits TextBox

#Region "Enums, Structures etc."
    Private Structure RestorableItem
        Public Property EditType As EditType
        Public Property Position As Integer
        Public Property Text As String
        Public Overrides Function ToString() As String
            Return "Position: " & Me.Position & " Action: " & Me.EditType.ToString & " Text: " & Me.Text
        End Function
    End Structure

    Private Enum EditType
        None
        Inserted
        Deleted
        BackSpace
    End Enum

    Private Enum WindowMessages
        WM_LBUTTONDOWN = &H201
        WM_RBUTTONDOWN = &H204
        WM_MBUTTONDOWN = &H207
        WM_KEYDOWN = &H100
        WM_KEYUP = &H101
    End Enum
#End Region

    Dim WithEvents UndoRedoHandler As UndoRedoClass(Of RestorableItem)
    Dim IsTyping As Boolean

    Public Shadows ReadOnly Property CanUndo() As Boolean
        Get
            Return UndoRedoHandler.CanUndo
        End Get
    End Property

    Public ReadOnly Property CanRedo() As Boolean
        Get
            Return UndoRedoHandler.CanRedo
        End Get
    End Property

    Public Sub New()
        UndoRedoHandler = New UndoRedoClass(Of RestorableItem)
    End Sub

    Public Shadows Sub Undo()
        UpdateLastRestorableItem()
        UndoRestorableItem(UndoRedoHandler.CurrentItem)
        UndoRedoHandler.Undo()
    End Sub

    Public Shadows Sub Redo()
        UndoRedoHandler.Redo()
        RedoRestorableItem(UndoRedoHandler.CurrentItem)
    End Sub

    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        Select Case m.Msg
            Case WindowMessages.WM_LBUTTONDOWN, WindowMessages.WM_RBUTTONDOWN, WindowMessages.WM_MBUTTONDOWN
                UpdateLastRestorableItem()

            Case WindowMessages.WM_KEYDOWN
                Dim keyCode As Keys = CType(m.WParam, Keys) And Keys.KeyCode
                Select Case keyCode
                    Case Keys.Up, Keys.Down, Keys.Left, Keys.Right, Keys.PageUp, Keys.PageDown, Keys.Home, Keys.End, Keys.Control, Keys.Alt, Keys.Escape, Keys.F1 To Keys.F12
                        UpdateLastRestorableItem()

                    Case Keys.Enter
                        AddRestorableItem(EditType.Inserted, Me.SelectionStart, vbCrLf)
                        IsTyping = True

                        '' Uncomment the following lines to save at words instead of sentences.
                        'Case Keys.Space
                        '    AddRestorableItem(EditType.Inserted, Me.SelectionStart, Space(1))
                        '    IsTyping = True

                    Case Keys.Back
                        If Me.SelectionLength = 0 Then
                            If Me.SelectionStart > 0 Then
                                AddRestorableItem(EditType.BackSpace, Me.SelectionStart - 1, Me.Text.Substring(Me.SelectionStart - 1, 1))
                            End If
                        Else
                            AddRestorableItem(EditType.Deleted, Me.SelectionStart, Me.SelectedText)
                        End If

                    Case Keys.Delete
                        If Me.SelectionLength = 0 Then
                            If Me.SelectionStart < Me.TextLength Then
                                AddRestorableItem(EditType.Deleted, Me.SelectionStart, Me.Text.Substring(Me.SelectionStart, 1))
                            End If
                        Else
                            AddRestorableItem(EditType.Deleted, Me.SelectionStart, Me.SelectedText)
                        End If

                    Case Else
                        If Not IsTyping Then
                            If Me.SelectionLength > 0 Then      '' overtyping?
                                AddRestorableItem(EditType.Deleted, Me.SelectionStart, Me.SelectedText)
                            End If
                            AddRestorableItem(EditType.Inserted, Me.SelectionStart, "")
                            IsTyping = True
                        End If
                End Select
        End Select

        MyBase.WndProc(m)
    End Sub

    Private Sub AddRestorableItem(editType As EditType, position As Integer, text As String)
        UpdateLastRestorableItem()
        With UndoRedoHandler
            If .CurrentItem.EditType = editType.Inserted AndAlso String.IsNullOrEmpty(.CurrentItem.Text) Then
                'reuse the current item
            Else
                UndoRedoHandler.AddItem(New RestorableItem)
            End If
            .CurrentItem.EditType = editType
            .CurrentItem.Position = position
            .CurrentItem.Text = text
        End With
    End Sub

    Private Sub UpdateLastRestorableItem()
        If IsTyping Then
            With UndoRedoHandler.CurrentItem
                If .EditType = EditType.Inserted Then
                    .Text = Me.Text.Substring(.Position, Me.SelectionStart - .Position)
                End If
                IsTyping = False
            End With
        End If
    End Sub

    Private Sub UndoRestorableItem(ByVal restorableItem As RestorableItem)
        'NOTE: we need to do reverse of what is saved in the RestorableItem
        With restorableItem
            Me.SelectionStart = .Position
            Select Case .EditType
                Case EditType.Inserted
                    Me.SelectionLength = .Text.Length
                    Me.SelectedText = ""
                Case EditType.BackSpace
                    Me.SelectedText = .Text
                Case EditType.Deleted
                    Me.SelectedText = .Text
            End Select
        End With
    End Sub

    Private Sub RedoRestorableItem(ByVal restorableItem As RestorableItem)
        With restorableItem
            Me.SelectionStart = .Position
            Me.SelectionLength = 0
            Select Case .EditType
                Case EditType.Inserted
                    Me.SelectedText = .Text
                Case EditType.BackSpace
                    Me.SelectionLength = 1
                    Me.SelectedText = ""
                Case EditType.Deleted
                    Me.SelectionLength = .Text.Length
                    Me.SelectedText = ""
            End Select
        End With
    End Sub
End Class

The Test Form

We complete our test form by adding the following code:

Option Strict On

Public Class Form1
    Private Sub UndoButton_Click(sender As System.Object, e As System.EventArgs) Handles UndoButton.Click
        TextBoxEx1.Undo()
        TextBoxEx1.Select()
        EnableOrDisableControls()
    End Sub

    Private Sub RedoButton_Click(sender As System.Object, e As System.EventArgs) Handles RedoButton.Click
        TextBoxEx1.Redo()
        TextBoxEx1.Select()
        EnableOrDisableControls()
    End Sub

    Private Sub TextBoxEx1_TextChanged(sender As System.Object, e As System.EventArgs) Handles TextBoxEx1.TextChanged
        EnableOrDisableControls()
    End Sub

    Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
        EnableOrDisableControls()
    End Sub

    Private Sub EnableOrDisableControls()
        UndoButton.Enabled = TextBoxEx1.CanUndo
        RedoButton.Enabled = TextBoxEx1.CanRedo
    End Sub
End Class

Run the project. Type something in the TextBox. Click the Undo/Redo buttons to see how they go.

Enjoy!Smile

Explanation of the Code

Our UndoRedoClass already has basic support we need to perform undo/redo. We add support for handling key presses and mouse clicks in our TextBoxEx.

The Basic Strategy

We keep small edits in a class/structure as the user is typing into our textbox. When told to undo, we just undo the last edit we know to the current document. For this we need to do the reverse of what the user did. e.g. If they typed something, we need to remove that; if they deleted something we need to insert that.

Similarly, for redo, we reapply whatever is stored in our last edit.

All we need to know for this is the type of edit (insert/delete), the position where the edit was made, and the edited text. (To keep our class simple, we consider an “overtype” operation as two operations – a delete and an insert. However, you may modify the class to add support for overtyping easily.)

For this we constructed a structure in our class, with properties defined to keeps these three things:

    Private Structure RestorableItem
        Public Property EditType As EditType
        Public Property Position As Integer
        Public Property Text As String
        Public Overrides Function ToString() As String
            Return "Position: " & Me.Position & " Action: " & Me.EditType.ToString & " Text: " & Me.Text
        End Function
    End Structure

Overriding the WndProc Method to Capture Keys of Our Interest

We override the WndProc method to capture keys of our interest.

1. The Navigation Keys – We need to capture these keys so that we know when the user has finished typing. The list of such keys is not exhaustive, so this is easy. So basically Up, Down, Left, Right, Page Up, Page Down, Home, End etc. etc.

2. The Separator Keys – These keys determine when to start a new RestorableItem object. I have used the Enter key only in the demo code, but you can add more like SpaceBar (to save at words) etc.

3. The Backspace Key – We need to handle this key to know when something was deleted from the textbox.

4. The Delete Key – We need to handle this key to know when something was deleted from the textbox.

5. The Mouse Clicks – When the user clicks the mouse anywhere, the cursor position will be lost. We need to save our RestorableItem before that.

Creating RestorableItem Objects

The RestorableItems are created whenever the user types something or deletes something or otherwise moves out of the current cursor position in any way.

The following method does that:

    Private Sub AddRestorableItem(editType As EditType, position As Integer, text As String)
        UpdateLastRestorableItem()
        With UndoRedoHandler
            If .CurrentItem.EditType = editType.Inserted AndAlso String.IsNullOrEmpty(.CurrentItem.Text) Then
                'reuse the current item
            Else
                UndoRedoHandler.AddItem(New RestorableItem)
            End If
            .CurrentItem.EditType = editType
            .CurrentItem.Position = position
            .CurrentItem.Text = text
        End With
    End Sub

Saving RestorableItems

We save our RestorableItems when the user has finished typing. It’s no use creating and saving at every key press. So we wait until we notice a change or the user tries to navigate to a different position inside the textbox. Delete operations are saved on the fly, while insert (typing) is saved when the user press Enter or any other predefined separator key.

Performing Undo/Redo

The following two methods are actually the methods that initiate an undo/redo operation. We call these from our Form when the undo/redo buttons are clicked:

    Public Shadows Sub Undo()
        UpdateLastRestorableItem()
        UndoRestorableItem(UndoRedoHandler.CurrentItem)
        UndoRedoHandler.Undo()
    End Sub

    Public Shadows Sub Redo()
        UndoRedoHandler.Redo()
        RedoRestorableItem(UndoRedoHandler.CurrentItem)
    End Sub

Notice that in case of Undo, we first perform the Undo operation, and then call the Undo on our UndoRedoHandler object. We need to do this because the CurrentItem is not in the undo stack. So we need to undo whatever changes are present in the CurrentItem and then call the Undo method on the UndoRedoHandler object.

The methods that perform the actual undo/redo go as follows:

    Private Sub UndoRestorableItem(ByVal restorableItem As RestorableItem)
        'NOTE: we need to do reverse of what is saved in the RestorableItem
        With restorableItem
            Me.SelectionStart = .Position
            Select Case .EditType
                Case EditType.Inserted
                    Me.SelectionLength = .Text.Length
                    Me.SelectedText = ""
                Case EditType.BackSpace
                    Me.SelectedText = .Text
                Case EditType.Deleted
                    Me.SelectedText = .Text
            End Select
        End With
    End Sub

    Private Sub RedoRestorableItem(ByVal restorableItem As RestorableItem)
        With restorableItem
            Me.SelectionStart = .Position
            Me.SelectionLength = 0
            Select Case .EditType
                Case EditType.Inserted
                    Me.SelectedText = .Text
                Case EditType.BackSpace
                    Me.SelectionLength = 1
                    Me.SelectedText = ""
                Case EditType.Deleted
                    Me.SelectionLength = .Text.Length
                    Me.SelectedText = ""
            End Select
        End With
    End Sub

The basic strategy is that we do the opposite of whatever is stored in our RestorableItem when undoing a change, while we do the exact same thing whatever is in our RestorableItem when redoing any change.

Disclaimer

This code was intended only for knowledge sharing and to be used for the purpose of this demo only. It is in no way complete for use in business applications (in its present state). Many situations are not handled like copy/paste etc. So if you intend to use it for business applications, make sure that you have made appropriate changes and tested it fully.

Source Code and Executable File

Advertisements

7 Responses to “Undo/Redo Capable TextBox (winforms)”

  1. Add Undo/Redo or Back/Forward Functionality to your Application « Pradeep1210's Blog Says:

    […] Undo/Redo Capable TextBox – This demo shows how to add undo/redo capabilities to the windows forms TextBox control. […]

  2. goomingi Says:

    It is so good! thank you about your article very much! ^^

  3. website Says:

    An cool post there mate ! Cheers for that .

  4. Dan Says:

    Thanks! This is great!

  5. Jason Says:

    I broke it. It gives me an error on this line if I type and undo really fast in quick succession:
    .Text = Me.Text.Substring(.Position, Me.SelectionStart – .Position)

  6. resnefimmatong9er Says:

    I got errors on the #Region part..all are undefined…Why? I am using VB 2008…


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: