Text change script happens **after** button pressed

On the ChangeScript of the text property of a TextField I update the underlying database field.

Pressing "Do", the value on the db is read and written on the green label:

Why I'm doing this? This is just the easiest way to show the problem that I have, that is: when i execute the 'DO' button ActionPerformed event, the database is not necessarily updated. If the update is quick, I find it updated. If it's a bit slow, i find it not yet updated. To simulate this you just need to add a delay in the text Change Script:

from time import sleep
sleep(1)
system.db.runPrepQuery("UPDATE MYTAB SET TEXT = ? WHERE ID = 1", [currentValue.value]);

Now if you try the example you'll see that the read text is always one step back.
If you type "A" then press "Do", you'll see nothing (or whatever value was in the record with id=1 of the table), but now if you type "B", when you press "Do" you'll see A. And this happens consistently.

The problem is that updating the record triggers some custom logic at the database level. Despite it's executed in the update transaction, it seems like the ActionPerformed event finishes before and so 'sees' the db not yet updated; this is a problem because that logic is infact validation logic that should prevent the further action contained in the button from being executed; this problem makes the application unreliable.

This actually happens because clicking on the button triggers at the same time Text Change Script and button action: whichever finishes first determines if the application works corerctly or not

All the solutions I came up with are either too complex (non trivial synchronization code in all my textfields and buttons) or require a redesign of the UI like forcing the user to press some type of confirmation button before of clicking the "Do" button.
Since either of these two solutions sound sub-optimal to me (the UI has been designed to minimize the number of clicks required to perform the action) I'm looking for a more 'orthodox' solution right from the source :-).
Thanks in advance

If you want to try the example, the table is just:

create table MYTAB ( ID INT PRIMARY KEY, TEXT VARCHAR(32))

And you'll need a record also:

insert into MYTAB (ID, TEXT) VALUES (1, '---')

I suspect your SSCCE is so boiled down that it's obscuring your original purpose, but...

Why would you hand application state off to the DB and then immediately read it back? You already have the application state, right there.

1 Like

There are calculated fields that get their values after business logic is executed at database level.

You need to migrate to a more async model.

I don't think updating the table constantly as the text field changes is a good fit; I would personally run that debounced or on a button press, but regardless - have that invocation return some sentinel value when it's complete. Make the Do button only enabled/available to press after you've gotten that return value that indicates the business logic/calculation/data insert is complete.

2 Likes

The model that we used seems to fit well our needs, so I wouldn't change it if I'm not sure that I got something better.

If there is documentation/examples on non trivial data-entry with validations I would definitely have a look at it; all I could find so far is very simple examples based on basic update queries; when I tried to elaborate on those I bumped into several problems down the way; I can summarize them with: there is no ORM, all that you get is the plain old sql.

In our case, the BL is getting more complex and we need a layer detatched from the UI events, but it's not clear how this can be implemented in ignition. The only point seems to be the project's script, but it also comes with limitations that make it difficult go this way (eg. it's difficult to share between different projects without putting them in a parent project, but this in turn seems to be suboptimal; everything seems not really meant to be versioned on git, but BL should be handled carfully so some kind of cvs is needed...; there are not many examples of oop etc.). Maybe it's just the lack of documentation on this topic, or maybe I couldn't find it.

With our db-based model, the only problem that emerged so far is this, that seems related with the high parallelism of events in perspective.

I noticed that if I put a long-running-task in a button event and I click on that button multiple times before the first event is completed, the next events are enqueued and get executed one after another.
If I have a second button, it has its own events-queue. I think that studying this topic I could find ideas for my problem. For example, messages get enqueued too?
I couldn't find documentation on these topics, can you help me?

You simply need to make your actions update a state machine that drives the enable property of your buttons. So you won't get extra operations when that task is busy. (This isn't just for Perspective.)

1 Like

In general I agree.
In this case it seems to be more difficult because this way buttons have to be disabled also when edit on any control is in progress, otherwise this approach wont work.
Ineed, suppose that the data is valid: the buttons are active. You start editing a control; since new value has not been sent to the db, the button is still active. and you press the button. When you do that, the change script causes the update of the database; say that value is wrong: the button should no longer be enabled.
Unfortunately this two actions cannot be serialized and seems to happen in any order and if the update takes too time, the button will be anabled and the action that should be prohibited will happen.

