Laden...

Undo/Redo Best Practices

Erstellt von der-andreas vor 10 Jahren Letzter Beitrag vor 7 Jahren 5.962 Views
D
der-andreas Themenstarter:in
27 Beiträge seit 2012
vor 10 Jahren
Undo/Redo Best Practices

Für ein WPF-Projekte möchte ich Undo/Redo implementieren.

Es gibt ein Objektmodell mit ca. 15 Entities. Zur Persistenz werden XML-Dateien verwendet, d.h. man öffnet ein Projekt und dies ist dann vollständig im Hauptspeicher geladen.

Ich möchte das Undo/Redo so generisch wie möglich implementieren, d.h. wenn neue Entities hinzukommen, sollte maximal etwas Konfiguration hinzukommen.

Meine Grundidee ist ausgehend vom Root-Objekt alle INotifyPropertyChanged und INotifyCollectionChanged Events aller Kindobjekte abzufangen und dann ein Journal damit zu füllen. Damit kann dann Undo/Redo ausgeführt werden.

Zur Anzeige in der UI müsste ggf. viele Einzeländerungen zu einer "inhaltlichen" Änderung zusammengeführt werden.

Meine UI bindet sich bereits an View-Models und die View-Models synchronisieren sich bereits automatisch, wenn Änderungen am Model vorliegen.

Hat wer damit Erfahrung? Meine größte Sorge ist eigentlich der Memory Bloat bei zu vielen Einzeländerungen, die man von der UI aus zu einer Änderung zusammenfassen würde.

49.485 Beiträge seit 2005
vor 10 Jahren

Hallo der-andreas,

wenn du es vollkommen generisch haben willst, kannst du nie ganz Fälle ausschließen, die zu einen Memory Bloat führen.

Grundsätzlich gibt es ja zwei Arten, um sich Undo-Informationen zu merken: a) den Zustand des (kompletten) Objekts (vor der Operation) und b) wie du geschrieben hast, ein Journal der erfolgten Einzeländerungen.

Wenn es wenige Einzeländerungen gibt, ist ein Journal sparsamer, wenn es (sehr) viele gibt, ist das Merken des Objektzustands sparsamer. Nur weiß man bei einer generischen Lösung eben nicht, was später der Fall sein wird.

Natürlich ist ein Journal feiner. Die Frage ist, ob man das braucht. Üblicherweise reicht es, eine komplette Benutzeraktion auf einmal rückgängig machen zu können. Es kommt eher vor, dass man mehrere Benutzeraktionen (z.B. mehrere Tastenanschläge bei der Texteingabe) auf einmal rückgängig machen will, als dass man eine Benutzeraktion in Einzelschritten rückgängig machen will.

Normalerweise weiß das GUI, wann eine Benutzeraktion beginnt bzw. die davor abgeschlossen ist und könnte das dem Undo-Mechanismus sofort mitteilen. Dann könnte dieser sich den Objektzustand merken. Da ein Benutzer nur relativ wenige Operationen pro Sekunde durchführen kann, wäre die Anzahl der Schnappschüsse dadurch relativ gering.

herbivore

5.658 Beiträge seit 2006
vor 10 Jahren

Hi der-andreas,

ich hatte mal etwas ähnliches in Angriff genommen, hier der Thread dazu: Undo/Redo mit MVVM

Ich hatte damals zuerst versucht, jedesmal eine Art Snapshot des Programmzustands zu erstellen, wenn eine Änderung stattgefunden hatte. Dazu hatte ich das komplette ViewModel serialisiert und dann bei einer Undo- oder Redo-Aktion wieder deserialisert. Dieser Ansatz ist sehr einfach, führt aber zu Problemen. Beispielsweise werden dann bei Undo/Redo-Aktionen Listen mit Werten komplett ersetzt, anstatt nur die geänderten Elemente auszutauschen. Dadurch gehen Referenzen auf z.B. selektierte Objekte verloren, und es erfordert sehr viel Arbeit, die UI wieder auf den aktuellen Stand zu bringen.

Ich hab mich dann für die "Journal"-Version entschieden, wobei das Journal eine Auflistung von DoActions und UndoActions für jeden Schritt ist, die Actions sind dabei anonyme Methoden. Für ein Property sieht das dann in etwa so aus:


private bool someProperty = true;

