Way to avoid race condition/timer for preset values vs values saved to the database?

This is a class of problem I’ve faced many times and would like to try to figure out the “right” way to handle.

I have a Popup window that is populated by a root container dataset property called initialData. Each component on my window that is populated by it has a property called initialValue that is an expression of the form try({Root Container.initialData}[0, 'someDBColumn'], 'defaultValueForNewForm) and this works nicely - if there is some thing to load, it loads, and if there isn’t, then the form is in addMode and I can set my default value for the form as well. The relevant property of the component is then bound to this initialValue property - selectedValue for dropdowns, text for textfields, etc.

Sometimes in my UI, when a user changes something, I want to set preset values for them based on some other dataset to fill out part of the form. They can then modify this if they want, but much of the time they won’t be changing it so it helps them fill out the form quicker/more accurately.

For example, when they change a Vendor, I use the selectedValue of this dropdown in a namedQuery binding for a root container property called vendorData, grab stuff from it and populate components as necessary like this

image

with this script

allowCalcUpdates = event.source.allowCalcUpdates

if event.propertyName == 'vendorData' and allowCalcUpdates and event.newValue.rowCount > 0:
	ds = event.newValue
	remitAddress = ds.getValueAt(0, 'remitAddress')
	event.source.getComponent('remitToAddress').text = remitAddress
	paymentTerms = ds.getValueAt(0, 'paymentTerms')
	event.source.getComponent('termsId').selectedValue=paymentTerms
	taxCode = ds.getValueAt(0, 'taxCode')
	event.source.getComponent('taxCode').selectedValue = taxCode
	shipViaId = ds.getValueAt(0, 'shipViaId')
	event.source.getComponent('shipViaId').selectedValue = shipViaId
	vendorOrderingContactId = ds.getValueAt(0, "orderingContactId")
	event.source.getComponent('vendorContactId').selectedValue = vendorOrderingContactId

allowCalcUpdates is a boolean root container property. I use it on a bunch of property change scripts as a condition. If it is true, then when a user changes this drop down for instance, then we do set the presets. If its false, we just skip that part - this then allows the window to load data from the db that may not fit the presets without triggering anything.

The idea is the window starts with allowCalcUpdates as false, the data loads from the db, then it is true, so that if the user then makes any other changes that would do presetting of values - we then allow it. But I need it off before/while the db loads so I don’t overwrite custom changes.

I have tried this two ways.

The first one I know is racey but it works 99% of the time, but when someone hits a bad piece of network lag it fails. The first method is this script on the internalFrameActivated extenstion function

system.gui.getParentWindow(event).getComponentForPath('Root Container').allowCalcUpdates = False

def updateCalcProperties(event = event):
	import system.gui
	system.gui.getParentWindow(event).getComponentForPath('Root Container').allowCalcUpdates = True

system.util.invokeLater(updateCalcProperties, 1000)

So this provides 1 second for the initialData to load and the initialValue properties to evaluate, and populate the screen and then turns allowCalcUpdates on, so if the user makes any more changes, we are using presets. This most of the time, but recently had an issue where the database was slow one day, and this was failing a lot due to the long query load time - customized values that were already saved being overwritten by preset values.

My second method I tried be a little more programmatic about it and to try to catch when things are loaded. I made a new root container property called datasetsLoaded with the expression

len({Root Container.initialData})>0
&&
len({Root Container.projectData})>0
&&
len({Root Container.locationChoices})>0

where these are all datasets that help load up the screen.

I then change my allowCalcUpdates from being a a non-binded property that is only written to via that internalFrameActivated script to an expression like this (and commented out my internalFrameActivated script)

if({Root Container.addMode},
//if a new form, we can enable the presets imediately
True,
{Root Container.datasetsLoaded})

But I realize too this doesn’t provide enough time. The datasetsLoaded turns true sets allowCalcUpdates to true before the component bindings finish and then the propertyChange events are running.

So if I save with this custom value
image

on reload with this programmatic way I see this

2022-05-05-08-38-16

In both instances of opening it you see the preset address, in the second instance you can see the customized address first and then see it overwritten via the root container script.

So it seems like even with the datasetsLoaded expression, I would need to catch that in the root container property change event, then do another system.util.invokeLater(setAllowCalcUpdatesToTrue, 1000) and I am back to just guessing/hoping things are loaded.

I think the datasetsLoaded method with a system.util.invokeLater seems more robust, but I am still just hoping the timer is enough - if the client for some reason really slows down randomly at the wrong time, it too would fail.

Is there a way to accomplish this without introducing some race condition via system.util.invokeLater()?

Tagging @pturmel - wonder what your thoughts are on this.

Is the question basically this: is there any way at all to execute code only after all component bindings are executed at least once (during the startup of a window/template)?

I couldn’t find a way either, and so instead of using bindings, I just implemented a Load() custom method which loads all the data manually :confused:

I’ll be monitoring this thread, I hope there’s another way.

1 Like

Will revisit later network is spotty on train to London.

2 Likes

Hi there, I think the method system.util.invokeLater serves that purpose. Look at the doc page:
https://docs.inductiveautomation.com/display/DOC81/system.util.invokeLater

I’ll have to try it.

Was experimenting. Doing the these three things -

  1. Having a property called datasetsLoaded like
len({Root Container.initialData})>0
&&
len({Root Container.projectData})>0
&&
len({Root Container.locationChoices})>0
  1. Having another root container property boolean called allowCalcUpdates (false on window open).

and

  1. On root container property change
if event.propertyName == "datasetsLoaded" and event.newValue:
    def allowCalcUpdates(event=event):
        event.source.allowCalcUpdates=True
    system.util.invokeLater(allowCalcUpdates)

seems to work as as I need without needing to out any number as the second argument guessing how many milliseconds it will take so this is fine enough for me - I just didn’t like feeling like I was guessing/creating a race condition by hardcoding millisecond that may/may not be right.

I am guessing the way it works:

  1. My datasets are loaded via named query
  2. Each individual dataset being loaded triggers the try({Root Container.dataset}[0, 'column'], 'defaultValue') type expressions that rely on the dataset.
  3. datasetsLoaded gets set to True
  4. Root container property change gets fied
  5. System.util.invokeLater “adds” allowCalcUpdates=True to the queue of events to do/run (not quite sure on the terminology).

@pturmel does this seem like how things would work under the hood? I know I learned the try({Root Container.someDataset}[0, 'someCol'], 'defaultValue') from you, I wonder if this is how you handle this sort of situation? If not, what do you do?

Similar. I ensure the default value for the lookup is something that yields no results elsewhere. That way, as queries load, chaining yields progressive validity.