...
The
...
mobile
...
service
...
solution
...
manages
...
the
...
synchronization
...
of
...
the
...
offline
...
data.
...
Stoc |
---|
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
By
...
default
...
Servoy
...
will
...
load
...
the
...
first
...
200
...
pks
...
when
...
a
...
form
...
is
...
used.
...
This
...
is
...
not
...
needed
...
in
...
the
...
service
...
solution
...
and
...
can
...
be
...
prevented
...
by
...
calling
...
databaseManager.setCreateEmptyFormFoundsets()
...
in
...
the
...
solution
...
onopen
...
global
...
method:
...
Code Block |
---|
function onSolutionOpen() {
// prevent prefilling of form foundsets with default query
databaseManager.setCreateEmptyFormFoundsets()
}
{code}
h3. Constructing a offline data package for the mobile client
The |
The ws_read(version,name)
...
on
...
the
...
offline_data
...
form
...
has
...
to
...
return
...
an
...
OfflineDataDescription
...
(=JSON)
...
object
...
filled
...
with
...
foundset
...
data
...
the
...
developer
...
wants
...
the
...
mobile
...
client
...
to
...
retrieve.
...
An
...
OfflineDataDescription
...
instance
...
is
...
created
...
with:
...
Code Block |
---|
var retval = plugins.mobileservice.createOfflineDataDescription('data_'); {code} |
Note:
...
the
...
argument
...
is
...
optional
...
and
...
is
...
used
...
to
...
call
...
rest
...
endpoints
...
for
...
row
...
data
...
at
...
forms
...
starting
...
with
...
this
...
prefix
The next step is determining what related data should be traversed and sent to the client. By default (since Servoy 7.2) any relations that exist in a "Mobile Shared Module" attached to the "Mobile Service Solution" will be included in the traversal.
Alternatively, if you provide a list of relation names to include, this will override the inclusion of the relations contained in the "Mobile Shared Module", and only include those relations that you specify.
Code Block |
---|
var traverse = new Array(); traverse.push([ 'accountmanager_to_companies'); traverse.push(, 'companies_to_contacts'); {code} ] |
Lastly
...
we
...
have
...
instruct
...
the
...
OfflineDataDescription
...
to
...
collect
...
the
...
data,
...
starting
...
with
...
root
...
foundset
...
(containing
...
records)
...
and
...
return.
...
Code Block |
---|
retval.addFoundSet(fs_contact, traverse); return retval; {code} |
Basically
...
addFoundSet
...
in
...
the
...
service
...
solution
...
exposes
...
an
...
unrelated
...
foundset
...
to
...
the
...
mobile
...
client,
...
which
...
can
...
be
...
used
...
in
...
an
...
unrelated
...
way
...
like
...
in
...
a
...
(first)
...
form
...
or
...
databaseManager.getFoundset(...)
...
For
...
each
...
record
...
in
...
the
...
provided
...
(unrelated)
...
foundset
...
the
...
specified
...
relations
...
are
...
traversed
...
and
...
all
...
data
...
taken.
...
In order to provide a mobile client with user specific data the ws_authenticate(useruid,password)
...
method
...
should
...
be
...
added:
...
Code Block |
---|
function ws_authenticate(useruid, password) { //TODO find user and check password against pwhash column if (password === 'demo') { //static demo check return { var retval = new Object(); retval.username =username: useruid; return} retval; } return false; } {code} |
In
...
the
...
ws_read
...
method
...
we
...
can
...
utilize
...
the
...
authenticate
...
username
...
variable
...
via
...
Code Block |
---|
var authenticate_info = questionParams.ws_authenticate[0]; globals.username = authenticate_info.username; {code} |
Here
...
the
...
authenticate
...
username
...
is
...
put
...
into
...
a
...
global
...
variable
...
which
...
in
...
turn
...
can
...
be
...
used
...
like:
...
Code Block |
---|
//prepare personal data var fs_contact = globals.contact_data$username; //global related foundset using username global var, containing theto account manager contact {code}record |
Full
...
ws_read
...
method
...
for
...
personalized
...
data.
...
Code Block |
---|
function ws_read(version,name) { var questionParams = arguments[arguments.length-1]; //create return value var retval = plugins.mobileservice.createOfflineDataDescription('data_'); //setting the key for user_select relation var authenticate_info = questionParams.ws_authenticate[0]; globals.username = authenticate_info.username; //prepare personal data var fs_contact = globals.contact_data$username; //global related foundset using username global var, containing the to account manager contact /** * @type {Array<String>} */ record var traverse = new Array(); traverse.push([ 'accountmanager_to_companies');, traverse.push('companies_to_contacts'); ] retval.addFoundSet(fs_contact, traverse); return retval; } {code} h3. |
...
...
Row/record
...
data
...
is
...
retrieved
...
in
...
separate
...
calls
...
for
...
each
...
entity,
...
for
...
example
...
for
...
'orders
...
' row
...
data
...
results
...
in
...
a
...
call
...
to
...
'orders
...
' form
...
is
...
made
...
on
...
ws_read
...
method.
...
Note:
...
If
...
a
...
prefix
...
is
...
provided
...
in
...
the
...
offlinedata
...
the
...
call
...
will
...
end
...
up
...
at
...
prefix+entityname,
...
example
...
for
...
prefix
...
'data_
...
' the
...
call
...
happens
...
on
...
form
...
'data_orders
...
'
ws_read
...
is
...
with
...
a
...
list
...
of
...
pks
...
it
...
wants
...
as
...
row
...
data
...
for,
...
example
...
code:
...
Code Block |
---|
function ws_read(version, method) { var questionParams = arguments[arguments.length-1]; if (method == 'list') { /** * @type {String} */ var ids = questionParams.ids[0]; if (ids != null && ids != '') { var idsa = ids.split(',', -1); if (idsa.length > 0) { var json = plugins.mobileservice.getRowDescriptions(foundset.getDataSource(), idsa) return json; } } } throw 404; } {code} |
TIP:
...
since
...
ws_read
...
for
...
entities
...
is
...
likely
...
the
...
same,
...
it
...
might
...
be
...
beneficial
...
to
...
create
...
a
...
base
...
form
...
containing
...
this
...
logic
...
and
...
extend
...
from
...
this
...
form
...
ws_update
...
is
...
called
...
for
...
changes
...
made
...
by
...
mobile
...
client,
...
example
...
code:
...
Code Block |
---|
function ws_update(data, version, pk) { if (foundset.find()) { foundset.contact_id = pk; var count = foundset.search(); if (count > 0) { var rec = foundset.getRecord(1); databaseManager.copyMatchingFields(data,rec,true); databaseManager.saveData(rec); } } } {code} |
ws_create
...
is
...
called
...
for
...
new
...
records
...
on
...
the
...
mobile
...
client,
...
example
...
code:
...
Code Block |
---|
function ws_create(data, version, pk) { var rec = foundset.getRecord(foundset.newRecord()); databaseManager.copyMatchingFields(data,rec,true); databaseManager.saveData(rec); } {code} |
Note:
...
the
...
retrieved
...
PK
...
(and
...
derived
...
FK's)
...
is
...
always
...
UUID's
...
if
...
the
...
underlaying
...
datamodel
...
is
...
not
...
UUID
...
based,
...
keep
...
and
...
apply
...
a
...
mapping
...
!
...
ws_delete
...
is
...
called
...
for
...
deleted
...
record
...
in
...
the
...
mobile
...
client,
...
example
...
code:
...
Code Block |
---|
function ws_delete(version, pk) { if (foundset.find()) { var table = databaseManager.getTable(controller.getDataSource()); var pkname = table.getRowIdentifierColumnNames()[0] foundset[pkname] = pk; var count = foundset.search(); if (count > 0) { var rec = foundset.getRecord(1); foundset.deleteRecord(rec); } } } {code} h5. Transaction based syncing By default the mobile client will do a rest call per changed row, these are separated calls so every call will be on a fresh new client on the serverside. Because of this a transaction can't be used for all the changes of 1 mobile client and then fail a sync when something goes wrong. If the sync should be in 1 call, so that a transaction can be used over all the calls to the above |
By default the mobile client will do a rest call per changed row, these are separated calls so every call will be on a fresh new client on the serverside.
Because of this a transaction can't be used for all the changes of 1 mobile client and then fail a sync when something goes wrong. If the sync should be in 1 call, so that a transaction can be used over all the calls to the above ws_update/ws_create/ws_delete
...
methods,
...
a
...
ws_update
...
method
...
must
...
be
...
created
...
on
...
the
...
offline_data
...
form.
...
This
...
ws_update
...
method
...
then
...
gets
...
a
...
full
...
data
...
package
...
of
...
all
...
the
...
changes
...
that
...
a
...
mobile
...
client
...
has,
...
this
...
will
...
then
...
be
...
dispatched
...
over
...
all
...
the
...
methods
...
that
...
are
...
described
...
above.
...
An
...
example
...
of
...
a
...
ws_update
...
method
...
on
...
the
...
offline_data
...
form:
...
Code Block |
---|
function ws_update(data, version, authenticateResult) { try { databaseManager.startTransaction(); plugins.mobileservice.performSync(data, version, authenticateResult); databaseManager.commitTransaction(); } catch (e) { databaseManager.rollbackTransaction(); // log the error and return false to that the mobile client will know the sync did fail. application.output(e, LOGGINGLEVEL.ERROR); return false; } } {code} This code above starts a transaction then calls the performSync method of the mobile service plugin. This plugin will dispatch all the changes to the various |
This code above starts a transaction then calls the performSync method of the mobile service plugin. This plugin will dispatch all the changes to the various ws_update/create/delete
...
methods of the entity forms. If something goes wrong then an exception will be thrown and the transaction will rollback.
If using get/setUserProperty() in the Service Solution, the properties are stored in the Mobile Client that connected to the Service Solution.
As such, a user property can be used to store for example an authentication token to be used between multiple calls to the Service Solution from the Mobile Client.
When performing custom Ajax calls to the Service Solution from the Mobile Client, for example using jQuery.ajax, extra steps need to be taken to make sure the user properties are available in the Service Solution and properly persisted. The user properties are to be send with the Ajax request as a special Request Header, identified by the name 'servoy.userproperties'. The current values for the user properties of the Service Solution are stored in the Mobile Client in the localstorage, from which they can be retrieved using the code sessionStorage.getItem('servoy.userproperties'). In the Response of the custom Ajax call, the possible altered value can be stored back into the localstorage using this code: sessionStorage.setItem('servoy.userproperties', value)
Below an example of how to make a custom Ajax request using JQuery.ajax
Code Block |
---|
var url = ....
$.ajax({
url: url,
beforeSend: function (request) {
...
request.setRequestHeader("servoy.userproperties:", sessionStorage.getItem("servoy.userproperties"));
...
}
}).done(function(data, textStatus, jqXHR) {
sessionStorage.setItem("servoy.userproperties", jqXHR.getResponseHeader("servoy.userproperties")
});
|