fromJSON() not updating the links

Hello,

I’m implementing a collaborative editor. In this moment, different browsers edit the diagram and every time a modification is done a JSON diagram is sent to the other browsers.

It was working nicely but, for some reason, when I relink a link, the update is not seen in the other browsers. In this case specifically the code I’m using is:

myDiagram.addDiagramListener(“LinkRelinked”, function(e){sendChanges(‘LinkRelinked’);});

(…)

function sendChanges(e)
{
console.log(e);
if(subscribed)
channel.trigger(“client-modified”, {“diagram” : myDiagram.model.toJson()});
}


“subscribed” is true by the time this is invoked and channel.trigger() is a function to send information to other browsers.

In the side of the browsers that are receiving the changes, I just do this:

go.Model.fromJson(data.diagram, myDiagram.model);


“data.diagram” is a string with the new JSON.
I’ve debugged this a little bit and it seems that the received “data.diagram” has the correct information about the links but, it does not make any change in the diagram.

The curious thing is that, if I modify again the original diagram by relinking the link again, the other browsers apply the changes that should originally apply before.

To make things clear, what I’m saying is this temporal sequence of actions:

  1. Browser 1 relink a link and send JSON1

  2. Browser 2 receives the JSON1 but the diagram remains the same

  3. Browser 1 relink again and send JSON2

  4. Browser 2 receives the JSON2 but the diagram is updated according to JSON1



Any hint where the error might be? For other diagram events (such as “SelectionMoved”/“SelectionDeleted”/…) the same code is used and things go nicely…

Hmmm. I think there’s a bug in Model.fromJson when you supply the second argument and that model is currently in use by a Diagram. We’ll fix this bug soon.

In the meantime, you can get around this issue by either calling Diagram.rebuildParts() afterwards, or by not calling Model.fromJson with a second argument and instead assigning the return value to your Diagram.model property.

On a more general topic – is there only one “editor” at a time that modifies a diagram, and all others are “viewers”? Otherwise you’ll need to deal with merging issues.

It seems rebuildParts() didn’t work with me. About not calling Model.fromJson with a second argument it has the problem that the entire diagram is loaded and the user can see that and it is strange.
But if it is a bug I will wait for the correction :)


About your last question, there are several “editors”. As you could see by the code I made, I just simply pass JSON among the browsers instead of a more sophisticated way like passing just the changes. Although I must implement this feature, it will not be very used, and there will be a maximum of 2/3 users editing at the same time. And that’s why I’m not dealing with merging issues by now.

Actually, I am unable to reproduce the bug that you are reporting.

If calling Diagram.rebuildParts() isn’t working for you, that’s very strange, since that removes all of the old Nodes and Links and rebuilds them from the model data.

So can you provide more details about your node data and your link data (before and after) so that I can have a chance at reproducing the problem?

I tried calling go.Diagram.rebuildParts() but that didn’t work and looking to the API I saw that the correct to use is “myDiagram.rebuildParts()”, but without the effect wanted.


However, I think the problem is in another place. I looked further and I discovered that myDiagram.model.toJSON() is not creating correctly the JSON.

I changed my sendChanges() function into this:

function sendChanges(e)
{
console.log(e);
if(subscribed && has_current_state)
{
var str = myDiagram.model.toJSON();
channel.trigger(“client-modified”, {“diagram” : str});
console.log('Sent: ’ + str);
}
}


It was looking to this “Sent” log that I discovered toJSON() is not creating what is expected. I don’t know how to describe this clearly here, but I will try.

1) First, I create the nodes and I link 2 of them, resulting into this (LinkDrawn was the event raised):


Sent: { “class”: “go.GraphLinksModel”,
“linkFromPortIdProperty”: “fromPort”,
“linkToPortIdProperty”: “toPort”,
“nodeDataArray”: [
{“category”:“start”, “item”:“Start”, “key”:-1, “loc”:"-278.390625 -243"},
{“category”:“endError”, “item”:“End with error”, “key”:-2, “loc”:"-128.390625 -310"},
{“category”:“endOK”, “item”:“Expected end”, “key”:-3, “loc”:"-138.390625 -222"}
],
“linkDataArray”: [ {“from”:-1, “to”:-2, “fromPort”:“R”, “toPort”:“L”} ]}



