Add an observer onto a component's dynamic options

Hello everyone,

Is there a way to trigger an observer when a user clicks on the “Apply” button in the options of a dynamic component?

My goal is to retrieve the value of the attributes chosen by the user each time he modifies the options of the dynamic component.

Thanks for your help

There is no public actionable observer callback for the click on another author’s extension web interface (ie a button,) unless that author provides it and explicitly publishes the means for extensions by other authors to use it. The DC extension is proprietary and the only public interfaces are the manual use of it’s two web dialogs and the Interact tool.

At one time I thought I had filed an API feature request for a Dictionary observer class, but I cannot find it at the moment.


If the change in attributes cause a change to the dynamic instance, then some other observer callbacks may fire.

So test to see if EntityObserver#onChangeEntity fires for either the dictionary (which I think is a bug that does not happen,) or the component instance itself. This means you’ll need to attach this class of observer to all those DC instances you wish to “watch”.

EDIT: I think I posted a few years ago that this is possible. See this topic thread:

Sometimes making a DC property change causes the entities collection to change (such as adding or deleting copies of subcomponent instances such as pickets in a fence.) This means you’ll need to also monitor the DC definition’s entities collection with an EntitiesObserver.

To make it more difficult, if more than 1 instance of a DC component exists, and the change involves changes to the entities (as described above,) the DC extension will transparently clone the definition and make changes to the clone and reassign the “changed” instance as a child of the new cloned definition.
So simply “watching” the instance’s definition’s entities collection will not catch the change. You may also need to use InstanceObserver, DefinitionObserver and/or DefinitionsObserver to catch when a new definition is cloned from an old one.

Hacking a closed source extension is difficult and problematic in many ways. Foremost is the possibility that this extension could be changed at any time, or withdrawn in the future in preference to the newer Live Component feature.

One possible workaround is to try to change your thinking about when your extension will “retrieve the values of attributes” from “every time the user clicks Apply” to just before your extension does what it needs to do with the latest attribute values.

Note I also discussed this at least 5 years ago …

Thank you very much Dan,

Using your example, I have created my own EntityObserver class which is activated when the selection at the “My_componant” definition.

Here is my code:

module JulienDufren
  class Dynamic_componant_change
    # Saves all dynamic attributes in an array with their values
    def save_all_dynamic_attributes(all_attribut)
      mod = Sketchup.active_model
      sel = mod.selection   
      sel.grep(Sketchup::ComponentInstance).each do |s|
        s.definition.instances.each do |inst| 
	      inst.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
		    all_attribut << "#{key} = #{value}"
	      end	
	    end
      end 
    end
    # Remove unnecessary dynamic attributes from the array 
    def remove_useless_dynamic_attributes(all_attribut, user_attribut)
      exclude = []
      ["_formula","_formlabel","_options","_formulaunits","_access","_formulaunits",
	    "_units","_label","_has_movetool_behaviors","_hasbehaviors","_iscollapsed","_name"].each do |ex|
        all_attribut.each do |att|
          if att.include?(ex)
            exclude << att
	      end
	    end
      end
	  user_attribut << all_attribut - exclude
    end
    # Saves all dynamic attributes of the selection with their values in a file external to SketchUp always accessible!
    def save_dynamic_attributes
      all_attribut = []
      save_all_dynamic_attributes(all_attribut)
      user_attribut = []
	  remove_useless_dynamic_attributes(all_attribut,user_attribut)
	  user_attribut.flatten!
	  user_attribut.each do |att|
	    key = att.split(" =")[0]
	    value = att.split("= ")[1]
	    Sketchup.write_default("Plugin_name", "#{key}", "#{value}")
	  end
    end	
	# Activate the observer and delete it to prevent it from remaining constantly active.
    def onChangeEntity(entity)
      save_dynamic_attributes
	  lenx = Sketchup.read_default("Plugin_name", "lenx")
	  leny = Sketchup.read_default("Plugin_name", "leny")
	  lenz = Sketchup.read_default("Plugin_name", "lenz")
	  p "My component: Height = #{lenz.to_f.to_mm} , Width = #{lenx.to_f.to_mm} , Depth = #{leny.to_f.to_mm}"	  
    end
  end
  class My_class
    # Trigger the observer if the selected component definition name is = "My_componant"
    def self.save_dynamic_attribut
      mod = Sketchup.active_model
      sel = mod.selection
      sel.grep(Sketchup::ComponentInstance) do |s|
        if s.definition.name.include?("My_componant")	  
          s.add_observer(Dynamic_componant_change.new)
          s.attribute_dictionary("dynamic_attributes").add_observer(Dynamic_componant_change.new)
          s.definition.attribute_dictionary("dynamic_attributes").add_observer(Dynamic_componant_change.new)
		end
	  end
    end
	save_dynamic_attribut
  end
