ChangeListener mit "Rückgängig" Funktion

Hallo Liebe byte-welt Community :),

ich bin in meinem JavaFX Projekt auf ein Problem gestoßen.
Und zwar habe ich eine Klasse FavString, sie stellt bspw. eine Kategorie mit dem namen text dar, isFavorite enthält die Information ob es als Favorit gespeichert wurde… Logisch

public class FavString
{
	private BooleanProperty isFavorite;
	private StringProperty text;
}```

Nun möchte ich aber wenn jemand die Favoriten in der GUI bearbeitet die Datenbank aktualisieren.
Der Benutzer kann beide Properties ändern, also text und isFavorite.



Ich habe mir auch schon überlegt auf beiden Properties jeweils einen ChangeListener zuzuweisen, doch muss innerhalb des Listeners immer der Inhalt von text immer bekannt sein (sonst kann DB nicht aktualisiert werden).
Das ist nur möglich wenn ich die ChangeListener als Inner-classes definiere (da ich sonst keinen Zugriff auf text habe), nicht wahr?
Dann müsste ich aber auch jedem FavString eine Referenz auf mein "Backend-Objekt" (bietet Datenbank Zugriff) übergeben..
Ich weiß nicht wie "sauber" das dann ist..


Ich habe das "gelöst" indem ich für FavString selbst einen ChangeListener erlaube/erstelle, was wiederum mindestens genauso unsauber auf mich wirkt und ein weiteres Problem hat:
Es werden bei jeder Änderung 2 neue FavStrings erzeugt. (Soll das mich überhaupt interessieren? Eine Änderung wird vom Benutzer in der GUI gemacht und keine 100 Änderungen/Sekunde)
Der FavString changeListener wird in einer anderen Klasse, welche ObservableLists mit FavStrings enthält, dem Favstring hinzugefügt.


FavString:
```/*
* Unwichtige Methoden wie getter, toString
* und konstruktoren zur besseren Lesbarkeit gelöscht konstruktoren haben nur übergebene Werte gesetzt..)
*/
public class FavString
{
    private BiConsumer<FavString, FavString> changeListener; //Der "ChangeListener" bzw genauer FavStringChangeListener
    private final StringProperty text = new SimpleStringProperty();
    private final BooleanProperty isFavorite = new SimpleBooleanProperty(false);

    private void fireChangeEvent(FavString oldVal, FavString newVal)
    {
        if (changeListener == null)
            return;

        changeListener.accept(oldVal, newVal);
    }

	//ChangeListener auf beide Properties setzen damit sie den übergebenen listener aufrufen..
	//Die Listener erstellen neue FavStrings damit der "neue" Listener Zugriff auf Informationen beider Properties hat..
    public void setChangeListener(BiConsumer<FavString, FavString> listener)
    {
        isFavorite.addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) ->
        {
            FavString oldStr = new FavString(oldValue, this.text.getValue());
            FavString newStr = new FavString(newValue, this.text.getValue());

            fireChangeEvent(oldStr, newStr);
        });
        text.addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) ->
        {
            FavString oldStr = new FavString(isFavorite.getValue(), oldValue);
            FavString newStr = new FavString(isFavorite.getValue(), newValue);

            fireChangeEvent(oldStr, newStr);
        });
		

        this.changeListener = listener;
    }
}```


FavStringChangeListener:
```public class FavStringChangeListener implements BiConsumer<FavString, FavString>
{
    private final TablePTC table; //Identifiziert Datenbanktabelle
    private final PTCAccessor ptc;  //bietet Zugriff auf Datenbank

    public FavStringChangeListener(TablePTC tbl, PTCAccessor ptca)
    {
        this.table = tbl;
        this.ptc = ptca;
    }

	/*
	* Keiner der übergebenen Parameter ist der Originale Favstring
	* Original könnte ich im Konstruktor übergeben, aber.. siehe Problem unten
	*/
    @Override
    public void accept(FavString oldVal, FavString newVal)
    {
		boolean oldIsFavorite = oldVal.getBooleanProperty().get();
        boolean newIsFavorite = newVal.getBooleanProperty().get();
		
        String oldText = oldVal.getStringProperty().get();
        String newText = newVal.getStringProperty().get();

        try
        {
            //isFavorite status changed
            if (oldIsFavorite != newIsFavorite)
                ptc.setFavoritePC(table, newIsFavorite, newText);
            else if(!oldText.equals(newText)) //text changed
                ptc.updatePTC(table, oldText, newText);
        }
        catch (SQLException ex)
        {
			//Fehlernachricht anzeigen
			//Werte von Original zurücksetzen
        }
    }
}

