Nach abgebrochenen Änderungen mit vorhergehenden Daten weiterarbeiten

Hi,

ich habe ein kleines Programm in dem ich einige hierarchisch aufgebaute POJOs in einem JTree darstelle. Ich lesen die Daten von der Platte und erzeuge daraus die POJOs die ich dann in ein TreeModel einfüge, das ich dem JTree übergebe. Der JTree sitzt in einem Bearbeitungsdialogfenster, in welchem man Elemente in den Baum einfügt, ändert, löscht oder verschiebt. Das Programm arbeitet letztendlich mit den POJO-Daten.
Wenn man nun den Dialog abbricht und wieder öffnet, sind die zuletzt gemachten Änderungen immer noch im Baum zu sehen. Ich möchte nun aber dann den Datenzustand im Baum haben, der vor der Bearbeitung mal eingelesen wurde, also auch gerade im Programm verarbeitet wird.

Habt ihr eine Idee zur Lösung für das Problem?

Da gibt es mehrere Möglichkeiten.

Dem Dialog eine Kopie übergeben, Änderungen an der Kopie vornehmen, bei speichern Originial durch die Kopie ersetzen, sonst nichts.

Eventsourcing, dem Dialog eine Kopie übergeben, alle Events die die Kopie verändern aufzeichnen, und beim speichern, die Events auf das original Anwenden.

Alle Änderungen als Events speichern, sowie einer Undo-Funktion.
Event(“setData”, newData), UndoEvent(“setData”, oldData)
Dann beim Abbruch eben alle UndoEvents abspielen bis man wieder am Ausgangszustand angekommen ist.

Daten vor dem Öffnen des Dialogs serialisieren und bei abbruch deserialisieren.

Klingt für mich, als ob das TreeModel weiterverwendet wird und tippe mal darauf, dass es die Daten entweder nicht aktuallisiert (wenn welche vorhanden sind) oder schon vom Aufrufer “falsch” bekommen hat.

Also würde ich jetzt 2 Lösungen “sehen”:

  1. Dem Dialog die Daten neu geben.
  2. Prüfen ob die Daten vom Aufrufer manipuliert worden sind. Indem Fall dafür sorgen, dass der Dialog nur eine Kopie von den Daten bekommt.

Müsste ich dazu das root-Objekt, welches alle geschachtelten POJOs enthält klonen? Bisher werden nur Referenzen auf das root-Objekt der geschachtelten POJOs im Programm weitergegeben. das root-Objekt ist auch der root vom JTree.

Nicht das TreeModel, aber das root-Objekt der POJOs, das letztendlich auch der root des TreeModels ist, lebt weiter. Und auf dieses Objekt zeigen die Referenzen.

Also, vielleicht sollte ich noch etwas weiter ausholen und noch ein paar Details mehr verraten.

Die geschachtelte Objekt-Hierarchie ist in einer XML-Datei gespeichert. Beim Programmstart lese ich diese Datei aus, das erzeugte Objekt ist das root-Objekt meines Projektes. Eine Referenz dieses Objekts übergebe ich anschließend der GUI die es verarbeitet und die Inhalte an den richtigen Stellen anzeigt.

Nun möchte ich das Projekt und damit die Anzeigen im Programm verändern.
Dazu nehme ich eine Referenz auf das root-Objekt und übergebe es einem Controller, der sich um den Aufbau von Model und View der Bearbeitungsroutine kümmert. Der Controller übergibt die root-Objekt-Referenz einem TreeModel. In einem Bearbeitungsdialog wird nun also in einem Baum das root-Objekt verarbeitet und angezeigt.

Wenn der Benutzer den Baum fertig bearbeitet hat, klick er auf den OK-Button im Dialog. Darauf hin holt der Controller das root-Objekt (genauer: immer noch eine Refrenz) beim TreeModel ab und reicht sie nach oben zum Haupt-Controller, der diese Referenz an die Haupt-View weiter gibt. Die Veränderungen am root-Objekt werden nun angezeigt.
Per Klick auf einen Menüpunkt kann der Benutzer nun die Änderungen in die XML-Datei zurückschreiben lassen.

