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ę dodawania. 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.
Całkiem przyjemne, tylko co jeśli nie da się napisać metody Rollback()?
tzn?Może przykład?
Na przykład edytor graficzny. Jak napisać Rollback() dla narysowania lini?
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.
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 🙂
@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).
ja bym zastosował stos.
prosto i bez zbędnych komplikacji i z minimalną ilością kodu
@janusz
Jak juz pisalem stos NIE nadaje sie do prawdziwych rozwiazan (nie mozna usuwac elementow)