2) Then, I move one of the nodes so “linkDataArray” will show the points, resulting into this (SelectionMoved was the event raised):


Sent: { “class”: “go.GraphLinksModel”,
“linkFromPortIdProperty”: “fromPort”,
“linkToPortIdProperty”: “toPort”,
“nodeDataArray”: [
{“category”:“start”, “item”:“Start”, “key”:-1, “loc”:"-278.390625 -243"},
{“category”:“endError”, “item”:“End with error”, “key”:-2, “loc”:"-91.390625 -307"},
{“category”:“endOK”, “item”:“Expected end”, “key”:-3, “loc”:"-138.390625 -222"}
],
“linkDataArray”: [ {“from”:-1, “to”:-2, “fromPort”:“R”, “toPort”:“L”, “points”:[-263.390625,-243,-253.390625,-243,-184.890625,-243,-184.890625,-307,-116.390625,-307,-106.390625,-307]} ]}



3) Finally, I move the link so it links other nodes (LinkRelinked was the event raised):



Sent: { “class”: “go.GraphLinksModel”,
“linkFromPortIdProperty”: “fromPort”,
“linkToPortIdProperty”: “toPort”,
“nodeDataArray”: [
{“category”:“start”, “item”:“Start”, “key”:-1, “loc”:"-278.390625 -243"},
{“category”:“endError”, “item”:“End with error”, “key”:-2, “loc”:"-91.390625 -307"},
{“category”:“endOK”, “item”:“Expected end”, “key”:-3, “loc”:"-138.390625 -222"}
],
“linkDataArray”: [ {“from”:-1, “to”:-3, “fromPort”:“R”, “toPort”:“T”, “points”:[-263.390625,-243,-253.390625,-243,-184.890625,-243,-184.890625,-307,-116.390625,-307,-106.390625,-307]} ]}




As you can see, “linkDataArray” is not generated correctly, because the “points” parameter is the same as before.
However, just to be sure, I moved the node with key -2 (because it has no link). As you can see below, “points” changed, although I didn’t made anything to the link:



