tags:

views:

224

answers:

4

I have an event handler for the TextBox.TextChanged event on a form of mine. In order to support undo, I'd like to figure out exactly what has changed in the TextBox, so that I can undo the change if the user asks for it. (I know the builtin textbox supports undo, but I'd like to have a single undo stack for the whole application)

Is there a reasonable way to do that? If not, is there a better way of supporting such an undo feature?

EDIT: Something like the following seems to work -- are there any better ideas? (It's times like this that I really wish .NET had something like the STL's std::mismatch algorithm...

    class TextModification
    {
        private string _OldValue;
        public string OldValue
        {
            get
            {
                return _OldValue;
            }
        }
        private string _NewValue;
        public string NewValue
        {
            get
            {
                return _NewValue;
            }
        }
        private int _Position;
        public int Position
        {
            get
            {
                return _Position;
            }
        }
        public TextModification(string oldValue, string newValue, int position)
        {
            _OldValue = oldValue;
            _NewValue = newValue;
            _Position = position;
        }
        public void RevertTextbox(System.Windows.Forms.TextBox tb)
        {
            tb.Text = tb.Text.Substring(0, Position) + OldValue + tb.Text.Substring(Position + NewValue.Length);
        }
    }

    private Stack<TextModification> changes = new Stack<TextModification>();
    private string OldTBText = "";
    private bool undoing = false;

    private void Undoit()
    {
        if (changes.Count == 0)
            return;
        undoing = true;
        changes.Pop().RevertTextbox(tbFilter);
        OldTBText = tbFilter.Text;
        undoing = false;
    }

    private void UpdateUndoStatus(TextBox caller)
    {
        int changeStartLocation = 0;
        int changeEndTBLocation = caller.Text.Length;
        int changeEndOldLocation = OldTBText.Length;
        while (changeStartLocation < Math.Min(changeEndOldLocation, changeEndTBLocation) &&
            caller.Text[changeStartLocation] == OldTBText[changeStartLocation])
            changeStartLocation++;
        while (changeEndTBLocation > 1 && changeEndOldLocation > 1 &&
            caller.Text[changeEndTBLocation-1] == OldTBText[changeEndOldLocation-1])
        {
            changeEndTBLocation--;
            changeEndOldLocation--;
        }
        changes.Push(new TextModification(
            OldTBText.Substring(changeStartLocation, changeEndOldLocation - changeStartLocation),
            caller.Text.Substring(changeStartLocation, changeEndTBLocation - changeStartLocation),
            changeStartLocation));
        OldTBText = caller.Text;
    }

    private void tbFilter_TextChanged(object sender, EventArgs e)
    {
        if (!undoing)
            UpdateUndoStatus((TextBox)sender);
    }
A: 

Yes, don't tie it directly to the textbox. Your forms' state should be in some model object somewhere that isn't directly tied to the form (MVC is one way to do this, MVVM is another). By decoupling them like that, you can compare the new textbox value to the current model value whenever a change request comes in.

Paul
Yes, I know that. The issue is doing that actual comparison. Considering a TextChanged event can be fired when A. a character is typed, B. a character is erased, or C. a section of text is replaced (i.e. pasting over a hilight), there's no general way of figuring that out. I'd rather avoid heuristics if I can.
Billy ONeal
+5  A: 

You might be better off using the Enter and Leave events instead. When entering, store the current text in a class variable, then when leaving compare the new text to the old.

Chris Persichetti
Hmm.. this might work.
Billy ONeal
You will have problems with enter and leave. There is an antipattern refering to excesive use of events.
Daniel Dolz
@Daniel Dolz: "Excessive use of events"? Two events is hardly "Excessive"
Billy ONeal
ok, good luck.Keep an eye on enter and leave is border conditions, that is, when form is loading or closing. I am not sure if "Leave" event will be invoked.
Daniel Dolz
A: 

Actually, all I can think of is having some kind of collection where you store different string versions (so you can undo many times, not just once). I would store the reference to TextBox's collections in TextBox.Tag, so it is straightforward to store/use it.

Last but not least, you update your collection of strings during the event TextChange. With no much work, you can maintain a full history, gettinjg the previous value from your own structure.

Daniel Dolz
But then every time someone types a character I need to store the entire contents of the textbox. That would be *huge*.
Billy ONeal
Note: Hugeness wouldn't bother me if I didn't have to persist a good chunk of this to disk
Billy ONeal
You can discard the oldest ones...
Daniel Dolz
@Daniel Dolz: Err... no, I cant.
Billy ONeal
A: 

This is probably overkill for what you're trying to accomplish, but CSLA support n-level undo. CSLA is a great business objects framework written by Rocky Lhotka. The business objects handle the undo history and it flows to the UI through data binding.

Switching your app to use CSLA would be a big commitment, but another option would be to look through the freely available source code to see how he implemented it.

whatknott