The Mysterious Table Viewer

Heute soll es um meinen speziellen Freund, den „TableViewer“ gehen, speziell darum, wie man einen TableViewer implementiert, der unter Umständen etwas komplexer ist (mit Comboboxen etc…).

Witzigerweise ist es nämlich wie mit beinahe allem, es gibt hierzu keine brauchbaren Tutorials, also muss man sich selbst helfen und die try-and-error-Variante fahren.

Da ich mich eine Zeitlang wirklich enorm über dieses Thema geärgert habe, habe ich beschlossen, dieses Thema in einem kleinen Eintrag zu erläutern (nicht zuletzt für mich selbst, damit ich beim nächsten Mal weiß wo ich nachzuschauen habe).

Als Beispiel implementieren wir einfach mal einen TableViewer, der einer Liste einer bestimmten Person aus einer Liste eine bestimmte Rolle zuordnen kann.

Hierzu bauen wir uns zuerst wieder das typische Personen-Modell zusammen:

Klasse Person:

public class Person {

private String name;

private Rolle rolle;

public Person(String name, Rolle rolle) {

super();

this.name = name;

this.rolle = rolle;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Rolle getRolle() {

return rolle;

}

public void setRolle(Rolle rolle) {

this.rolle = rolle;

}

public String toString() {

return getName() + getRolle().toString();

}}

Enum Rolle:

public enum Rolle {

ADMIN, CUSTOMER, USER

}

Klasse PersonService:

public class PersonService implements IPersonService {

private static PersonService service = new PersonService();

private IObservableList persons = new WritableList();

public PersonService() {

persons.add(new Person(„Hans“, Rolle.ADMIN));

persons.add(new Person(„Fritz“, Rolle.CUSTOMER));

persons.add(new Person(„Gerhard“, Rolle.USER));

persons.add(new Person(„Harald“, Rolle.ADMIN));

}

@SuppressWarnings(„unchecked“)

public List<Person> getPersons() {

return (List<Person>) Collections.checkedList(persons, Person.class);

}

public IObservableList observePersons() {

return persons;

}

public void addPerson(Person person) {

persons.add(person);

}

public static PersonService getInstance() {

return service;

}

public List<Rolle> getRollen() {

return Arrays.asList(Rolle.values());

}

}

Zusätzlich bauen wir einen View, der eine Tabelle bereitstellt, die:

