What you'll be learning:
Workflow scripting, Difficulty Level: Legendary
Your workflow script can successfully update a given reference table in the Mobilengine cloud with edits, deletions, or additions that a user submits via a form on his or her mobile.
The latest version of the script can even accommodate more than one user, if you can guarantee that each separate user only updates or deletes rows that no other user will change. This needs some more work: Sometimes, users do have dedicated contacts that no-one else will deal with, but mostly you want all your users to have editing rights to every single item on a contacts list.
Important! To follow along with this tutorial, and effectively emulate two separate users making parallel changes in real time, you will need two separate Android devices. If you've only got one, use a desktop Android emulator such as BlueStacks.
Make the script check if the edited row still exists
In an ideal world, if none of your users were ever offline, everyone would have the same version of the reference data on their devices. As it is, each user has a snapshot of the reference data as it was when he or she last synchronized his or her device with the cloud.
The problem: User A, whose form is using the same reference table as User B's mobile client, has just deleted a row, but User B has not downloaded current reference data in an hour, and so does not know this. If User B submits the form with changes to the now-deleted row, it's up to the server-side script to sort out the conflicting edits: accept one user's (User A's) change, and send the other user (User B) or users an explanation why their change was not accepted.
The solution: your new workflow script checks whether the row that the user submitted with edits is still available in the reference data.
If the row is not available, the script checks whether the user with a more recent form
submission wanted to delete the row anyway. If yes, problem solved. If the user wanted to
modify the reference table row which is not there anymore, the script sends the user the
contactsPop
form with a message explaining the situation.
-
Create the conflict message form that the script will send to users who lose their changes.
contactsPop
is a tiny form, but there are a number of interesting things about it:-
The
hidden="true"
attribute makes sure that the user will not see it in his or her list of forms on the left of the mobile interface. -
The
parameters
attribute is a list of<name>:<data type>
pairs of each of the parameters that you want to pass into the form from the workflow script. You want to display a string-type message to the user, somsg:string
looks right. -
In the body of the form, use the
reference="REF" ref_arg="@parameter.<name of parameter>"
attributes to reference the value of the parameter that you defined in theForm
element. You will specify the value for the parameter in the associated workflow script.
Save the form in the solution artifacts folder.
<Form name="contactsPop" description="Contact not in database" hidden="true" parameters="msg:string" typed="true" dateformat='(dtf yyyy"-"MM"-"dd" "HH":"mm":"ss)' numberformat='{decimalSeparator:"."}'> <Control type="panel" name="root"> <Control type="label" name="popLabel" reference="REF" ref_arg="@parameter.msg"/> </Control> </Form>
-
-
Now for the workflow script proper. Open the latest version, and in the header, specify the form that you just set up.
In the top
foreach
loop below the two variables you introduced earlier, make the script skip the current row if it was not marked or edited by the user (lines 13-14). The continue statement, widely used in loops, sounds is unfamiliar, look it up in the C++ documentation, or see how it works in JavaScript.server program contactUpdater for form contacts using reftab contact; using form contactsPop; { foreach(var row in form.root.contactsTable) { var markedDeleted = row.deleteContact.value == "true"; var isEdited = row.columnName.value != row.hiddenName.value || row.columnPhone.value != row.hiddenPhone.value || row.columnCategory.value != row.hiddenCategory.value || row.columnAddress.value != row.hiddenAddress.value || row.columnMail.value != row.hiddenMail.value;
if(!markedDeleted && !isEdited) continue; var dbRow = db.contact.Read({id:row.idLabel.value}).SingleOrDefault(); if(dbRow == null) { if(!markedDeleted) { forms.contactsPop.Pop(form.user.name,{ msg:"Your edit to\n" + row.columnName.value + "\nwas not stored, because another user\n" + "had deleted the contact from the database."}); } continue; }
if(markedDeleted) db.contact.Delete({id:row.idLabel.value}); else db.contact.Update({id:row.idLabel.value},{ name:row.columnName.value, phone:row.columnPhone.value, category:row.columnCategory.value, address:row.columnAddress.value, mail:row.columnMail.value}); } foreach (var row in form.root.addContact) db.contact.Insert({id:row.newContactIdLabel.value, name:row.newContactName.value, phone:row.newContactPhone.value, category:row.newContactCategory.value, address:row.newContactAddress.value, mail:row.newContactMail.value}); } -
Next, you need to check whether the row exists in the database or not. Use the Read() method on the
contact
reference table to return the row with the relevant ID value. Call theSingleOrDefault()
method, which returns either the only member of the list that you call it on, ornull
if there is no first item.The method will return an error if there is more than one rows with the same ID, so by calling it, you make sure that you don't accidentally have a list instead of a single value in the
dbRow
variable. There are a number of useful methods in the scripting language for each data type - the upcoming workflow scripting reference library will handle all of them in detail. -
Line 15 in the script above puts the combined result of the
Read()
andSingleOrDefault()
methods into thedbRow
variable. -
Check whether the
dbRow
variable isnull
(line 16). If it is (meaning it's not found), check whether the user marked the row for deletion. If he or she did not, the problem that you dreaded would happen just did: user A deleted some data that User B would like to modify.Unfortunately, apart from apologizing, there's not much you can do about this: run the
Pop()
method (also known as: 'make it display on the user's phone') on thecontactsPop
form with an explanatory message.The
forms
built-in variable stores all the forms that you included in the using section of the script header. Use it to address thecontactsPop
form.Don't forget the continue statement before you close the
if (dbRow == null)
conditional loop. -
Overwrite the workflow script in your solution artifacts folder, publish, and test: Open the form on two mobile devices.
If you need help with accessing the same form on two separate devices, check back to the previous tutorial.
Delete a contact on one device, and submit.
Then, edit the same contact on the second device, and submit, then synchronize.
Wait for the inevitable conflict message.Figure 117. The conflict message that your workflow script sends the user who lost his or her changes
Make the script check for conflicting edits
You have a solution for attempts to change non-existent rows. You still need to work out what to do when separate users make conflicting changes but don't delete the row in a jointly-edited reference table.
If you think it through, it all comes down to adding two more if
statements:
-
First you want to be able to check whether the row that the user is trying to edit has changed in the reference table since the user's mobile device last downloaded the reference table.
Depending on the circumstances, the last sync could have happened a few minutes, a few hours, or even a few days ago - there could be significant differences between the version in the cloud and the version on the device.
-
Second, if there is a difference, you want to know whether the change that the user submitted is identical to the change in the reference table.
If the new change and the older change are the same, you got lucky - your script doesn't need to do anything.
If the changes are different, you have to make a decision about which change to accept. It usually makes more sense to go with the last-change-wins business logic, but for the sake of this pseudo-setup, the workflow should accept the earlier change. The user who made the later change should get an explanatory message.
This message is the third change that you need to make to the workflow script.
server program contactUpdater for form contacts using reftab contact; using form contactsPop; { foreach(var row in form.root.contactsTable) { var markedDeleted = row.deleteContact.value == "true"; var isEdited = row.columnName.value != row.hiddenName.value || row.columnPhone.value != row.hiddenPhone.value || row.columnCategory.value != row.hiddenCategory.value || row.columnAddress.value != row.hiddenAddress.value || row.columnMail.value != row.hiddenMail.value; if(!markedDeleted && !isEdited) continue; var dbRow = db.contact.Read({id:row.idLabel.value}).SingleOrDefault(); if(dbRow == null) { if(!markedDeleted) { forms.contactsPop.Pop(form.user.name,{msg:"Your edit to\n" + row.columnName.value + "\nwas not stored, because another user\n" + "had deleted the contact from the database."}); } continue; } var hasChangedInDb = dbRow.name != row.hiddenName.value || dbRow.phone != row.hiddenPhone.value || dbRow.category != row.hiddenCategory.value || dbRow.address != row.hiddenAddress.value || dbRow.mail != row.hiddenMail.value;if(hasChangedInDb) { var areDbAndEditedDifferent = dbRow.name != row.columnName.value || dbRow.phone != row.columnPhone.value || dbRow.category != row.columnCategory.value || dbRow.address != row.columnAddress.value || dbRow.mail != row.columnMail.value; if(markedDeleted || areDbAndEditedDifferent) { forms.contactsPop.Pop(form.user.name,{msg:"Your edit to \n" + row.columnName.value + "\nwas not stored, because it conflicted with another user's change to the same data. Try submitting your change again later."}); } continue; }
if(markedDeleted) { db.contact.Delete({id:row.idLabel.value}); } else { db.contact.Update({id:row.idLabel.value},{ name:row.columnName.value, phone:row.columnPhone.value, category:row.columnCategory.value, address:row.columnAddress.value, mail:row.columnMail.value}); } } foreach (var row in form.root.addContact) { db.contact.Insert({id:row.newContactIdLabel.value, name:row.newContactName.value, phone:row.newContactPhone.value, category:row.newContactCategory.value, address:row.newContactAddress.value, mail:row.newContactMail.value}); } }
-
In the first of the two extra
if
statements, the new script checks whether the edited row has changed in the reference table since it was last downloaded to the user's device.On lines 26-30 the script compares the row in the
contact
reference table with the values of the hidden labels in the form that display the version of the row when it was last synchronized.Just like before, the result of the comparison is boiled down to a single variable (
hasChangedInDb
) using a series of||
operators. This makes the structure of the script much clearer. -
If the current reference table and its local version are not the same, the second extra
if
statement comes into play on lines 33-37. There's a check if the user submission is perhaps identical to the change in the reference table. After all, there could have been a typo in the contact list, and the earlier and the later editor both want to correct it.If that's not the case, and the changes to the reference table are different, the script rejects the later change, and sends a conflict message with
contactsPop
as before.The text of the conflict message in the
contactsPop
form is new to reflect the new situation. -
That's it. The second
foreach
loop stays the same. Overwrite the existing workflow script in the solution artifacts folder, and publish. -
Try out your new toy. As before, open the form on two mobile devices.
Modify one of the contacts on one of the Android devices and submit the change while the form is open on the other device.
Then, edit the same row on the second device, making sure that you make a different change, and submit.
Synchronize the devices with the cloud, and watch the workflow script send the preset error message.
You can use form submission data to manipulate reference data on the server any way you like.
Updating, inserting, or deleting rows?