Ich schreibe ein Strukturmodellierungswerkzeug für eine Bauingenieuranwendung. Ich habe eine riesige Modellklasse, die das gesamte Gebäude darstellt und Sammlungen von Knoten, Linienelementen, Lasten usw. enthält, die ebenfalls benutzerdefinierte Klassen sind.
Ich habe bereits eine Undo-Engine codiert, die nach jeder Änderung am Modell eine Deep-Copy-Datei speichert. Jetzt begann ich zu überlegen, ob ich anders hätte codieren können. Anstatt die Deep-Copies zu speichern, könnte ich vielleicht eine Liste jeder Modifikatoraktion mit einem entsprechenden umgekehrten Modifikator speichern. Damit ich die umgekehrten Modifikatoren auf das aktuelle Modell anwenden kann, um sie rückgängig zu machen, oder die Modifikatoren, um sie zu wiederholen.
Ich kann mir vorstellen, wie Sie einfache Befehle ausführen würden, die Objekteigenschaften usw. ändern. Aber wie wäre es mit komplexen Befehlen? Zum Beispiel das Einfügen neuer Knotenobjekte in das Modell und das Hinzufügen einiger Linienobjekte, die Verweise auf die neuen Knoten beibehalten.
Wie würde man das umsetzen?
quelle
Antworten:
Die meisten Beispiele, die ich gesehen habe, verwenden hierfür eine Variante des Befehlsmusters . Jede rückgängig zu machende Benutzeraktion erhält eine eigene Befehlsinstanz mit allen Informationen, um die Aktion auszuführen und zurückzusetzen. Sie können dann eine Liste aller ausgeführten Befehle verwalten und diese nacheinander zurücksetzen.
quelle
Ich denke, sowohl Andenken als auch Befehl sind nicht praktikabel, wenn Sie sich mit einem Modell der Größe und des Umfangs befassen, die das OP impliziert. Sie würden funktionieren, aber es wäre viel Arbeit, sie zu warten und zu erweitern.
Für diese Art von Problem müssen Sie meines Erachtens Unterstützung für Ihr Datenmodell einbauen, um differenzielle Prüfpunkte für jedes am Modell beteiligte Objekt zu unterstützen . Ich habe das einmal gemacht und es hat sehr gut funktioniert. Das Wichtigste, was Sie tun müssen, ist, die direkte Verwendung von Zeigern oder Referenzen im Modell zu vermeiden.
Jeder Verweis auf ein anderes Objekt verwendet einen Bezeichner (wie eine Ganzzahl). Wann immer das Objekt benötigt wird, suchen Sie die aktuelle Definition des Objekts aus einer Tabelle. Die Tabelle enthält eine verknüpfte Liste für jedes Objekt, die alle vorherigen Versionen enthält, sowie Informationen darüber, für welchen Prüfpunkt sie aktiv waren.
Das Implementieren von Rückgängig / Wiederherstellen ist einfach: Führen Sie Ihre Aktion aus und richten Sie einen neuen Prüfpunkt ein. Rollback aller Objektversionen auf den vorherigen Prüfpunkt.
Es erfordert etwas Disziplin im Code, hat aber viele Vorteile: Sie benötigen keine tiefen Kopien, da Sie den Modellstatus differenziell speichern. Sie können die Menge an Speicher, die Sie verwenden möchten ( sehr wichtig für Dinge wie CAD-Modelle), entweder nach Anzahl der Wiederholungen oder nach verwendetem Speicher festlegen. Sehr skalierbar und wartungsarm für die Funktionen, die auf dem Modell ausgeführt werden, da sie nichts tun müssen, um das Rückgängigmachen / Wiederherstellen zu implementieren.
quelle
Wenn Sie von GoF sprechen, wird im Memento- Muster speziell das Rückgängigmachen behandelt.
quelle
Wie bereits erwähnt, ist das Befehlsmuster eine sehr leistungsstarke Methode zum Implementieren von Rückgängig / Wiederherstellen. Aber es gibt einen wichtigen Vorteil, den ich im Befehlsmuster erwähnen möchte.
Wenn Sie das Rückgängigmachen / Wiederherstellen mithilfe des Befehlsmusters implementieren, können Sie große Mengen an doppeltem Code vermeiden, indem Sie die an den Daten ausgeführten Vorgänge (bis zu einem gewissen Grad) abstrahieren und diese Vorgänge im Rückgängig- / Wiederherstellungssystem verwenden. Zum Beispiel in einem Texteditor sind Ausschneiden und Einfügen komplementäre Befehle (abgesehen von der Verwaltung der Zwischenablage). Mit anderen Worten, die Rückgängig-Operation für einen Ausschnitt ist Einfügen und die Rückgängig-Operation für eine Einfügung ist Ausschneiden. Dies gilt für viel einfachere Vorgänge wie das Eingeben und Löschen von Text.
Der Schlüssel hier ist, dass Sie Ihr Rückgängig- / Wiederherstellungssystem als primäres Befehlssystem für Ihren Editor verwenden können. Anstatt das System wie "Objekt rückgängig machen, Dokument ändern" zu schreiben, können Sie "Objekt rückgängig machen, Wiederherstellungsvorgang für Objekt rückgängig machen ausführen, um das Dokument zu ändern".
Zugegeben, viele Leute denken sich: "Nun duh, ist das nicht Teil des Punktes des Befehlsmusters?" Ja, aber ich habe zu viele Befehlssysteme gesehen, die zwei Befehlssätze haben, einen für Sofortoperationen und einen für Rückgängig / Wiederherstellen. Ich sage nicht, dass es keine Befehle gibt, die für Sofortoperationen und Rückgängigmachen / Wiederherstellen spezifisch sind, aber das Reduzieren der Duplizierung macht den Code wartbarer.
quelle
paste
alscut
^ -1 gedacht .Vielleicht möchten Sie sich für das Rückgängigmachen auf den Paint.NET-Code beziehen - sie haben ein wirklich schönes Rückgängig-System. Es ist wahrscheinlich ein bisschen einfacher als das, was Sie brauchen, aber es könnte Ihnen einige Ideen und Richtlinien geben.
-Adam
quelle
Dies kann ein Fall sein, in dem CSLA anwendbar ist. Es wurde entwickelt, um Objekte in Windows Forms-Anwendungen komplex rückgängig zu machen.
quelle
Ich habe komplexe Rückgängig-Systeme erfolgreich mit dem Memento-Muster implementiert - sehr einfach und hat den Vorteil, dass natürlich auch ein Redo-Framework bereitgestellt wird. Ein subtilerer Vorteil besteht darin, dass aggregierte Aktionen auch in einem einzelnen Rückgängig enthalten sein können.
Kurz gesagt, Sie haben zwei Stapel von Erinnerungsobjekten. Einer für Rückgängig, der andere für Wiederherstellen. Bei jeder Operation wird ein neues Andenken erstellt. Im Idealfall handelt es sich dabei um einige Aufrufe, um den Status Ihres Modells, Dokuments (oder was auch immer) zu ändern. Dies wird dem Rückgängig-Stapel hinzugefügt. Wenn Sie einen Rückgängig-Vorgang ausführen, führen Sie nicht nur die Rückgängig-Aktion für das Memento-Objekt aus, um das Modell wieder zu ändern, sondern entfernen das Objekt auch vom Rückgängig-Stapel und schieben es direkt auf den Redo-Stapel.
Wie die Methode zum Ändern des Status Ihres Dokuments implementiert wird, hängt vollständig von Ihrer Implementierung ab. Wenn Sie einfach einen API-Aufruf durchführen können (z. B. ChangeColour (r, g, b)), stellen Sie ihm eine Abfrage voran, um den entsprechenden Status abzurufen und zu speichern. Das Muster unterstützt aber auch das Erstellen tiefer Kopien, Speicher-Snapshots, die Erstellung temporärer Dateien usw. - es liegt ganz bei Ihnen, da es sich lediglich um eine virtuelle Methodenimplementierung handelt.
Um aggregierte Aktionen auszuführen (z. B. Benutzer-Umschalt - Wählt eine Ladung von Objekten aus, für die eine Operation ausgeführt werden soll, z. B. Löschen, Umbenennen, Ändern von Attributen), erstellt Ihr Code einen neuen Rückgängig-Stapel als einzelnes Andenken und übergibt diesen an die eigentliche Operation an Fügen Sie die einzelnen Operationen hinzu. Ihre Aktionsmethoden müssen also nicht (a) über einen globalen Stapel verfügen, über den Sie sich Sorgen machen müssen, und (b) können gleich codiert werden, unabhängig davon, ob sie isoliert oder als Teil einer aggregierten Operation ausgeführt werden.
Viele Rückgängig-Systeme befinden sich nur im Arbeitsspeicher, aber Sie können den Rückgängig-Stapel auf Wunsch beibehalten, denke ich.
quelle
Ich habe gerade über das Befehlsmuster in meinem agilen Entwicklungsbuch gelesen - vielleicht hat das Potenzial?
Sie können von jedem Befehl die Befehlsschnittstelle implementieren lassen (die über eine Execute () -Methode verfügt). Wenn Sie rückgängig machen möchten, können Sie eine Rückgängig-Methode hinzufügen.
Mehr Infos hier
quelle
Ich bin mit Mendelt Siebenga über die Tatsache, dass Sie das Befehlsmuster verwenden sollten. Das von Ihnen verwendete Muster war das Memento-Muster, das mit der Zeit sehr verschwenderisch werden kann und wird.
Da Sie an einer speicherintensiven Anwendung arbeiten, sollten Sie entweder angeben können, wie viel Speicher die Rückgängig-Engine belegen darf, wie viele Rückgängig-Ebenen gespeichert werden oder auf welchem Speicher sie beibehalten werden. Wenn Sie dies nicht tun, werden Sie bald auf Fehler stoßen, die darauf zurückzuführen sind, dass der Computer nicht genügend Speicher hat.
Ich würde Ihnen raten, zu prüfen, ob es ein Framework gibt, das bereits ein Modell für Undos in der Programmiersprache / dem Framework Ihrer Wahl erstellt hat. Es ist schön, neue Dinge zu erfinden, aber es ist besser, etwas zu nehmen, das bereits in realen Szenarien geschrieben, debuggt und getestet wurde. Es wäre hilfreich, wenn Sie hinzufügen würden, in was Sie dies schreiben, damit die Leute Frameworks empfehlen können, die sie kennen.
quelle
Codeplex-Projekt :
Es ist ein einfaches Framework, mit dem Sie Ihren Anwendungen Rückgängig- / Wiederherstellungsfunktionen hinzufügen können, die auf dem klassischen Befehlsentwurfsmuster basieren. Es unterstützt das Zusammenführen von Aktionen, verschachtelten Transaktionen, die verzögerte Ausführung (Ausführung beim Transaktions-Commit auf oberster Ebene) und den möglichen nichtlinearen Rückgängig-Verlauf (bei dem Sie zwischen mehreren Aktionen wählen können, die wiederholt werden sollen).
quelle
Die meisten Beispiele, die ich gelesen habe, verwenden entweder den Befehl oder das Erinnerungsmuster. Mit einer einfachen Deque-Struktur können Sie dies aber auch ohne Entwurfsmuster tun .
quelle
Eine clevere Methode zum Rückgängigmachen, die Ihre Software auch für die Zusammenarbeit mit mehreren Benutzern geeignet macht, ist die Implementierung einer operativen Transformation der Datenstruktur.
Dieses Konzept ist nicht sehr beliebt, aber gut definiert und nützlich. Wenn Ihnen die Definition zu abstrakt erscheint, ist dieses Projekt ein erfolgreiches Beispiel dafür, wie eine operative Transformation für JSON-Objekte in Javascript definiert und implementiert wird
quelle
Als Referenz finden Sie hier eine einfache Implementierung des Befehlsmusters für Rückgängig / Wiederherstellen in C #: Einfaches Rückgängig / Wiederherstellen- System für C # .
quelle
Wir haben das Laden und Speichern des Serialisierungscodes für "Objekte" für ein bequemes Formular zum Speichern und Wiederherstellen des gesamten Status eines Objekts wiederverwendet. Wir verschieben diese serialisierten Objekte auf den Rückgängig-Stapel - zusammen mit einigen Informationen darüber, welche Operation ausgeführt wurde, und Hinweisen zum Rückgängigmachen dieser Operation, wenn nicht genügend Informationen aus den serialisierten Daten gewonnen wurden. Beim Rückgängigmachen und Wiederherstellen wird häufig nur ein Objekt durch ein anderes ersetzt (theoretisch).
Es gab viele VIELE Fehler aufgrund von Zeigern (C ++) auf Objekte, die nie behoben wurden, als Sie einige ungerade Rückgängig-Wiederherstellungssequenzen ausführten (diese Stellen wurden nicht aktualisiert, um sicherere rückgängig gemachte "Bezeichner" rückgängig zu machen). Bugs in diesem Bereich oft ... ähm ... interessant.
Einige Vorgänge können Sonderfälle für die Geschwindigkeits- / Ressourcennutzung sein - z. B. das Bemessen von Objekten oder das Verschieben von Objekten.
Die Mehrfachauswahl bietet auch einige interessante Komplikationen. Zum Glück hatten wir bereits ein Gruppierungskonzept im Code. Der Kommentar von Kristopher Johnson zu Unterelementen kommt dem, was wir tun, ziemlich nahe.
quelle
Ich musste dies tun, als ich einen Löser für ein Peg-Jump-Puzzlespiel schrieb. Ich habe jede Bewegung zu einem Befehlsobjekt gemacht, das genügend Informationen enthält, die entweder ausgeführt oder rückgängig gemacht werden können. In meinem Fall war dies so einfach wie das Speichern der Startposition und der Richtung jeder Bewegung. Ich habe dann alle diese Objekte in einem Stapel gespeichert, damit das Programm beim Zurückverfolgen problemlos so viele Bewegungen rückgängig machen kann, wie es benötigt.
quelle
Sie können die vorgefertigte Implementierung des Rückgängig / Wiederherstellen-Musters in PostSharp ausprobieren. https://www.postsharp.net/model/undo-redo
Sie können Ihrer Anwendung Funktionen zum Rückgängigmachen / Wiederherstellen hinzufügen, ohne das Muster selbst zu implementieren. Es verwendet das Recordable-Muster, um die Änderungen in Ihrem Modell zu verfolgen, und es funktioniert mit dem INotifyPropertyChanged-Muster, das auch in PostSharp implementiert ist.
Sie erhalten UI-Steuerelemente und können den Namen und die Granularität der einzelnen Vorgänge festlegen.
quelle
Ich habe einmal an einer Anwendung gearbeitet, in der alle Änderungen, die durch einen Befehl am Modell der Anwendung vorgenommen wurden (z. B. CDocument ... wir haben MFC verwendet), am Ende des Befehls beibehalten wurden, indem Felder in einer internen Datenbank aktualisiert wurden, die im Modell verwaltet wird. Wir mussten also nicht für jede Aktion einen eigenen Rückgängig- / Wiederherstellungscode schreiben. Der Rückgängig-Stapel erinnerte sich bei jeder Änderung eines Datensatzes (am Ende jedes Befehls) einfach an die Primärschlüssel, Feldnamen und alten Werte.
quelle
Der erste Abschnitt von Entwurfsmuster (GoF, 1994) enthält einen Anwendungsfall zum Implementieren des Rückgängigmachens / Wiederherstellens als Entwurfsmuster.
quelle
Sie können Ihre ursprüngliche Idee performant machen.
Verwenden Sie persistente Datenstrukturen und führen Sie eine Liste mit Verweisen auf den alten Zustand . (Dies funktioniert jedoch nur dann wirklich, wenn alle Daten in Ihrer Statusklasse unveränderlich sind und alle Operationen eine neue Version zurückgeben. Die neue Version muss jedoch keine tiefe Kopie sein. Ersetzen Sie einfach die Kopie der geänderten Teile -on-write '.)
quelle
Ich habe festgestellt, dass das Befehlsmuster hier sehr nützlich ist. Anstatt mehrere umgekehrte Befehle zu implementieren, verwende ich Rollback mit verzögerter Ausführung auf einer zweiten Instanz meiner API.
Dieser Ansatz erscheint sinnvoll, wenn Sie einen geringen Implementierungsaufwand und eine einfache Wartbarkeit wünschen (und sich den zusätzlichen Speicher für die 2. Instanz leisten können).
Ein Beispiel finden Sie hier: https://github.com/thilo20/Undo/
quelle
Ich weiß nicht, ob dies für Sie von Nutzen sein wird, aber als ich bei einem meiner Projekte etwas Ähnliches tun musste, habe ich UndoEngine von http://www.undomadeeasy.com heruntergeladen - eine wunderbare Engine und es war mir wirklich egal, was sich unter der Motorhaube befand - es funktionierte einfach.
quelle
Meiner Meinung nach könnte das UNDO / REDO auf zwei Arten allgemein umgesetzt werden. 1. Befehlsebene (als Befehlsebene bezeichnet Rückgängig / Wiederherstellen) 2. Dokumentebene (als globales Rückgängig / Wiederherstellen bezeichnet)
Befehlsebene: Wie viele Antworten zeigen, wird dies mithilfe des Memento-Musters effizient erreicht. Wenn der Befehl auch das Journalisieren der Aktion unterstützt, wird ein Wiederherstellen problemlos unterstützt.
Einschränkung: Sobald der Umfang des Befehls abgelaufen ist, ist das Rückgängigmachen / Wiederherstellen nicht mehr möglich, was zum (globalen) Rückgängigmachen / Wiederherstellen auf Dokumentebene führt
Ich denke, Ihr Fall würde in das globale Rückgängigmachen / Wiederherstellen passen, da er für ein Modell geeignet ist, das viel Speicherplatz benötigt. Dies ist auch geeignet, um auch selektiv rückgängig zu machen / zu wiederholen. Es gibt zwei primitive Typen
In "Alle Speicher rückgängig machen / wiederholen" wird der gesamte Speicher als verbundene Daten (z. B. ein Baum, eine Liste oder ein Diagramm) behandelt, und der Speicher wird von der Anwendung und nicht vom Betriebssystem verwaltet. Daher werden neue und gelöschte Operatoren in C ++ überladen, um spezifischere Strukturen zu enthalten, um Operationen wie a effektiv zu implementieren. Wenn ein Knoten geändert wird, b. Halten und Löschen von Daten usw. Die Funktionsweise besteht im Wesentlichen darin, den gesamten Speicher zu kopieren (vorausgesetzt, die Speicherzuordnung wird bereits von der Anwendung mithilfe fortschrittlicher Algorithmen optimiert und verwaltet) und in einem Stapel zu speichern. Wenn die Kopie des Speichers angefordert wird, wird die Baumstruktur basierend auf der Notwendigkeit einer flachen oder tiefen Kopie kopiert. Eine tiefe Kopie wird nur für die Variable erstellt, die geändert wird. Da jede Variable mithilfe einer benutzerdefinierten Zuordnung zugewiesen wird, Die Anwendung hat das letzte Wort, wann sie bei Bedarf gelöscht werden muss. Die Dinge werden sehr interessant, wenn wir das Rückgängigmachen / Wiederherstellen partitionieren müssen, wenn es passiert, dass wir eine Reihe von Operationen programmgesteuert und selektiv rückgängig machen / wiederholen müssen. In diesem Fall erhalten nur diese neuen Variablen oder gelöschten Variablen oder geänderten Variablen ein Flag, sodass Rückgängig / Wiederherstellen nur diesen Speicher rückgängig macht / wiederherstellt. Die Dinge werden noch interessanter, wenn wir ein teilweises Rückgängigmachen / Wiederherstellen innerhalb eines Objekts durchführen müssen. In diesem Fall wird eine neuere Idee des "Besuchermusters" verwendet. Es heißt "Object Level Undo / Redo" oder gelöschte Variablen oder geänderte Variablen erhalten ein Flag, so dass Rückgängig / Wiederherstellen nur diesen Speicher rückgängig macht / wiederherstellt. Die Dinge werden noch interessanter, wenn wir ein teilweises Rückgängigmachen / Wiederherstellen innerhalb eines Objekts durchführen müssen. In diesem Fall wird eine neuere Idee des "Besuchermusters" verwendet. Es heißt "Object Level Undo / Redo" oder gelöschte Variablen oder geänderte Variablen erhalten ein Flag, so dass Rückgängig / Wiederherstellen nur diesen Speicher rückgängig macht / wiederherstellt. Die Dinge werden noch interessanter, wenn wir ein teilweises Rückgängigmachen / Wiederherstellen innerhalb eines Objekts durchführen müssen. In diesem Fall wird eine neuere Idee des "Besuchermusters" verwendet. Es heißt "Object Level Undo / Redo"
Sowohl 1 als auch 2 können Methoden wie 1. BeforeUndo () 2. AfterUndo () 3. BeforeRedo () 4. AfterRedo () haben. Diese Methoden müssen im grundlegenden Befehl "Rückgängig / Wiederherstellen" (nicht im Kontextbefehl) veröffentlicht werden, damit alle Objekte diese Methoden ebenfalls implementieren, um eine bestimmte Aktion zu erhalten.
Eine gute Strategie besteht darin, eine Mischung aus 1 und 2 zu erstellen. Das Schöne ist, dass diese Methoden (1 und 2) selbst Befehlsmuster verwenden
quelle