Eclipse Sapphire

Deklaratives UI-Design mit Sapphire

Nepomuk Seiler

UI-Programmierung folgt oft einem ähnlichen Schema: Widgets auswählen, initialisieren, layouten und Synchronisierung mit dem Modell. Das führt zu viel Boilerplate-Code, unter dem die eigentliche Absicht vergraben wird. Eclipse Sapphire schafft hier Abhilfe.

Sapphire ist ein Eclipse-Plug-in im Incubator-Status [1], das die Erstellung einheitlicher und wartbarer UIs vereinfachen will, indem das UI soweit wie möglich deklarativ beschrieben werden kann. Als Hauptkonzept wird dabei der Property-Editor verwendet. Dieser Editor wird passend zur darunter liegenden Property gerendert, zum Beispiel eine Combobox für ein Feld mit sich dynamisch ändernden, zulässigen Werten. Für das UI-Layout unterstützt Sapphire eine breite Palette an SWT/JFace und Eclipse-spezifischen Komponenten wie das Eclipse Forms API, den grafischen Editor Graphiti, JDT-Funktionen und die TabbedPropertyView. Als Modell unterstützt Sapphire im Moment nur XML-Dateien, die durch ein annotiertes Interface beschrieben werden. Die Klassen werden mithilfe des Eclipse Annotation Processors (APT) generiert. In diesem Artikel werden wir anhand eines kleinen Modelleditors einige Konzepte von Sapphire betrachten, die einen leichten Einstieg erleichtern sollen. Das Modell stellt einen einfachen Graph mit Knoten und Kanten dar, wobei jeder Knoten einen Namen, eine factoryId, einen Typ und eine Liste von Eigenschaften besitzt. Die Kanten besitzen ebenfalls einen Namen, Ziel- und Quellknoten, eine Gewichtung und einen Ziel- und Quell-Port. Das Projekt befindet sich unter [2].

Erst das Modell, …

Das Modell wird durch eine Reihe von annotierten Interfaces beschrieben. Listing 1 zeigt ein einfaches Modell für das property-Element, das ein Knoten in seinem properties-Listenelement hält. Das statische Feld TYPE dient später zur Instanzierung dieses ModelElements, wodurch die generierte Implementierung nicht bekannt sein muss. Das @ in @XmlBinding(path = „@key“) bezeichnet dieses Feld als Attribut und nicht als Element.

Listing 1
@GenerateImpl
public interface IProperty extends IModelElement {

  ModelElementType TYPE = new ModelElementType(IProperty.class);

  /* === Key === */

  @XmlBinding(path = "@key")
  @Label(standard = "key")
  @Required
  ValueProperty PROP_KEY = new ValueProperty(TYPE, "key");

  Value getKey();

  void setKey(String value);
  
  /* === Value === */

  @XmlBinding(path = "@value")
  @Label(standard = "value")
  @Required
  ValueProperty PROP_VALUE = new ValueProperty(TYPE, "value");

  Value getValue();

  void setValue(String value);
}

Als Nächstes betrachten wir das INode-Interface in Listing 2, das die Knoten im Graph beschreibt. Um für ein Element eine Liste von erlaubten Werten bereitzustellen, reicht es, ihr als Typ eine Enumeration zuzuweisen, wie es hier beim Type-Element der Fall ist. Eine Verfeinerung dieses Konzepts wird für die factoryId benutzt, die eine Java-Klasse erwartet, die das Interface IProcessor implementiert. Zuletzt wird das IProperty-Element als Liste gebunden, wobei alle Elementnamen spezifiziert werden. Abhängigkeiten können auch innerhalb des Modells bestehen.

Listing 2
@GenerateImpl
public interface INode extends IModelElement {

  ...

  /* === Type === */

  @Type(base = NodeType.class)
  @XmlBinding(path = "@type")
  @Label(standard = "type")
  @Required
  ValueProperty PROP_TYPE = new ValueProperty(TYPE, "type");

  Value getType();

  void setType(String value);

  void setType(NodeType value);

  /* === Factory ID === */

