How to ignore workspace on change event while loading XML

987 views
Skip to first unread message

Jody Florian

unread,
Jan 5, 2017, 6:48:51 AM1/5/17
to Blockly
I am having problems with listening to changes on a workspace. Or rather, ignoring.

When I update or set a workspace's contents with XML like so:

    myXml = "...";
    isLoading = true;
    workspace.clear();
    Blockly.Xml.domToWorkspace(
            Blockly.Xml.textToDom(myXml), workspace );   
    isLoading = false;

the handler passed to workspace.addChangeListener() is fired after this.isLoading is set to false. Which means I can't find a way to ignore calls to it during that time. (For example, by checking isLoading in the handler, or by removing the handler before the above code and re-adding it after).

The reason this is a problem is I need to know whether the user has changed the workspace, rather than the workspace being updated by the server (e.g. when being edited real-time by another user).

Is there a trick I'm missing?

Some hacks I can think of:

Ugly hack: Ignore changes for a time period after having been updated by the server.

Bad(?) hack: Compare the XML last sent to and/or from the server with the Xml of the workspace within the onChange handler to determine if the handler is being fired due to a user's change or due to the loading of Xml from the server, but I can't get my head around the logic for that, given race conditions and dirty updates etc. I mean it's not critical that little changes are lost or overwritten from time to time, but it would be nice to get this as clean as possible.

Thanks


Andrew Stratton

unread,
Jan 5, 2017, 8:28:42 AM1/5/17
to Blockly
If you are ignoring the event changes while isLoading is true, could you have your handler check for the end of loading and then set isLoading to false at the end.

Not sure how you detect that everything is loaded though - you might have to insert a dummy block to trigger the end of loading?!

Let me know if you have any success with this - I have a similar issue.

Cheers
  Andy

Jody Florian

unread,
Jan 5, 2017, 9:51:29 AM1/5/17
to Blockly
Andy - thanks for your response.

The isLoading variable was only an attempt to solve the problem, by testing for it in the handler to see if it should ignore the event, (so setting it to false there is besides the point)

I will try digging through the Blockly code to find an answer. Perhaps a hook can be added by overriding a method somewhere, or something.

Another hack I've thought of is to ignore the first n change events after loading. But this could easily change between blockly versions, and without thorough testing, could not be relied upon to even be constant within a given version.

picklesrus

unread,
Jan 5, 2017, 12:42:39 PM1/5/17
to Blockly
You can temporarily turn off events with  Blockly.Events.disable() and then turn them back on with Blockly.Events.enable();
If you do that while you're loading from your server, would that help? 

Jody Florian

unread,
Jan 5, 2017, 2:16:20 PM1/5/17
to Blockly
Pickles, thanks - good to know - but I think either I'm missing something or have not explained everything.

Your suggestion would have the same effect as the other two ways, ie:
- removing the handler while loading the XML (I only provide one to the workspace) or
- using the isLoading flag which makes the handler return when it is called

All three ways rely on the code knowing when the change events firing as a result of domToWorkspace have completed. (I seem to consistently see 4 fired each time, but not sure I should always rely on exactly 4).

The way my web app works: multiple users can be working on the same workspace simultaneously, real-time. When one user drags a block and releases it, the change event fires on his workspace, the client sends the update to the server and it's broadcast to all clients viewing that workspace. Each of those clients update what their user is seeing by calling clear(), xmlToDom() and domToWorkspace(). But without more code, this enters in an infinite loop between clients and server, since when XML is loaded to a workspace, it triggers the workspace change handler several times.

I've got it working by wrapping the code that re-enables the workspace event handler in a setTimeout of 500ms but it obviously won't work if someone has a slow device/browser

picklesrus

unread,
Jan 5, 2017, 2:46:37 PM1/5/17
to Blockly
Oh neat! You're working on collaboration!

Instead of handling an incoming server event by clearing and then loading xml, you probably want to do something more like the "mirror" blockly demo.   You can create an Event object from the event's json and then call event.run() instead. The code probably explains it better than me: https://github.com/google/blockly/blob/master/demos/mirror/index.html#L74

