What you'll be learning
So you've got an easy-to-use, searchable, navigable, and, above all, editable contact list. What could possibly go wrong? Well, if the form is expected to be used by more than a single user at any one time, there's a chance that two users will want to edit the same contact at the same time.
In this section, you'll prepare your workflow script for such conflicting parallel user edits.
Detecting conflicting parallel edits.
The local snapshot of the database
Whenever a (non-dashboard) form that is running on the client queries a reference table on the server, a snapshot of the reference table as it was at the moment that the form was opened is stored in the client app. This snapshot is what the queries to the reference table will access and display to the user, and the server-side reference table will not be queried again as long as the form is open. Even if the server-side reference table is updated in the meantime, the user won't see the changes because opening a form effectively blocks data synchronization. This is to prevent confusing the user - data will never change while the user is looking at ot editing it.
The trouble with this is that if User A submits edits to reference data while User B is editing a previous version of the same reference data, someone is bound to lose out when User B submits the form. If they were indeed editing the very same row of the reference table, one edit will overwrite the other.
If your workflow script doesn't deal with the possibility of parallel edits, then whoever taps Submit last wins out, and the user whose changes are lost won't know what's happened to their data. Rocky Jupiter's policy is actually the opposite: if someone is working in thir form with a local snapshot of the database that's gone out of date, their edits should be rejected by default, and they need to be notified of the loss of their change.
To detect outdated reference table snapshots and reject corresponding updates, you'll need to update the workflow script that fires when an Edit contact form is submitted.
Compare local and server versions of the edited contact
A workflow script can compare values that come from a form submission to values
in a reference table any day of the week. All you have to do is ask:
Read()
the row in question from the database, and contrast
each of its fields with the values that come from the form. If any one of the
fields differs, the user's local version of the reference table is out of date -
a remote user has edited the same row while our current user was editing it.
You can then make the Update()
call dependent on whether the
user's copy of the database is consistent with the current version of the
reference data in the cloud.
This condition is doubly true for the Delete()
branch of the
original script: if someone very recently changed a contact's details, it's
probably a relevant contact, and another a user really shouldn't be allowed to
delete
it.
client server program edits for form editor using reftab contact; using reftab messages; { if(form.save.submitter) { if(form.record == null) { db.contact.Insert({id:guid.Generate().ToStringN(), name:form.newName.text, phone:form.newPhone.text, category:form.newCategory.selectedText, address:form.newAddress.text, mail:form.newMail.text}); } else {var current = db.contact.Read({id:form.record.id}).Single(); if(current.name == form.record.name && current.phone == form.record.phone && current.category == form.record.category && current.address == form.record.address && current.mail == form.record.mail) { db.contact.Update({id:form.record.id}, {name:form.newName.text, phone:form.newPhone.text, category:form.newCategory.selectedText, address:form.newAddress.text, mail:form.newMail.text, edit_status:null}); }
} } if (form.delete.submitter) {var current = db.contact.Read({id:form.record.id}).Single(); if(current.name == form.record.name && current.phone == form.record.phone && current.category == form.record.category && current.address == form.record.address && current.mail == form.record.mail) { db.contact.Delete({id:form.record.id}); }
} if (form.discard.submitter && form.record != null) { db.contact.Update({id:form.record.id},{edit_status:null}); } }
Save and publish, then open on the mobile device. What now, you ask? How on earth will you ever know if this script works if you're the only user even aware of this workflow solution?
You can simulate a conflicting parallel edit by modifying a row directly in the input data spreadsheet via the Backoffice site. Log in, and navigate to the Input data tab.
Make sure that the contact list form is open on your mobile device so that
a local snapshot of the contact
reference table is generated.
To make this local snapshot obsolete, keep the form open on the device while you
do the following steps.
Click the Export link, and select the
contact
reference table in the dialog. Your default
spreadsheet editor will start up and open a copy of the refercne table.
In the editor, modify a detail in one of the rows, and save the
spreadsheet.
Next, overwrite the contact
reference table with this new
version on the Backoffice site by clicking Import on the
Input data tab, and specifying the spreadsheet you've
just saved.
If you haven't closed the contact list form on your mobile device, the local version of the reference table in your Mobilengine client is outdated. Now you're set to test how the workflow script: click the pencil icon next to the row you've modified in the spreadsheet editor, and make a different change to the contact, tap Save, and hold your breath.
When Search the contact database next opens, your
local edits to the contact are lost. Job number one well done. Now to notify the
user whose edits your script has ignored.
Handling conflicting parallel edits.
A new reference table
Whenever you want to notify the user, a dashboard form is your best bet, since it's impossible to ignore and updates automatically. You could insert a message for unfortunate users into a custom reference table, and then query the message from this table in a dashboard form.
The bare minimum for this notifications reference table would be a unique identifier for the message, the name of the user that the message concerns, and last but not least, the message itself. Here's the previous sentence translated into the reference table declaration language:
<Reftab Name="messages" xmlns="http://schemas.mobilengine.com/reftab/v1" Push="true" Notify="true"> <Columns> <Column Name="id" Type="Text" PrimaryKey="true" NotNull="true"></Column> <Column Name="username" Type="Text" PrimaryKey="true" NotNull="true"></Column> <Column Name="message" Type="Text" NotNull="true"></Column> </Columns> </Reftab>
Download and save a completely empty input data spreadsheet with just the three columns baked in, and publish the solution folder to set up the reference table on the server. Now onward to actually populating this table.
You've got mail
It isn't actually that much of a change: all you need to add is an
else
branch after both the original
Update()
and Delete()
branches, that
inserts a row into the messages
reference table with the
current user and a reference to the row with the lost
edit.
client server program edits for form editor using reftab contact;using reftab messages;
{ if(form.save.submitter) { if(form.record == null) { db.contact.Insert({id:guid.Generate().ToStringN(), name:form.newName.text, phone:form.newPhone.text, category:form.newCategory.selectedText, address:form.newAddress.text, mail:form.newMail.text}); } else { var current = db.contact.Read({id:form.record.id}).Single(); if(current.name == form.record.name && current.phone == form.record.phone && current.category == form.record.category && current.address == form.record.address && current.mail == form.record.mail) { db.contact.Update({id:form.record.id}, {name:form.newName.text, phone:form.newPhone.text, category:form.newCategory.selectedText, address:form.newAddress.text, mail:form.newMail.text, edit_status:null}); }else { db.contact.Update({id:form.record.id}, {edit_status:null}); db.messages.Insert({id:guid.Generate().ToStringN(), username:form.info.user.name, message:"Your edit to \n" + current.name + "\nwasn't stored because \n" + "it conflicted with another user's change to the same contact."}); }
} } if (form.delete.submitter) { var current = db.contact.Read({id:form.record.id}).Single(); if(current.name == form.record.name && current.phone == form.record.phone && current.category == form.record.category && current.address == form.record.address && current.mail == form.record.mail) { db.contact.Delete({id:form.record.id}); }else { db.contact.Update({id:form.record.id}, {edit_status:null}); db.messages.Insert({id:guid.Generate().ToStringN(), username:form.info.user.name, message:current.name + "\nwasn't deleted because \n" + "another user has just updated the contact"}); }
} if (form.discard.submitter && form.record != null) { db.contact.Update({id:form.record.id},{edited_status:null}); } }
The
extra else
branch makes sure that the contact in question is
once again eligible for editing ({edit_status:null}
, and
that there is a message for the relevant user whenever there is a failed edit or
delete attempt. It's all downhill from here: all you need is a laughably simple
dashboard form that displays the messages that were meant for the spcific
user.
The messages
table is queried in a repeater
so
that the user will see any and all their discarded edits. At the heart of the
repeater
is a textview
control (line 18).
displaying the message. The form displays a reassuring message if there are no
conflict messages assigned to the user (lines 21-23). Finally, the dashboard
provides a submitbutton
to acknowledge and delete the conflict
message from the messages
reference table (line 19). Don't
forget to declare a variable to store the identifier for each of the displayed
rows (lines 13-17)! It will come in handy later when your process the form
submission.
<form id='messageBoard' menuName='Messages' platforms='ios' xmlns='http://schemas.mobilengine.com/fls/v2' dashboard='true'> <header></header> <repeater id='conflicts' record='c' recordset='{SELECT c.id, c.username, c.message FROM messages c WHERE c.username==sysp.user}'> <block> <declarations> <let id='key' shape='scalar' value='{c.id}'/> </declarations> </block> <textview text='{c.message}'/> <submitbutton id='ack' text='OK'/> </repeater> <if cond='{(SELECT COUNT(*) FROM conflicts.rows c) = 0}'> <textview text='All your updates to the contact list have been stored'/> </if> </form>
Note
that when inside a repeater
, you need to wrap variable
declarations inside a block
element for proper scope
nesting. Revisit the first time you did this for more info.
Of
course, adding a submitbutton
to a form also requires writing a
workflow script to handle the form's submission.
This one isn't a biggie: it just goes and deletes the row that the
submitbutton
belongs to from the messages
reference
table.
Save and publish. Now, do the same runaround with exporting, modifying and reimporting the contact list while the contact list form is open in the mobile client as before. This time, however, when you close the contact form after your edit was discarded, the dashboard form that pops up greets you with a message.
Tying off loose ends
Just a tiny bit of house-keeping left: there's still a minuscule chance that some parallel user will delete the exact row that hapless Petar is trying to edit. It's not very likely, but it could happen, that in the few seconds between Petar tapping the pencil icon next to a contact in the form, and the Edit contact form opening in the client, the contact in question could disappear from the reference table. Let's go ahead and future-proof the editor form.
<form id='editor'...> ...<if cond='{record IS NOT NULL}'>
... <submitbutton id="save" text='Save'nextForm='{forms.messageBoard}'
/> <if cond='{record IS NOT NULL}'> <submitbutton id='delete' text='Delete contact'nextForm='{forms.messageBoard}'
/> </if> <submitbutton id='discard' text='Discard edits' nextForm='{forms.contacts}'/></if> <if cond='{record IS NULL}'> <textview text='The contact you selected has since been deleted from the database.'/> </if>
</form>
The
editor controls will now only display if the contact that the user sets out to
edit still exists. The final change is that the contact editing
submitbutton
s now direct the user to the dashboard so that
if there is a discarded edit, the user will know about it ASAP.
Goodness gracious! You've pushed the Mobilengine workflow building envelope so much it can be seen from the Moon!