  @Type(base = JavaTypeName.class)
  @Reference(target = JavaType.class)
  @JavaTypeConstraint(kind = { JavaTypeKind.CLASS, JavaTypeKind.ABSTRACT_CLASS,               JavaTypeKind.INTERFACE },
      ,type = {"de.lmu.ifi.dbs.knowing.core.japi.IProcessor"})
  @XmlBinding(path = "@factoryId")
  @Label(standard = "factoryId")
  @Required
  ValueProperty PROP_FACTORY_ID = new ValueProperty(TYPE, "factoryId");

  ReferenceValue getFactoryId();

  void setFactoryId(String value);

  void setFactoryId(JavaTypeName value);

  /* === Properties === */

  @Type(base = IProperty.class)
  @XmlListBinding(path = "properties", mappings = { @XmlListBinding.Mapping(
        element = "property", type = IProperty.class) })
  @Label(standard = "Properties")
  ListProperty PROP_PROPERTIES = new ListProperty(TYPE, "properties");

  ModelElementList getProperties();

}

In Listing 3 erwartet das Source-Element einen gültigen Knoten. Der Pfad wird absolut vom Root-Element aus gesetzt. Zur Auflösung der INodes wird ein Service benutzt. Es gibt eine Reihe von Services im Paket org.eclipse.sapphire.services. In diesem Fall wird der ReferenceService implementiert.

Listing 3
@GenerateImpl
public interface IEdge extends IModelElement {
  
  ...

  /* === Source ID === */

  @Reference(target = INode.class)
  @Service(impl = NodeReferenceService.class)
  @XmlBinding(path = "@source")
  @Label(standard = "Source")
  @Required
  @PossibleValues(property = "/nodes/id")
  ValueProperty PROP_SOURCE = new ValueProperty(TYPE, "source");

  ReferenceValue getSource();
  void setSource(String value);
  
  ...
  
}

Das Root-Element in Listing 4 ist als IDataProcessingUnit bezeichnet und erweitert IExecutableModelElement, wodurch dieses Element durch einen Eclipse New Wizard erstellt werden kann. Als Letztes muss das Modell noch durch den Eclipse Annotation Processor erzeugt werden. In den Projekteigenschaften unter JAVA COMPILER | ANNOTATION PROCESSING aktiviert man die PROJECT SPECIFIC SETTINGS. Im Kindelement Factory Path von ANNOTATION PROCESSING sollte org.eclipse.sapphire.sdk.build.processor angegeben sein.

Listing 4
@GenerateImpl
@XmlBinding(path = "DataProcessingUnit")
public interface IDataProcessingUnit extends IExecutableModelElement {

  ...


  /* == == */
  @DelegateImplementation(IDataProcessingUnitOp.class)
  Status execute(ProgressMonitor monitor);
}
… dann die UI

Die UI wird über eine XML-Datei (sdef) beschrieben. Sapphire bietet dazu einen eigenen Editor an, der die manchmal etwas ausufernden Dokumente beherrschbar macht. Beispielhaft wird die Erstellung eines MultiPageEditors erklärt, der drei Editoren, ein Diagramm, Master-Details und Source, zur Bearbeitung des IDataProcessingUnit-Modells bereitstellt. In einer sdef-Datei werden Parts erstellt, die eine bestimmte Funktion erfüllen, zum Beispiel WizardPages, Editors oder Composites. Diese Parts lassen sich auch ineinander schachteln, wodurch leicht kleine wiederverwendbare Komponenten geschrieben werden können. Für den MultiPageEditor werden zwei Parts erstellt: Die Master Details Editor Page stellt im Master eine Baumübersicht des Modells dar und eine Details Page für jedes in Content Nodes beziehungsweise Child Nodes definiertes Element. Im DPU-Editor wird als Content Node das Root-Element DataProcessingUnit gewählt und als Child Nodes die Listenelemente nodes und edges. Über Sections [3] können die Editoren gut strukturiert werden (Abb. 1).

Abb. 1: DPU-Editor

Die Diagram Editor Page ermöglicht es, einen einfachen grafischen Editor mittels Graphiti [4] zu erstellen. Dazu müssen nachdem Hinzufügen des Parts die Nodes, Edges und Connection Bindings erstellt werden. Sapphire verfügt über eine kleine Syntax, um auf Modellelemente innerhalb der UI zuzugreifen. In Abbildung 2 wird als Instance ID das ID-Element von INode als Wert über