That just means your button shouldn't be enabled. Any change to a entry field should change your states, too.

1 Like

Agree, so in which event am I supposed to disable it?

https://docs.inductiveautomation.com/display/DOC81/Perspective+-+Button

Maybe my question was not clear, so I corrected it: my problem is not what property to use, but when to do so.

Look at the event options for your data entry field.

I've already checked, that's why I asked you which event I should use.

TextField is the most generic control for handling input text. It doesn't have an event associated with text change, but only 'low-level' keyboard events. Besides the non-trivial task of interpreting keystrokes, you completely miss any other way of inserting content like right-click + insert.
It seems there is no way to detect in a reliable way the edit-initiation, the moment in which the state becomes invalid and the buttons should be disabled.
For what I saw and understood there is staff for building a solution that sort of works, not a robust one.

1 Like

While this isn't ideal, if you uncheck defer updates on the text box, every character typed will initiate the data change event (or at least should). This in turn is running your update query way more then necessary, but in general, I've used this method to enable/disable buttons on the screen as well as the user types in the text entry field.

As others have stated though, there surely must be better ways of handling this.

2 Likes

Given that there are multiple ways to work on this issue, i tried to study a little bit more the synchronization of update and read-back. The problem seems to be the that those two actions have to happen one-after-the-other and not simultaneously.
So why not using a lock to prevent the second to happen when the first is in progress.
I added a script moudle called MyScript and with the following content:

import threading
databaseLock = threading.Lock()

Then in my Text Change Script I've:

from time import sleep
import threading

MyScript.databaseLock.acquire()
try:
	sleep(5)
	system.db.runPrepUpdate("UPDATE MYTAB SET TEXT = ? WHERE ID = 1", [currentValue.value]);
finally:
	MyScript.databaseLock.release()

and in my button a very similar code:

import threading

MyScript.databaseLock.acquire()

try:
	rst = system.db.runPrepQuery("SELECT * FROM MYTAB WHERE ID=1")

	self.getSibling("lblRead").props.text = rst.getValueAt(0, "TEXT")
finally:
	MyScript.databaseLock.release()	

This works in the sense that I always read what I've just written.
Still has to be perfected because this way the lock is shared over multiple sessions while I want it to operate at a page/view scope.
I don't know how to associate this lock object to a page: setting it to a view propety seems to convert it into something else and when i tried to get it back i got a PropertyTreeScriptWrapper$ObjectWrapper with no elements inside.

Not currently possible.

1 Like

Thank you, I thought that was the best option so I tried that one at first even if I suspected that it would not be possibile.
What about associating the lock object to the session? That would be good enough since a user is not supposed to run multiple pages and even if they did they would only lock themselves, that is not a big deal.

Not currently possible.

Consider using two buttons. One to call the database to validate the input, and one to perform your final task. Tie the enable of both to a state custom property, one opposite of the other. Have the data change events, with defer turned off, and/or key events, do nothing but set the state. Have the validate button perform all validation tasks in a script, and have it reset the state at the end (if validation succeeds).

If you're wanting to just add a delay to the component, what about doing something by calling a custom method on your component with a delay like this (5 seconds in this case):

from threading import Timer
Timer(5, self.myCustomMethod).start()

Then just let your custom method do whatever you're wanting.

I've already checked, that's why I asked you which event I should use.

TextField is the most generic control for handling input text. It doesn't have an event associated with text change, but only 'low-level' keyboard events. Besides the non-trivial task of interpreting keystrokes, you completely miss any other way of inserting content like right-click + insert.
It seems there is no way to detect in a reliable way the edit-initiation, the moment in which the state becomes invalid and the buttons should be disabled.
For what I saw and understood there is staff for building a solution that sort of works, not a robust one.

Hi Agostino,

I'm wondering if you have find a solution for this? I have the similar issue. The text field update its text prop only when the component lose focus, which means the users have to click elsewhere or press enter to commit the change. Using low-level keyboard events or forcibly delay the script really isn't the robust way to handle such a simple functionality.

Thanks in advance.