  1. Alle User anzeigt
  2. Die Möglichkeit bietet, die Rolle eines Users zu ändern
  3. Die Möglichkeit bietet, neue User anzulegen
  4. Mit Databinding arbeitet

Zunächst mal bearbeiten wir die createPartControl-Methode des Views, und legen einen TAbleViewer an:

final TableViewer viewer = new TableViewer(container);

viewer.getTable().setHeaderVisible(true);

viewer.getTable().setLinesVisible(true);

Der TableViewer soll 2 Columns haben (Person und Rolle):

TableViewerColumn person = new TableViewerColumn(viewer, SWT.BORDER);

person.getColumn().setText(„Person“);

person.getColumn().setWidth(100);

TableViewerColumn rolle = new TableViewerColumn(viewer, SWT.BORDER);

rolle.getColumn().setText(„Rolle“);

rolle.getColumn().setWidth(100);

Nun setzen wir den Label- und ContentProvider. Da wir mit DataBinding arbeiten, bietet sich der ObservableListContenentProvider an (der übrigens ausgezeichnet funktioniert!!)

Damit das jedoch richtig funktioniert, passen wir den PersonService folgendermaßen an:

public class PersonService implements IPersonService {

private IObservableList persons = new WritableList();

public PersonService() {

persons.add(new Person(„Hans“, Rolle.ADMIN));

persons.add(new Person(„Fritz“, Rolle.CUSTOMER));

persons.add(new Person(„Gerhard“, Rolle.USER));

persons.add(new Person(„Harald“, Rolle.ADMIN));

}

@SuppressWarnings(„unchecked“)

public List<Person> getPersons() {

return (List<Person>)Collections.checkedList(persons, Person.class);

}

public IObservableList observePersons(){

return persons;

}

public void addPerson(Person person) {

persons.add(person);

}

public static PersonService getInstance() {

return new PersonService();

}

}

Jetzt können wir die ObservableList aus dem Service direkt als Input in den TableViewer verwenden, und alle Änderungen an der PersonenListe werden sofort in der Tabelle widergespiegelt.

viewer.setLabelProvider(new LabelProvider());

viewer.setContentProvider(new ObservableListContentProvider());

viewer.setInput(PersonService.getInstance().observePersons());

Startet man die Anwendung jetzt, sieht das ungefähr so aus:

tableviewer

Zumindest ist es eine Tabelle…

Ok, wir müssen natürlich definieren, in welche Spalte welche Werte angezeigt werden sollen, das erledigt der LabelProvider, also ändern wir das folgendermaßen ab:

viewer.setLabelProvider(new ITableLabelProvider() {

@Override

public Image getColumnImage(Object element, int columnIndex) {

return null;

}

@Override

public String getColumnText(Object element, int columnIndex) {

switch (columnIndex) {

case 0:

return element.toString();

case 1:

return ((Person) element).getRolle().toString();

default:

return „“;

}

}

@Override

public void addListener(ILabelProviderListener listener) {

// TODO Auto-generated method stub

}

@Override

public void dispose() {

// TODO Auto-generated method stub

}

@Override

public boolean isLabelProperty(Object element, String property) {

// TODO Auto-generated method stub

return false;

}

@Override

public void removeListener(ILabelProviderListener listener) {

// TODO Auto-generated method stub

}

});

Noch eleganter wäre, nicht dem TableViewer an sich, sondern jeder Column einen eigenen LabelProvider zur Verfügung zu stellen.

column.setLabelProvider(new ColumnLabelProvider(){

}

Wie dem auch sei, Neuer Versuch:

tableviewer_21

Ok, was wir jetzt aber wollen, ist zum Einen, dass man den Namen der Person editieren kann (durch einfache Eingabe) und zum Anderen wollen wir die Rolle über eine ComboBox auswählen.

Jetzt wirds spannend, weil hier die Dokumentation im Web mehr als spärlich wird.

Eclipse bietet hierzu die Klasse „EditingSupport“, der JavaDoc Kommentar ist hierzu mehr als sprechend:

„EditingSupport is the abstract superclass of the support for cell editing.“

Spitze! und so zieht sich das durch, man findet kein!! ich wiederhole : KEIN wirklich brauchbares Beispiel im Web, bis heute!!

Ok, zurück zum Thema:

Was das Editieren von Columns in einer Tabelle ermöglicht sind sogenannte CellEditoren. Diese Editoren können Text-, COmbo-,Checkbox etc.. sein.

Was uns hier interessiert ist zum Einen der Texteditor zum Eingeben des Personennamens und ein Comboeditor zum Eingeben der Rolle.

Zunächst die Namenseingabe:

person

.setEditingSupport(new ObservableValueEditingSupport(viewer,

ctx) {

private TextCellEditor textEditor;

@Override

protected IObservableValue doCreateCellEditorObservable(

CellEditor cellEditor) {

return SWTObservables.observeText((Text) cellEditor

.getControl(), SWT.Modify);

}

@Override

protected IObservableValue doCreateElementObservable(

Object element, ViewerCell cell) {

return BeansObservables.observeValue(element, „name“);

}

@Override

protected CellEditor getCellEditor(Object element) {

if (textEditor == null) {

textEditor = new TextCellEditor((Composite) viewer

.getControl());

}

return textEditor;

}

});

Man sieht, im Prinzip muss man nur 2 Methoden implementieren, die jeweils ein ObservableValue für den CellEditor (also entweder ein Text oder eine Combo) bereitstellt, und eine Methode, die ein ObservableValue für das beabachtete Objekt (in diesem Fall ein Person-Objekt bereitstellt).

Klickt man jetzt auf ein Feld in der Tabelle ändert sich dieses in ein Text-Feld und man kann den Wert editieren!

tableviewer_3

Problem ist, drückt man jetzt auf Enter wird zwar der Wert im Modell geändert, nicht jedoch automatisch der Viewer refreshed, d.h. dass weiterhin der alte Wert angezeigt wird, wenn man sich nicht im Edit-Modus befindet.

Hierzu kann man folgenden Listener registrieren, der das bewerkstelligt.

viewer.getColumnViewerEditor().addEditorActivationListener(

new ColumnViewerEditorActivationListener() {

@Override

public void afterEditorActivated(

ColumnViewerEditorActivationEvent event) {

// TODO Auto-generated method stub

}

@Override

public void afterEditorDeactivated(

ColumnViewerEditorDeactivationEvent event) {

viewer.refresh();

}

@Override

public void beforeEditorActivated(

ColumnViewerEditorActivationEvent event) {

// TODO Auto-generated method stub

}

@Override

public void beforeEditorDeactivated(

ColumnViewerEditorDeactivationEvent event) {

// TODO Auto-generated method stub

}

});

So, weiter gehts mit der Rollen-Column, hier haben wir im Prinzip nochmals das Gleiche Prozedere, nur dass wir hier einen ComboViewer in einem TableViewer verbauen wollen.

rolle.setEditingSupport(new ObservableValueEditingSupport(viewer, ctx) {

private ComboBoxViewerCellEditor cellEditor;

@Override

protected IObservableValue doCreateCellEditorObservable(

CellEditor cellEditor) {

return ViewersObservables

.observeSingleSelection(((ComboBoxViewerCellEditor) cellEditor)

.getViewer());

}

@Override

protected IObservableValue doCreateElementObservable(

Object element, ViewerCell cell) {

return BeansObservables.observeValue(element, „rolle“);

}

@Override

protected CellEditor getCellEditor(Object element) {

if (cellEditor == null) {

cellEditor = new ComboBoxViewerCellEditor(

(Composite) viewer.getControl());

cellEditor.setContenProvider(new ArrayContentProvider());

cellEditor.setLabelProvider(new LabelProvider());

cellEditor.setInput(Rolle.values());

}

return cellEditor;

}

});

tb4

Ok, zuletzt brauchen wir noch die FUnktionalität, eine neue Person anzulegen.

Hierzu hätte ich gerne ein Contextmenü. DAs ist ganz einfach realisiert:

private void initContextMenu() {

MenuManager manager = new MenuManager();

viewer.getControl().setMenu(

manager.createContextMenu(viewer.getControl()));

manager.add(new Action(„Person hinzufügen“) {

@Override

public void run() {

PersonService.getInstance().addPerson(

new Person(„“, Rolle.USER));

viewer.refresh();

}

});

}

tb51

By the way, es hindert uns hier auch nichts daran, dieses Context Menü über das Menüframework zu erweitern.

Hierzu müssen wir lediglich das Menü über die ViewSite registrieren, damit wir global darauf zugreifen können.

getSite().registerContextMenu(mgr,null);

Zu beachten ist hier lediglich, dass innerhalb eines Parts jedes ContextMenu unter der ID des Parts registriert wird.

Um das Context Menü deklarativ zu bevölkern braucht man jetzt nur noch etwas wie:

<menuContribution
locationURI=“popup:de.md.commands.menuview“>
<command
commandId=“de.md.commands.helloworld“
label=“Hallo Popup“
style=“push“>
</command>
</menuContribution>


Mit einem Viewer, der den EditorSupport verwendeet, hat man das Problem, dass dieser voraussetzt, dass
der TableViewer mit dem Flag „SWT.FULL_SELECTION“ erzeugt wurde.
Oft ist es aber nicht erwünscht, dass man die ganze Zeile selektiert, sondern oft möchte man nur eine einzelne
Zelle selektieren (am besten noch so wenig wie möglich mit der Maus sondern per Tastatur).
Hier stösst man schnell an die Grenzen des mit dem normalen Tableviewer machbaren.

Doch wie so oft gibt es hier einige mehr als nützliche Klassen, die ich im folgenden mal kurz dokumentieren möchte.

final TableViewerFocusCellManager focusCellMgr = new TableViewerFocusCellManager(
viewer, new FocusCellOwnerDrawHighlighter(viewer));

Der TableViewerFocusCellManager bietet die Möglichkeit, das Highlighting der Zellen anzupassen, damit nicht immer
die gesamte Zeile markiert ist, sondern nur diejenige, die wir gerade auch wirklich selektiert haben. Das macht der
FocusCellOwnerDrawHighlighter für uns. Die genauere Funktionsweise dahinter braucht uns zunächst nicht zu interessieren,
da diese Zeile schon alles macht, was wir brauchen.

Weiter gehts:
ColumnViewerEditorActivationStrategy actStrategy = new ColumnViewerEditorActivationStrategy(
viewer) {
protected boolean isEditorActivationEvent(
ColumnViewerEditorActivationEvent event) {
return event.eventType == ColumnViewerEditorActivationEvent.TRAVERSAL
|| event.eventType == ColumnViewerEditorActivationEvent.MOUSE_DOUBLE_CLICK_SELECTION
|| event.eventType == ColumnViewerEditorActivationEvent.MOUSE_CLICK_SELECTION
|| (event.eventType == ColumnViewerEditorActivationEvent.KEY_PRESSED);
}

};

Hier wirds dann interessant, denn oft hat man von Fachbereichen die Anforderung, ich möchte aber, dass wenn ich hier auf Enter drücke (oder hoch, oder runter, oder was ganz anderes), dass dann die Combobox aufgeht….
Hierfür gibts so etwas wie eine ColumnViewerEditorActivationStrategy.
Die hat eine CallbackMethode isEditorActivationEvent, die im Prinzip bei jeder User-Interaktion überprüft, ist das Event gewünscht um den Viewer
zu triggern? Wenn ja, CellEditor aktivieren, wenn nein… weiter.
Im obigen Beispiel verwenden wir das Durchlaufen mit Tab (Traversal), MouseEvents und KeyEvents als Activation Events für den Celleditor.

Das ganze binden wir jetzt noch an unseren TableViewer

TableViewerEditor.create(viewer, focusCellMgr, actStrategy,
ColumnViewerEditor.TABBING_HORIZONTAL
| ColumnViewerEditorActivationEvent.TRAVERSAL
| ColumnViewerEditor.KEYBOARD_ACTIVATION);

Hier hat man zusätzlich noch die Möglichkeit, über Flags zu konfigurieren, wie die einzelnen Zellen durchlaufen werden sollen.

ColumnViewerEditor.TABBING_HORIZONTAL
Bei durchlaufen mit Tab soll immer eine Zelle horizontal weitergesprungen werden.

ColumnViewerEditor.KEYBOARD_ACTIVATION
KeyboardActivation einschalten

ColumnViewerEditorActivationEvent.TRAVERSAL
CellAktivierung beim Durchlaufen einschalten.

Das ist so einfach!! und funktioniert wunderbar, nur ein Problem gibts damit. Wenn man das Horizontale Tabbing einschaltet, dann kommt man
mit Tab nie wieder aus dem TableEditor heraus. Wenn jemand hierfür eine brauchbare Lösung hat, bitte immer her damit!!

Eine beispielhafte Implementierung von Herold, einem Leser, findet man hier, vielen Dank dafür!

Advertisements

20 Gedanken zu „The Mysterious Table Viewer

  1. mhoe

    Hey, netter Blog!

    Das mit dem Editing Support und dem CellLabelProvider ist tatsächlich recht nützlich.

    Leider hat das ganze einen kleinen Haken: wenn man auf diese Weise Buttons, ComboBoxes, etc. in in eine große Tabelle einfügt, dann kann das zu Performance/Ressourcen-Problemen führen: Solche Controls werden ja (wie bei SWT üblich) z.B. durch Betriebsystem
    Buttons realisiert. Der TableViewer ist nun leider nicht intelligent genug, nicht dargestellte Controls wieder freizugeben. Deshalb können da schnell mal tausende OS-Buttons erzeugt werden…

    Bei grossen Tabellen ist wohl besser, Buttons mittels OwnerDraw selbst zu zeichnen. Ich hab’s noch nicht ausprobiert, aber ein Beispiel gibts hier:
    http://wiki.eclipse.org/index.php/JFaceSnippets#Snippet010OwnerDraw

    PS: Splitshade? Den kenn ich doch…;-)