psenv::pushli(); eval($_oclass[„{„]); psenv::popli(); ?>

id} gesetzt. Jeder Editor kann Property View Contributions definieren, wodurch selektierte Elemente im PropertyView editierbar werden (Abb. 2 unten).

Abb. 2: Diagram Editor Page

Das bietet sich besonders für den grafischen Editor an, weil er noch eine rudimentäre Funktionalität besitzt. Als letzter Schritt muss der MultiPageEditor noch erstellt werden (Listing 5). Sapphire bietet dazu eine Superklasse Sapphire Editor an, von der abgeleitet werden muss. Ebenso ist für jeden Editortyp eine Klasse vorhanden, der passend instanziert werden muss. Als Parameter erhalten diese Editoren immer das Modellelement, den Pfad zur sdef-Datei inklusive der Part ID. Speziell anzumerken ist, dass aufgrund der Diagram Editor Page die doSave-Methode überschrieben werden muss. Danach muss der Editor in gewohnter Eclipse-Manier als Extension Point definiert und konfiguriert werden.

Listing 5
public class DPUSapphireEditor extends SapphireEditor {

  private StructuredTextEditor sourceEditor;
  private MasterDetailsEditorPage overviewPage;
  private SapphireDiagramEditor diagramPage;

  private IModelElement dpuModel;

  public DPUSapphireEditor() {
    super(Activator.PLUGIN_ID);
  }

  @Override
  protected IModelElement createModel() {
    dpuModel = IDataProcessingUnit.TYPE.instantiate(new RootXmlResource(
          new XmlEditorResourceStore(this, sourceEditor)));
    return dpuModel;
  }

  @Override
  protected void createSourcePages() throws PartInitException {
    sourceEditor = new StructuredTextEditor();
    sourceEditor.setEditorPart(this);

    final FileEditorInput rootEditorInput = (FileEditorInput) getEditorInput();

    int index = addPage(sourceEditor, rootEditorInput);
    setPageText(index, "source");
  }

  @Override
  protected void createFormPages() throws PartInitException {
    IPath path = new Path(PLUGIN_ID + DPU_SDEF + "/dpu.editor.overview");
    overviewPage = new MasterDetailsEditorPage(this, dpuModel, path);
    addPage(1, overviewPage);
    setPageText(1, "overview");
    setPageId(pages.get(1), "Overview", overviewPage.getPart());
  }

  @Override
  protected void createDiagramPages() throws PartInitException {
    IPath path = new Path(PLUGIN_ID + DPU_SDEF+ "/dpu.editor.diagram");
    diagramPage = new SapphireDiagramEditor(dpuModel, path);
    SapphireDiagramEditorInput diagramEditorInput = null;

    try {
      diagramEditorInput = SapphireDiagramEditorFactory                createEditorInput(dpuModel.adapt(IFile.class));
    } catch (Exception e) {
      SapphireUiFrameworkPlugin.log(e);
    }

    if (diagramEditorInput != null) {
      addPage(0, diagramPage, diagramEditorInput);
      setPageText(0, "diagram");
      setPageId(pages.get(0), "diagram", diagramPage.getPart());
    }
  }
  
  @Override
  public void doSave(final IProgressMonitor monitor) {
    super.doSave(monitor);
    diagramPage.doSave(monitor);
  }
}
Fazit

Eclipse Sapphire ist ein noch sehr junges Projekt, was sich in größeren API-Änderungen mit jeder neuen Version bemerkbar macht. Dennoch sieht man das vorhandene Potenzial an den noch Sapphire-spezifischen Annotationen. Denkbar wäre die UI-Generierung aus JPA-Annotationen, was die Erstellung von UIs für CRUD-Anwendungen erheblich vereinfachen würde.

Nepomuk Seiler ist studentischer Mitarbeiter an der LMU München und entwickelt im Rahmen seiner Bachelorarbeit ein Datamining-Framework und entsprechendes Eclipse Tooling.
Geschrieben von
Nepomuk Seiler
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.