Loading ruby observers into the active model

Hello :wave:

I have been using some of the ruby observers in my extension:
Sketchup::InstanceObserver, Sketchup::EntitiesObserver, Sketchup::ModelObserver

If I initialize them directly inside the loader.rb file they work for the first model that loads (until I do a File>New or File>Open). After this they don’t.

I added an AppObserver and each time there is a callback to onOpenModel or onNewModel
I attach the observers I defined in the same loader.rb.

This solved my issue but I am wondering what happens to the observers I attached earlier in the previous models. (if the user keeps clicking File>Open) Do I need to handle these when I load a new file each time? or does Sketchup magically do some sort of garbage collection?

class myObserver < Sketchup::AppObserver
  def onNewModel(model)
    #add all of the implemented observers
  end
  def onOpenModel(model)
    #add all of the implemented observers
  end
  def onQuit()
    #execute onQuit
  end
end
Sketchup.add_observer(myObserver.new)

First of all, there currently is no #onPreCloseModel observer callback (although it has been requested,) which a coder would need in order to go and remove observers. The code would also need to keep references to all the observer objects in order to pass to the various #remove_obsever methods.

Yes, I think it does. When a “watched” object becomes unreferenced by Ruby and marked for GC I think that SketchUp removes that object from it’s callback queues.

In your example, your code is not keeping a reference to the AppObserver object, instead you are letting the SketchUp observer queue hold the only reference. When the application is in the process of shutting down and has processed all the #onQuit callbacks, it will clear it’s queue and all the observers will become unreferenced.
But even before this all the model collection observer queues would also be cleaned up and their observers dereferenced.


Now, several things.

You should include the #expectsStartupModelNotifications callback so that the initial model opened (from a double-click on a skp file) or created (the new blank model) will get the observers attached.

On Mac it becomes very important to make sure that the model reference points at the correct model object as multiple may be open at the same time.
But, on Windows there is a weird thing where the singleton model reference is reused when the model changes. But the collection references for the new model do get new references. As was explained to us, when you attach the same kind of observer to the same object, it’s previous item in the queue is deleted and a new item is added at the end of the callback queue. (But things could change.) Anyway, we have not needed to worry on Windows about reattaching a model observer to the singleton model reference.

Where it becomes important to remove observers is when the tasks done or the watching upon the object are temporary. Ie, for an example, say you only need to watch something when a certain native tool is active. (You would use a ToolsObserver to detect tool changes and attach the temporary observer if the active tool id was a certain value, and remove the observer when it again changes.)
Another example would be if you knew that a component definition was going to be created and you need to do something when it was a valid object. You would temporarily attach a DefinitionsObserver to react when the next definition was fully created, and then detach the observer.

Anyway, … references must be kept in order to remove observers later. The best place to keep these references, is the instance of the ModelObserver attached to the model object.

Now, with regard to an extension’s AppObserver object, … there is only 1 application object … SketchUp itself. So in reality your extension does not need a class object. Classes are used when you need multiple copies (instances) of something.
The various API’s #add_observer methods do not do any type checking to verify that their arguments are subclasses of anything in particular. The observer mechanism only requires that the object’s callback methods be publicly accessible.
So when you only need 1 code object, a module is the correct kind of object. Since your extension must be within a submodule (of your namespace module,) it makes sense that the extension submodule itself be the singleton AppObserver object.
Something that many newer API observer coders miss is that “a module” is an instance of class Module and “a class” is an instance of class Class, which is a subclass of class Module. So “a class” and “a module” are very similar kinds of objects, but a class has been given the ability to have multiple instances each holding different state data.
What I’m getting at is that having your extension submodule be it’s own AppObserver object is not so weird a thing as it is also an instance object.

Example …

module SomeAuthor
  module SomeNiftyExtension

    extend self

    def attach(model)
      model.add_observer(ModelSpy.new(model))
    end

    def expectsStartupModelNotifications
      true
    end

    def onNewModel(model)
      attach(model)
    end

    def onOpenModel(model)
      attach(model)
    end

    def onQuit()
      # execute onQuit tasks
    end

    class ModelSpy < Sketchup::ModelObserver
      attr_reader :model
      def initialize(model)
        @model = model
        # Hash to hold observer references:
        @spy = {}
        # Add all of the implemented observers:
        model.definitions.add_observer(@spy[:definitions]= DefinitionsSpy.new)
        # ... etc ...
      end
      # ... ModelObserver callback method definitions ...
    end

    class DefinitionsSpy < Sketchup::DefinitionsObserver
      def onComponentAdded(definitions, definition)
        # Do something with the new definition ...
      end
    end

    # .. other observer class definitions ...

    # RUN ONCE AT STARTUP:
    if !defined?(@loaded)
      # Define UI objects here ...

      # Attach this module as an AppObserver object:
      Sketchup.add_observer(self)

      # Mark this extension as loaded:
      @loaded = true
    end

  end # extension submodule
end # namespace module

Another nifty thing is that since the v2016 release all the empty callback methods in the API observer superclasses were deleted so there is really no functionality for observer classes to inherit.
This fact and that also #add_observer does not type check means that you can create hybrid observers that contain the callback methods for multiple “abstract” observer superclasses.
It makes it much easier to pass data between callback methods if they are in the same class.

I often combine DefinitionsObserver and DefinitionObserver in the same class. And sometimes I will wrap all of the collection observers within a hybrid ModelObserver class. A sometimes when I don’t need separate model state variables I’ll put a mix of callbacks right in the extension submodule and it becomes a singleton observer object attached to whatever I need to attach it to, both permanently and temporarily.

2 Likes

thanks for the detailed response… super helpful for future reference also…

I see how this would be helpful for some developers.

and this would resolve the issue for me.

thanks for the tip, I was currently initializing it inside of the main module for the initial open. I will look into this callback -sounds like a better way to do it.