Also, in case it helps, in this doc Neil talks about how he would do realtime collaboration in a doc he shared about a year ago:
The doc doesn't describe exactly what we have now (e.g. we use json) and we did not get as far as implementing the server piece that the doc mentions, but there is some good stuff in there.

I'm excited to see this in use!  Any feedback/questions on how this stuff works are quite welcome.

Ris Misner

unread,
Jan 5, 2017, 4:19:45 PM1/5/17
to Blockly
I ran into a similar problem.

The impression I get is that domToWorkspace is asynchronous: It causes the workspace to be updated after it returns, instead of blocking until the workspace has been updated.  That's why you can't set a flag around it: by the time domToWorkspace does its asynchronous work, you've already cleared the isLoading flag or re-enabled events.  I tried registering my change handler after the call to domToWorkspace but that didn't fix the problem: by the time domToWorkspace does its asynchronous work, my call to domToWorkspace had returned some time ago, and I had already registered my change handler, so my change handler was called 3 or 4 times (it depends on the xml contents).

Without digging into the Blockly core to change how it fires the events, I solved this issue for my purposes by keeping track of a server mirror value that's separate from my working copy (very similar to the "hack" that you described).  The server mirror tracks the last value sent to or received from the server, and I only send new values to the server if my current working copy doesn't equal the server mirror.

I also put my uploads on a delay.  I don't upload anything to the server until the workspace change events stop firing for a few seconds.  That's part of my solution to handling a batch of 3 or 4 (unknown number of) change notifications from a single call to domToWorkspace.

It seems to me that any other solution would require changing the Blockly core, either to
A) remove the asynchronous element from domToWorkspace so that it would update the workspace and fire the change events synchronously before returning, or to
B) modify the details of the events fired by domToWorkspace to include some data indicating that the change came from domToWorkspace XML restoration as opposed to user actions, or
C) a combination of those two changes.

Jody Florian

unread,
Jan 6, 2017, 4:57:05 AM1/6/17
to Blockly
picklesrus - that mirror demo is unidirectional - updates only go from the master to the slave. And either way, the problem can still be demonstrated within that demo by adding some code: At line 64 add this code:

slaveWorkspace.addChangeListener(function(){
    console
.log("Doh!");
});

When I drag a block from the toolbox onto the workspace, it fires twice. What is needed is as Ris Misner pointed out, either:
 - make events fired by domToWorkspace fire synchronously rather than asynchronously, or
 - have events fired by domToWorkspace include a flag or similar indicating that the events were caused by domToWorkspace.

Similarly, other alternatives might be:
 - provide a method for enabling / disabling events resulting from domToWorkspace
 - allow a callback to be passed with domToWorkspace which gets called once the asynchronous events are finished (so other code knows when to resume listening for change events).

Regarding Neil's method you linked to, it looks like a great way to do it - it's certainly more efficient than my method - but I took the easier route with my code - it just sends the whole workspace XML to the server on every change (with a small delay which ignores all but the most recent change to prevent excessive trips to/from the server).

@RisMisner - Regarding the solution I mentioned that you said I said was a hack (!) I didn't want to imply it was bad practice - merely that I would be uncomfortable using it because I cannot get my head around it sufficiently to be sure I could get it right. It may very well be the best solution - unless I'm doing it!

picklesrus

unread,
Jan 6, 2017, 7:44:43 PM1/6/17
to Blockly
I think there might be a misunderstanding of when domToWorkspace is called.  It is not called when you drag a block out of the toolbox.  There are two events that fire though - a "create" and a "move" There could actually be more, especially if you have groups of blocks in your toolbox.  All of the create & move events associated with dragging a block out of the toolbox and placing on the workspace should have the same group id though.  If you use the group id to combine events and only call domToWorkspace once, would that help at all?

To clarify something else about domToWorkspace:
If you do the following:
Blockly.Events.disable()
Blockly.Xml.domToWorkspace(xml, workspace);
Blockly.Events.enable();

You should should not get any events for the blocks it drops in the workspace during domToWorkspace.  If you do, there's a bug somewhere.

