Scripting Component - how to unsubscribe Document Events

(my question is for a scripting component using c# , not a custom plug-in component developed via the grasshopper API)

My starting point was, to track changes of a clipping-Plane in the RhinoDocument.
This can by done via a ID/Guid Parameter set to the ClippingPlane.
But Grasshopper will not track this changes.

My (more general) question: if my c# script component subscribes:

RhinoDoc.ReplaceRhinoObject += OnReplaceObj;

where can i unsubscribe the Event, if the script is updated or unloaded / closed.
attached definition - each time you edit the script will register a new listener to the event.

looks like RemovedFromDocument and DocumentContextChanged are not override-able in the scripting component ?

for phyton components i found “sdk mode” with enter/exit

what I am missing ?
thanks and kind regards - tom

trackClippingPlane_01.gh (6.4 KB)

This is tricky.

When you update your script (e.g. adding a white-space in your source), the script will be recompiled and get a different component name. This means, you can’t unsubscribe it easily within the entire runtime of Rhino because you lost the reference. But it will stay in memory and cause very weird behaviour, at least if you are not aware of this.

A solution would be to store the handler reference somewhere in global accessible data store. In IronPython this is the Sticky dictionary. Not sure if you can add it there or exploit a similar store somewhere else. E.g. Userdata? You can then unsubscribe the handler before you subscribe a new one, or by another logic.
I remember not to find a good solution for this problem. So I simply created a custom plugin and prevented to perform event-handling on script components at all.

Hi @Tom_P,

Something for your entertainment.

Test_Tom_P.gh (4.0 KB)

– Dale

you mean brain-gymnastics ?

many Thanks for your support.

… but the dispose() is not called.
when i add a few characters and save the script-Editor, the old instance still is active, not disposed.

so i will get:

before each _drag i open the script and add

// bla
// bla bla
// bla bla bla

commandline output:

Command: _Drag
MyEventHandler.OnReplaceRhinoObject
Command: _Drag
MyEventHandler.OnReplaceRhinoObject
1 closed extrusion added to selection.
Command: _Drag
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
Command: _Drag
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
Command: _Drag
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
Command: _Drag
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject

Edit:
added some Random “shortId” that will show the surviving, lost instances:

Command: _Drag
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject
MyEventHandler.OnReplaceRhinoObject from 6053
MyEventHandler.OnReplaceRhinoObject from 1711
MyEventHandler.OnReplaceRhinoObject from 5156

Test_Dale_F.gh (7.0 KB)

:thinking:

thanks for a second look - tom

Dispose will be called when the document is closed.

the following seams to work.
I use Component.InstanceGuid as a key to store the latest / current instance of the Script_Instance in the Documents RuntimeData.

when the eventhandler runs, i check if the called instance still matches the latest one.
If not i unhook / unsubscribe the event - so this only happens once.

So in some Initialization i do

Script_Instance currentInstance = RhinoDocument.RuntimeData.GetValue<Script_Instance>(Component.InstanceGuid,doc => this);

and for the Event-Handling:

private void OnReplaceRhObj(object sender, RhinoReplaceObjectEventArgs args)
    {
        Script_Instance currentInstance = RhinoDocument.RuntimeData.TryGetValue<Script_Instance>(Component.InstanceGuid);
        if (currentInstance == null || currentInstance != this)
        {
            if (m_replaceRhObj != null)
            {
                RhinoApp.WriteLine($"{shortId} is unsubscribing");
                RhinoDoc.ReplaceRhinoObject -= m_replaceRhObj;
            }
            return;
        }
        RhinoApp.WriteLine($"{shortId} is listening to ReplaceObject");
        if (args.OldRhinoObject.Id == trackId)
        {
            GrasshopperDocument.ScheduleSolution(5,m => {this.Component.ExpireSolution(false);});
        }
    }

trackClippingPlane_02.gh (5.9 KB)

@dale and @TomTom
what do you think of this approach - do i miss something ?
thanks - kind regards - tom

many thanks for digging in.
after reading your thoughts and looking at Dales suggestion i come up with above approach … what do you think ?

Yes I think this looks good. I think by the time I was writing complex scripts, there was no RuntimeData dictionary.

Still its a bit hard to understand what you are doing here, if you don’t know about the problems with event-handling in script components. Just in case you share the script to co-workers.

I still believe it would be better to not use event handling in scripts and rather think about a plugin solution. I think its dangerous if scripts alter the default intended behaviour of Grasshopper, because the “pipeline” is not designed to dynamically react to document changes.