end

Here is a dynamic component attachment that works with my code.
My_componant.skp (51.8 KB)

In my example, I didn’t remove the watcher!
Should it be removed to avoid problems?

Why does my code send me several times the dimensions of my dynamic component?
Is this an error on my part?
Do you have any suggestions for me on my code so that I can improve it?

Thanks in advance

Readability

  1. Try to keep your code lines to 80 characters or less, especially if you will be posting to a forum. Very long lines causing line wrap and make the code hard to read.

  2. Please use correct indentation. Many of your code blocks are not indented correctly which cause a reader to need to copy the code into an editor and correct the indents just to read it.
    Use space not tabs. (Set your editor to replace TAB characters with 2 spaces.)

  3. Many of your code lines have blank space(s) at the end. This is not needed.

Usability

  1. Use a Hash rather than an Array to copy dictionary data. The Sketchup::AttributeDictionary class has the Ruby Core Enumerable module mixed in, which gives dictionaries a #to_h method that makes using a hash “easy as pie”.
    * If you look at the top of the Sketchup::AttributeDictionary class documentation page (or any class page,) you will see a listing labeled “Includes” which tells you what libraries have been mixed into the class.
    Using a hash via #to_h will replace your whole

    inst.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
      all_attribut << "#{key} = #{value}"
    end
    

    block of code.

  2. You are not filtering only dynamic components from your selection.
    Your code is assuming every instance is dynamic. You need to select only those whose definition has a "dynamic_attributes" dictionary.

    def is_dynamic?(inst)
      inst.definition.attribute_dictionary("dynamic_attributes")
    end ###
    
    # Gets all dynamic instances from selection
    def get_all_dynamic_instances
      mod = Sketchup.active_model
      sel = mod.selection   
      sel.grep(Sketchup::ComponentInstance).select do |inst|
        is_dynamic?(inst)
      end
    end ###
    
  3. Your code is saving all of the attributes in ONE collection without separating them. Each set of an instance’s attributes need to be kept separate and stored by the instance name property which is not a dynamic attribute. It is a Ruby API property (ie, inst.name).
    This means you need a collection of attribute sets. This means a hash of nested hashes.

  4. Also, your code is assuming that all dynamic attributes will be attached to the instance.
    This is not true. Only those that differ from the definition default values will exist in the instance’s dictionary.
    (In fact, if none of the attributes differ, then the instance itself will not even have a dynamic dictionary.)

  5. This kind of model specific data does not belong in the application / plugin defaults files.
    Save such data in the model’s file folder as a JSON file. We have covered this in this forum category quite a few times. (I myself have posted method snippets to do this.)

  6. Your list of attributes to exclude is missing quite a few of the DC “system” hidden attributes (those that begin with an underscore.)

The SketchUp engine in some cases calls observer callbacks multiple times.
There are likely to be open issues in the official issue tracker for these cases.

Also the DC extension code might make several changes to an instance or it’s definition’s entities which might be the cause of multiple observer callbacks.

You will need to code defensively. But if you use a hash as said above, then the attributes will just overwrite the same key/value in the hash and you will not get multiple entries as you do when just appending an array.

You remove an observer attachment when you no longer need to watch something.

