Redo/Undo Manager

T

toxn

Gast
Hallo,

ich arbeite derzeit an einer relativ komplexen Projektmanagementsoftware und diese soll nun mit einer Undo/Redo-Funktionalitaet versehen werden, da Deratiges von jeder modernen Applikation unterstuezt werden sollte.
Gibt es da irgendwelche Best Practive Methoden, denn das Problem erscheint mir nicht nur mi ein paar Zeilen zu loesen zu sein und erfordert eine gute Planung.
Hat vielleicht jemand einen Tip fuer mich ?

Im Moment habe ich vor, in einer Managerklasse die ausgefuehrte Aktion zu speichern und bei einem Redoaufruf die dazugehoerige Gegenaktion auszufuehren. Das wird aber sehr viel Arbeit bei der Fuelle an Aktionen die meine Applikation unterstuezt. Eine andere sehr speicherintensive Methode waere, nach jedem neuen Zustand einfach das komplette Projekt zu clonen und bei Redo dieses zu laden. Das halte ich aber fuer wenig gut.

Dann schonmal danke fuer die Hilfe
 
Könnt es mir auch so vorstellen : Angenommen der User nimmt seine Eingaben vor (z.B. bei Feld1). Die Eingabe wird fix in eine bestimmte Speicheradresse geschrieben. Dieser Wert landet dann in eine Art Log-File. Wird wieder eine Änderung in Feld1 vorgenommen, so wird wieder der Wert in die selbe Speicheradresse geschrieben + ins Logfile. Bei Undo kann dann über das Logfile der entsprechende Wert wieder in die fixe Speicheradresse zurückgeschrieben werden. Es muß also nicht immer das komplette Projekt ausgelesen werden. Hoffe es ist verständlich.
 
Ganz so einfach ist es in meinem Fall leider nicht, da die einzelnen Objekte sehr viele Referenzen auf andere Objekte haben. Das muesste fuer mich bedeuten, dass ich nicht nur den Speicher fuer das Objekt clone sondern auch fuer die Referenzen oder seh ich das falsch ? Und einfach via Logfile ist auch nicht ganz so einfach realisierbar da es sich wirklich um komplexe Objeke handelt die sehr viele Eigenschaften haben. Ich versuche es aktuell mit einem Stack fuer die History und pushe da immer die jeweilige Aktion inklusive aller geclonten Daten. Aber so richtig funktionieren tut es noch nicht. Vielleicht habe ich auch noch einen Denkfehler und so ist die Realisierbarkeit fraglich.
 
Deine Idee ist schon genau die richtige, wie man das in der Praxis auch macht.

Alle Aktionen in einer Schlange einreihen. Bei einem Undo, dann rückwärts wieder "zurückarbeiten".

Die Kür ist dabei nun, wie man das Klassentechnisch umsetzt.

Nach jedem Funktionsaufruf musst du zu den EingabeParametern die Undo-Parameter generieren und in der Schlange einreihen. Bei einem Texteditor, müsstest du z.B. die gelöschten Zeichen speichern.

Viel Spass beim modellieren.
 
Wichtig ist halt - (Re)Do ist etwas anderes als Undo, weil die entsprechenden Aktionen intern vollständig rückwärts ablaufen müssen. Dazu kommt noch, dass man manche Sachen nicht "Undo"en kann, also z.B. Datei unwiederbringlich gelöscht oder eine Datensatzänderung zurücknehmen, die nach Dir in der Zeit bis zum Undo noch jemand anderes bearbeitet haben könnte (zumindest in nebenläufigen Systemen) - zu einer Action gehört nämlich immer auch a) ein exakter Datenbestand und b) ein Zeitpunkt.

Der Redo/Undo-Stack, auf den man seine Commands pusht, ist dabei noch das Allereinfachste. Will aber nicht abschrecken, das ist ein sehr spannendes Problem, was man erstmal mit dem einfachst-möglichen Ansatz lösen kann und dann verfeinert. :) Das in ein bestehendes System reinzupatchen ist natürlich ohne größeres Refactoring eine Strafe. Cool wäre es dann, wenn alle Actions Redo/Undo bereitstellen bzw. man z.B. über Reflection (oder ein anderes geeignetes Mittel der Sprache) feststellen kann, ob ein Objekt Redo/Undo überhaupt unterstützt.

Edit:
Zur Zustandsspeicherung kannst du z.B. das Memento-Pattern verwenden.

Edit2: Abhängige Objekte gehören natürlich dazu, aber das Memento, was Do() daraus generiert, sollte das an dieser Stelle sowieso schon alles berücksichtigen.
Edit3: Idealerweise hat jedes abhängige Objekt dazu ein Do(), was auch ein Memento generiert. Wichtig ist dann a) die Reihenfolge und b) dass sich alle Aktionen von allen abhängigen Objekten zurückrollen lassen.
 
