Renaming Vision Template Parameters Removes Corresponding Template Instance Parameter Values

Not a clue, to be honest. There's a lot of stuff happening in the Vision property editor that's hard to track through even with full access to the code. I won't have time to drive through it all anytime soon.

1 Like

No worries, thank you for the input.

Regardless, please let me know if you see any glaring issues in the code I posted above. Seems like I am super close to having it work, just missing that final piece to prevent needing to copy/paste the component to have the designer realize the bindings exist.

So you might get somewhere with DynamicPropertyProviderCustomizer.renameProperty (note that this is a static method).

Interestingly, it actually looks like find & replace is supposed to work for this:

But an internal bug in the find and replace code (probably because templates are special unicorns) means that it fails:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException: Cannot invoke "java.util.TreeMap.remove(Object)" because "props" is null
	at com.inductiveautomation.factorypmi.designer.search.searchobjects.DynamicPropertyNameSearchObject.setText(DynamicPropertyNameSearchObject.java:95)
	at com.inductiveautomation.ignition.designer.findreplace.SearchReplaceDialog.replace(SearchReplaceDialog.java:582)
	at com.inductiveautomation.ignition.designer.findreplace.SearchReplaceDialog.lambda$initControlPanel$3(SearchReplaceDialog.java:340)
	at java.desktop/javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1972)
	at java.desktop/javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2313)
	at java.desktop/javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:405)
	at java.desktop/javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:262)
	at java.desktop/javax.swing.plaf.basic.BasicButtonListener.mouseReleased(BasicButtonListener.java:279)
	at java.desktop/java.awt.Component.processMouseEvent(Component.java:6671)
	at java.desktop/javax.swing.JComponent.processMouseEvent(JComponent.java:3385)
	at java.desktop/java.awt.Component.processEvent(Component.java:6436)
	at java.desktop/java.awt.Container.processEvent(Container.java:2266)
	at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5041)
	at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2324)
	at java.desktop/java.awt.Component.dispatchEvent(Component.java:4866)
	at java.desktop/java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4969)
	at java.desktop/java.awt.LightweightDispatcher.processMouseEvent(Container.java:4583)
	at java.desktop/java.awt.LightweightDispatcher.dispatchEvent(Container.java:4524)
	at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2310)
	at java.desktop/java.awt.Window.dispatchEventImpl(Window.java:2807)
	at java.desktop/java.awt.Component.dispatchEvent(Component.java:4866)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:797)
	at java.desktop/java.awt.EventQueue$3.run(EventQueue.java:742)
	at java.desktop/java.awt.EventQueue$3.run(EventQueue.java:736)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:97)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:769)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:767)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:766)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:207)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:128)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:117)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:113)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:105)
	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:92)

But an NPE is a bug I can fix, so I can put this on the agenda to fix in some future >= 8.3.x release.

EDIT: This is IGN-14005 if you want to contact support about it and have them tell you when it's fixed.

2 Likes

I am trying to use the DynamicPropertyProviderCustomizer class now and I am not getting error errors but the bindings do not seem to be getting moved to the new parameters.

#Import libraries
from com.inductiveautomation.factorypmi.designer.property.customizers import DynamicPropertyProviderCustomizer as dppc

# Define old template path
oldTemplatePath = str("OldTemplates/ExampleTemplate")

# Define new template path
newTemplatePath = str("NewTemplates/ExampleTemplate")

# Define old / new template parameters
parameterTransferList = [
{"OLD":"Old_P1", "NEW":"New_P1"},
{"OLD":"Old_P2", "NEW":"New_P2"},
{"OLD":"Old_P3", "NEW":"New_P3"},
{"OLD":"Old_P4", "NEW":"New_P4"}
]

# Define bindings to be deleted
parameterDeleteList = [
"Old_P5",
"Old_P6"
]

# Confirm user intent
if bool(system.gui.confirm("You are about to convert all OLD template instances in the same container as this button to NEW template instances.\n\nOLD: [" + oldTemplatePath + "]\nNEW: [" + newTemplatePath + "]\n\nAre you sure you wish to continue?", "Confirm", False)):
	
	# Get scripting objects
	window = system.gui.getParentWindow(event)
	container = event.source.parent
	interactionController = window.getInteractionController()
	#context = interactionController.getContext()
	
	# Loop through all components in parent container
	for component in container.components:
		
		try:
			templatePath = component.templatePath
		except:
			templatePath = None
		
		# Modify ActuatorComponent template instances
		if templatePath == oldTemplatePath:
			
			# Transfer old parameters to new parameters
			for parameter in parameterTransferList:
				
				binding = interactionController.getPropertyAdapter(component, parameter["OLD"])
				
				if binding is not None:
					#interactionController.setPropertyAdapter(component, parameter["NEW"], binding)
					#interactionController.removePropertyAdapter(component, parameter["OLD"])
					
					#binding.setTargetPropertyName(parameter["NEW"])
					
					dppc.renameProperty(component, parameter["OLD"], parameter["NEW"])
					
				else:
					value = component.getPropertyValue(parameter["OLD"])
					component.setPropertyValue(parameter["NEW"], value)
			
			# Delete unused bindings
			for parameter in parameterDeleteList:
				
				binding = interactionController.getPropertyAdapter(component, parameter)
				
				if binding is not None:
					interactionController.removePropertyAdapter(component, parameter)
					
				else:
					pass
			
			# Change component template path
			component.templatePath = newTemplatePath
			
		else:
			pass
	
	# Notify user
	system.gui.messageBox("Template conversion complete", "Template Conversion Complete")
	