Bei beiden Ansätzen ergibt sich jedoch ein noch Problem, das weswegen ich überhaupt Frage:
Wenn eine Exception bei der Aktualisierung der Datenbank auftritt sollen die Daten des FavStrings zurückgesetzt werden.
Das kann ich im ChangeListener machen klar, aber wenn ich die Daten zurücksetzen will (also wieder verändern) wird wieder der ChangeListener aufgerufen was wohl zu einem undefinierbaren Problem führen kann…

Wie kann ich das umgehen?
Eine extra Methode erstellen um die Daten zu verändern ohne den Listener aufzurufen (Listener löschen, Daten ändern, Listener hinzufügen)?

Ist das überhaupt eine kluge Idee die Datenbank über den ChangeListener zu aktualisieren?
Wie kann ich das sonst machen? Der Controller-Klasse der Oberfläche das Backend-Objekt übergeben und die Datenbank von dort aus aktualisieren?

Bin über jede Hilfe, Kritik und Denkanstöße dankbar :slight_smile:

  • Beliar

Ohne mich mit JavaFX im Detail oder mit Datenbanken auch nur oberflächlich auszukennen, ein paar Gedanken:

Ob wirklich bei jeder Änderung die Datenbank aktualisiert werden muss, weiß ich nicht. Bei Swing gibt’s sowas wie einen InputVerifier (Java Platform SE 7 ) , mit dem man - vereinfacht gesagt - eine Operation durchführen kann, wenn das Eingabefeld verlassen werden soll. Speziell wenn man irgendwas tippt, und dann zwar nicht 100 aber vielleicht 10 mal pro Sekunde die DB aktualisiert werden soll, fände ich das etwas suspekt. WENN das gemacht werden soll, stellt sich mir auch die Frage, wie lange so ein Update (im schlechtesten Fall) dauern kann. Wenn es mehr als 100ms sein könnten, sollte man sich das genau überlegen (kann mich aber auch täuschen - alles eher so Bauchgefühl).

Die Frage, mit welcher Granularität die ChangeListener verwaltet werden, ist interessant, aber schwer pauschal zu beantworten. (Also die Frage, ob man ChangeListener an beide Fields hängt, oder EINE ChangeListener-Verwaltung für die FavString-Klasse baut). Unabhängig davon hat sich mir nicht ganz erschlossen, was du meintest mit

Das ist nur möglich wenn ich die ChangeListener als Inner-classes definiere (da ich sonst keinen Zugriff auf text habe), nicht wahr?

Eigentlich kommen in der Methode des ChangeListeners ja sowohl die Quelle des Events, als auch der alte und der neue Wert an - braucht man denn mehr?

Dass bei einem Fehler in der Datenbank ein „Rollback“ gemacht werden soll, wirft einige potentielle Probleme auf. Die „unendliche Kette“ von Benachrichtigungen kann man in so einem Fall oft recht pragmatisch durch ein flag lösen, GROB im Sinne von

Observable observable = new Observable();
observable.addChangeListener(new ChangeListener() {

    boolean rollingBack = false;

    @Override
    void changed(T oldValue, T newValue) {
        if (rollingBack) return;

        boolean success = writeToDatabase(newValue);
        if (!success) {
            rollingBack = true;
            observable.setValue(oldValue);
            rollingBack = false;
        }
    }
});

