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