Managing events in Script_Instance (C#)

Hi all,
I’m wondering if anyone has found an elegant way to unsubscribe from events in the C# script component when it is overwritten/modified. Currently, running the code (which subscribes to events), modifying the code and then running it again means we have two Script_Instance subscribed to those events, except only one of them is associated with the Grasshopper document.

This is a minor bump of this topic on the old forum.

Ideally, we would have something equivalent to a Finalizer/Destructor that executes when the component is overwritten or intended to be no longer referenced. An actual finalizer looks like:

  private void RunScript(object x, object y, ref object A)
  {
    Rhino.RhinoApp.WriteLine("Script_Instance awake");
    System.GC.Collect();
    System.GC.WaitForPendingFinalizers();
    Rhino.RhinoApp.WriteLine("Script_Instance execution here...");
  }

  // <Custom additional code> 

  ~Script_Instance()
  {
    Rhino.RhinoApp.WriteLine("Script_Instance destroyed");
  }

  // </Custom additional code> 

But the issue is that it’s not going to get picked up by GC when it’s subscribed to events outside of itself (i.e. document events, 3rd party events).

So the question is, what’s the best (and ideally elegant…) way of knowing when a script instance is no longer in use, so we can unsubscribe any events and dispose of it properly?

One ‘solution’ to this is the following, but I’d rather have a more elegant (and not necessarily .dll dependent) solution for it:

All we do here is check a static container to see if the new script instance assigned to the component matches the old script instance, and if required execute the new constructor and the old destructor.

public static class GHUtilities
{
    private static Dictionary<Guid, Tuple<GH_ScriptInstance, Action>> _scriptInstanceAssignments = new Dictionary<Guid, Tuple<GH_ScriptInstance, Action>>();

    public static void LifeCycle(this GH_ScriptInstance instance, IGH_Component component, Action constructor, Action destructor)
    {
        lock (_scriptInstanceAssignments)
        {
            if (_scriptInstanceAssignments.TryGetValue(component.InstanceGuid, out Tuple<GH_ScriptInstance, Action> oldConfig))
            {
                if (oldConfig?.Item1 != instance)
                {
                    oldConfig?.Item2();
                    constructor();
                    _scriptInstanceAssignments[component.InstanceGuid] = new Tuple<GH_ScriptInstance, Action>(instance, destructor);
                }
            }
            else
            {
                constructor();
                _scriptInstanceAssignments[component.InstanceGuid] = new Tuple<GH_ScriptInstance, Action>(instance, destructor);
            }
        }
    }
}

And then usage is:

  public override void AfterRunScript()
  {
    this.LifeCycle(Component, 
      () => {
      Rhino.RhinoApp.WriteLine("Constructor");
      // Subscribe to events here
      
      }, () => {
        Rhino.RhinoApp.WriteLine("Destructor");
      // Unsubscribe from events here
      });
  }

Notes:

  • Usage of AfterRunScript() rather than BeforeRunScript() because on first run this.Component is not assigned.
  • This is a POC not production ready, as it doesn’t handle changing of instance guids, and should also watch for the component not being required further (i.e. monitoring objectsdeleted, documentclosed etc. and removing references to allow the script instances to be cleaned up)

Looking through the Reflect() of the script instance and container component didn’t appear to offer any event or overridable function when the script instance is changed.

The problem is that a script instance will not be disposed explicitly, and because it still has events will not be garbage collected either :slight_smile:

Indications that a component with events should stop doing what it’s doing:

  • Component is deleted (OnPingDocument() is null)
  • Component is moved to another document
  • Component is recompiled / code is changed
  • Component or document is disabled / locked

I generally use something like this:

public bool ShouldContinue() {
	var isDocEnabled = GrasshopperDocument.Enabled;
	var isComponentEnabled = !Component.Locked;
	var isDocumentDeleted = GrasshopperDocument == null;
	// this can also mean that you only changed documents, so perhaps for your use you might not want to do this.
	var isCurrentDocument = GrasshopperDocument == Instances.ActiveCanvas.Document;
	var isRecompiled = ScriptId != Component.Message;
	return isDocEnabled && isComponentEnabled && !isDocumentDeleted && isCurrentDocument && !isRecompiled;
}

    // this is a bit hacky, but works for detecting recompiles.
string ScriptId = null;
public override void BeforeRunScript()
{
	ScriptId = Component.Message = Guid.NewGuid().ToString();
}

If ShouldContinue() returns false, I generally unsubscribe/clean up/ dispose of the script.

1 Like

Ah yes, storing the ID in the message makes sense. Really I’m trying to find a way of largely ‘hiding’ the back end so that from a users perspective you can just add a one-time constructor/destructor.

Another consideration in your above example would be that a reference to the script instance would also need to be stored such that the ‘old’ destructor could be called to remove any subscribed events (presumably would also have to be with reflection)

Exactly:

Typically I would use this in for events that would fire after they’re wanted.

if (!ShouldContinue()) {
    // remove events and stuff.
}

Meaning events only get removed after an unwanted event has fired. I no events ensue, nothing will be removed. And possibly might not be picked up by an garbage collector, but I can generally live with that.

I dont think unwiring events belong in the destructor. (Discussion by Eric Lippert). And if the object is unwired and unreferenced it will eventually automatically be picked up by the garbage collector.

Ah, right so ShouldContinue is called from within the event handler.

And for clarity - I really meant a manual dispose and initialization method (which gets called by the Component or another manager), rather than a literal destructor/finalizer or constructor.