Über race conditions und Multithreading muss man sich ggf. Gedanken machen.

Viel wichtiger fände ich aber die Fragen: 1. Wie äußert sich der Fehler? Stell’ dir vor, du hast in einem Programm ein Textfeld wo Hallo drin steht, und du gehst in das Textfeld und tippst da ein Wel und auf einmal springt es zurück auf Hallo. Ähwas? :confused: Also, irgendeine Nachricht/Info wäre da sicher nicht verkehrt. Und 2. Wenn man mehrere ChangeListener da dran hängt, z.B. drei Stück, und der zweite ist der DB-Listener, der einen Fehler verursacht, dann bekommen die anderen beiden Listener ggf. ziemlich konfuse, unterschiedliche Änderungs-Infos.

Alles nicht sooo ernst nehmen, das sind nur Sachen, die mir spontan in den Sinn kamen. Vielleicht sagt noch jemand was „fundierteres“ dazu :slight_smile:

Also der ChangeListener muss nicht lokal sein, wenn das Datenfeld (FavString) Getter für seine Properties hat, weil das Datenfeld ja dem Listener über den Event übergeben wird (".getSource()"). Für dein Problem solltest du dir evtl. mal VetoablePropertyChangeSupport im Paket “java.beans” anschauen.

Vielen Dank für die Antworten!

Der Benutzer wird über einen Fehler auf jeden Fall benachrichtig, dennoch müssen die Daten zurückgesetzt werden.
Und habt beide recht, wenn ich das Original-Objekt mit übergebe kann ich über die getter den Wert von text holen… (wie konnt ich da nicht dran denken o.o)
Ich werde später sobald ich dazu komme beide eurer Lösungen ausprobieren.

Letzte Frage, von wegen best practice.
Sollte ich die/den ChangeListener direkt im FavString definieren und hinzufügen, oder wie ich es getan habe von außerhalb jedem einzeln hinzufügen?

[QUOTE=Beliar]Letzte Frage, von wegen best practice.
Sollte ich die/den ChangeListener direkt im FavString definieren und hinzufügen, oder wie ich es getan habe von außerhalb jedem einzeln hinzufügen?[/QUOTE]Hmm… an der Frage erkenne ich deutlich, dass du dir meinen Vorschlag unbedingt mal ansehen solltest.
Du hast eine Hauptklasse “DatenFeld” und diese hat erstens einen privaten Member von VetoablePropertyChangeSupport, zweitens die öffentlichen Methoden add- und removePropertyChangeListener, welche an den privaten Member aus erstens delegiert und drittens eine protected Methode firePropertyChange (bzw. fireVetoableChange) throws PropertyVetoException, welche ebenfalls an den privaten Member delegiert, aber nur von Klassen aufgerufen werden kann, welche “DatenFeld” erweitern. In den Settern der erweiternden Klassen muss dann vor dem eigentlichen Setzen der Properties in dem Feld selber nur noch firePropertyChange mit den Parametern Propertyname, aktueller und neuer Wert aufgerufen werden und die Exception gefangen werden. Wenn keine Exception fliegt, kann man auch den Wert in der Klasse ändern.


import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;

public abstract class DataField {
	private VetoableChangeSupport changeSupport = new VetoableChangeSupport(
			this);

	protected DataField() {
	}

	public void addPropertyChangeListener(VetoableChangeListener vcl) {
		changeSupport.addVetoableChangeListener(vcl);
	}

	public void removePropertyChangeListener(VetoableChangeListener vcl) {
		changeSupport.removeVetoableChangeListener(vcl);
	}

	protected void firePropertyChange(String propertyName, Object oldValue,
			Object newValue) throws PropertyVetoException {
		changeSupport.fireVetoableChange(propertyName, oldValue, newValue);
	}
}

class FavString extends DataField {
	private boolean isFavorite;
	private String text;

