How to implement good and efficient undo/redo functionality for a TextBox
Asked Answered
O

8

32

I have a TextBox which I would like to implement undo/redo functionality for. I have read that it might have some slight undo functionality already, but that it is buggy? Anyways, I would like to implement both undo and redo functionality also just to learn how you would go ahead and do that.

I have read about the Memento Pattern and looked some on a Generic Undo/Redo example on CodeProject. And the pattern kiiind of makes sense. I just can't seem to wrap my head around how to implement it. And how to do it effeciently for the contents of a TextBox.

Of course I could just store textbox.Text when TextChanges, but that would hug up quite a lot of memory pretty fast, especially if the TextBox contained a lot of text.

So anyways, I'm looking for some advice on how to implement a good, clear and efficient way of implementing this functionality. Both in general and especially for a TextBox c",)

Ottavia answered 28/2, 2009 at 9:35 Comment(0)
J
18

The .NET System.ComponentModel namespace comes with an IEditableObject interface, you could also use INotifyPropertyChanging and INotifyPropertyChanged. MVC Pattern would also make it that your interface responds to changes in the model through events thus updating or restoring the value of your textbox.

Effectively the Memento Pattern.

Have you had a look into these? Here is a how to.

A simple and quicker version would be to store the state of the textbox OnTextChanged. Each undo would return the last event in an Array. The C# Stack Type would be handy here. You could clear the state once you are off the interface also or after Apply.

Jordanna answered 28/2, 2009 at 9:41 Comment(2)
so, would you make an extended version of a textbox then? implementing that interface?Ottavia
How many levels of undo would you require n levels or 1 ? Also would the undo behaviour be on an object or solely on the data of the textbox?Jordanna
A
9

Here's a way to achieve it with minimal code: (This is the code behind of a win form with a single textbox on it)

public partial class Form1 : Form
{
    Stack<Func<object>> undoStack = new Stack<Func<object>>(); 
    public Form1()
    {
        InitializeComponent();
    }
    private void textBox_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.U && Control.ModifierKeys == Keys.Control && undoStack.Count > 0)
            undoStack.Pop()();            
    }
    private void textBox_KeyPress(object sender, KeyPressEventArgs e)
    {            
        if (e.KeyChar != 'u' || Control.ModifierKeys != Keys.Control)
        {
            var textBox = (TextBox)sender;
            undoStack.Push(textBox.Text(textBox.Text));
        }
    }
}
public static class Extensions
{
    public static Func<TextBox> Text(this TextBox textBox, string text)
    {            
        return () => { textBox.Text = text; return textBox; };
    }
}

By implementing an extension method for other input types the undoStack can service the whole of your UI, undoing all UI actions in order.

Apologetic answered 14/9, 2010 at 2:57 Comment(3)
Why use a stack of functions instead of a stack of strings?Renata
@Jack Read the last sentence again. This code can be extended to allow you to undo any change in any control, across the whole form, in reverse order.Scofield
@Apologetic - Are you not saving the entire text into stack each time - thereby making this terribly inefficient?Samuel
D
3

I need to reset the selection, too, into its original positions when undoing / redoing. Watch "class Extensions", at the bottom of my just basic and well working code, for a form with just one textbox "textBox1" to try:

public partial class Form1 : Form
{
    Stack<Func<object>> undoStack = new Stack<Func<object>>();
    Stack<Func<object>> redoStack = new Stack<Func<object>>();

    public Form1()
    {
        InitializeComponent();
        textBox1.KeyDown += TextBox1_KeyDown;
    }

    private void TextBox1_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.KeyCode == Keys.ControlKey && ModifierKeys == Keys.Control) { }
        else if (e.KeyCode == Keys.U && ModifierKeys == Keys.Control)
        {
            if(undoStack.Count > 0)
            {
                StackPush(sender, redoStack);
                undoStack.Pop()();
            }
        }
        else if (e.KeyCode == Keys.R && ModifierKeys == Keys.Control)
        {
            if(redoStack.Count > 0)
            {
                StackPush(sender, undoStack);
                redoStack.Pop()();
            }
        }
        else
        {
            redoStack.Clear();
            StackPush(sender, undoStack);
        }
    }

    private void StackPush(object sender, Stack<Func<object>> stack)
    {
        TextBox textBox = (TextBox)sender;
        var tBT = textBox.Text(textBox.Text, textBox.SelectionStart);
        stack.Push(tBT);
    }
}