else:
	system.gui.messageBox("Template conversion canceled, no changes were made", "Template Conversion Canceled")

Am I using the renameProperty method correctly?

Here's the relevant bit (bug and all) from how find & replace is attempting to perform essentially the operation you're trying. You may get something useful out of this; obviously it's Java but aside from the casting it should translate pretty clearly.

MutableDynamicPropertyProvider provider = (MutableDynamicPropertyProvider) component;
DynamicPropertyDescriptor[] existing = provider.getProperties();
Set<String> collisions = new HashSet<String>();
for (DynamicPropertyDescriptor dpd : existing) {
    collisions.add(dpd.getName());
}

// Validates name for validity, uniqueness
DynamicPropertyProviderCustomizer.checkDynamicPropertyName(provider, newName, collisions);

PropertyAdapter toReinstall = DynamicPropertyProviderCustomizer.renameProperty(component, oldName, newName);

TreeMap<String, DynamicPropertyDescriptor> props = provider.getDynamicProps();

props.remove(oldName);
property.setName(newName);
property.setDisplayName(newName);
props.put(newName, property);

InteractionController ic = DynamicPropertyUtil.getInteractionController((Component) provider);

if (ic != null && toReinstall != null) {
    ic.setPropertyAdapter(component, newName, toReinstall);
}

DynamicPropertyUtil.firePropertyChange((JComponent) provider, newName, null, property.getValue());

comp.changed();
2 Likes

Thanks for sending that code snippet! I Tried my best to adapt it to my script and ran into some issues with type casting the component object into a MutableDynamicPropertyProvider and I could not tell where the property variable in the following lines came from:

property.setName(newName);
property.setDisplayName(newName);

Here is the code I came up with, but it always throws an error saying that property is NoneType and does not have a .setName method. It will also throw an error on component.changed() stating that component does not have a .changed() method.

# Import java classes
from java.util import TreeMap
from com.inductiveautomation.factorypmi.designer.property.customizers import DynamicPropertyProviderCustomizer
from com.inductiveautomation.factorypmi.application.binding.util import DynamicPropertyUtil

# Define old template path
oldTemplatePath = str("OldTemplates/ExampleTemplate")

# Define new template path
newTemplatePath = str("NewTemplates/ExampleTemplate")

# Define old / new template parameters
parameterTransferList = [
{"OLD":"Old_P1", "NEW":"New_P1"},
{"OLD":"Old_P2", "NEW":"New_P2"},
{"OLD":"Old_P3", "NEW":"New_P3"},
{"OLD":"Old_P4", "NEW":"New_P4"}
]

# Confirm user intent
if bool(system.gui.confirm("You are about to convert all OLD template instances in the same container as this button to NEW template instances.\n\nOLD: [" + oldTemplatePath + "]\nNEW: [" + newTemplatePath + "]\n\nAre you sure you wish to continue?", "Confirm", False)):
	
	# Get scripting objects
	window = system.gui.getParentWindow(event)
	container = event.source.parent
	interactionController = window.getInteractionController()
	
	# Loop through all components in parent container
	for component in container.components:
		
		try:
			templatePath = component.templatePath
		except:
			templatePath = None
		
		# Modify ActuatorComponent template instances
		if templatePath == oldTemplatePath:
			
			# Transfer old parameters to new parameters
			for parameter in parameterTransferList:
				
				old_binding = interactionController.getPropertyAdapter(component, parameter["OLD"])
				
				if old_binding is not None:
					# Rename the property
					# This returns a PropertyAdapter that may need to be reinstalled later
					new_binding = DynamicPropertyProviderCustomizer.renameProperty(component, parameter["OLD"], parameter["NEW"])
					
					# Get the map of dynamic properties from the provider
					# Remove the old property name from the map
					component_props = TreeMap(component.getDynamicProps())
					component_props.remove(parameter["OLD"])
					
					# Get the changed property from the map
					# Update the property object with the new name and display name
					changed_prop = component_props.get(parameter["NEW"])
					changed_prop.setName(parameter["NEW"])
					changed_prop.setDisplayName(parameter["NEW"])
					
					# Put the updated property back into the map with new name
					component_props.put(parameter["NEW"], changed_prop)
					
					# Reinstall binding to interaction controller
					interactionController.setPropertyAdapter(component, parameter["NEW"], new_binding)
					
					# Notify the UI that the property has changed
					# This triggers any listeners or UI updates associated with the property
					DynamicPropertyUtil(component, parameter["NEW"], None, changed_prop.getValue())
					
					
					# Mark the component as changed
					component.changed()
					
				else:
					value = component.getPropertyValue(parameter["OLD"])
					component.setPropertyValue(parameter["NEW"], value)
			
			# Change component template path
			component.templatePath = newTemplatePath
			
		else:
			pass
	
	# Notify user
	system.gui.messageBox("Template conversion complete", "Template Conversion Complete")
	