Nehmen wir aber nun mal an, dass der Benutzer seine Arbeit am Baum abbricht. Dann sollen die bis dahin gemachten Änderungen auch nicht an der GUI sichbar werden und natürlich auch nicht persistiert werden. Das funktioniert auch so weit.
Aber wenn man nun den Bearbeitungsdialog nun wieder öffnet, sind die gemachten Änderung dort noch/wieder sichtbar.
Wenn man jetzt nicht alle Änderungen kennt, die vorher gemacht wurden, baut man nun ein root-Objekt auf, dass man so nie haben wollte…

Ich möchte also beim Öffnen des Bearbeitungsdialoges immer den Zustand im Baum vorfinden, der auch real in der GUI angezeigt wird.

Ich hoffe, nun ist das Problem besser verständlich und ihr habt nun ein paar Ideen zur Lösung für mich.

Naja, sehe es doch mal wie mit einer Datenbank. Da gibst du ja auch nicht die gemanagten Objekte raus sondern DTOs. Änderungen daran landen nur in der Datenbank, wenn sie über den entsprechenden Controller laufen.

Das kannst du ja übertragen. Was wir als Kopie bezeichnen wäre dein DTO (kann ja 1:1 ausehen wie das Pojo). Die Komponenten bekommen nur eine Kopie davon. Wenn jede Komponente Ihre eigenen Kopien bekommen, dann können auch alle deine Komponenten im Programm daran rum manipulieren wie sie möchten ohne Seiteneffekte zu erzeugen.

Das klingt, als wäre eine wichtige Frage: Wie wird die „Bearbeitungsroutine“ (bzw. das GUI dafür) aufgebaut. Auf Basis der bisherigen Beschreibung klingt das, als chonologischer Pseudocode aufgeschrieben, so:

void controller(Root root) {
    TreeModel treeModel = createTreeModel(root);
    EditDialog editDialog = createEditDialog(treeModel);

    int whatTheUserDid = editDialog.show();
    if (whatTheUserDid == CONFIRM) {
        // Save the MODIFIED (!) treeModel:
        storeThisStuffSomewhere(treeModel); 
    }
    else if (whatTheUserDid == CANCEL) {
        // Editing was cancelled. 
        ...
        // A little bit later, the user may start a new editing 
        // process, with ANOTHER dialog ...
        EditDialog anotherEditDialog = createEditDialog(treeModel);
        int whatTheUserDidLater = anotherEditDialog .show();
        ...
    }
}

D.h. beim zweiten Editieren würde ein neuer Bearbeitungsdialog angezeigt. Aaaber wie im Pseudocode angedeutet: Dafür würde das selbe TreeModel verwendet, wie vorher (also das, wo schon die Änderungen drin sind!).

Die Abhilfe im Pseudocode wäre „„einfach““:

        // Weg:
        // EditDialog anotherEditDialog = createEditDialog(treeModel); 
        // Hin:
        TreeModel freshTreeModel = createTreeModel(root);
        EditDialog anotherEditDialog = createEditDialog(freshTreeModel);

d.h. stark vereinfacht: Das GUI-Modell (d.h. das TreeModel), das im Bearbeitungsdialog angezeigt wird, muss „frisch“ aus dem originalen, unveränderten „Root“-Objekt erstellt werden.

(In der Praxis kann das natürlich frickelig sein, aber … sollte das nicht konzeptuell das Problem lösen können?)

Start vereinfacht sieht der Controller so aus:

class EditController {
   private MainController mc;
   private ProjectTreeModel treeModel;
   private JDialog dialog;

   EditController(MainController mc) {
      this.mc = mc;
   }

   void newProject() {
      editProject(new Project());
   }

   void editProject(Project root) {
      treeModel = new ProjectTreeModel(root);
      showEditDialog(treeModel);
   }

   //Benutzer hat Bearbeitung abgeschlossen
   public void setProject() {
      dialog.dispose();
      Project project = (Project) treeModel.getRoot();
      mc.setCurrentProject(plsProject);  
   }

   //Benutzer hat abgebrochen
   public void cancel() {
      dialog.dispose();
   }

   private void showEditDialog(ProjectTreeModel treeModel) {
      ProjectPanel panel = new ProjectPanel(this, treeModel);
      dialog = new JDialog(mc.getMainWindow(), "Titel", true);
      dialog.add(panel);
      dialog.setVisible(true);
   }
}

