Does anyone know of a good way to make a dynamically ordered container? I am working on a maintenance/manual override view with a table listing out all relevant I/O connected to the PLC. Along the right side is a container (Currently a Flex Column) that is parent to Coordinate Container that is the manual override control.
The toggle switch controls the selected output, and the check box assigns the container to the selected output.
The Override container block has been designed to allow for multiple instances to be added. I would like to always have one unassigned block visible. This isn't hard, however if for example there are four outputs being controlled, and five blocks visible. If the first block is deselected("Lock selection" is unchecked) all the remining blocks should be moved up and all new selections should be placed at the bottom of the coulomb. I think this can be done with coordinate containers, but I'm not sure if there is a better way.
Flex repeaters take a list of dictionaries for their instances, and the order of the items in the list is the displayed order. Move/change the item's position in the list and the change will be reflected in the flex repeater.
Is it posable to dynamically create instances, or do they need to be defined? I'm thinking of having one instance defined, and add another when an output is selected.
This is completely possible. I have a case where once the input is completed in one instance, I add a new blank instance to allow more entry. You pretty much just need to add or remove items from the instance list via script, and the flex repeater will update accordingly.
Just have a value change script on the value from the selection that either adds or removes the extra instance from the list depending on the current value.
You could go even simpler and have both instances already defined, and bind the visibility of the second to the selection value.
I finally found time to work on this project again. The dynamic ordering is mostly working, it is using a message handler to update the FlexRepeter Instance structure.
def onMessageReceived(self, payload):
"""
This method will be called when a message with the matching type code
arrives at this component.
Arguments:
self: A reference to this component
payload: The data object sent along with the message
"""
bInst = {"instanceStyle": {"classes": "","marginTop": 10},"instancePosition": {},"Table": {"activeRow": 0,"address": "", "discription": ""},"locked": False}
inst = []
length = 0
try:
inst = list(self.props.instances)
length = len(inst)-1
if inst is None or length < 0:
exception
# system.perspective.print("used existing structure "+str(length))
except:
# system.perspective.print("created new structure")
inst = []
inst.append(bInst)
length = len(inst)
loop = True
if length < 0 or inst == None:
loop = False
index = 0
if loop:
# for y in range(0,1):
for x in inst:
# remove unused eliments
if not x['locked'] and length > 0:
if index != length:
inst.pop(index)
system.perspective.print("removed item at index: "+str(index))
# check if last eliment is locked
if index == length and x['locked']:
inst.append(bInst)
system.perspective.print("added Item")
length = len(inst)-1
index += 1
self.custom.index = length
# update main display component
self.props.instances = inst
self.refreshBinding('props.instances')
Is there a better way to send the message then a "onMouseMove" event on the root container? I tried using a Gateway Timer Script, but that didn't work. It seams like a waste of server resources to run this script every time the mouse curser moves. I'm also not sure how an "onMouseMove" event works with a touch screen.
All Perspective scripts run in the gateway server. If triggered by UI interactions like MouseMove, then yes, quite the waste. Consider including buttons in each row that trigger the desired motions upon mouse click. Much more reasonable traffic and scripting overhead.
Does that work like the "onClick" or "onMouseDown" events? I tried both of these with not much succus. Ideally I would like it to send the message each time a check box changes state.
Send a message using the onActionPerformed script of the checkbox.
Your loop can be reduced to
def onMessageReceived(self, payload):
"""
This method will be called when a message with the matching type code
arrives at this component.
Arguments:
self: A reference to this component
payload: The data object sent along with the message
"""
bInst = {"instanceStyle": {"classes": "","marginTop": 10},"instancePosition": {},"Table": {"activeRow": 0,"address": "", "discription": ""},"locked": False}
inst = []
length = 0
try:
inst = list(self.props.instances)
length = len(inst)-1
if inst is None or length < 0:
exception
# system.perspective.print("used existing structure "+str(length))
except:
# system.perspective.print("created new structure")
inst = []
inst.append(bInst)
length = len(inst)
# Only update if there are items present and our instance exists
if length > 0 and inst is not None:
newInstanceList = [x for idx, x in enumerate(inst) if x['Locked'] and idx > 0]
if newInstanceList[-1]['Locked']:
newInstanceList.append(bInst)
else:
newInstanceList = inst
self.custom.index = len(newInstanceList)
# update main display component
self.props.instances = newInstanceList
self.refreshBinding('props.instances')
I'm not sure what exception is supposed to be...
Is it supposed to raise an exception so the except clause is executed ? That seems like a very convoluted way of doing things.
Otherwise, what is exception, and what do you expect will trigger the except ?
Also, If that's the case, then the following condition will always be true, since if the list is empty, you're putting things in it. Or am I completely misunderstanding what's going on ?
Then, you're filtering your list, keeping only those where locked is True, and right after that you're checking if the last element in the list is locked. This will either be True if the list is not empty, or raise an exception because you're accessing a property of an object that doesn't exist.
I'd rewrite it, but frankly I'm quite lost: I have no clue what the whole thing should do.
Note: I based this comment on Ryan's code, which he says should be equivalent to your own code. The original code contains other things that you really shouldn't do, like popping things from the list you're looping through - that's a recipe for disaster.
If you can explain in more details what you're trying to accomplish, I'll give it a shot.
I'm probably missing something simple, but sending the message from an actionPreformed event on the checkbox doesn't run the script. I tried a button in the same container as the FlexRepeter, and that didn't work ether.
I think exception throws an exception that breaks out of the try except statement. As far as I know it's unique to Ignition.
I am trying to build a dynamic list of control tiles for a maintenance interface. If the "lock selection" checkbox is checked, I want a new tile to be created below. If the checkbox on any tile is unchecked, I want that tile to be removed.
Some of the code I posted may not be very well written as this project is well beyond anything I've done in Ignition.
Did you use system.perspective.sendMessage? Did you have it send at the page level? Did you have the handler set to listen at the page level?
I send messages from templates to top level pages all the time. Its likely that your messages/message handlers were mis-configured.
Consider changing your code approach entirely. Send a message from the instance that was toggled including its index in the payload. Pop only that index from the list and return the new list, or in the case of adding an instance, check that the instance is last in the list and append a new item to the list.
That's the method that I use. The examples below are from one of my interfaces where I can select an instance and delete it or add an instance.
Add Item Method
def addMultiEntryInstance(sourceItems, DBHandler):
""" """
# Attribute check on passed object to prevent calling a method that does not exist
if not hasattr(DBHandler, "new"):
logger.warn("Provided handler has no method 'new'")
return sourceItems
sourceItems = shared.util.sanitizeIgnitionObject(sourceItems)
blankItem = DBHandler.new()
blankItem.update({"Instance": len(sourceItems)})
sourceItems.append(blankItem)
return sourceItems
Remove Item Method
def removeMultiEntryInstance(sourceItems, instanceIdx):
""" """
# Bounds check for instance index
if instanceIdx == -1 or instanceIdx >= len(sourceItems):
return sourceItems
sourceItems = shared.util.sanitizeIgnitionObject(sourceItems)
sourceItems.pop(instanceIdx)
if len(sourceItems) == 0:
return []
# Rebuild instance index numbers
for idx, item in enumerate(sourceItems):
item.update({"Instance": idx})
return sourceItems
The Message Handler is set to listen at the Page level. I don't know how to check if the message is being sent at the Page level. This is the first time I have used Messages in Ignition.
def runAction(self, event):
"""
This event is fired when the 'action' of the component occurs.
Arguments:
self: A reference to the component that is invoking this function.
event: An empty event object.
"""
if not self.props.selected:
self.getSibling("ToggleSwitch_1").props.selected = False
self.props.selected = False
messageType = 'updateList'
system.perspective.sendMessage(messageType)
system.perspective.print('msg sent')
Your example looks a lot simpler then what I came up with.
The manual specifies the additional parameters you can use when calling sendMessage. Also make sure the name of the message you are sending matches the name of the handler exactly (spaces/capitalization).
Is this action on a button? Or is it a toggle? This is on the checkbox, right? Is toggleSwitch_1 supposed to match the state of this component?
Your action script would need to be changed to something like:
Action Script
def runAction(self, event):
""" """
# Kept this in because I don't know what ToggleSwitch_1's purpose is
if not self.props.selected:
self.getSibling("ToggleSwitch_1").props.selected = False
# self.props.selected = False #this is already False, why are we setting it False again?
payload = {
'selected': self.props.selected,
'instance': self.view.params.instance
}
system.perspective.sendMessage("updateList", payload, scope="page")
return
Your message handler would then look something like this:
Message Handler Script
def onMessageReceived(self, payload):
""" """
if not payload:
return
selected = payload['selected']
instanceIdx = payload['instance']
bInst = {"instanceStyle": {"classes": "","marginTop": 10},"instancePosition": {},"Table": {"activeRow": 0,"address": "", "discription": ""},"locked": False}
# If checkbox was deselected, remove the instance from the list
if not selected and instanceIdx != len(self.custom.instances) - 1:
self.custom.instances = removeMultiEntryInstance(self.custom.instances, instanceIdx)
# If the instance is the last in the list, add another instance.
elif selected and instanceIdx == len(self.custom.instances) - 1:
instances = shared.util.sanitizeIgnitionObject(self.custom.instances)
instances.append(bInst)
instances[-1].update({"instance": len(instances) - 1})
self.custom.instances = instances
return
shared.util.sanitizeIgnitionObject
def sanitizeIgnitionObject(element):
""" """
if hasattr(element, '__iter__'):
if hasattr(element, 'keys'):
return dict((k, sanitizeIgnitionObject(element[k])) for k in element.keys())
else:
return list(sanitizeIgnitionObject(x) for x in element)
return element
It is a toggle that is the control for the output that is selected from a table. It is set to False when the checkbox is unselected. Thinking about it now, it's probably not needed.
I can see arguments for keeping that logic and removing it.
If you do keep it, bidirectionally bind its selected property to a custom property on the root container or view(name it something like enableOutputSelection).
Then you can change the line of code in the action script to self.view.custom.enableOutputSelection = False and it will work regardless of which container that toggle is located in.
Very helpful if you are still designing and moving stuff around.