Model Sources
LocalModelSource
for local models and the DiagramServer
for remote ones.
flowchart TD; ActionDispatcher ModelSource LocalModelSource DiagramServerProxy DiagramServer ActionDispatcher <-.->|Action| ModelSource ModelSource --- LocalModelSource ModelSource --- DiagramServerProxy DiagramServerProxy <-.->|Action| DiagramServerRegardless of where your model-source is located, Sprotty handles them in a similar fashion. All communication between
ActionDispatcher
and model source is always through actions and is bi-directional.
This is a powerful feature of Sprotty as it allows both flexibility regarding where and how the Diagram is generated, as well as changing or updating and reacting to interactions with the diagram simultaneously.The following sections will explain how to use and work with the different types of model sources.
Regardless of the model source we are using, the first thing we have to do is to register our model source in the front-end DI-container like this:
bind(TYPES.ModelSource).to(ModelSourceClassOrProxy).inSingletonScope();
After that, we can retrieve the model source with the following code to further configure and use it.
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource);
A LocalModelSource
instance allows us to set and modify the model through function calls, and keeps the model schema saved locally.
To see how to use this model source, let’s have a look at the following example:
import {SNode} from 'sprotty-protocol';
export default runExample() {
const container = createContainer('sprotty-showcase');
const modelSource = container.get<LocalModelSource>(TYPES.ModelSource);
modelSource.setModel({
type: 'graph'
children: [
<SNode>{
type: 'node',
id: 'main_node',
text: 'node1',
position: {x: 0, y: 0}
}
]
});
document.getElementById('addButton').addEventListener('click', () => {
modelSource.addElements([
{
parentId: 'graph',
element: <SNode>{
type: 'node',
id: 'new_node',
text: 'new node',
position: {x: 100, y: 100}
}
}])
})
}
In this example, we have a hard-coded data structure containing all the initial elements which are set in the model.
Be aware that by defining the model this way we are defining the model schema - a data structure describing the model - and not the actual model itself.
The model in this context means instances of the classes containing logic Sprotty uses for rendering. The model schema is used by Sprotty to generate the model.
Due to this, we should use the interfaces for our nodes, edges, etc. from sprotty-protocol
and not the classes from the sprotty
main package.
For disambiguation, model classes are suffixed with Impl
in Sprotty, while interfaces (contained in the package sprotty-protocol
) are not. This means that SNode
is an interface, while SNodeImpl
is a class.
After defining the model schema we can then use methods like addElements()
from our LocalModelSource
to add new nodes at the click of a button. The LocalModelSource
then handles updating the model and notifying the ActionDispatcher
about the update, so that the view can receive an animated update.
Through methods like the ones outlined above, the LocalModelSource
can also be used as a facade over the action-based API of Sprotty. It handles actions for bounds calculation and model updates.
When the model needs to be generated from a remote source, like in a worker or from a server, we can use Sprotty’s DiagramServer
model source. It communicates with the client through Action
objects which can be serialized to plain JSON.
On the client-side, instead of registering an actual ModelSource
we can use a DiagramServerProxy
. The Proxy handles the communication and forwards actions to the ActionDispatcher
. Out of the box, Sprotty offers the WebSocketDiagramServerProxy
for communicating through WebSockets with the DiagramServer
.
Should a different form of communication be necessary we would have to create a custom proxy.
Using the WebSocketDiagramServerProxy
is quite simple. We just need to call listen
on the ModelSource and pass it the WebSocket we’re communicating with.
const modelSource = container.get<WebSocketDiagramServerProxy>(TYPES.ModelSource);
modelSource.listen(websocket);
For creating the DiagramServer
itself, let’s look at an example.
// Creating a new websocket server
const wss = new WebSocketServer.Server({ port: 8080 });
// create our DiagramServices
const elkFactory: ElkFactory = () => new SocketElkServer();
const services: DiagramServices = {
DiagramGenerator: new RandomGraphGenerator(),
ModelLayoutEngine: new ElkLayoutEngine(elkFactory)
}
// Creating connection using websocket
wss.on("connection", ws => {
const diagramServer = new DiagramServer(action => {
ws.send(JSON.stringify(action));
}, services)
ws.on('message' data => {
diagramServer.accept(data.action);
});
});
In the example above, we assume we have a simple nodeJs WebSocket server and want to create a DiagramServer
for it.
As we can see, there are two parts to creating the DiagramServer
.
First, we need a dispatch method to send actions from the server to the client. This can be as simple as calling ws.send()
with the serialized action.
Second, we need the DiagramServices
. The DiagramServices
type looks like this:
export interface DiagramServices {
readonly DiagramGenerator: IDiagramGenerator
readonly ModelLayoutEngine?: IModelLayoutEngine
readonly ServerActionHandlerRegistry?: ServerActionHandlerRegistry
}
There are 3 components to the DiagramServices
. One is mandatory, the other two are optional:
- The
DiagramGenerator
which the server uses to create the schema of the Diagram - Optionally the
ModelLayoutEngine
, like theElkLayoutEngine
from sprotty-elk, if we want to do server-side layouting - Optionally the
ServerActionHandlerRegistry
for overwriting the default handling of incoming actions
In case communication between the DiagramServer
and client does not work through WebSockets, for example when the DiagramServer
is running in a worker or the sprotty client is in a vscode webview (see sprotty-vscode), we can easily implement our own proxy instead.
export class WebWorkerDiagramProxy extends DiagramServerProxy {
constructor(private worker: Worker) {
super()
const proxy = this;
worker.onmessage = function(event) {
proxy.messageReceived(event.data)
}
}
protected sendMessage(message: ActionMessage): void {
this.worker.postMessage(JSON.stringify(message));
}
}
Following the example above, first we need to extend DiagramServerProxy
. This already gives us most of our needed functionality and makes this proxy a ModelSource
.
Then we need to listen for incoming messages and pass them to the messageReceived()
function, which deserializes and passes them to the ActionDispatcher
.
Lastly, we need to implement the sendMessage()
method to allow actions coming from the ActionDispatcher
to be transferred to the DiagramServer
.
Now our custom model source proxy is able to propagate all actions between the ActionDispatcher
and our DiagramServer
running in the worker, which gives us access to all of sprotty’s functionality.