public static class Extensions
{
    public static Func<TextBox> Text(this TextBox textBox, string text, int sel)
    {
        return () => 
        {
            textBox.Text = text;
            textBox.SelectionStart = sel;
            return textBox;
        };
    }
}
Decipher answered 24/8, 2016 at 21:9 Comment(0)
R
2

A good solution can be found here:

Add Undo/Redo or Back/Forward Functionality to your Application

Undo/Redo Capable TextBox (winforms)

The code is in VB.NET, but you can easily convert it to C# without much efforts. Online converters are also available.

Rafflesia answered 20/4, 2012 at 11:56 Comment(1)
Thanks, this simple solution helped me a lotSupportable
R
2

This is the most helpful page I found on the topic, more generic, suitable for different object types on the undo/redo stack.

Command Pattern

When I got to implementing it, I was surprised how simple and elegant it ended up being. That makes it a win for me.

Residency answered 13/11, 2012 at 7:22 Comment(1)
I like this. I implemented just now from scratch Undo/Redo with the Command Pattern. Surprisingly or not I end up with a very similiar approach as has been implemented in the provide link.Albright
E
0

I would listen for a change event, and when it occurs push the diff of the previous state and present state onto a stack. The diffs should be much smaller than storing the entire text. Also, you might not want to push a new undo state onto the stack at every edit... I'd lump all typing together until the user changes the cursor position, for example.

Earleenearlene answered 14/9, 2010 at 3:39 Comment(0)
A
0

The smartest way is with immutable persistent objects. Never make a change to an object only make new objects that change slightly from the old version. This can be done somewhat efficiently by only cloning parts of the tree on the hot path.

I have an example of an undo stack written with minimal code

 [Fact]
public void UndoStackSpec()
{
    var stack = new UndoStack<A>(new A(10, null));

    stack.Current().B.Should().Be(null);

    stack.Set(x => x.B, new B(20, null));

    stack.Current().B.Should().NotBe(null);
    stack.Current().B.P.Should().Be(20);

    stack.Undo();

    stack.Current().B.Should().Be(null);

}

where A and B as classes with private setters on all properties ie immutable

class A : Immutable
{
    public int P { get; private set; }
    public B B { get; private set; }
    public A(int p, B b)
    {
        P = p;
        B = b;
    }
}

class B : Immutable
{
    public int P { get; private set; }
    public C C { get; private set; }
    public B(int p, C c)
    {
        P = p;
        C = c;
    }
}

class C : Immutable
{
    public int P { get; private set; }
    public C(int p)
    {
        P = p;
    }
}

you can find the full source here https://gist.github.com/bradphelan/5395652

Abdu answered 16/4, 2013 at 13:19 Comment(3)
Is this the obfuscated version? Perhaps you could update your answer to provide examples with something slightly more verbose than single-letter variable names?Scofield
The variable names are irrelevant. They have no meaning other than they are being set and then undo is called and it is shown they are unset. The implementation is in the gist and gives all the source code you need to understand it fully.Abdu
The only time variable names are irrelevant is when the variables themselves are irrelevant, in which case they should be removed altogether. If the variables serve any purpose whatsoever, even if that purpose is limited to an arbitrary example class, they should be named in such a manner as to represent something tangible to ease the reader's consumption of the real ideas being presented by the code. The reason example code so often contains e.g. "class Car" or "class BankAccount" isn't because those things are important, but because they help the reader grasp the underlying concepts.Scofield
H
0

This is an Undo/Redo implementation following the KISS principle. You need a List of Actions with a cursor that points to the current action. You add actions to the list every time user do something, increase the cursor and remove actions that are after the cursor in the list, if the user press Undo, decrease the cursor, and if pressed Redo increase the cursor. Here's the pseudo-code.

public class UndoFeature
{
    public interface IRecoverableAction
    {
        void Execute();
        void Rollback();
    }

    private List<IRecoverableAction> actions;
    private int cursor { get; set; }

    void DoAction(IRecoverableAction action)
    {
        actions.Add(action);
        cursor++;
        action.Execute();
        //Removes items after cursor
        actions.RemoveRange(cursor, actions.Count - cursor);
    }

    void Undo()
    {
        actions[cursor].Rollback();
        cursor--;
    }

    void Redo()
    {
        actions[cursor].Execute();
        cursor++;
    }
}

Note: Range checking and other consideration not included in the code to keep it simple and easy to understand for everyone;

Hydroxyl answered 13/7, 2023 at 7:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.