	public final boolean isFavorite() {
		return isFavorite;
	}

	public final void setFavorite(boolean isFavorite) {
		boolean oldValue = this.isFavorite;
		this.isFavorite = isFavorite;
		try {
			firePropertyChange("isFavirite", oldValue, isFavorite);
		} catch(PropertyVetoException e) {
			this.isFavorite = oldValue;
		}
	}

	public String getText() {
		return text;
	}

	public void setText(String text) {
		String oldValue = this.text;
		this.text = text;
		try {
			firePropertyChange("text", oldValue, text);
		} catch(PropertyVetoException e) {
			this.text = oldValue;
		}
	}
}```

@Spacerat : Ich fand der Vorschlag im ersten Moment etwas suspekt, habe aber nichts gesagt, weil ich mir kein Urteil anmaßen kann und will, aber … diese ganzen „Observable…“-Klassen sind bei JavaFX schon sehr stark in eine Infrastruktur eingebettet (mit der ich mich - und das ist der Grund für meine Zurückhaltung - schlicht (noch) nicht auskenne) (Zeit, Zeit, Zeit…). Ob man da nun den Veto-Support aus den Bean so ohne weiteres dranflaschen kann oder sollte… da bin ich nicht so sicher. Es gibt zwar explizit solche Br… (oder Kr?)… ückenklassen wie http://docs.oracle.com/javase/8/javafx/api/javafx/beans/property/adapter/JavaBeanStringProperty.html , aber ob man sowas „in JavaFX nicht „„üblicherweise““ anders macht“, weiß ich nicht (sowas wie http://docs.oracle.com/javase/8/javafx/api/javafx/beans/InvalidationListener.html scheint’s nicht ganz zu sein, aber … fast?! :confused: Hm… wollte ich nur erwähnen, nochmal: Ich hab’ da keine Ahnung von der API an sich.

Die Allgemeine Frage, wie „feingranular“ man seine Modell-Klassen aufbaut, stellt sich aber immer. Eine pauschale Emfehlung kann man da kaum geben. Wenn der „FavString“ quasi DIE zentrale Klasse ist, um die sich alles dreht, könnte man sie als Modell machen, und Listener mit Methoden wie „textChanged()“ und „favoriteStatusChanged()“ anbieten. Aber zugegeben bin ich mir da AUCH nicht sicher, inwieweit die „Best Practices“ dort durch das Vorhandensein dieser feingranularSTen Observables anders sind, als wenn es die NICHT gibt…

(EDIT: Noch einer der erstbesten Links, die bei meiner Websuche nach „Vetoable JavaFX Observer“ aufgepoppt sind http://tbeernot.wordpress.com/2011/04/02/javafx-2-0-ea-binding/ - das geht thematisch schon in diese Richtung, aber ich hab’s noch nicht gelesen (ist aber ist schon recht alt))

@Marco13 : Feingranular klappt mit den Bean-Klassen auch, man kann die Properties ja mit Strings benennen und sogar Listener für spezielle Properties erstellen und adden. Ich wüsste keinen Grund, warum man Beans in JavaEX nicht mehr verwenden sollte, das Eventsystem hat sich ja dort nicht allzuviel geändert. Mag aber auch sein, dass ich mich da irre, weil ich bisher JavaFX nur in Swing eingebettet habe. Aber zu reinen JavaFX-Anwendungen dürfte der Unterschied ja nicht allzuhoch sein. Beans gehören ja auch zum Standard gängiger VMs inklusive Dalvik (Android), das sollte also kein Problem sein. Ansonsten bleibt einem nur, sich in JavaFX einen ähnlichen Mechanismus zu suchen oder im Zweifelsfalle selbst zu implementieren. Ich hatte Beans ja eigentlich auch nur als Denkanstoss aufgeführt. Zumindest ist dieses schon mal der Richtige Weg und zwar ganz konkret asynchron über einen EventDispatchThread.