else:
	system.gui.messageBox("Template conversion canceled, no changes were made", "Template Conversion Canceled")

You don't need to explicitly cast anything in Jython since it relies on duck typing - if it's a template instance in a Vision window, you're already guaranteed that it's going to be a MutableDynamicPropertyProvider. Which exposes a getDynamicProps() method that returns a map (think Python dictionary) of strings to DynamicPropertyDescriptor instances.
So it would be something like:
property = component.getDynamicProps().get("oldName")

I don't think you need that - it's to mark the window resource as "dirty" so that the designer knows it needs to commit and push changes when you next save the project, but opening the window in the designer will do that for you.

1 Like

Thank you again for the help troubleshooting my code! I was finally able to get seemingly reliable working solution for “converting” template instances from an old template to a new template without losing any of the binding or parameter values!

For those reading this who would like to do the same, here is my final code as of today:

Note: This code seeks the mimic the built-in find & replace code shared by Paul, however, I ran into the same NullPointerException errors that he mentioned too, and I will discuss my hypothesis on the source of this bug below.

This is intended to be placed in a button’s actionPreformed event script and will search for old template instances in the SAME CONTAINER as the button and will convert them to the new template as prescribed by the property dictionaries at the top of the script

# Import java classes
#from java.util import TreeMap
from com.inductiveautomation.factorypmi.designer.property.customizers import DynamicPropertyProviderCustomizer
from com.inductiveautomation.factorypmi.application.binding.util import DynamicPropertyUtil

# Define old template path
oldTemplatePath = str("OldTemplates/ExampleTemplate")

# Define new template path
newTemplatePath = str("NewTemplates/ExampleTemplate")

# Define old / new template parameters
parameterTransferList = [
{"OLD":"Old_P1", "NEW":"New_P1"},
{"OLD":"Old_P2", "NEW":"New_P2"},
{"OLD":"Old_P3", "NEW":"New_P3"},
{"OLD":"Old_P4", "NEW":"New_P4"}
]

# Define bindings to be deleted
parameterDeleteList = [
"Old_P5",
"Old_P6"
]

# Constructs a list of all parameters being removed from the component
parameterRemovalList = [p["OLD"] for p in parameterTransferList]
parameterRemovalList = parameterRemovalList + parameterDeleteList