However, the API will automatically remove the observer attachment if the object is deleted, or the model is closed.


I suggest using Aerilius’ Attribute Inspector extension if you are going to be coding an extension that looks at dynamic dictionaries. It is in the Warehouse.

1 Like

Hello @juliendufren2016, I read your code and then saw your question:

I noticed, “_formulaunits”, in your code below seems to appear twice.

Maybe this is the culprit.

Thank you for posting the code. I’m trying to learn so examples are very helpful.

Thanks for your feedback Dan!

I am preparing a response to all your remarks and I will post it as soon as I have finished analyzing all your comments.

Before that, I would like to share a problem that I do not understand!
Why does the method below not automatically exclude keys with the values indicated in the table?

    def save_all_dynamic_attributes(all_attribut)
      mod = Sketchup.active_model
      sel = mod.selection 	  
	  exclude = ["_value","_formula","_formulaunits","_access","_formlabel","_units",
	  "_options","_has_movetool_behaviors", "_hasbehaviors", "_lengthunits", 
	  "_name", "_last_","_lastmodified","_formatversion"
	  ]	  
      sel.grep(Sketchup::ComponentInstance).each do |s|
        s.definition.instances.each do |inst| 
	      inst.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
			exclude.each do |ex|
		      all_attribut[key] = value unless key.include?(ex)
			end
	      end	
	    end
      end 
    end

	all_attribut = {}
	save_all_dynamic_attributes(all_attribut)
	p all_attribut

This would save me from creating a second array to subtract useless keys.

Thanks Dan for your feedback!

Readability
I use Notepad++ and would like to know if there is something better?
1.Is it possible to force a line break after 80 characters?
2.How do I change the indentation without having to manually rearrange all my lines of code?
3.I can’t see the empty spaces at the end of the lines! How do you see them?

Usability

  1. In my new example I use a hash.

  2. I also filtered instances only with dynamic attributes.

  3. I don’t need to order attributes with instance name because there is only one instance where i need to copy the dynamic attributes.

  4. I don’t understand your remark because the dynamic attributes created manually are indeed attached to the instance.
    Here is an example that displays all manually created dynamic attributes:

mod = Sketchup.active_model
sel = mod.selection 	  
sel.grep(Sketchup::ComponentInstance).each do |s|
  s.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
    p "#{key} = #{value}"
  end
end
  1. The goal is to retain attributes and their values ​​all the time even on new projects.
    5a. JSON file allow to do it and if so how?
    5b.Does using the “Sketchup.write_defaultwrite_default” method cause technical problems?
    5c. Is it illegal to use “Sketchup.write_defaultwrite_default” for extension information?

  2. Thank you I will use the Inspector extension to know the complete list of hidden attributes to remove.

Code corrected according to the remarks that I think I understood:

