Back to top

Graphical Model

The graphical model is a serializable description of the diagram to be visualized on the client. It is the central communication artifact between client and server. The server creates the graphical model from an arbitrary source model by invoking a so-called GModelFactory and sends the graphical model to the client. Thus, the client doesn’t need to know from which source model it has been generated and how to manipulate the source model. However, the client interprets the graphical model in order to render a visualization of the diagram described in the graphical model.

Graphical Model Structure

The graphical model is composed of elements and edges. Each element or edge has a unique identifier and a type. The graphical model elements are organized in a tree, as defined by the parent-child relationship between elements, with a single root element. The graphical model library consists of several common base classes, such as nodes, ports, labels, compartments, but can be extended with additional properties or even new types, if needed.

The graphical model is typically composed of the following elements.

  • GModelRoot: Each graphical model must have exactly one root
    • GShapeElement: A graphical element is represented by a shape with visual bounds (position and size). Note that such elements can be nested based on their parent-child relationship. There are a the following concrete sub-types of shapes:
      • GNode: Representation of a logical diagram node
      • GPort: Ports are typically children of nodes and serve as connection points for edges
      • GLabel: Representation of a text label
      • GCompartment: A generic container element used for element grouping
    • GEdge: A diagram edge that connects a source element and a target element (typically nodes or ports).

Graphical model on the client

The default GLSP client uses Sprotty, an SVG-based diagramming framework, to render diagrams. Sprotty uses a model to represent a diagram too – the so-called SModel. The graphical model of GLSP is based on the SModel and, thus, can be seen as a compatible extension of the Sprotty model. To ensure a consistent development experience GLSP aliases all reused SModel types to the GModel namespace. For instance sprotty’s SModelElementImpl is equivalent to GLSP’s GModelElement. There should almost never be a reason to directly use the sprotty types. If possible always try to use the GModel API when developing on the client.

GModel: Graphical model on the server

The Java-based GLSP server uses EMF to represent and manage the graphical model internally. Note that this is just an internal way of representing the GModel at runtime but doesn’t mean that adopters need to represent their original source models with EMF too. The GLSP server uses EMF in order to reuse its model management and editing capabilities, its command stack and command-based editing. Therefore, the graphical model is described as an Ecore model and the corresponding Java classes are automatically generated from this model. Using GSON, the GModel is then serialized and deserialized to JSON before it is sent via JSON-RPC to the client.

The node-based GLSP server provides a graph model library, which defines the graph model types, such as GNode, GEdge, etc. alongside a builder API to make creating instances more convenient. However, as the node-based GLSP server and the GLSP client are both based on ES6, this graph library is based on graph model definitions that are used on the client.

Graphical Model Factory

After the initial loading of the source model – and also after each change of the source model – the GLSP server generates a graphical model from the source model in order to define what is to be rendered on the client.

The generation of the graphical model from the original source model is the responsibility of the GModelFactory. Therefore, the GModelFactory obtains the source model from the model state and generates a new graphical model from it. Implementations of the GModelFactory are by nature very specific to the source model and the diagram type. Thus, in almost every GLSP editor project, a custom GModelFactory implementation is provided.

The only exception are GLSP editors that directly operate on GModels; that is, the GModel is persisted and loaded directly by the registered implementation of the source model storage. In such cases, no transformation from the source model to the GModel needs to be provided as the source model already is the GModel. Thus, the so-called NullImpl of the GModelFactory can be used. An example for such a use case is provided in the GLSP Workflow example.

For all other use cases, an implementation of the GModelFactory needs to be provided and registered in the server DI module as follows.

Java GLSP Server
@Override
protected Class<? extends GModelFactory> bindGModelFactory() {
    return MyModelFactory.class;
}
Node GLSP Server
protected override bindGModelFactory(): BindingTarget<GModelFactory> {
    return MyModelFactory;
}

For the sake of an example, let’s assume that the source model is a simple list of named entities. Each entity should be visualized as a node with a label, which indicates its name. Then the corresponding ModelFactory could look as follows.

Java GLSP Server
public class MyModelFactory implements GModelFactory {

   @Inject
   protected MyModelState modelState;

