📜 ⬆️ ⬇️

What else is Undo / Redo capable of?

At first glance it seems that Undo / Redo is not engaged in anything other than rollback and repetition and cannot be engaged. But it is not so.

When implementing XtraRichEdit , the moment came when we needed to make a property that answers the question whether the document was changed or not. How exactly to do it, at first glance, it was quite obvious. It was necessary to create the isModified variable and set it to true when the document was changed. At that moment, when the user saved the document, it was necessary to assign it the value false. Of course, the original value of the variable was also false, which meant that the document was not changed.
Everything was simple and clear, and we set to work.

It quickly became clear that the document can be changed in many different ways. There were more than a hundred buttons that could be placed on the toolbar. And for each case of a document change, it was necessary to remember to prescribe the correct value in isModified.

Of course, we did not have the slightest desire to move in this way. It was clear that a place should be found in the code that was guaranteed to be executed at absolutely any change to the document. And this place was functional Undo / Redo.

Why exactly Undo / Redo? It was obvious. For any change in the document there should be the possibility to roll back this change. So the code that performs the change, sooner or later interacted with the Undo / Redo functionality and registered in the Undo buffer an element with the information necessary to roll back and repeat the action. Therefore, it was there that should have started our isModified variable.

Let me remind our implementation of Undo / Redo from the previous article :

int currentIndex = -1; public bool CanUndo { get { return currentIndex >= 0; } } public bool CanRedo { get { return items.Count > 0 && currentIndex < items.Count - 1; } } public void Undo() { if (!CanUndo) return; items[currentIndex].Undo(document); this.currentIndex--; } public void Redo() { if (!CanRedo) return; this.currentIndex++; items[currentIndex].Redo(document); } public void Add(HistoryItem item) { CutOffHistory(); items.Add(item); this.currentIndex++; } 

So, it was necessary to add here our sign of a change in the document:

 bool isModified; public bool IsModified { get { return isModified; } set { isModified = value; } } public void Add(HistoryItem item) { CutOffHistory(); items.Add(item); this.currentIndex++; this.isModified = true; } 

In the implementation of the Add method, we assign true to our variable, since for any addition of information to the Undo buffer, it is just evidence of a change in the document. In the place where the document is saved, we simply assign false to the IsModified property. Everything?

It turns out that not everything is so simple. Consider the situation where the user enters a single character in an empty document. Initially IsModified == false, since The document has not been changed. When a character is entered, it is written to the Undo buffer, IsModified becomes equal to true. So far, so good. And now we do rollback (Undo). Character input is canceled, but IsModified is still true and indicates that the document has been changed. And this is already wrong.

Well, we make an edit to the Undo method so that the system works correctly in this case:

 public void Undo() { if (!CanUndo) return; items[currentIndex].Undo(document); this.currentIndex--; this.isModified = CanUndo(); } 


And if in the previous scenario, the user after the rollback will produce Redo? It looks like the Redo method will also have to be edited:

 public void Redo() { if (!CanRedo) return; this.currentIndex++; items[currentIndex].Redo(document); this.isModified = CanUndo(); } 

What if…

Let the user enter 3 characters, then save the document. Then enters 2 more characters. We assume that only one entry is made to the undo buffer to enter one character. What will happen if after such input you execute Undo / Redo.

Let's make the table for clarity:

Pay attention to paragraphs 4, 8 and 14. In paragraph 4, the document was retained. A document will be considered unchanged if its state does not differ from the state at the time of the last saving. Paragraphs 8 and 14 correspond to this state.

Unfortunately, if we carefully analyze our implementation of the IsModified property, we find that it will not work correctly for our sequence of actions. However, the compiled table tells us how to make the correct, properly working implementation. Note that in clauses 4, 8, and 14, the CurrentIndex variable takes the same value of 2. That is, IsModified == false then CurrentIndex == 2.

It turns out that our approach using the Boolean variable isModified is fundamentally incorrect. In order for everything to work correctly, we need to save the CurrentIndex value in a separate variable at the moment of saving the document and rewrite the IsModified property as follows:

 const int ForceModifiedIndex = -2; int currentIndex = -1; int unmodifiedIndex = -1; public bool IsModified { get { return currentIndex != unmodifiedIndex; } set { if (value == IsModified) return; if (value) unmodifiedIndex = ForceModifiedIndex; else unmodifiedIndex = currentIndex; } } void CutOffHistory() { int index = currentIndex + 1; while (index < Count) { this[index].Dispose(); items.RemoveAt(index); } if (unmodifiedIndex > currentIndex) unmodifiedIndex = ForceModifiedIndex; } 

The unmodifiedIndex field is initially assigned a value of -1, equal to the value of the currentIndex field. This is done so that the new unchanged document's IsModified property returns false.

We changed the CutOffHistory method so that it assigns the ForceModifiedIndex value to the unmodifiedIndex variable in case the current unmodifiedIndex value is greater than the currentIndex. Why is that? The unmodifiedIndex> currentIndex condition in the CutOffHistory method means that the undo-buffer element corresponding to the last saving of the document appears in the deleted part of the change history. In other words, using Redo will no longer be able to bring the document to the state corresponding to the last save. And why did we assign ForceModifiedIndex == -2 to the unmodifiedIndex variable, why couldn't -1 be used? The fact is that if we assign -1 to this place, then by making Undo “all the way” we will get IsModified == false, and this will be the wrong behavior. We need any value other than -1, for which it is guaranteed that currentIndex cannot accept the same value. We chose -2. However, for clarity of the code, it made sense to start a named constant, which we did.

Now our implementation of IsModified will work correctly for all situations.

This is how it turned out that the Undo buffer is perfect for answering the question of whether changes have been made to the document since the last save.

Previous article on this topic

The very first article on the same topic.

Source: https://habr.com/ru/post/104168/

All Articles