module JulienDufren
  class Dynamic_cp_change
    # Create a hash of the dynamic attributes of the selection with the values
    def save_all_dynamic_attributes(attribute)
      mod = Sketchup.active_model
      sel = mod.selection 	  
	  exclude = ["_value","_formula","_formulaunits","_access","_formlabel","_units",
	  "_options","_label","_has_movetool_behaviors", "_hasbehaviors", "_lengthunits", 
	  "_name", "_last_","_lastmodified","_formatversion","_iscollapsed"
	  ]	 
      all_attribute = {}	  
	  delete_attribute = {}
      sel.grep(Sketchup::ComponentInstance).each do |s|
        s.definition.instances.each do |inst| 
		  if inst.definition.attribute_dictionary("dynamic_attributes")
	        inst.attribute_dictionaries["dynamic_attributes"].each_pair do |key, value|
		      all_attribute[key] = value
			  exclude.each do |ex|
		        delete_attribute[key] = value if key.include?(ex)
			  end
			end
	      end		  
	    end
      end 
	  att = delete_attribute.each_with_object(all_attribute.dup){|(k, v), h| h.delete(k)} 
	  attribute.merge!(att)
    end
    # Saves all dynamic attributes of the selection with their values in a file external 
	# to SketchUp always accessible!
    def save_dynamic_attributes
      attribute = {}
	  save_all_dynamic_attributes(attribute)
	  attribute.each do |key, value|
	    Sketchup.write_default("Plugin_name", "#{key}", "#{value}")
	  end
	end
	# Activate the observer and delete it to prevent it from remaining constantly active.
    def onChangeEntity(entity)
      save_dynamic_attributes
	  lenx = Sketchup.read_default("Plugin_name", "lenx")
	  leny = Sketchup.read_default("Plugin_name", "leny")
	  lenz = Sketchup.read_default("Plugin_name", "lenz")
	  p "My component: Height = #{lenz.to_f.to_mm} , Width = #{lenx.to_f.to_mm} 
	  , Depth = #{leny.to_f.to_mm}"	  
    end
  end
  class My_class
    # Trigger the observer if the selected component definition name is = "My_componant"
    def self.save_dynamic_attribut
      mod = Sketchup.active_model
      sel = mod.selection
      sel.grep(Sketchup::ComponentInstance) do |s|
	    defi_attribut = s.definition.attribute_dictionary("dynamic_attributes")
		inst_attribut = s.attribute_dictionary("dynamic_attributes")
        if s.definition.name.include?("My_componant")	  
          s.add_observer(Dynamic_cp_change.new)
          inst_attribut.add_observer(Dynamic_cp_change.new)
          defi_attribut.add_observer(Dynamic_cp_change.new)
		end
	  end
    end
	save_dynamic_attribut
  end
end	

You’re welcome.

I like to post the complete codes to find them in the future.
I also do it to share my progress with others and to be corrected by experienced programmers.

Don’t know. Don’t really care as it’s not the best way. Try …

h = inst.attribute_dictionary('dynamic_attributes').to_h
h.delete_if { |key,val| key.start_with('_') }

The hash h now contains only attributes that do not begin with an underscore.
You can merge with the hash passed into the method like:

all_attribut.merge(h)

I also still mostly use Notepad++. Microsoft VS Code is also good.

If NP++ has already replaced the TABs with spaces then either do it manually or you might search for a NP++ Ruby cleanup plugin.

You can manually change the indents for a whole block by highlighting the block and click TAB (to move the block to the right 1 indent) or SHIFT+TAB (to move the block 1 indent to the left.)

Click the cursor to the far left of the lines and this will move the caret to the end of the line.
When you see it has space between the caret and the end of the line, you have unneeded spaces.
This can happen when you accidentally type a TAB at the end of the line.

Then why is your code going through the definition and iterating it’s instances collection ?

Again, this does not belong in the application defaults, unless this is somehow a default for every model that will ever be opened.

As I said, I (and others) have already posted this. Learn how to search the category.

Thanks for all the info Dan!
No problem, I’ll do some research on creating JSON files.

Some of my previous posts …

Error YAML::dump - #12 by DanRathbun

Storing Plugin Data Revisited (Wall Presets) - #19 by DanRathbun

By compiling all the tips in this thread and my research, I think I found the best solution to my problem.

Basic objective:
Purge definitions without instances produced by my dynamic components.

Best solution:
Sketchup::EntitiesObserver.onElementModified

Why?
This watcher is the only one able to fire when a nested component instance is modified!
Unlike Sketchup::EntityObserver, this observer doesn’t require you to browse all instances to watch them!

METHOD
Add an attribute to all definitions of the selection to watch:

def self.set_custom_attribute_all_definitions( ents )
  ents.grep(Sketchup::ComponentInstance) do |i| 
	i.definition.set_attribute "my_dictionary", "my_attribute", "my_value"
	set_custom_attribute_all_definitions( i.definition.entities )
  end
end
mod = Sketchup.active_model
sel = mod.selection
set_custom_attribute_all_definitions( sel )

Add the EntitiesObserver to purge my definitions without instances in the model:

