Pull to refresh
0

На что ещё способно Undo/Redo

Reading time 4 min
Views 15K
На первый взгляд кажется, что ничем другим кроме отката и повтора Undo/Redo не занимается и заниматься не может. Но это не совсем так.



При реализации XtraRichEdit настал момент, когда нам надо было сделать свойство, которое отвечает на вопрос, изменён документ или нет. Как именно его делать, на первый взгляд было вполне очевидно. Надо было завести переменную isModified и выставлять ей значение true, когда документ изменялся. В тот момент, когда пользователь сохранял документ, надо было присвоить ей значение false. Разумеется, изначальное значение переменной также было false, что означало, что документ не изменён.

Всё было просто и понятно и мы принялись за дело.

Довольно быстро выяснилось, что документ может изменяться множеством разных способов. Одних кнопок, которые можно было разместить на панели инструментов, насчитывалось более сотни. И для каждого случая изменения документа надо было не забыть прописать правильное значение в isModified.

Конечно же двигаться таким путём у нас не было ни малейшего желания. Было ясно, что следует найти такое место в коде, которое гарантированно исполнялось при абсолютно любом изменении документа. И этим местом оказался функционал Undo/Redo.

Почему же именно Undo/Redo? Это было очевидно. Для любого изменения в документе должна быть возможность отката этого изменения. Значит код, выполняющий изменение, рано или поздно взаимодействовал с функционалом Undo/Redo и регистрировал в буфере Undo элемент с информацией, необходимой для отката и повтора действия. Поэтому именно там следовало завести нашу переменную isModified.

Напомню нашу реализацию Undo/Redo из предыдущей статьи:

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++;
}

Итак, надо было добавить сюда наш признак изменения в документе:

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;
}

В реализации метода Add мы присваиваем true в нашу переменную, т.к. при любом добавление информации в буфер Undo как раз и свидетельствует об изменении докумета. В том месте, где происходит сохранение документа, мы просто присваиваем false в свойство IsModified. Всё?

Оказывается, не всё так просто. Рассмотрим ситуацию, когда пользователь вводит один символ в пустой документ. Изначально IsModified == false, т.к. документ не изменён. При вводе символа происходит запись в буфер Undo, IsModified становится равным true. Пока всё хорошо. А теперь делаем откат (Undo). Ввод символа отменяется, но IsModified продолжает быть равным true и указывать на то, что документ изменён. А это уже неверно.

Ну что ж, вносим правку в метод Undo, чтобы система работала правильно и в этом случае:

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

Работает.

А если в предыдущем сценарии пользователь после отката произведёт Redo? Похоже ещё и метод Redo придётся править:

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

А если…

Пусть пользователь вводит 3 символа, потом сохраняет документ. Затем вводит еще 2 символа. Полагаем, что на ввод одного символа делается одна запись в буфер undo. Что будет, если после такого ввода выполнить Undo/Redo.

Составим для наглядности таблицу:



Обратим внимание на пункты 4, 8 и 14. В пункте 4 документ был сохранён. Документ будет считаться неизменённым, если его состояние не отличается от состояния на момент последнего сохранения. Этому состоянию как раз соответствуют пункты 8 и 14.

К сожалению, если мы внимательно проанализируем нашу реализацию свойства IsModified, то обнаружим, что для нашей последовательности действий она будет работать неправильно. Впрочем составленная таблица подсказывает нам, как сделать правильную правильно работающую реализацию. Обратите внимание, что в пунктах 4, 8 и 14 переменная CurrentIndex принимает одно и то же значение 2. Т.е. IsModified == false тогда, когда CurrentIndex == 2.

Получается, что наш подход с использованием булевской переменной isModified в корне неверен. Для того, чтобы всё работало правильно, нам необходимо в момент сохранения документа запоминать значение CurrentIndex в отдельной переменной и переписать свойство IsModified следующим образом:

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;
}

Полю unmodifiedIndex изначально присвоено значение -1, равное значению поля currentIndex. Это сделано для того, чтобы у нового неизменённого документа свойство IsModified возвращало false.

Метод CutOffHistory мы изменили таким образом, чтобы он присваивал значение ForceModifiedIndex переменной unmodifiedIndex в случае если текущее значение unmodifiedIndex больше currentIndex. Почему так? Условие unmodifiedIndex > currentIndex в методе CutOffHistory означает, что элемент undo-буфера, соответствующий последнему сохранению документа, оказывается в удаляемой части истории изменений. Иными словами, при помощи Redo будет более невозможно привести документ в состояние, соответствующее последнему сохранению. А почему мы присвоили ForceModifiedIndex == -2 в переменную unmodifiedIndex, почему нельзя было использовать -1? Дело в том, что если мы присвоим -1 в этом месте, то сделав Undo «до упора» мы получим IsModified == false, а это будет неверным поведением. Нам необходимо любое значение кроме -1, для которого гарантируется, что currentIndex не сможет принять такое же значение. Мы выбрали -2. Впрочем для наглядности кода имело смысл завести именованную константу, что мы и сделали.

Теперь наша реализация IsModified будет работать правильно для любых ситуаций.

Вот так оказалось, что буфер Undo прекрасно подходит для того, чтобы дать ответ на вопрос, были ли внесены изменения в документ с момента последнего сохранения.

Предыдущая статья на эту тему

Самая первая статья на эту же тему
Tags:
Hubs:
+32
Comments 29
Comments Comments 29

Articles

Information

Website
www.developersoft.ru
Registered
Founded
1998
Employees
201–500 employees
Location
Россия