For the mirror demo, I took a look at events after adding a change listener to the mirror workspace. I do see multiple events.  I see the create and the move which I expected.  There is a move event without a group id that I don't quite understand.  I'll dig in a bit more next week and see if I can sort out whether it should be there or not.

Rachel Fenichel

unread,
Jan 6, 2017, 9:06:06 PM1/6/17
to Blockly
What pickles just suggested is, as requested, "a method for enabling / disabling events resulting from domToWorkspace".

When dragging a block out of the toolbox, a block is created on the workspace, then moved.  During the creation domToBlock is called.  This fires events.  domToBlock is also called from domToWorkspace, which does not directly fire events.

Erik Pasternak

unread,
Jan 9, 2017, 7:50:25 PM1/9/17
to Blockly
Hi Jody,

Excited to see people playing around with collaboration. We've been discussing it on and off ourselves for at least a year but haven't had the time to implement it ourselves. There's also a lot of subtle complexity to the problem which we tried to design for in the implementation of events, but I'm sure there are some pieces missing.

Neil's doc from last year provided a good summary of the design we had in mind. There's two main areas that still need investigation and significant work based on that design.
  • We currently don't distinguish events generated by the user and generated by an incoming event stream. There's differences in the events themselves, such as events from another instance won't have a workspace id or old values. But we probably need a slightly different code path for events that aren't user generated, and it'd take some work to find all the ways they should differ. The fact that sometimes new events get fired that weren't part of the incoming stream is one example where we get this wrong.
  • A lot of work needs to be done around rewinding and replaying your local events in the order specified by the server. Our intention was to ignore events from the server during a user interaction to keep the UI from jumping around while dragging a block (for instance), but this means after the user interaction finishes we need to push our changes to the server and receive an update from the server with the events we just generated, then rewind them and apply in the order the server recorded.
We've also  discussed having an end group event, but don't think it's necessary. Groups are almost always created by a single user action, and the simple way to batch them is to wait for the user interaction to finish before sending events to the server (which should succeed or fail as a group). This should also solve your problem of when to send a group of events.

Regarding your approach of serializing the entire workspace, how do you resolve conflicts? It seems like if two people were working at the same time they would be clobbering each other's work fairly frequently.

Cheers,
Erik

Mark Friedman

unread,
Jan 9, 2017, 8:02:05 PM1/9/17
to blo...@googlegroups.com
On Mon, Jan 9, 2017 at 4:50 PM, 'Erik Pasternak' via Blockly <blo...@googlegroups.com> wrote:
Hi Jody,

Excited to see people playing around with collaboration. We've been discussing it on and off ourselves for at least a year but haven't had the time to implement it ourselves. There's also a lot of subtle complexity to the problem which we tried to design for in the implementation of events, but I'm sure there are some pieces missing.

Neil's doc from last year provided a good summary of the design we had in mind. There's two main areas that still need investigation and significant work based on that design.
  • We currently don't distinguish events generated by the user and generated by an incoming event stream. There's differences in the events themselves, such as events from another instance won't have a workspace id or old values. But we probably need a slightly different code path for events that aren't user generated, and it'd take some work to find all the ways they should differ. The fact that sometimes new events get fired that weren't part of the incoming stream is one example where we get this wrong.
  • A lot of work needs to be done around rewinding and replaying your local events in the order specified by the server. Our intention was to ignore events from the server during a user interaction to keep the UI from jumping around while dragging a block (for instance), but this means after the user interaction finishes we need to push our changes to the server and receive an update from the server with the events we just generated, then rewind them and apply in the order the server recorded.

If you haven't already, it might be worth taking a look at the Google Drive Realtime API as a source of ideas for some of this.  IIRC, they do distinguish the event streams and they do some work to ensure proper ordering.
 
We've also  discussed having an end group event, but don't think it's necessary. Groups are almost always created by a single user action, and the simple way to batch them is to wait for the user interaction to finish before sending events to the server (which should succeed or fail as a group). This should also solve your problem of when to send a group of events.

Regarding your approach of serializing the entire workspace, how do you resolve conflicts? It seems like if two people were working at the same time they would be clobbering each other's work fairly frequently.