class MyEntitiesObserver < Sketchup::EntitiesObserver
  def onElementModified(entities, entity)
	if entity.is_a? (Sketchup::ComponentDefinition) 
      if entity.count_instances == 0
		if entity.attribute_dictionaries
	      val = entity.get_attribute "my_dictionary","my_attribute"
		  if val == "my_value"
	        entity.entities.each do |i|
	          mod = Sketchup.active_model 
	          mod.start_operation("Purge Definition", true, true, true)				
		      i.parent.entities.clear! if i.valid?
			  entities.remove_observer(self)
			  mod.commit_operation
		    end
	      end
		end
	  end
	end 
  end
end
mod = Sketchup.active_model  
@change_entities ||= MyEntitiesObserver.new  
mod.entities.add_observer(@change_entities) 

These code examples are a very simplified version of the methods I’m going to use!
The goal is to show the direction I have chosen. :wink:

First of all, the crazy excessive nesting is a signal of poor coding practice.
It makes the code hard to read and therefore difficult to understand.

You can use test conditional statements to “bailout” of the method early, …
and sometimes compound boolean expression to combine statements:

class MyEntitiesObserver < Sketchup::EntitiesObserver

  def onElementModified(entities, entity)
    return unless entity.is_a?(Sketchup::ComponentDefinition)
    return if entity.count_instances > 0 || entity.attribute_dictionaries.nil?
    val = entity.get_attribute("my_dictionary", "my_attribute")
    return unless val == "my_value"
    mod = entity.model 
    entity.entities.each do |i|
      mod.start_operation("Purge Definition", true, true, true)        
        i.parent.entities.clear! if i.valid?
        entities.remove_observer(self)
      mod.commit_operation
    end # each ... do
  end ### onElementModified()

end # class

However this block does not make sense to me:

    mod = entity.model 
    entity.entities.each do |i|
      mod.start_operation("Purge Definition", true, true, true)        
        i.parent.entities.clear! if i.valid?
        entities.remove_observer(self)
      mod.commit_operation
    end # each ... do

It is iterating entity.entities, but inside if any of the children are valid, then it “seems to be” clearing the same collection that it is iterating. This is a big no-no in programming which leads to errors.

I mean … i.parent is the same as entity, is it not?
And so, entity.entities could be cleared whilst it is being iterated.


I’m not sure when SketchUp will do the definition cleanup and remove empty definitions. We would hope that this does not happen immediately because if entity becomes invalid and more observers are waiting in the queue and will receive the entity reference which they assume would be valid, errors might occur. When errors occur in observer callback there have been crashes in the past.

1 Like

By publishing my code I suspected that many constructive remarks were going to be given to me.

That’s why it does it every time. :wink:

Certainly I’ve gotten into bad habits, but I understand nested code better!
As I am self-directed and Ruby is my first language, I trust you, I will opt for the return method.

I’m going to use Sketchup::ToolsObserver to monitor when the user activates the Interacts with dynamic components icon.
As long as this tool is active, the observer will be too.

dezmo you’re right so I rewrote the code following your advice and that of Dan:

class MyEntitiesObserver < Sketchup::EntitiesObserver
  def onElementModified(entities, entity)
	return unless entity.is_a?(Sketchup::ComponentDefinition)
    return if entity.count_instances > 0 || entity.attribute_dictionaries.nil?
	val = entity.get_attribute "my_dictionary","my_attribute"
    return unless val == "my_value"
	mod = entity.model
	mod.start_operation("Purge Definition", true, true, true)				
    entity.entities.clear! if entity.valid?
	entities.remove_observer(self)
    mod.commit_operation
  end
end
mod = Sketchup.active_model  
@change_entities ||= MyEntitiesObserver.new  
mod.entities.add_observer(@change_entities)

I am currently unable to reproduce the error on a tested component that I could share.

I will continue my research following your advice and I will post a more complete code later with a component producing the error if possible.

1 Like

Now lets talk about the transparent operation arguments. Why do have them set to true ?

The 4th arg will merges your operation with the previous one.

The 3rd arg is deprecated, … which merges your operation with the next one.
How do you know what kind of operation will be coming next ?

