Dynamically add component to Root Container

I want to dynamically add a custom component to root container but I get an error saying: Cannot add directly to a container.
How can I achieve that? Thank you for your help.

Can you show the script you’ve put together so far?

The idea is to serialize some components and store them to db and later get the bytes back, deserialize them and put them back on the screen.

[code]from com.inductiveautomation.ignition.common.xmlserialization.serialization import XMLSerializer
from com.inductiveautomation.ignition.common.xmlserialization.deserialization import XMLDeserializer
from com.inductiveautomation.factorypmi.application.script import PyComponentWrapper
window= event.source.parent
elements= event.source.parent.getComponent(‘Dropdown’)
bytes= []
ser= XMLSerializer()
deSer= XMLDeserializer()
bytes= ser.serializeBinary(window, False)
print bytes
context= deSer.deserializeBinary(bytes)

list= context.getRootObjects()
comp= list[0]
print comp

event.source.parent.getComponent(‘Container’).add(comp)[/code]

You are going to have a lot of trouble with this, if you can get it to work at all. Mainly because bindings are not stored inside each component, but as part of the window they are created in. There’s a whole bunch of machinery that appears to only work properly when the designer adds a component or when a window is deserialized from a project resource. None of it is documented and toying with property adapters is very much discouraged.
The template canves exists to make these sorts of dynamic user interfaces possible in a supported way.

Is it possible to implement something like that in the designer?

F.e. I have a list of motors in the tags, and I want to loop over the motors (filtering them on some naming conventions and folder structure we have) and add motor templates with the correct bindings and naming on a container in the designer.

I don’t want to use the template canvas, as the motors will still have to be positioned, and other components will have to be added manually. The template canvas is great for dynamic screens, but I prefer a static screen in this case, just partially auto-generated.

I also don’t mind it to be hacky, as such generating code won’t be used in production, but just per page for the project setup.

I still don’t see why you can’t use the template canvas. Look at this blog post to see what is possible: http://nickmudge.info/post/drag-and-drop-Ignition-templates-on-the-template-canvas

A better solution would be to have a scripting function that can dynamically create template instances and add them to containers. This is something I've been thinking of working on. I am curious how many people would want this.

The paragraph above is from that article, and it's definitely what I would want. I don't want users (as in machine operators) to modify the pages, but I want to make it easier for our SCADA designers to (partially) generate our Ignition pages.

Once the page is designed, it shouldn't be altered a lot. But sometimes it might still need big changes (like switching to a different template the needs a different binding). If this could all be scripted in the designer and saved to the page, it would be great.

Implementing this in a template canvas would be way too hacky, and it would need a lot more code. Like saving the page setup and synchronising it to the other clients on a project update. Even if you implemented the drag-and-drop for canvas templates (which is a great feature), I still expect there to be a huge amount of work left. While I'd expect that by accessing the right Java function, it should be possible to show something on the container (albeit in a hacky way, but that's fine when it's only used by our designers).

Actually it is possible since I made it to work. I have a script which reads all the components on the screen and store the important data in Json format into the DB. To get the components back on the screen you have to create new instances of the same components that are stored in the DB and add them the describing data so that they are the same as before (same location, value etc.). But to achieve that you have to play with Ignition Javadocs . But I must warn you that there’s a lot of work behind it and also it might not work anymore with any new updates without reviewing the code.

I did a bunch of programmatic template instance creation operations a couple years ago. It was fragile and flaky and the resulting bindings sometimes didn’t all work until you closed and re-opened the window in the designer. Bindings are a pain to create from scratch and all of those interfaces are unsupported by IA.
That said, this function might get you started:

##########
# Given a container component with a 'templates' property of the form used in
# the Template Canvas, construct template instances for each row of the dataset
# as if the container was a canvas.  However, all rows must provide x,y,w,h
# and the instances will inherit the same positioning mode as the container.
# Only works in the designer.
#
from com.inductiveautomation.factorypmi.application.components.template import TemplateHolder
from org.json import JSONObject

def fakeCanvas(container, ds=None):
	if (system.util.getSystemFlags() & system.util.DESIGNER_FLAG) == 0:
		return
	if ds is None:
		ds = container.getPropertyValue('templates')
	heads = ['name', 'template', 'x', 'y', 'width', 'height', 'parameters']
	headsi = tuple([ds.getColumnIndex(x) for x in heads])
	index = {}
	for c in container.components:
		index[c.name] = c
	for _r in range(ds.rowCount):
		name, template, x, y, width, height, parameters = tuple([ds.getValueAt(_r, _c) for _c in headsi])
		if name in index:
			c = index[name]
			del index[name]
		else:
			c = TemplateHolder()
			c.name = name
			container.addComponent(c)
			c.initTemplate(container.appContext)
		c.templatePath = template
		print c.name, parameters
		if parameters:
			jo = JSONObject(parameters)
			for k in jo.keys():
				v = jo.get(k)
#				print 'Setting %s to %s on %s' % (c.name, k, v)
				try:
					setattr(c, k, v)
				except AttributeError:
					c.setPropertyValue(k, v)
		system.gui.reshapeComponent(c, x, y, width, height)

Thanks a lot for getting me started.

I ended up defining this simpler custom function, which can be easily called once per template to create. The params are a plain Python dict, so very easy to create in another script.

It’s indeed true that the page needs to be closed an reopened to assign the bindings. But that’s only a minor issue for us.

addTemplate(container=None, elementName="", templatePath="", x=0, y=0, width=30, height=30, params=None)
	"""
	@param container: container object to add the templates to
	@param elementName: name of the new GUI element
	@param x, y, width, height: dimensions and position of the element in the container
	@param params: additional params in a dictionary
	"""
	from com.inductiveautomation.factorypmi.application.components.template import TemplateHolder

	c = TemplateHolder()
	c.name = elementName
	container.addComponent(c)
	c.initTemplate(container.appContext)
	c.templatePath = templatePath
	if params != None:
		for key in params:
			print key + " " + str(params[key])
			try:
				setattr(c, key, params[key])
			except AttributeError:
				c.setPropertyValue(key, params[key])
	system.gui.reshapeComponent(c, x, y, width, height)

One thing I still wonder is if it’s possible to define an UDT binding on the template. Just for experimental reasons though, as we want to compare the speed between templates that take tag paths as attributes vs templates that take UDT definitions.

Found a way to add UDT’s to it, it needs to be done async, to ensure templates are created before assinging the UDT values:

def getUdt(tagpath, template)
	from com.inductiveautomation.ignition.common.sqltags.parser import TagPathParser
	from com.inductiveautomation.factorypmi.application.binding import UDTProperty
	parser = TagPathParser()
	print type(parser.parse(tagpath))
	udt = UDTProperty(parser.parse("Path/To/UDT/Definition"))
	udt.initialize(component, "ST_Motor", component.parent.appContext)
	udt.setDrivingTagPath(parser.parse(tagpath)) 
	print type(udt)
	return udt

# assign UDT's in async way to ensure templates are loaded before assignment
def assignUdts():
	for tag in system.tag.browseTags(parentPath = tagFolder, sort="ASC"):
		# all templates were called after their tag names, so get them by tag name
		tmp = container.getComponent(tag.name)
		udt = getUdt(tag.path, tmp)
		tmp.ST_Motor = udt
system.util.invokeLater(assignUdts, 5000)

Some things are still hard-coded, but at least it works now.

1 Like