Bufor Undo \ Redo

Większość dzisiejszych aplikacji typu desktop posiada bufor stanu – popularnie nazywany “undo\redo” (skróty CTRL+Z, CTRL+Y). Najpopularniejszym zastosowaniem są edytory tekstu. Pisząc aplikację w C# z wykorzystaniem standardowym kontrolek TextBox taką funkcjonalność już będziemy mieli. Jednak czasami zachodzi rozwinięcie standardowej funkcjonalności o elementy specyficzne dla danej aplikacji. Najczęściej występuje to w różnego rodzaju edytorach (np. edytory map). W dzisiejszym poście przyjrzymy się implementacji w c#.

Kluczem do rozwiązania problemu jest zapamiętywanie w sprytny sposób stanu aplikacji  lub danych potrzebnych do odtworzenia tego stanu. Zdefiniujmy więc interfejs opisujący stan:

public interface IUndoRedoState
{
    void Commit();
    void Rollback();
}

Każdy stan można zatwierdzić lub odwołać (cofnąć). Załóżmy, że piszemy kalkulator obsługujący między innymi  operację dodawaniaUśmiech. Wtedy  taki stan może zostać zdefiniowany następująco:

public class SumUndoRedoState: IUndoRedoState
{
    private int _Number=-1;
    private Calculator _Calc = null;
    
    public SumUndoRedostate(Calculator calculator,int operand )
    {
        _Number = operand;
        _Calc = calculator;
    }
    public void Commit()
    {    
        _Calc.Result += _Number;
    }
    public void Rollback()
    {
        _Calc.Result -= _Number;
    }
}

Potrzebna nam jeszcze jedna klasa – manager który będzie zarządzał stanami:

class UndoRedoManager
    {
        public IList<UndoRedoState> m_States = new List<UndoRedoState>();
        private int m_CurrentStateIndex = 0;        
        
        public void AddState(UndoRedoState state)
        {            
            state.Commit();
            m_States.Insert(m_CurrentStateIndex, state);
            m_States.RemoveRange(m_CurrentStateIndex + 1, m_States.Count - 1 - m_CurrentStateIndex);
            m_CurrentStateIndex++;

            if (m_States.Count > 5)
            {
                m_States.RemoveAt(0);
                m_CurrentStateIndex = m_States.Count;
            }         
        }        
        public void Clear()
        {
            m_CurrentStateIndex = 0;
            m_States.Clear();            
        }                
        public bool CanBeCommited
        {
            get
            {
                if (m_CurrentStateIndex < m_States.Count)
                    return true;
                else
                    return false;
            }
        }
        public bool CanBeRollbacked
        {
            get
            {
                if (m_CurrentStateIndex - 1 >= 0)
                    return true;
                else
                    return false;
            }

        }
        public void CommitLast()
        {            
            if (CanBeCommited == false)
                return;
                
            UndoRedoState state = m_States[m_CurrentStateIndex];

            state.Commit();
            m_CurrentStateIndex++; 
        }
        public void RollbackLast()
        {            
            if (CanBeRollbacked == false)
                return;
                
            UndoRedoState state = m_States[m_CurrentStateIndex - 1];

            state.Rollback();
            m_CurrentStateIndex--;
        }
    }

Manager zawiera kolekcję wszystkich stanów. Pierwsza metoda (AddState) dodaje stan do kolekcji i wykonuje Commit. Użytkownik zatem może wywołać:

UndoRedoManager manager=new UndoRedoManager();
manager.AddState( new SumUndoRedoState(calc,10) );

Manager kolejno doda stan do kolekcji, wykona operację (dodanie liczby 10),  usunie stany występujące w kolekcji po aktualnym stanie aplikacji a na końcu zaktualizuje indeks aktualnego stanu – m_CurrentStateIndex.

Manager zawiera również metodę do wyczyszczenia stanów (Clear) oraz właściwości sprawdzające czy operacja Commit\Rollback może być wykonana – można powiązać pod przyciski Undo\Redo w interfejsie.

Metody CommitLast, RollbackLast  powinny być wywoływane w momencie Redo oraz Undo (Ctrl-Y, Ctrl-Z).

Oczywiście operacja dodania jest typowo akademickim przykładem. W praktyce jednak podobną konstrukcję można wykorzystać dla złożonych zadań. Osobiście skorzystałem z tego rozwiązania w swoim edytorze map, który wykonuje różne operacje typu “Dodaj obiekt”, “Modyfikuj siatkę terenu z zadanym współczynnikiem wgniecenia” itp.

8 thoughts on “Bufor Undo \ Redo”

  1. Jeśli robisz edytor graficzny to i tak operacja rysowania to nie powinna być sama obsługa OnPaint, tylko powinieneś sobie jeszcze przechowywać gdzieś listę wszystkich narysowanych obiektów. Wtedy Rollback usuwa taki element z listy i przy odrysowaniu już go nie będzie.

  2. Rozwiązanie dobre, chociaż IMO trochę przekombinowane

    1 – IUndoRedoState to taki wzorzec command na sterydach (z dodatkową metodą). Można to by było zaznaczyć bo undoRedoState po pierwszym przeczytaniu niewiele mówi.

    2 – najgorsza jest chyba ta lista! Przecież to właściwie okrojony, domowej roboty stos!

    3 –
    if (m_CurrentStateIndex – 1 >= 0) { return true; }
    else
    { return false; }
    nabijasz sobie linie kodu? 😛
    return m_CurrentStateIndex – 1 >= 0; – jak dla mnie równie czytelne

    4 – drobnostka, ale czemu używasz notacji węgierskiej? Już od dawna jest niezalecana nawet w C++ a w C# nigdy nie była.

    To chyba tyle narzekań, pozdrawiam 🙂

  3. @MSM
    Hello, dzięki za komentarz.
    1. Mi osobiście Command nie narzucił się na myśl, chodź analogia jest.
    2. Stack nie ma takich operacji jak RemoveRange więc w tym wypadku nie jest dobrym rozwiązaniem.
    3. Prawda – kod pochodzi z przed 2 lat.
    4. Uzywam tylko do pól – oznaczam je_ (tutaj też m_ bo cześć kodu jest stara, cześć nowa).

  4. ja bym zastosował stos.
    prosto i bez zbędnych komplikacji i z minimalną ilością kodu

  5. @janusz
    Jak juz pisalem stos NIE nadaje sie do prawdziwych rozwiazan (nie mozna usuwac elementow)

Leave a Reply

Your email address will not be published.