    Gruß,

    MH

    Antwort
  2. SplitShade

    Tatsächlich;-)?
    Hey mhoe, wie gehts dir??
    Lange nichts mehr gehört.

    Du hast recht, diese Problematik ist mir auch schon aufgefallen, und das ist nicht zu vernachlässigen, allerdings gefällt mir der OwnDraw-Ansatz auch nicht wirklich.

    andererseits ist es ja so, dass aufgrund der entsprechenden Abfrage immer der gleiche Button verwendet wird,
    also haben wir prinzipiell nur einen anstatt n Comboboxen etc. Das müsste man mal genauer analysieren…

    Ich werde mir das bei GElegenheit (wenn ichs mal brauchen sollte;-)) anschauen.

    Antwort
  3. Peter I.

    Eine Frage habe ich zu dem Kontextmenü: wie genau funktioniert das? Ich rufe die init-Methode nach den ganzen Einstellungen zum Viewer auf und bekomme aber immer eine Exception an der Stelle:
    viewer.getControl().setMenu(
    manager.createContextMenu(viewer.getControl()));

    Hoffe du kannst mir helfen…. ansonsten echt super, gibt so wenig Infos zu dem TableViewer

    Antwort
  4. SplitShade

    Hi Peter,

    wenn Du ein wenig genauer wirst (welche Exception, was genau passiert?)
    kann ich Dir vielleicht helfen.

    Antwort
  5. sth_Weird

    super erläuterung, bin eher zufällig drauf gestoßen, hab aber schon öfters mit dem tableviewer gekämpft. eigentlich mag ich ja c# viieeel lieber als java vor allem was die gui programmierung anbelangt, mit dem table viewer könnte mich java locken da der als viel flexibler angepriesen wird wie der datagridview bei c#, und dieses control brauch ich häufig. aber bei den besch… tutorials die es dazu gibt tun mir alle leid die sich damit rumschlagen müssen. dies ist endlich mal ein guter beitrag, ich habe ihn noch nicht ausprobiert aber durchgelesen und er macht genau das was ich brauch mit den comboboxen, ich freu mich das auszuprobieren und wenn’s klappt dann hat java mal wieder ein paar pluspunkte gut gemacht 🙂

    Antwort
    1. splitshade Autor

      Hi,

      es freut mich immer, wenn ich den Absprung von C# nach Java ein wenig Unterstützen kann,
      ich habe leider überhaupt keine Erfahrung mit C#, deswegen kann ich dazu nicht allzu viel sagen,
      die Einfachheit des TableViewers ist aber definitiv schön:-)

      Antwort
  6. splitshade Autor

    Falls es jemanden interessiert, ich habe den Artikel gerade erweitert um einige coole Möglichkeiten, die Tastatursteuerung betreffend

    Antwort
  7. herold

    Ich habe Probleme, deinen Code zum Laufen zu kriegen, da die Variable DataBindingContext ctx im setEditingSupport() nirgends erklärt ist.
    Mit einem neuen DataBindingContext an dieser Stelle kriege ich nur editierbare Zellen, die beim Verlassen wieder ihren alten Wert einnehmen.

    Toll wäre es auch, wenn du den Code posten könntest. Das Nachformatieren und Organisieren der Importe ist für Anfänger doch recht zeitraubend.

    Antwort
    1. splitshade Autor

      Hallo, vielen Dank für den Hinweis, ich werde versuchen, den Code in den nächsten Tagen mal bereit zu stellen (wenn ich ihn denn noch irgendwo finde;-)

      prinzipiell müsste es mit einem neuen DatabindingContext funktionieren, evtl. musst du den Viewer refreshen, nachdem die Daten
      verändet wurden, versuch doch in der Methode setValue einfach mal ein fröhliches „cellEditor.getViewer().refresh()“

      Antwort
      1. herold

        Vielen Dank für die schnelle Antwort.
        Ich bin aber leider nicht weiter gekommen.
        Welches setValue() meinst du? Im Beipielcode finde ich es nicht.

  8. Martin

    Hallo,

    für alle die es interessiert, genau zu dem hier veröffentlichten Blogeintrag wird im nächsten Eclipse-Magazin ein Artikel von mir erscheinen. Über Kommentare freue ich mich natürlich(auch zum Artikel;-))

    Antwort
  9. Thomas Malcom

    First, thanks for the great tutorial
    I implemented your example in an RCP application to test the databinding.
    The problem is that the synchronisation between the view and model is not working.
    i initialized the ctx variable via
    DataBindingContext ctx = new DataBindingContext();

    could you please send me (or post) me the whole example?
    Thanking you very much in advance

    best regards thomas

    Antwort
    1. herold

      Hi, Thomas!

      I stepped into the same problems, see my conversation above.
      With splitshades hints, I finally got it working. I still have a working project that I can send you.

      If you are interested, send your e-Mail-address to splitshade.5.kasulzke@spamgourmet.com

      @splitshade: how about posting my version here? please contact me by mail.

      Antwort
      1. splitshade Autor

        Hi Herold,

        ich kann Deinen Sourcecode hier nicht hosten, wenn Du mir aber einen Link zuschickst, häng ich diesen in die Notes unten im Artikel.

        Grüße

        Martin

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s