Das NetBeans-Lookup für Nicht-RCP-Projekte benutzen
Im Artikel Java-Aktionen richtig benutzen überlegte ich, wie Java-Action
s ihre Daten erhalten und sich aktivieren oder deaktivieren abhängig davon, ob es relevante Daten gibt. Ich wollte nicht Swing-Component
s nach Daten befragen, die Actions dort als Listener registrieren und auf Statusänderungen des GUI reagieren. Als Lösung schlug ich Lookups vor – Container mit Elementen, die Beobachter benachrichtigen, falls sich ihr Inhalt ändert.
Ein solches Lookup benutzen NetBeans Platform-Anwendungen, es kann in Nicht-Platform-Anwendungen eingesetzt werden. Die Projekte integrieren dazu das JAR org-openide-util-lookup.jar
, es ist unterhalb des NetBeans-Installationsverzeichnisses im Verzeichnis platform/lib
.
Zugrunde liegende Idee
- Ein Anwendungsmodul hat einen oder mehrere Lookups
- Wird im GUI etwas ausgewählt oder abgewählt, gelangen relevante Daten, die das GUI-Element repräsentiert, in eines der Lookups
- Die Actions beobachten das Lookup: Ist es leer, sind sie deaktiviert, enthält es Elemente, sind sie aktiviert. In
actionPerformed()
benutzen Actions die Elemente des Lookups.
Das NetBeans-Lookup benachrichtigt Beobachter nur, falls Daten eine bestimmten Typs im Lookup sind: Eine Action, die Personen in eine Datenbank speichert, will in der Regel nicht wissen, ob und wieviele Bücher im Lookup sind.
Beispiels-Implementierung
Ich habe ein Beispiels-Projekt implementiert und in ein ZIP-Archiv gepackt. Nach dem Entpacken kann es mit NetBeans geöffnet werden. Das Kontextemenü des Projects-Window bietet Run oder Debug an (auf die Klasse Main
mit der rechten Maustaste klicken).
(Nur) Der Übersichtlichkeit wegen verzichtete ich bei den Beispielen auf Parameterüberprüfung, in „Produktions-Code“ sollten die Parameter überprüft werden, beispielsweise eine NullPointerException geworfen, falls null überreicht wird anstelle einer erwarteten Objektreferenz.
LookupAction
Die LookupAction
habe ich kopiert aus dem Projekt JPhotoTagger. LookupAction
s beobachten einen Lookup. Bei Aufruf von actionPerformed()
ruft die LookupAction
bei den spezialisierten Actions die abstrakte Methode actionPerformed(Collection<? extends T> lookupContent)
auf, mit isEnabled(Collection<? extends T> lookupContent)
können sie entscheiden, ob sie aktiviert sein sollen, in der Regel werden sie true
liefern, falls der Lookup-Content Elemente enthält.
public abstract class LookupAction<T> extends AbstractAction implements LookupListener { private final Class<? extends T> lookupResultClass; private final Lookup lookup; private Lookup.Result<? extends T> lookupResult; protected LookupAction(Class<? extends T> lookupResultClass, Lookup lookup) { this.lookupResultClass = lookupResultClass; this.lookup = lookup; setLookupResult(); } protected abstract boolean isEnabled(Collection<? extends T> lookupContent); protected abstract void actionPerformed(Collection<? extends T> lookupContent); private void setLookupResult() { if (lookupResult == null) { lookupResult = lookup.lookupResult(lookupResultClass); lookupResult.addLookupListener(this); resultChanged(null); } } @Override public void resultChanged(LookupEvent evt) { setEnabledInDispatchThread(); } private void setEnabledInDispatchThread() { final boolean isEnabled = isEnabled(lookupResult.allInstances()); if (EventQueue.isDispatchThread()) { setEnabled(isEnabled); } else { EventQueue.invokeLater(new Runnable() { @Override public void run() { setEnabled(isEnabled); } }); } } protected Collection<? extends T> getLookupContent() { return (lookupResult == null) ? Collections.<T>emptyList() : lookupResult.allInstances(); } @Override public void actionPerformed(ActionEvent e) { setLookupResult(); actionPerformed(lookupResult.allInstances()); } }
Eigenes Lookup
Die Klasse ModifiableLookup
liefert ein Lookup und bietet Methoden an, dessen Inhalt zu modifizieren.
public final class ModifiableLookup implements Lookup.Provider { private final InstanceContent content = new InstanceContent(); private final Lookup lookup = new AbstractLookup(content); @Override public Lookup getLookup() { return lookup; } public void add(Object content) { this.content.add(content); } public void remove(Object content) { this.content.remove(content); } public void set(Collection<?> content) { this.content.set(content, null); } }
Beispiel: Ausgewählte Listenelemente in das Lookup einfügen
Will ich ausgewählte Items einer JList
in das Lookup einfügen, kann ich folgende Klasse benutzen:
public final class LookupListSelectionListener implements ListSelectionListener { private final ModifiableLookup lookup; public LookupListSelectionListener(ModifiableLookup lookup) { this.lookup = lookup; } @Override public void valueChanged(ListSelectionEvent e) { if (!e.getValueIsAdjusting()) { JList list = (JList) e.getSource(); List<Object> selectedValues = Arrays.asList(list.getSelectedValues()); lookup.set(selectedValues); } } }
Beispielsklassen, die in das Lookup sollen
public final class Human { private final String name; public Human(String name) { this.name = name; } public String getName() { return name; } @Override public String toString() { return name; } }
public class Dog { private final String name; public Dog(String name) { this.name = name; } @Override public String toString() { return name + " (Wuff)"; } }
public final class CreatureListModel extends DefaultListModel { private static final long serialVersionUID = 1L; public CreatureListModel() { addElement(new Human("Anton")); addElement(new Human("Berta")); addElement(new Human("Cäsar")); addElement(new Human("Dora")); addElement(new Human("Emil")); addElement(new Dog("Fiffi")); addElement(new Dog("Waldi")); } }
Kommunikation über ein zentrales Lookup
Ein Anwendungsmodul kann Lookups beispielsweise so bereitstellen:
public final class Lookups { public static final Lookups INSTANCE = new Lookups(); private final ModifiableLookup humansLookup = new ModifiableLookup(); public ModifiableLookup getHumansLookup() { return humansLookup; } private Lookups() { } }
Erweiterte LookupAction
Die erweiterte LookupAction
interessiert sich (nur) für Instanzen von Human
public final class ShowHumansInLookupAction extends LookupAction<Human> { private static final long serialVersionUID = 1L; public ShowHumansInLookupAction() { super(Human.class, Lookups.INSTANCE.getHumansLookup().getLookup()); putValue(Action.NAME, "Zeige ausgewählte Menschen"); } @Override protected boolean isEnabled(Collection<? extends Human> humans) { return !humans.isEmpty(); } @Override protected void actionPerformed(final Collection<? extends Human> humans) { JOptionPane.showMessageDialog(null, humans); } }
In einem GUI benutzen
Die entscheidenden Zeilen in einem GUI, die Action ist einem Button zugeordnet, Model und ListSelectionListener der Liste:
list.setModel(new CreatureListModel()); list.addListSelectionListener(new LookupListSelectionListener(Lookups.INSTANCE.getHumansLookup())); button.setAction(new ShowHumansInLookupAction());
Worauf achten beim Ausführen der Beispielsanwendung
- Ist nicht ausgewählt, setzt sich die Action automatisch auf deaktiviert, der Button mit der Action ist deaktiviert („ausgegraut“)
- Werden ein oder mehrere Menschen ausgewählt, ist die Action aktiviert, der Button reagiert auf Klicks (Betätigung)
- Sind nur Hunde ausgewählt, ist der Button deaktiviert
- Die Action erhält nur Menschen: Sind Hunde und Menschen ausgewählt, zeigt bei Klick auf den Button die Action den Lookup-Content an – es sind nur die erwarteten Objekte darin
Vorteile
- Die Action ist nicht an ein GUI gebunden (sie benötigt nur ein Lookup). Sie ist nicht anzupassen, falls später beispielsweise ein
JTree
die Geschöpfe anzeigt (getrennt nach Mensch und Tier). Es könnte auch ein Dialog zum Ändern der Daten eines Menschen benutzt werden: Der „Mensch im Dialog“ gelangt in das Lookup, das die Action benutzt. - Die Action fragt nicht nach den benötigten Daten, sie erhält sie
- Die Action aktiviert und deaktiviert sich automatisch
- Das Lookup könnte über Modulgrenzen hinweg benutzt werden, beispielsweise über das Java-Service Provider Interface (SPI) oder ein globales Lookup, wie es die NetBeans-Platform anbietet. Die Actions anderer Module sind nur mit dem passenden Lookup zu verknüpfen, sie müssen nicht wissen, woher die Daten kommen (wie sie an diese gelangen).
- Actions sind nur ein Beispiel, es können beliebige Klassen
LookupListener
sein