SelectionMoved active_admin.js:43525
Sent: { “class”: “go.GraphLinksModel”,
“linkFromPortIdProperty”: “fromPort”,
“linkToPortIdProperty”: “toPort”,
“nodeDataArray”: [
{“category”:“start”, “item”:“Start”, “key”:-1, “loc”:"-278.390625 -243"},
{“category”:“endError”, “item”:“End with error”, “key”:-2, “loc”:"-42.390625 -307"},
{“category”:“endOK”, “item”:“Expected end”, “key”:-3, “loc”:"-138.390625 -222"}
],
“linkDataArray”: [ {“from”:-1, “to”:-3, “fromPort”:“R”, “toPort”:“T”, “points”:[-263.390625,-243,-253.390625,-243,-207.390625,-243,-207.390625,-247,-138.390625,-247,-138.390625,-237]} ]




I’m sorry I didn’t notice this before. So, rebuildParts(), actually, is working fine…

What is your Link template?

I define the links as follows:

var _$ = go.GraphObject.make;

(…)

myDiagram.linkTemplate =
_$(go.Link,
{
routing: go.Link.AvoidsNodes,
curve: go.Link.JumpOver,
corner: 5, toShortLength: 4,
relinkableFrom: true, relinkableTo: true, reshapable:true
},
new go.Binding(“points”).makeTwoWay(),
_$(go.Shape, // the link path shape
{
isPanelMain: true,
name: “LABEL-PATH”,
stroke: mainColor, strokeWidth: 2
}),
_$(go.Shape, // the arrowhead
{
toArrow: “standard”,
name: “LABEL-ARROWHEAD”,
stroke: null, fill: mainColor
}),
_$(go.Panel, “Auto”,
{ visible: false, name: “LABEL” },
_$(go.Shape, “RoundedRectangle”, // the link shape
{ fill: “#F8F8F8”, stroke: null }),
new go.Binding(“visible”, “visible”).makeTwoWay(),
_$(go.TextBlock, “Decision”, // the label
{
textAlign: “center”,
name: “TEXT”,
font: “bold 10pt helvetica, arial, sans-serif”,
stroke: “black”,
editable: true,
isMultiline: false
},
new go.Binding(“text”, “text”).makeTwoWay())
)
);

// temporary links used by LinkingTool and RelinkingTool are also orthogonal
myDiagram.toolManager.linkingTool.temporaryLink.routing = go.Link.Orthogonal;
myDiagram.toolManager.relinkingTool.temporaryLink.routing = go.Link.Orthogonal;

OK, so you have the needed Binding: new go.Binding(“points”).makeTwoWay() That’s good.

I cannot explain what is wrong, since every link route computation will set Link.points. When your user reconnects a link to another port or node, it looks properly reconnected, doesn’t it? So that in memory the Link.points array should have a new set of Points, and those point values should be seen in the result of calling myDiagram.model.toJson(). [By the way, I would debug by selecting the Link and getting a reference to the Link in the console via myDiagram.selection.first().]

To debug this, could you verify that in memory the Points that are in the Link.points Array are indeed the correct, up-to-date ones corresponding to the route that the user sees after relinking? If it does have a set of new Points, then can you check that the Link’s model data has the same Array of Points, as Link.data.points? The next step is the conversion of the model to JSON-format text, but I have a hard time believing that Model.toJson() is producing the wrong values if the model data is correct.

Ok, for debugging myDiagram.selection.first(), I changed sendChanges() as follows:


function sendChanges(e)
{
console.log(e);
if(subscribed && has_current_state)
{
var str = myDiagram.model.toJSON();
channel.trigger(“client-modified”, {“diagram” : str});
//console.log('Sent: ’ + str);
if(myDiagram.selection.first().data.points != null)
console.log('Link with points: ’ + myDiagram.selection.first().data.points.n.toString());
else
console.log('Link: ’ + myDiagram.selection.first().data.points);
}
}



So:
1) First, I create the nodes and I link 2 of them, resulting into this (LinkDrawn was the event raised):


Link: undefined



However, instead of continuing with the previous actions I made, I put the command “myDiagram.selection.first().data.points.n.toString()” at the console, and I get:


“Point(-235.390625,-235),Point(-225.390625,-235),Point(-186.890625,-235),Point(-186.890625,-262),Point(-148.390625,-262),Point(-138.390625,-262)”



That is, inside the listener the Link.data.points is null, but after that, it is already defined. I tried with the other actions I made before and this thing happened in all them (I made break points inside the code to confirm): inside the listener, Link.data.points is not yet defined or correctly defined.


This means that Link.data.points is only updated after LinkDrawn/LinkRelinked event handled? But in the documentation it is written that these events are “when the LinkingTool has finished creating a new Link” / “when the RelinkingTool has finished reconnecting an existing Link”. That is, after updating Link.data.points, right?

I’ve also checked e.subject.data inside the LinkDrawn event and it has the same (incorrect, I guess) information about the link as myDiagram.selection.first().

Ah, yes. The positioning of nodes and routing of links isn’t complete until the end of the transaction. The “LinkRelinked” DiagramEvent happens before the end of the transaction, before any layout or link routing. So you might want to implement a Model ChangedEvent listener and look for a ChangedEvent that is ChangedEvent.isTransactionFinished:

model.addChangedListener(function(e) { if (e.isTransactionFinished) { ...send model... } });
Note that that will happen not only at the end of each committed transaction but also after each undo or redo.

Thanks, using addChangedListerner() instead solved all my problems.
However, please consider to change the documentation regarding the events, specially this two sentences here: “when the LinkingTool has finished creating a new Link” / “when the RelinkingTool has finished reconnecting an existing Link”. I think they are not accurate because as you can see everything could not have happened if I knew that in LinkDrawn/LinkRelinked (and I suppose in other related events).

By the way, I also tried to catch the type of events of ChangedListener (looking for modelChange variable), but it was always an empty string, although in the documentation (ChangedEvent | GoJS API) says it can have different values.
Indeed, I noticed that the variable oldValue and object.name have very meaningful terms to discover the type of event such as “ExternalCopy”/“Move”/“Linking”/… I think this should be more documented, also.

Thanks for the help and I hope you accept my suggestions.

Well, we can always improve our documentation. I’ll see what we can do.

Besides the ChangedEvent class API documentation that you mentioned, there is also a more general discussion at: http://gojs.net/latest/intro/events.html.

I have updated DiagramEvent | GoJS API for version 1.4.10.