Diagram Editors
Introduction
The foundation of implementing diagram editors is the GLSP framework.
This guide will give an introduction to connect GLSP diagram editors with the ModelHub.
Specifically, the GLSP diagram editor makes use of the ModelHub
and the interfaces of the ModelService
like the CoffeeModelServer
or the CoffeeModelService
for seamless integration and outsourced data management.
Furthermore, specific examples of the Coffee Editor NG implementation will be given as a guideline.
ModelHub Integration
SourceModelStorage
A source model storage handles the persistence of source models, i.e. loading and saving it from/into the model state.
A source model
is an arbitrary model from which the graph model of the diagram is to be created.
A source model loader obtains the information on which source model shall loaded from a
RequestModelAction; typically its client options. Once the source model is loaded, a model loader is expected
to put the loaded source model into the model state for further processing, such as transforming the loaded model
into a graph model (see GModelFactory below).
On saveSourceModel(SaveModelAction)
, the source model storage persists the source model from the model state.
In our case all of these responsibilites are outsourced to the ModelHub
, namely the model loading, subscribing to model changes, dirty state information and triggering the save of the model.
GModelFactory
A graph model factory produces a graph model from the source model contained in the model state.
In this case, we also use the source model from the model state to translate the source model into GModelRoot
. For more complex translations, e.g. for creating edges, the CoffeeModelServer
is used to resolve references.
Model Operations
To outsource the responsibility of executing operations directly on the model, operations should be forwarded to the CoffeeModelService.
The CoffeeModelService
offers tailored functions to manipulate the model, e.g. dedicated create or delete functions that take as arguments the specific diagram data like the position on the canvas.
Coffee Editor NG Example
WorkflowModelStorage
The WorkflowModelStorage
is responsible for loading and saving source models via the ModelHub
.
Load the source model
To load the source model for further usage and transformation into the graphical GModel, we fetch the CoffeeModelRoot
from the modelHub
as follows:
this.modelHub.getModel<CoffeeModelRoot>(modelUri);
Detailed implementation
async loadSourceModel(action: RequestModelAction): Promise<void> {
const modelUri = this.getUri(action);
const coffeeModel = await this.modelHub.getModel<CoffeeModelRoot>(modelUri);
if (!coffeeModel || !isMachine(coffeeModel) || (isMachine(coffeeModel) && coffeeModel.workflows.length < 1)) {
throw new GLSPServerError('Expected Coffee Model with at least one workflow');
}
this.modelState.setSemanticRoot(modelUri, coffeeModel);
this.subscribeToChanges(modelUri);
}
Subscribe to model changes
To subscribe to model changes, modelHub
offers a subscribe
function:
this.subscription = this.modelHub.subscribe(modelUri);
this.subscription.onModelChanged = async (modelId: string, newModel: object) => {
// handle model update
}
Detailed implementation
private subscribeToChanges(modelUri: string): void {
this.subscription = this.modelHub.subscribe(modelUri);
this.subscription.onModelChanged = async (modelId: string, newModel: object) => {
if (!newModel || !isMachine(newModel) || newModel.workflows.length < 1) {
throw new GLSPServerError('Expected Coffee Model with at least one workflow');
}
this.modelState.setSemanticRoot(modelId, newModel);
const actions = await this.submissionHandler.submitModel();
const dirtyStateAction = actions.find(action => action.kind === SetDirtyStateAction.KIND);
if (dirtyStateAction && SetDirtyStateAction.is(dirtyStateAction)) {
dirtyStateAction.isDirty = this.modelHub.isDirty(this.modelState.semanticUri);
}
this.actionDispatcher.dispatchAll(actions);
};
}
Save model
To trigger model saving, modelHub
offers a save
function:
this.modelHub.save(modelUri)
Detailed implementation
async saveSourceModel(action: SaveModelAction): Promise<void> {
const modelUri = action.fileUri ?? this.modelState.semanticUri;
if (modelUri) {
await this.modelHub.save(modelUri);
}
}
CoffeeGModelFactory
To resolve references, such as edge sources or targets, we use the CoffeeModelServer
to get the resolved data needed to properly translate the source model into a GModelRoot
.
This shows one flow from the example source model superbrewer3000.coffee
:
...
{
"id": "checkWaterToDecision",
"type": "Flow",
"source": "SuperBrewer3000.BrewingFlow.Check Water",
"target": "SuperBrewer3000.BrewingFlow.MyDecision"
},
...
The following snippet shows how to create a GEdge from such a flow object:
@inject(CoffeeModelServer)
protected modelServer: CoffeeModelServer;
...
protected async createEdge(edge: Flow): Promise<GEdge> {
const [sourceNode, targetNode] = await Promise.all([
this.modelServer.resolve<Node>(edge.source as Required<NodeReferenceInfo>),
this.modelServer.resolve<Node>(edge.target as Required<NodeReferenceInfo>)
]);
return GEdge.builder()
.id(edge.id)
.sourceId(sourceNode.id)
.targetId(targetNode.id)
.build();
}
Model Operations via the CoffeeModelService
To showcase how to forward model operations to the CoffeeModelService,
let’s take a look at the CreateWorkflowNodeOperationHandler
.
This handler is an abstract node creation operation handler that is used to create all possible types of nodes supported by the Coffee diagram editor, e.g. manual tasks or fork nodes.
We wrap the model operation in a GModelRecordingCommand
- in our case a customized CoffeeModelCommand
that aligns the customized types of the GModelState
for example.
The model operation itself is basically the operation data collection we get from the diagram editor, which we hand over to the CoffeeModelService
.
The modelService provides a dedicated function to create a new node, expecting the diagram editor’s specific data like position and type of node.
override createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined> {
return new CoffeeModelCommand(
this.modelState,
this.serializer,
() => this.createWorkflowNode(operation)
);
}
async createWorkflowNode(operation: CreateNodeOperation): Promise<void> {
const container = this.modelState.semanticRoot;
const modelService = this.modelHub.getModelService<CoffeeModelService>(COFFEE_SERVICE_KEY);
const modelId = this.modelState.semanticUri;
await modelService?.createNode(modelId, container.workflows[0], {
posX: this.getLocation(operation)?.x ?? Point.ORIGIN.x,
posY: this.getLocation(operation)?.y ?? Point.ORIGIN.y,
type: this.getNodeType(operation)
});
}
Similarly, this is the way to implement model operations, also for other diagram editor use cases like deleting model elements, resizing or repostioning model elements or edit model element labels.