Actually I haven’t done much research on the start_operation method and I don’t understand everything.

  • In this example I put all the values on “True” so that the cancellation of the operation is done in a single click on “Undo”.

  • Is if the structure window is open, the code execution time also seems faster to me.

In other cases:
The goal is to more easily undo final methods that encompass many methods that open and close operations.

Surely it’s risky?

As promised here is my final method to purge entities without instances generated by my dynamic components and the “Interact with dynamic components” tool:

# Purge entities without instances that have "my_attribute" attribute!
class MyEntitiesObserver < Sketchup::EntitiesObserver
  def all_instances_in_entities( ents, array )
	ents.grep(Sketchup::ComponentInstance) do |i|
	  array << i
      all_instances_in_entities( i.definition.entities, array )
	end
  end  	
  def onElementModified(entities, entity)
	return unless entity.is_a?(Sketchup::ComponentDefinition) 
    return if entity.count_instances > 0 || entity.attribute_dictionaries.nil? 
	val = entity.get_attribute "my_dictionary","my_attribute"
    return unless val == "my_value"
	return unless entity.valid?
	mod = entity.model
	mod.start_operation("Purge Definition", true, true, true)	
	inst = []
    all_instances_in_entities( entity.entities, inst ) 	
	inst.each{ |i| i.parent.entities.clear! if i.valid? }
    mod.commit_operation
  end
end
# Active Tools Observer whenever the "Interact DC" is activated
# Or removes it if another tool is selected!
class MyToolsObserver < Sketchup::ToolsObserver
  def onActiveToolChanged(tools, tool_name, tool_id)
	mod = tools.model
	if tool_name == "RubyTool"
      @change_entities ||= MyEntitiesObserver.new  
      mod.entities.add_observer(@change_entities) 
    else
      mod.entities.remove_observer(@change_entities)	  
    end		
  end  
end
# Attach "ToolsObserver" when opening a project or a new model
class MyAppObserver < Sketchup::AppObserver
  def activate_observer_tools( model )
    @mytoolsobserver ||= MyToolsObserver.new  
    model.tools.add_observer(@mytoolsobserver) 	
  end
  def onNewModel(model)
    activate_observer_tools( model )
  end
  def onOpenModel(model)
    activate_observer_tools( model )
  end	
end 
Sketchup.add_observer(MyAppObserver.new) 

I had to adapt the methods in order to purge the entities in depth by looking for all the nested instances with their entities.

Otherwise some entities without instances continued to exist in the model.

Ps: Sorry I failed to create a dynamic component that create the same error.

You made an error of judgment on me because I test my codes before posting them!
However, I can understand your confusion with code that is out of context that you can’t test.

Try your last code, you will see that there are errors with the “count_used_instances” method!

You also use “purge_unused” which is a method that removes all user definitions.
And I don’t want to remove any definitions that aren’t mine.

Thank you for this information.

I ended up creating a component that reproduces almost the same problem:
DEFINITIONS ERROR BOX.skp (188.3 KB)

SketchUp_mrXVZvJsWo

Since the component is much simpler, so is the solution:

class MyEntitiesObserver < Sketchup::EntitiesObserver
  def onElementModified(entities, entity)
	return unless entity.is_a?(Sketchup::ComponentDefinition) 
    return if entity.count_instances > 0 || entity.attribute_dictionaries.nil? 
	val = entity.get_attribute "my_dictionary","my_attribute"
    return unless val == "my_value"
	return unless entity.valid?
    entities.model.start_operation("Purge Definition", true, true, true)
    p "DEFINITION PURGED : #{entity.name}"
    entity.entities.clear!		
	entities.model.commit_operation
  end
end
mod = Sketchup.active_model
@myentitiesobserver ||= MyEntitiesObserver.new  
mod.entities.add_observer(@myentitiesobserver)	

SketchUp_vxDUiwjiID

Ps: I was wrong in answering on this topic because all my last posts concern the other topic.
Is it possible to move all the latest posts to the “Create an array of all modified entities” topic?