Ich vermute, dass das Problem darin liegt, dass die verwendeten Referenzen allesamt auf das root-Objekt zeigen.
Der MainController setzt die vom EditController erhaltene Referenz in die GUI.
Als ich nämlich mal eine abgebrochene Bearbeitung gespeichert habe, war diese nach dem Neustart der Anwendung der aktuelle Zustand. Das darf aber so nicht sein. Die Oberfläche soll den neuen Inhalt nach durchgeführter Bearbeitung anzeigen. Aber solange das nicht auf die Platte geschrieben wird, darf die Änderung beim Neustart nicht Stand der Dinge sein.

OK, das hier…

void editProject(Project root) {
  treeModel = new ProjectTreeModel(root);
  showEditDialog(treeModel);
}

sieht eigentlich aus als würde dort jedes mal ein neues TreeModel erstellt, wenn die Bearbeitung anfängt. Demnach trifft meine Vermutung, dass dort ein verändertes TreeModel wiederverwendet wurd, (vermutlich :wink: ) nicht zu.

Das heißt dann ja, dass das ProjectTreeModel bei Änderungen direkt in das Project reinschreibt, aus dem es erzeugt wurde (was auch durch Project project = (Project) treeModel.getRoot(); bekräftigt wird). Und ich nehme an, dass die Bearbeitung gestartet wird, indem jemand von außen sowas wie

editController.editProject(mc.getCurrentProject());   

aufruft (speziell eben in dem Fall, wo jemand nach einer abgebrochenen Bearbeitung eine neue Bearbeitung startet, und das ganze nicht von newProject aus anfängt)

Und … vermutlich ist Project ein Platzhalter für einen „ziemlich komplizierten Teil des Codes“. Ansonsten wäre der Lösungsvorschlag analog zum obigen: Vor der Bearbeitung eine Kopie erstellen, und nur beim Bestätigen den veränderten Zustand an den Haupt-Controller zurückgeben:

   void editProject(Project root) {
      Project copy = new Project(root); // XXX Kopie erstellen 
      treeModel = new ProjectTreeModel(copy); // XXX Kopie übergeben
      showEditDialog(treeModel);
   }

   //Benutzer hat Bearbeitung abgeschlossen
   public void setProject() {
      dialog.dispose();
      Project project = (Project) treeModel.getRoot();

      // XXX Hier wird die Kopie (die im ProjectTreeModel liegt und dort
      // bearbeitet wurde) an den MainController übergeben, und wird
      // damit zum "echten" Projekt, das auch außerhalb bekannt ist
      mc.setCurrentProject(plsProject);  
   }

Aber falls „Project“ eben irgendwas kompliziertes ist, wo ggf. irgendwelche Listener dranhängen oder irgendwas anderes, was nicht ohne weiteres kopiert werden kann, dann muss man sich wohl was anderes überlegen…

(Eher aus Neugier - vielleicht nicht relevant: Das ProjectTreeModel ist vermutlich ein DefaultTreeModel?! Oder bist du den steinigen Weg gegangen, das reine TreeModel interface selbst zu implementieren? Das ganze Tree-Zeug kann ja ziemlich frickelig sein… :unamused: )

1 „Gefällt mir“

Danke, Marco13, dass du dich so reinhängst.

Ich denke, du hast das Problem vollständig erfasst. Und es scheint, als ob ich mit meiner Annahme leider richtig liege.

Project ist, so nenne ich es gerne, ein “Matroschka-Objekt”. Es schachtelt insgesamt 5 recht einfach gehaltene Objekttypen hierarchisch. Da bietet sich also sehr gut ein JTree zur Visualisierung an. Und wie du auch treffend bemerkt hast, habe ich das TreeModel über den steinigen Weg implementiert. Das war eine echte Erfahrung, weil es auch das erste TreeModel ist, das ich alleine gebaut habe. Im TreeModel wird das Projekt in seine Einzelteile zerlegt, damit man damit arbeiten kann. Und natürlich liegt hier wohl auch “der Hund begraben”. Es gibt nur dieses eine Objekt und alle Referenzen, die es benutzen/weiterreichen verweisen darauf.

Du meinst also, in diesem Fall sollte ich das Objekt kopieren. Gut, ich habe mich gestern bereits mit dem Thema Klonen beschäftigt. Das ist alles andere als trivial. Selbst der von dir vorgeschlagene Copy-Konstruktor erscheint mir nicht einfach. Ich bin mir auch nicht sicher, ob man mit einem Copy-Konstruktor im Projekt auch die tiefer liegenden Objekt mit kopiert.