FWIW, that was ultimately the killer (i.e. source of subtle bugs) in the version of realtime collaboration for Blockly that I developed a while back, which relied on sending XML representations back and forth (although not necessarily the entire workspace).  I tried a variety of approaches to mitigate the issue but ultimately failed.

-Mark
 

--
You received this message because you are subscribed to the Google Groups "Blockly" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blockly+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Jody Florian

unread,
Jan 10, 2017, 6:50:11 AM1/10/17
to Blockly
Picklesrus, Rachel, thank you - doing the following worked perfectly after all, my mistake.

Blockly.Events.disable()
Blockly.Xml.domToWorkspace(xml, workspace);
Blockly.Events.enable();

Erik - I have not found a solution as such. In fact, I haven't even tried any of it with anyone else yet so I don't know how well it would really work.

I am probably being naive, but...

I probably am building this initially just for my son-in-law and myself to collaborate while on the phone, so clobbering would have to be accepted unless both him and I can communicate about taking turns; something both of us need practice at anyway.

Saying that, I may be wrong but don't think it would be much of a problem with both of us moving blocks - I hope clobbering would be easy to spot and quick to fix. But when working on text, with fast, continuous typing I can see it getting extremely frustrating. To keep it simple I may either resort to enforced turn taking, or showing multiple workspaces on screen; one per active user, with all but one being read-only.

Jody

Erik Pasternak

unread,
Jan 10, 2017, 12:27:42 PM1/10/17
to Blockly
For your use case the simple approach definitely makes sense. Let us know how it goes and if you run into any other issues!

Cheers,
Erik


--
You received this message because you are subscribed to a topic in the Google Groups "Blockly" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/blockly/4ixCVy15sXc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to blockly+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.



--
Erik Pasternak | Master of the House | epas...@google.com     

Ris Misner

unread,
Jan 10, 2017, 4:01:47 PM1/10/17
to Blockly
    Blockly.Events.disable()

    Blockly.Events.enable();


This is working for me as well. Thank you!

Austin Bart

unread,
Jan 15, 2017, 3:15:38 PM1/15/17
to Blockly
Well, amusingly I was solving this problem at the same time last week, but on a cruise ship without internet. Wish I could have seen this thread, would have saved me a lot of trouble.

Either way, I thought it might be vaguely useful to share this additional function I created: Blockly.Xml.domToWorkspaceDestructive
This combines the Blockly.clear functionality into the Blockly.Xml.domToWorkspace method. I figured their events should be squashed together.

Also, my solution to merging the events was the setTimeOut adding a few seconds of delay to the update, so that group events don't propogate. It all felt a bit hackish, so I'll be curious to see how it holds up in production.

~Cory

David Daza

unread,
Mar 17, 2017, 10:58:17 PM3/17/17
to Blockly, jody.f...@gmail.com
Take a look: http://socketblockly.herokuapp.com/ :) Open two tabs and enjoy

Erik Pasternak

unread,
Mar 20, 2017, 12:52:45 PM3/20/17
to Blockly, jody.f...@gmail.com
That's really impressive. There's some odd things that happen with undo/redo, but for the most part they seem to work out. The one issue I did notice is that a new tab doesn't get the current state of the workspace if there are other tabs open already. This means a new tab can accidentally wipe the workspace which may not be recoverable.

--
You received this message because you are subscribed to a topic in the Google Groups "Blockly" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/blockly/4ixCVy15sXc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to blockly+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

David Daza

unread,
Mar 20, 2017, 7:10:16 PM3/20/17
to Blockly, jody.f...@gmail.com
Thanks Erik for your feedback, we (me and my friend) really appreciate it. We're  aware of that issue but we have reached our main goal. It's a feature that we're planning to implement in another bigger project. However, as soon as it's finished we're going to get hands on "socket-blockly" again. Hope to come back soon and of course you all are welcome to collaborate here


El jueves, 5 de enero de 2017, 6:48:51 (UTC-5), Jody Florian escribió:
Reply all
Reply to author
Forward
0 new messages