Zuletzt bearbeitet:
@7H3 N4C3R

danke auch dir fuer deine Hilfe. Leider ist es eben wie du sagst. Das diese Funktionalitaet notwendig ist, wurde erst sehr spaet beschlossen und somit ist es etwas kniffelig das sauber umzusetzen. Das Memento Pattern hatte ich in einer frueheren so in etwa mal implementiert, aber ich bin daran gescheitert, da die Objekte eben sehr komplex sind und viele Referenzen auf andere Objekte beinhalten. Bedeutet also wenn ich dieses Objekte jetzt mit new "clone" bleiben die Referenzen auf die alten Objekten die gleichen. Somit musste ich diese auch wieder "clonen" usw. Das endetet in sehr viel unuebersichtlichem Code wobei ich auch nicht von mir behaupten wuerde, dass ich der erfahrenste Coder ever bin.
Aktuell verfolge ich den Weg ueber eine Managerklasse, die jede Aktion(die dann auch das Changeset beinhaltet) in einer Liste speichert und sobald ein Undo gefordert ist wird ueber die Klasse die mein Projektmodell verwaltet, eine Gegenaktion aufgerufen. Ist relativ aufwendig aber funktioniert bisher.
Es handelt sich im Uebrigen um eine Adobe Air Applikation. Hier mal ein Codeschnipsel
Code:
        public class HistoryManager
	{
		private var _history:ArrayCollection;
		private var currentIndex:int;
		private var maximumStackSize:int;
		
		private var _modellPresenter:ModellPresenter;
		
		public function HistoryManager()
		{
			_history = new ArrayCollection();
			currentIndex = -1;
			maximumStackSize = GlobalData.HISTORY_STACK_SIZE;			
		}

		public function actionDone(event:RedoUndoEvent):void
		{
			// save actiondata
			var action:ActionData = event.actionData;
			
			currentIndex++;
			_history.addItem(action);			
		}
				
		public function unDo():void
		{
			var action:ActionData;
			
			if(currentIndex >= 0)
				action = history.getItemAt(currentIndex) as ActionData;
			else
				return;
			
			currentIndex--;
			
			// evaluate action
			if(action.type == RedoUndoEvent.NODE_ADD)			
				modellPresenter.history_removeNode(action);
			
			if(action.type == RedoUndoEvent.RELATION_ADD)
				modellPresenter.history_removeRelation(action);
			
			if(action.type == RedoUndoEvent.NODE_MOVED)
				modellPresenter.history_moveNodeTo(action, true);
		}
		
		public function reDo():void
		{
			var action:ActionData;
			
			if(currentIndex >= (_history.length - 1))
				return;
			
			currentIndex++;
			
			action = history.getItemAt(currentIndex) as ActionData;						
			
			// evaluate action
			if(action.type == RedoUndoEvent.NODE_ADD)			
				modellPresenter.history_addNode(action);
			
			if(action.type == RedoUndoEvent.RELATION_ADD)
				modellPresenter.history_addRelation(action);
			
			if(action.type == RedoUndoEvent.NODE_MOVED)
				modellPresenter.history_moveNodeTo(action, false);
		}
				
		[...]

	}

Das muss natuerlich noch sehr erweitert werden. Aber fuer einen ersten Test war es ganz erfolgversprechend.

Edit: Dein Edit hab ich eben erst gelesen. Wie gesagt das mit der Abhaeigkeit war eben nicht ganz so einfach so moeglich bzw. habe ich es so nicht hinbekommen ;)
 
Zuletzt bearbeitet:
toxn schrieb:
Das Memento Pattern hatte ich in einer frueheren so in etwa mal implementiert, aber ich bin daran gescheitert, da die Objekte eben sehr komplex sind und viele Referenzen auf andere Objekte beinhalten. Bedeutet also wenn ich dieses Objekte jetzt mit new "clone" bleiben die Referenzen auf die alten Objekten die gleichen. Somit musste ich diese auch wieder "clonen" usw.
Ein Memento kann natürlich Mementos enthalten, die die Kindobjekte wiederherstellen.

Spannend ist, ob die Objekte eine Kopiersemantik haben. Wenn man erstmal an Referenzen rumdoktorn muss, wird es echt ätzend, wenn hier nicht von Anfang an ein Automatismus vorgesehen wurde.

Der Spaß fängt ja erst richtig an, wenn dann noch Objekte gelöscht werden und man sie beim Undo wieder in einen Objektbaum einpflegen muss. Am einfachsten ist es vielleicht, ein rekursives Clone sauber zu implementieren. Das bringt natürlich viel Overhead mit sich, sowohl Laufzeit als auch Speicherverbrauch.
Wenn alle Objekte ihre Daten in einer generischen Struktur halten (z.B. etwas Recordset-artiges) macht es die Implementierung von Redo/Undo ebenfalls wesentlich leichter.
 
Zurück
Oben