Ich habe auch schon darüber nachgedacht, das Project-Objekt vor dem Weiterreichen in die “Editier-Abteilung” zu serialisieren. Ich bin mir nicht sicher und freue mich über jede weitere Hilfe oder Meinung.

Hmja… Serialisieren war etwas, was mir auch kurz in den Sinn kam, aber … ich bin kein Fan von Serialisierung für Dinge, die nicht damit zu tun haben, ein POJO auf die Platte oder über’s Netz zu verschicken. Ich denke, dass man sich da viele Fragen stellen (und - und das ist das Problem - sie auch beantworten :wink: ) muss, die man sich auch bei einer programmatischen Kopie stellen muss. Allerdings hat man über Serialisierung ggf. weniger Kontrolle. Da muss man sich dann auch überlegen, welche Fields transient sind, ob/wie man readObject und writeObject implementieren muss, und was man mit dieser lästigen List<ProjectListener> - Liste macht, wo als einer der Listener ggf. irgendeine Instanz einer anonymen inneren Klasse liegt, die zum MainController gehört…

Vielleicht wäre es - im spziellen, wenn z.B. Project schon Serializable ist - die mit Abstand einfachste Lösung. Die Kopie wäre dann ja mit einem newProject = readFromBuffer(writeToBuffer(oldProject)); erledigt. Aber im allgemeinen würde ich „Serialisierung als clone()-Ersatz“ recht skeptisch sehen.

(Auch wenn das nicht nicht als „Empfehlung“ oder ein „Abraten“ rüberkommen soll. Die Aussagen hier sind ja alle unter dem Vorbehalt, dass ich das Projekt nicht kenne…)


Wenn das TreeModel händisch implementiert ist, und praktisch eine "TreeModel-View auf das echte Project" ist, wirst du ja vermutlich auch mit einigen CellEditor-Klassen deinen Spaß gehabt haben. Es ist bisher natürlich schwer, abzuschätzen, wie schwierig es ist, die echten Änderungen „mitzuschreiben“.

(Nebenbei: Da einen echten UndoManager einzuspannen ist zwar cool, aber auch wieder ein bißchen aufwändig. Ich hatte das mal in einem Projekt gemacht, und wenn man erstmal ein bißchen vom Boilerplate-Code für die Verwaltung hat, geht es einigermaßen, aber in Kombination mit einem großen, hierarchischen Objekt und kleinen CellEditors in einem Tree könnte das auch wieder kompliziert werden)


Aber ganz allgemein sehe ich eben diese beiden Alternativen: Entweder auf einer Kopie arbeiten, oder jede einzelne Änderung so mitprotokollieren, dass sie rückgängig gemacht wird.

Eine Kopie des Objektes zu erstellen dürfte in den meisten Fällen einfacher sein. Wieder subjektiv: Ich versuche aber, Dinge wie Cloneable und clone() zu vermeiden. Genaugenommen kann ich mich nicht erinnern, das jemals echt implementiert zu haben. Aber wenn schon Joshua Bloch in „Effective Java“ (Item 11) da zur Zurückhaltung rät, hat man immerhin etwas, worauf man sich berufen kann :wink: .

Die Frage, ob Unterobjekte mitkopiert werden (sollen) stellt sich dabei praktisch immer. Grundsätzlich finde ich, dass Methoden/Konstruktoren da mehr Möglichkeiten bieten:

  • public static Project deepCopy(Project other) { ... }
  • public static Project shallowCopy(Project other) { ... }
  • public static void transferAllListeners(Project source, Project target) { ... }
    (letzteres würde schon ziemlich wehtun, aber wäre wenigstens „systematisch“ und klar…)

(Nochmal die Betonung: Alles unter Vorbehalt. Vielleicht ist ein clone() oder Serializable die bessere/einfachere Lösung…)

Um die Implementierung von CellEditoren bin ich Gott sei Dank herumgekommen. Die Arbeiten am Baum beschränken sich auf das Einfügen, Ändern, Löschen und verschieben von Knoten.

Damit das echte Kopien werden, müsste ich also einen Konstruktor bzw. eine Methode in den betreffenden Klassen schreiben, die tiefe Kopien ihrer gehaltenen Objekte erzeugen. Und natürlich auf von den Listen der enthaltenen “Kindknoten”.
Sowas habe ich noch nicht gemacht. Dürfte spannend werden.

Ich denke, darauf zielt deine Empfehlung ab. Und ich sehe das auch eher so wie du.