# Confirm user intent
if bool(system.gui.confirm("You are about to convert all OLD template instances in the same container as this button to NEW template instances.\n\nOLD: [" + oldTemplatePath + "]\nNEW: [" + newTemplatePath + "]\n\nAre you sure you wish to continue?", "Confirm", False)):
	
	# Get vision objects
	window = system.gui.getParentWindow(event)
	container = event.source.parent
	interactionController = window.getInteractionController()
	
	# Loop through all components in parent container
	for component in container.components:
		
		# Check if component has a templatePath property
		try:
			templatePath = component.templatePath
		except:
			templatePath = None
		
		# Modify old template instances
		if templatePath == oldTemplatePath:
			
			# Loop through component parameters in the transfer dictionary list
			# Move bindings and values from old parameter to new parameter
			for parameter in parameterTransferList:
				
				old_binding = interactionController.getPropertyAdapter(component, parameter["OLD"])
				
				if old_binding is not None:
					# Rename the property
					# This returns a PropertyAdapter that may need to be reinstalled later
					new_binding = DynamicPropertyProviderCustomizer.renameProperty(component, parameter["OLD"], parameter["NEW"])
					
					# Get the map of dynamic properties from the component
					# Get the changed property from the map
					component_props = dict()
					for prop in component.getProperties():
						component_props.update({prop.getName(): prop})
						
					changed_prop = component_props[parameter["OLD"]]
					
					# This code block removes old properties from the component
					# This code results in an error due to a NPE commented below
					"""
					# Remove the old property from the compoent
					if DynamicPropertyUtil().checkRemovalLegality(component, parameter["OLD"], [parameterRemovalList]) is None:
						DynamicPropertyUtil().removeProperty(component, parameter["OLD"], []) # Throws: java.lang.NullPointerException: java.lang.NullPointerException: Cannot invoke "java.util.TreeMap.remove(Object)" because the return value of "com.inductiveautomation.vision.api.client.binding.MutableDynamicPropertyProvider.getDynamicProps()" is null
						
					else:
						pass
						
					"""
					
					# This code block mimics the behavior of the built-in find & replace Java code
					# This code resulted in duplicate parameters being being added to the properties list
					"""
					# Remove the old property name from the map
					# Put the updated property back into the map with new name
					del component_props[str(parameter["OLD"])]
					component_props.update({parameter["NEW"]: changed_prop})
					
					# Pass the updated map to the component
					component_props_map = TreeMap()
					
					for key, value in component_props.items():
						component_props_map.put(key, value)
					
					component.setDynamicProps(component_props_map)
					"""
					
					# Reinstall binding to interaction controller
					interactionController.setPropertyAdapter(component, parameter["NEW"], new_binding)
					
					# Notify the UI that the property has changed
					# This triggers any listeners or UI updates associated with the property
					DynamicPropertyUtil().firePropertyChange(component, parameter["NEW"], None, changed_prop.getValue())
					
				else:
					value = component.getPropertyValue(parameter["OLD"])
					component.setPropertyValue(parameter["NEW"], value)
					
			# Loop through component parameters in the removal dictionary list
			# Remove bindings and values from old parameter
			for parameter in parameterDeleteList:
				
				old_binding = interactionController.getPropertyAdapter(component, parameter)
				
				if old_binding is not None:
					# Delete the binding
					interactionController.removePropertyAdapter(component, parameter)
					
				else:
					pass
					
				# Get the map of dynamic properties from the component
				# Get the changed property from the map
				component_props = dict()
				for prop in component.getProperties():
					component_props.update({prop.getName(): prop})
					
				changed_prop = component_props[parameter]
				
				# This code block removes old properties from the component
				# This code results in an error due to a NPE commented below
				"""
				# Remove the old property from the compoent
				if DynamicPropertyUtil().checkRemovalLegality(component, parameter, [parameterRemovalList]) is None:
					DynamicPropertyUtil().removeProperty(component, parameter, []) # Throws: java.lang.NullPointerException: java.lang.NullPointerException: Cannot invoke "java.util.TreeMap.remove(Object)" because the return value of "com.inductiveautomation.vision.api.client.binding.MutableDynamicPropertyProvider.getDynamicProps()" is null
					
				else:
					pass
					
				"""
				
				# Notify the UI that the property has changed
				# This triggers any listeners or UI updates associated with the property
				DynamicPropertyUtil().firePropertyChange(component, parameter, None, changed_prop.getValue())
			
			# Change component template path to new template path
			component.templatePath = newTemplatePath
			
		else:
			pass
	
	# Notify user
	system.gui.messageBox("Template conversion complete", "Template Conversion Complete")
	
else:
	system.gui.messageBox("Template conversion canceled, no changes were made", "Template Conversion Canceled")

In regard to some of the exceptions being thrown by the find & replace code:

The issue seems to be stemming from the MutableDynamicPropertyProvider interface’s getDynamicProps() method always returning null (I could not ever get it to return anything other than null).

To get around this issue: I used the DynamicPropertyProvider interface’s getProperties() method and constructed a TreeMap from the returned list to simulate the getDynamicProps() method.

This caused the following lines of Java code from the find & replace snippet Paul sent to throw a NPE since “props” is null

TreeMap<String, DynamicPropertyDescriptor> props = provider.getDynamicProps();

props.remove(oldName);

This same issue with getDynamicProps() is also causing the DynamicPropertyUtil class’ removeProperty() method to always throw an exception (see below), preventing that method from being used

Executed code:

DynamicPropertyUtil().removeProperty(component, parameter["OLD"], [])

Thrown Exception:

java.lang.NullPointerException: java.lang.NullPointerException: Cannot invoke "java.util.TreeMap.remove(Object)" because the return value of "com.inductiveautomation.vision.api.client.binding.MutableDynamicPropertyProvider.getDynamicProps()" is null

The main issue I can still see with my code implementation is that some of the old parameters from the original template instance are still lingering invisible in the background (can see them in the XML if I copy the new template instance). Not sure if this is a big deal or not.

Shoutout to @pturmel and @paul-griffith for taking the time to provide a bunch of useful information!

1 Like