public bool SomeProperty
{
    get { return someProperty; }
    set
    {
        if (someProperty == value)
            return;

        bool oldValue = someProperty;
        UndoRedoAction action = new UndoRedoAction()
        {
            DoAction = (() => someProperty = value),
            UndoAction = (() => someProperty = oldValue)
        };

        UndoManager.Execute(action);
    }
}

Diese Funktionalität hab ich dann in ein UndoRedoProperty<TValue> gekapselt, so daß der Code dann so aussieht:


private UndoRedoProperty<bool> someProperty = new UndoRedoProperty<bool>(true);

public bool SomeProperty
{
    get { return someProperty.Value; }
    set { someProperty.Value = value; }
}

Für Auflistungen, Dictionaries usw. habe ich ebenfalls Klassen geschrieben, die die IList / ICollectionChangedNotification / IDictionary - Interfaces implementieren und bei Änderungen entsprechend reagieren können.

Um Commands zu einem Undo/Redo-Schritt zusammenzufassen habe ich folgende Konstruktion:


using (UndoManager.UsingUndoRedo("Open Document"))
{
	openDocumentCommand.Execute(parameter);
}

In der using-Anweisung können beliebige Änderungen ausgeführt werden, der UndoManager faßt sie dann zu einem Schritt zusammen, der in der UI als "Undo Open Document" angezeigt wird. Der Einfachheit halber habe ich dann auch eine UndoRedoCommand-Klasse angelegt, die das alles automatisch durchführt.

Fazit: Ich habe einen UndoManager, der die einzelnen Schritte speichert und eine Undo- und eine Redo-Methode hat, um den Programmzustand zu ändern, wenn der User das Undo- oder Redo-Command aufruft. Um Property-Änderungen zu überwachen, habe ich eine UndoRedoProperty-Klasse, ebenso für Auflistungen und für Commands.

Das ganze funktioniert sehr gut, hat aber den Nachteil, daß es sich nur mit sehr viel Aufwand in bestehende Anwendungen integrieren läßt.

Christian

Weeks of programming can save you hours of planning

4.221 Beiträge seit 2005
vor 10 Jahren

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

D
der-andreas Themenstarter:in
27 Beiträge seit 2012
vor 10 Jahren

Danke für die weiteren Anregungen.

Ich werde wohl den Weg über das Journal probieren.

Das komplizierteste ist wohl das Zusammenfassen zu Nutzeraktionen. Den Ansatz mit dem Zeitintervall werde ich auf jeden Fall mit aufnehmen!

Zur Performance-Verbesserung fällt mir auch ein, dass man innerhalb einer Nutzeraktion dann die PropertyChanged-Events auch durchaus zusammenführen kann (Name 10x bearbeitet -> 1. changed Event und letztes Changed Event zusammenführen).

Hier hatte auch noch mal meine Gedanken zusammengefasst:
http://qualityspy.wordpress.com/2013/09/15/undoredo/

D
der-andreas Themenstarter:in
27 Beiträge seit 2012
vor 10 Jahren

Ich bin nun zu einem "Ergebnis" gekommen. Ich habe ein Open-Source Projekt daraus gemacht:

http://sourceforge.net/projects/pb-common/

Hier auch ein Artikel dazu:

Kleffel's Software Blog: .NET Undo-Redo Framework

C
1 Beiträge seit 2016
vor 7 Jahren

Hallo Christian,

ich bin gerade dabei ein Undo/Redo-Framework für mein Projekt auszusuchen und das hier gezeigte Beispiel sieht sehr gut aus 👍
Gibt es ein Beispielprojekt, in dem die hier nicht gezeigten Code-Blöcke/Klassen gezeigt werden?

Danke und Gruß
Dieter

49.485 Beiträge seit 2005
vor 7 Jahren

Hallo Carpi1968,

eine komplette Implementierung einer Undo-Bibliothek findest du in Multilevel-Undo/-Redo mit dem Command-Muster.

herbivore

5.658 Beiträge seit 2006
vor 7 Jahren

Hi Carpi1968,

ich hab den Code leider nicht, allerdings wird es im Internet inzwischen eine Menge Implementierungen geben, die speziell für WPF entwickelt worden und die Funktionalitäten der INotifyPropertyChanged- und ICollectionChangedNotification-Schnittstellen dafür nutzen.

Christian

Weeks of programming can save you hours of planning