   @Override
   public void createGModel() {

      List<Entity> entities = modelState.getModel().getEntities();

      List<GModelElement> entityNodes = entities.stream().map(entity -> //
      new GNodeBuilder("node:entity")
         .layout("vbox")
         .add(new GLabelBuilder()
            .text(entity.getName())
            .build())
         .build())
         .collect(Collectors.toList());

      GGraph newModel = new GGraphBuilder()
         .id("entity-graph")
         .addAll(entityNodes)
         .build();

      modelState.updateRoot(newModel);
   }
}
Node GLSP Server
@injectable()
export class MyModelFactory implements GModelFactory {
  @inject(MyModelState)
  protected modelState: MyModelState;

  createModel(): void {
    const entities = this.modelState.getModel().getEntities();

    const entityNodes = entities.map((entity) =>
      new GNodeBuilder(GNode)
        .id("node:entity")
        .layout("vbox")
        .add(new GLabelBuilder(GLabel).text(entity.name).build())
        .build()
    );

    const newModel = new GGraphBuilder(GGraph)
      .id("entity-graph")
      .addChildren(...entityNodes)
      .build();

    this.modelState.updateRoot(newModel);
  }
}

In the createGModel() method the entities are retrieved from the model state, as they have been added there by the source model storage (see Source Model Storage). Then a new GNode is created for each entity. Finally all new nodes are added as children of a newly created GGraph and the graphical root element in the model state is updated.

Note that we have used the GModelBuilder API in this example to construct new graphical elements. This builder API offers a convenient way to construct new graphical model elements in a concise and fluent fashion. It is the preferred method and should be used over plain constructor creation.

Extending the Graphical Model

GLSP provides a set of default graphical model element classes that can be used to construct the graphical model and already cover a large set of use cases. For advanced use cases the existing base model elements can be customized or additional elements can be introduced. As an example, let’s have a look at the custom WeightedEdge element introduced by the GLSP Workflow example.

GLSP Client

A WeightedEdge is a special edge that has an optional “probability” property. We can define such an element by simply subclassing the GEdge class:

export class WeightedEdge extends GEdge {
  probability?: string;
}

And then the new WeightedEdge type has to be configured in the diagram module (di.config.ts).

const workflowDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) => {
    ...
    configureModelElement(context, 'edge:weighted', WeightedEdge, WorkflowEdgeView);
    ...
}

Node GLSP Server

For the node GLSP server the new WeightedEdge type can be declared similar to the GLSP client by subclassing the GEdge class.

export class WeightedEdge extends GEdge {
  probability?: string;
}

To use the builder API for WeightedEdge creation we also have to implement a WeightedEdgeBuilder that extends the default GEdgeBuilder.

export class WeightedEdgeBuilder<
  E extends WeightedEdge = WeightedEdge
> extends GEdgeBuilder<E> {
  probability(probability: string): this {
    this.proxy.probability = probability;
    return this;
  }
}

Java GLSP Server

When using the Java GLSP server, a new Ecore model that extends the default “graph.ecore” model has to be created to declare new model elements. For more details, please have a look at the “workflow-graph.ecore” model in the GLSP Workflow example. Once the WeightedEdge is specified in the Ecore model, the corresponding source code has to be generated. Now the GraphExtension API can be used to configure the “workflow-graph.ecore” for the workflow diagram language. A class that implements the the corresponding interface has to created:

public class WFGraphExtension implements GraphExtension {

   @Override
   public EPackage getEPackage() { return WfgraphPackage.eINSTANCE; }

   @Override
   public EFactory getEFactory() { return WfgraphFactory.eINSTANCE; }

}

And then configured in the WorkflowDiagramModule:

@Override
protected Class<? extends GraphExtension> bindGraphExtension() {
    return WFGraphExtension.class;
}

To use the builder API for WeightedEdge creation we also have to implement a WeightedEdgeBuilder that extends the default AbstractGEdgeBuilder.

Generic Args

Every graphical model element type has a generic “args” property, which can be used to store additional properties as key-value pairs. These arguments can be used as a more lightweight alternative to extending the graphical model classes, especially if only simple extensions are needed.


➡️ Let’s look at how the graphical model is rendered on the client next!