What is background to have ServerHandler.addCallbackElement method? - google-apps-script

Frequently GAS users (me too) do not use the ServerHandler.addCallbackElement method or use in a way which does not cover all controls.
What is a background to have this method at all? Why GAS developers introduced it? Is it simpler to pass all input widgets values to all server handlers as parameters?
The documentation does not provide answers to these questions.
I see the following causes
Adding widgets as callback elements reduces traffic between browsers and GAS servers in case of several handlers which handle different sets of controls. Here is a question. How much traffic it saves? I think maximum a few kilobytes, usually hundreds of bytes. Is it worth, considering the modern internet connections speed, even mobile connections.
A form contains a table-like edit controls with multiple buttons and it is comfortable to handle row elements with the same name. This issue is easily avoided by using tags. See the following example. If the tags are used for other purposes it is not a problem to parse the source button id and extract the row number.
Limits of technology used behind the scenes. If there are such limits, then what are they?
function doGet(e) {
var app = UiApp.createApplication();
var vPanel = app.createVerticalPanel();
var handler = app.createServerHandler("onBtnClick");
var lstWidgets = [];
for (var i = 0; i < 10; i++) {
var hPanel = app.createHorizontalPanel().setTag('id_' + i);
var text = app.createTextBox().setName("text_" + i);
text.setText(new Date().valueOf());
var btn = app.createButton("click me").addClickHandler(handler);
btn.setTag(i).setId('id_btn' + i);
var lbl = app.createLabel().setId("lbl_" + i);
hPanel.add(text);
hPanel.add(btn);
hPanel.add(lbl);
lstWidgets.push(text);
lstWidgets.push(btn);
vPanel.add(hPanel);
}
// The addCallbackElement calls simulate situation when all widgets values are passed to a single server handler.
for (var j = 0; j < lstWidgets.length; j++) {
handler.addCallbackElement(lstWidgets[j]);
}
app.add(vPanel);
return app;
}
function onBtnClick(e) {
var app = UiApp.getActiveApplication();
var i = e.parameter[e.parameter.source + '_tag'];
var lbl = app.getElementById("lbl_" + i);
lbl.setText("Source ButtonID: " + e.parameter.source + ', Text: ' + e.parameter["text_" + i]);
return app;
}

Great Question.
"How much traffic it saves?" I don't think we know yet, but I expect it will get more efficient over time. Here is another discussion on performance. Only extensive testing and improvements from Google will really allow us to identify best practices, for now all I can say is that ClientHandlers are clearly going to be better than ServerHandlers whenever possible.
As JavaScript developers I think we are predominantly use to doing stuff client-side, then we think of PHP/ASP as server-side tools. My understanding so far is that our GAS code is actually running both client and server side (at the very least it's calling server side functionality) but it sure seems like there's more going on server-side than we realize, and on the client-side this seems to result in somewhat "compiled" code. I kinda recognize some of this multi-tier deployment from my Java experience.
Since there are a lot of ways of doing the same thing, Google can take advantage of the fact that our code is not directly interpreted (by either side) to do things that would not necessarily make sense if we were writing the code by hand. This is why I think it will become more efficient than other solutions, eventually but probably not yet. For now I'd suggest steering clear of GAS if you are worried about performance. Maybe just for fun try looking at the source of your client-side Web-Apps at runtime (view source). So in order for them to do things most efficiently, I imagine they will benefit by having us define things in a very high-level way. This gives them the most flexibility in how they interpret our code.
To specifically address your second question I personally think of the Handler Function onBtnClick() as running on the Server-Side, whereas the Tags you refer to (and most of the doGet) would be in the browser's engine on the client-side. I can see how the functionality would be much more flexible (efficient and powerful) on the server-side if they have an idea ahead of time as to how much memory they would need to handle specific events/requests. (Clearly if each getElementById() call was running a separate request, that would be like clicking a link to a new mini-webpage each time.)
So now the question is why can't my handler just automatically create parameters with just the stuff I use in my handler function? The only reason we are asking this question in the first place is because there is some stuff in the UiApp which seems to be available on both ends. The UiApp is already in the scope of both the doGet and onClick but the variables defined in doGet are not, so these values need to be either
explicitly saved like ScriptProperties.setProperty() or
put into the UiApp somewhere with an Id or
explicitly given to the Handler function using addCallbackElement()
Notice how you had to addCallbackElement(lstWidget), because it was not created with an app.create... constructor within the UiApp object. My guess is that GAS is implementing XML compliant SOAP calls to a web-service on the Google end, we may be able to figure this out by really studying the client-side source code. Just to reiterate we could also use setProperty() it does not really matter, or even save them via JDBC and then retrieve them with another connection from within your handler function but somehow the data needs to be passed from the Client to the Server and vice-versa.
From a programming perspective there is a lot of stuff available in the scope of your client-side doGet function that you probably would never want to pass to the server, or there may be functions in the scope of the server-side doClick() with the same name as functions on the client-side but they may actually be calls to totally different library functions maybe even on totally different hardware (even though from the developer's perspective they work the same way).
Maybe the Google team has not yet really decided on how the UiApp really works yet, otherwise they would just force or at least allow us to put everything in there. Yet another observation when we call UiApp.getActiveApplication() based on it's name it does not seem like a constructor, but rather a method that returns a private instance from the UiApp object. (Object being a class that was previously instantiated and supposedly initialized somewhere.) I may not have 100% answered your question but I sure did try, any further insight from the community would clearly be appreciated.
Now I may be straying off-topic but I also imagine the actual product will continue to change as they do more to improve performance in the long-term, and if we still feel like we are writing client-side code as a developer then that is a success for Google. Now please correct me if I have stated anything wrong, I have just recently started using these tools and plan to follow up on this question with more specifics as I learn more but as of right now that is my best interpretation.

If you use a formpanel all the sub elements will be sent to your dopost function. With the button as source. And your UIapp will be cleaned.
If you don't want that use a callback to specify what element and siblings will be sent.
This is how the UIapp is designed.

Related

What's the lightest way to add the smallest amount of dynamics to a static HTML site?

I have a personal website that's all static html.
It works perfectly for my needs, except for one tiny thing.
I want to dynamically change a single word on a single page: the name of the current map for a game server I'm running.
I can easily run a cron job to dump the name of the map into a file in the site's html directory, call it mapname.txt. This file contains a single line of text, the name of the map.
How would I update, say, game.html to include this map name?
I would very strongly prefer to not pull in some massive framework, or something like php or javascript to accomplish this.
I want the lightest weight solution possible. Using sed is an option, although definitely a hacky one. What's the tiniest step up from static html?
If you say "dynamically", do you mean:
If the information changes ...
A) the user should see it after they have re-loaded the page?
B) the page should update without the need to reload?
For A, you can use PHP (or any other language your server supports) to read the data from the file and print it into the web page. This will happen on server side.
For B, you can use JS that queries the file and updates the HTML. This will happen on client side.
To change text there are a few way though only two appropriate methods.
First is using textContent:
document.getElementById('example').textContent = 'some example text';
Secondly is the older nodeValue however it's a bit more tricky since you have to specify the exact textNode (e.g. .firstChild):
document.getElementById('example').firstChild.nodevalue = 'some example text';
You're 100% on the mark about not using frameworks or libraries, almost everything exists without the suck.
I'm not going to test this though this is a very stripped down version of my ajax function from my web platform. Some people might scream about the Fetch API however the syntax is an absolute mess. I recommend figuring out how to standardize this function so you can use it for everything instead of making copies of the code for every instance. Mine supports both GET and POST requests.
function ajax(method, url, param_id_container_pos, id_container)
{
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.timeout = 8000;
xhr.open(method,url,true);
xhr.send(null);
xhr.onreadystatechange = function()
{
if (xhr.readyState == 4)
{
if (xhr.getResponseHeader('content-type'))
{
var type = xhr.getResponseHeader('content-type').split('/')[1];
if (type.indexOf(';') >- 1) {type = type.split(';')[0];}
}
else {var type = 'xml';}//Best guess for now.
console.log(type,xhr);
console.log(xhr.responseText);
//console.log(type,xhr.responseXML);
//document.getElementById('example').textContent = xhr.responseText;
}
}
}
You're also going to have to ensure that the url is set to an absolute path. I use path variable in my platform (see my profile for the link, wildly clean and organized code).
There are plenty of ways to make this function reusable and I highly recommend doing that. For now use the last non-curley-bracket line to update your line of text.

Questions on extending GAS spreadsheet usefulness

I would like to offer the opportunity to view output from the same data, in a spreadsheet, TBA sidebar and, ideally another type of HTML window for output created, for example, with a JavaScript Library like THREE.
The non Google version I made is a web page with iframes that can be resized, dragged and opened/closed and, most importantly, their content shares the same record object in the top window. So, I believe, perhaps naively, something similar could be made an option inside this established and popular application.
At the very least, the TBA trial has shown me it useful to view and manipulate information from either sheet or TBA. The facility to navigate large building projects, clone rooms and floors, and combine JSON records (stored in depositories like myjson) for collaborative work is particularly inspiring for me.
I have tried using the sidebar for different HTML files, but the fact only one stays open is not very useful, and frankly, sharing record objects is still beyond me. So that is the main question. Whether Google people would consider an extra window type is probably a bit ambitious, but I think worth asking.
You can't maintain a global variable across calls to HtmlService. When you fire off an HtmlService instance, which runs in the browser, the server side code that launched it exits.
From that point control is client side, in the HtmlService code. If you then launch a server side function (using google.script.run from client side), a new instance of the server side script is launched, with no memory of the previous instance - which means that any global variables are re-initialized.
There are a number of techniques for peristing values across calls.
The simplest one of course is to pass it to the htmlservice in the first place, then to pass it back to server side as an argument to google.script.run.
Another is to use property service to hold your values, and they will still be there when you go back, but there is a 9k maximum entry size
If you need more space, then the cache service can hold 100k in a single entry and you can use that in the same way (although there is a slight chance it will be cleaned away -- although it's never happened for me)
If you need even more space, there are techniques for compressing and/or spreading a single object across several cache entries - as documented here http://ramblings.mcpher.com/Home/excelquirks/gassnips/squuezer. This same method supports Google Drive, or Google cloud storage if you need to persist data even longer
Of course you can't pass non-stringifiable objects like functions and so on, but you can postpone their evaluation and allow the initialized server side script to evaulate them, and even share the same code between server, client or across projects.
Some techniques for that are described in these articles
http://ramblings.mcpher.com/Home/excelquirks/gassnips/nonstringify
http://ramblings.mcpher.com/Home/excelquirks/gassnips/htmltemplateresuse
However in your specific example, it seems that the global data you want is fetched from an external api call. Why not just retrieve it client side in any case ? If you need to do something with it server side, then pass it to the server using google.script.run.
window.open and window.postMessage() solved both the problems I described above.
I hope you will be assured from the screenshot and code that the usefulness of Google sheets can be extended for the common good. At the core is the two methods for inputting, copying and reviewing textual data - spreadsheet for a slice through a set of data, and TBA for navigation of associations in the Trail (x axis) and Branches (y axis), and for working on Aspects (z axis) of the current selection that require attention, in collaborations, from different interests.
So, for example, a nurse would find TBA useful for recording many aspects of an examination of a patient, whereas a pharmacist might find a spreadsheet more useful for stock control. Both record their data in a common object I call 'nset' (hierarchy of named sets), saved in the cloud and available for distribution in collaborative activities.
TBA is also useful for cloning large sets of records. For example, one room, complete with furniture can be replicated on one floor, then that floor, complete with rooms can be replicated for a complete tower.
Being able to maintain parallel nset objects in multiple monitor windows by postMessage means unrivalled opportunities to display the same data in different forms of multimedia, including interactive animation, augmented reality, CNC machine instruction, IOT controls ...
Here is the related code:
From the TBA in sidebar:
window.addEventListener("message", receiveMessage, false);
function openMonitor(nset){
var params = [
'height=400',
'width=400'
].join(',');
let file = 'http://glasier.hk/blazer/model.html';
popup = window.open(file,'popup_window', params);
popup.moveTo(100,100);
}
var popup;
function receiveMessage(event) {
let ed,nb;
ed = event.data;
nb = typeof ed === "string"? ed : nb[0];
switch(nb){
case "Post":
console.log("Post");
popup.postMessage(["Refreshing nset",nset], "http:glasier.hk");
break;
}
}
function importNset(){
google.script.run
.withSuccessHandler(function (code) {
root = '1grsin';
trial = 'msm4r';
orig = 'ozs29';
code = orig;
path = "https://api.myjson.com/bins/"+code;
$.get(path)
.done((data, textStatus, jqXHR) => {
nset = data;
openMonitor(nset);
cfig = nset.cfig;
start();
})
})
.sendCode();
}
From the popup window:
$(document).ready(function(){
name = $(window).attr("name");
if(name === "workshop"){
tgt = opener.location.href;
}
else{
tgt = "https://n-rxnikgfd6bqtnglngjmbaz3j2p7cbcqce3dihry-0lu-script.googleusercontent.com"
}
$("#notice").html(tgt);
opener.postMessage("Post",tgt);
$(window).on("resize",function(){
location.reload();
})
})
}
window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
let ed,nb;
ed = event.data;
nb = typeof ed === "string"? ed : ed[0];
switch(nb){
case "Post": popup.postMessage(["nset" +nset], "*"); break;
default :
src = event.origin;
notice = [ed[0]," from ",src ];
console.log(notice);
// $("#notice").html(notice).show();
nset = ed[1];
cfig = nset.cfig;
reloader(src);
}
}
I should explain that the html part of the sidebar was built on a localhost workshop, with all styles and scripts compiled into a single file for pasting in a sidebar html file. The workshop also is available online. The Google target is provided by event.origin in postMessage. This would have to be issued to anyone wishing to make different monitors. For now I have just made the 3D modelling monitor with Three.js.
I think, after much research and questioning around here, this should be the proper answer.
The best way to implement global variables in GAS is through userproperties or script properties.https://developers.google.com/apps-script/reference/properties/properties-service. If you'd rather deal with just one, write them to an object and then json.stringify the object (and json.parse to get it back).

Trigger Google Apps Script by email

I'm looking for examples of a pattern where a demon script running within a GoogleAppsForBusiness domain can parse incoming email messages. Some messages will will contain a call to yet a different GAScript that could, for example, change the ACL setting of a specific document.
I'm assuming someone else has already implemented this pattern but not sure how I go about finding examples.
thx
You can find script examples in the Apps Script user guide and tutorials. You may also search for related discussions on the forum. But I don't think there's one that fits you exactly, all code is out there for sure, but not on a single script.
It's possible that someone wrote such script and never published it. Since it's somewhat straightforward to do and everyone's usage is different. For instance, how do you plan on marking your emails (the ones you've already read, executed, etc)? It may be nice to use a gmail filter to help you out, putting the "command" emails in a label right away, and the script just remove the label (and possibly set another one). Point is, see how it can differ a lot.
Also, I think it's easier if you can keep all functions in the same script project. Possibly just on different files. As calling different scripts is way more complicated.
Anyway, he's how I'd start it:
//set a time-driven trigger to run this function on the desired frequency
function monitorEmails() {
var label = GmailApp.getUserLabelByName('command');
var doneLabel = GmailApp.getUserLabelByName('executed');
var cmds = label.getThreads();
var max = Math.min(cmds.length,5);
for( var i = 0; i < max; ++i ) {
var email = cmds[i].getMessages()[0];
var functionName = email.getBody();
//you may need to do extra parsing here, depending on your usage
var ret = undefined;
try {
ret = this[functionName]();
} catch(err) {
ret = err;
}
//replying the function return value to the email
//this may make sense or not
if( ret !== undefined )
email.reply(ret);
cmds[i].removeLabel(label).addLabel(doneLabel);
}
}
ps: I have not tested this code
You can create a google app that will be triggered by an incoming email message sent to a special address for the app. The message is converted to an HTTP POST which your app receives.
More details here:
https://developers.google.com/appengine/docs/python/mail/receivingmail
I havn't tried this myself yet but will be doing so in the next few days.
There are two ways. First you can use Google pub/sub and handle incomming notifications in your AppScrit endpoint. The second is to use the googleapis npm package inside your AppScript code an example here. Hope it helps.
These are the steps:
made a project on https://console.cloud.google.com/cloudpubsub/topicList?project=testmabs thing?
made a pubsub topic
made a subscription to the webhook url
added that url to the sites i own, i guess? I think I had to do DNS things to confirm i own it, and the error was super vague to figure out that was what i had to do, when trying to add the subscription
added permission to the topic for "gmail-api-push#system.gserviceaccount.com" as publisher (I also added ....apps.googleusercontent.com and youtrackapiuser.caps#gmail.com but i dont think I needed them)
created oauth client info and downloaded it in the credentials section of the google console. (oauthtrash.json)

Is there a way to 'listen' for a database event and update a page in real time?

I'm looking for a way to create a simple HTML table that can be updated in real-time upon a database change event; specifically a new record added.
In other words, think of it like an executive dashboard. If a sale is made and a new line is added in a database (MySQL in my case) then the web page should "refresh" the table with the new line.
I have seen some information on the new using EVENT GATEWAY but all of the examples use Coldfusion as the "pusher" and not the "consumer". I would like to have Coldfusion both update / push an event to the gateway and also consume the response.
If this can be done using a combination of AJAX and CF please let me know!
I'm really just looking to understand where to get started with real-time updating.
Thank you in advance!!
EDIT / Explanation of selected answer:
I ended up going with #bpeterson76's answer because at the moment it was easiest to implement on a small scale. I really like his Datatables suggestion, and that's what I am using to update in close to real time.
As my site gets larger though (hopefully), I'm not sure if this will be a scalable solution as every user will be hitting a "listener" page and then subsequently querying my DB. My query is relatively simple, but I'm still worried about performance in the future.
In my opinion though, as HTML5 starts to become the web standard, the Web Sockets method suggested by #iKnowKungFoo is most likely the best approach. Comet with long polling is also a great idea, but it's a little cumbersome to implement / also seems to have some scaling issues.
So, let's hope web users start to adopt more modern browsers that support HTML5, because Web Sockets is a relatively easy and scalable way to get close to real time.
If you feel that I made the wrong decision please leave a comment.
Finally, here is some source code for it all:
Javascript:
note, this is a very simple implementation. It's only looking to see if the number of records in the current datatable has changed and if so update the table and throw an alert. The production code is much longer and more involved. This is just showing a simple way of getting a close to real-time update.
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.js"></script>
<script type="text/javascript" charset="utf-8">
var originalNumberOfRecsInDatatable = 0;
var oTable;
var setChecker = setInterval(checkIfNewRecordHasBeenAdded,5000); //5 second intervals
function checkIfNewRecordHasBeenAdded() {
//json object to post to CFM page
var postData = {
numberOfRecords: originalNumberOfRecsInDatatable
};
var ajaxResponse = $.ajax({
type: "post",
url: "./tabs/checkIfNewItemIsAvailable.cfm",
contentType: "application/json",
data: JSON.stringify( postData )
})
// When the response comes back, if update is available
//then re-draw the datatable and throw an alert to the user
ajaxResponse.then(
function( apiResponse ){
var obj = jQuery.parseJSON(apiResponse);
if (obj.isUpdateAvail == "Yes")
{
oTable = $('#MY_DATATABLE_ID').dataTable();
oTable.fnDraw(false);
originalNumberOfRecsInDatatable = obj.recordcount;
alert('A new line has been added!');
}
}
);
}
</script>
Coldfusion:
<cfset requestBody = toString( getHttpRequestData().content ) />
<!--- Double-check to make sure it's a JSON value. --->
<cfif isJSON( requestBody )>
<cfset deserializedResult = deserializeJSON( requestBody )>
<cfset numberOFRecords = #deserializedResult.originalNumberOfRecsInDatatable#>
<cfquery name="qCount" datasource="#Application.DBdsn#" username="#Application.DBusername#" password="#Application.DBpw#">
SELECT COUNT(ID) as total
FROM myTable
</cfquery>
<cfif #qCount.total# neq #variables.originalNumberOfRecsInDatatable#>
{"isUpdateAvail": "Yes", "recordcount": <cfoutput>#qCount.total#</cfoutput>}
<cfelse>
{"isUpdateAvail": "No"}
</cfif>
</cfif>
This isn't too difficult. The simple way would be to add via .append:
$( '#table > tbody:last').append('<tr id="id"><td>stuff</td></tr>');
Adding elements real-time isn't entirely possible. You'd have to run an Ajax query that updates in a loop to "catch" the change. So, not totally real-time, but very, very close to it. Your user really wouldn't notice the difference, though your server's load might.
But if you're going to get more involved, I'd suggest looking at DataTables. It gives you quite a few new features, including sorting, paging, filtering, limiting, searching, and ajax loading. From there, you could either add an element via ajax and refresh the table view, or simply append on via its API. I've been using DataTables in my app for some time now and they've been consistently cited as the number 1 feature that makes the immense amount of data usable.
--Edit --
Because it isn't obvious, to update the DataTable you call set your Datatables call to a variable:
var oTable = $('#selector').dataTable();
Then run this to do the update:
oTable.fnDraw(false);
UPDATE -- 5 years later, Feb 2016:
This is much more possible today than it was in 2011. New Javascript frameworks such as Backbone.js can connect directly to the database and trigger changes on UI elements including tables on change, update, or delete of data....it's one of these framework's primary benefits. Additionally, UI's can be fed real-time updates via socket connections to a web service, which can also then be caught and acted upon. While the technique described here still works, there are far more "live" ways of doing things today.
You can use SSE (Server Sent Events) a feature in HTML5.
Server-Sent Events (SSE) is a standard describing how servers can initiate data transmission towards clients once an initial client connection has been established. They are commonly used to send message updates or continuous data streams to a browser client and designed to enhance native, cross-browser streaming through a JavaScript API called EventSource, through which a client requests a particular URL in order to receive an event stream.
heres a simple example
http://www.w3schools.com/html/html5_serversentevents.asp
In MS SQL, you can attach a trigger to a table insert/delete/update event that can fire a stored proc to invoke a web service. If the web service is CF-based, you can, in turn, invoke a messaging service using event gateways. Anything listening to the gateway can be notified to refresh its contents. That said, you'd have to see if MySQL supports triggers and accessing web services via stored procedures. You'd also have to have some sort of component in your web app that's listening to the messaging gateway. It's easy to do in Adobe Flex applications, but I'm not sure if there are comparable components accessible in JavaScript.
While this answer does not come close to directly addressing your question, perhaps it will give you some ideas as to how to solve the problem using db triggers and CF messaging gateways.
M. McConnell
With "current" technologies, I think long polling with Ajax is your only choice. However, if you can use HTML5, you should take a look at WebSockets which gives you the functionality you want.
http://net.tutsplus.com/tutorials/javascript-ajax/start-using-html5-websockets-today/
WebSockets is a technique for two-way communication over one (TCP) socket, a type of PUSH technology. At the moment, it’s still being standardized by the W3C; however, the latest versions of Chrome and Safari have support for WebSockets.
http://html5demos.com/web-socket
Check out AJAX long polling.
Place to start Comet
No, you can't have any db code execute server side code. But you could write a service to poll the db periodically to see if a new record has been added then notify the code you have that needs pseudo real-time updates.
The browser can receive real-time updates via BOSH connection to Jabber/XMPP server. All bits and pieces can be found in this book http://professionalxmpp.com/ which I highly recommend. If you can anyhow send XMPP message upon record addition in your DB, then it is relatively easy to build the dashboard you want. You need strophe.js, Jabber/XMPP server (e.g. ejabberd), http server for proxying http-bind requests. All the details can be found in the book. A must read which I strongly believe will solve your problem.
The way I would achieve the notification is after the database update has been successfully committed I would publish an event that would tell any listening systems or even web pages that the change has occurred. I've detailed one way of doing this using an e-commerce solution in a recent blog post. The blog post shows how to trigger the event in ASP.NET but the same thing can easily be done in any other language since ultimately the trigger is performed via a REST API call.
The solution in this blog post uses Pusher but there's not reason why you couldn't install your own real-time server or use a Message Queue to communication between your app and the realtime server, which would then push the notification to the web page or client application.

Asynchronous Ajax call in SCORM API

I am creating a javascript API for SCORM 2004 4th Edition. For those who don't know about SCORM, basically it is an API standard that eLearning courses can use to communicate with an LMS (Learning Management System). Now the API has to have the following method:
Initialize(args)
GetValue(key)
SetValue(key, value)
Terminate(args)
Commit(args)
GetDiagnostic(args)
GetErrorString(args)
GetLastError()
Now Initialize has to be called before anything else, and Terminate must the last. GetValue/SetValue can be called anywhere in between there. What I am doing is in the Initialize method I am getting some JSON from a web service and storing that in the API (to be used when using the GetValue/SetValue methods later). The problem I am coming across is that the AJAX call via jQuery is asynchronous, so the Initialize method call could be done before the JSON is loaded. With that being the way it is, a call to GetValue after calling Initialize could cause unexpected issues b/c the JSON that GetValue uses isn't there yet. My question is this: What can I do to ensure that the JSON is loaded before the GetValue/SetValue methods are called? I know the simple answer is to make it synchronous, but that is not advised mostly, and it doesn't seem to want to do that for me. Here is my code regarding that:
function GetJSON(){
var success = false;
$.ajaxSetup({async:false}); //should make it synchronous
$.getJSON("http://www.mydomain.com/webservices/scorm.asmx/SCORMInitialize?
learnerID=34&jsoncallback=?",
function(data){
bind(data);
success = true;
}
);
return success;
}
function bind(data){
this.cmi = eval("(" + data.d + ")");
$.ajaxSetup({async:true}); //should make it asynchronous again
}
Does anyone have any ideas? I would really appreciate it!
You've articulated the problem well. After the SCO calls Initialize, the CMI data needs to be immediately available for the SCO to make subsequent GetValue calls. However, making synchronous AJAX calls isn't advised, if there is a hangup in the request, it can lock up the entire browser until the request returns or times out. The solution is to pre-load all of the required data before the SCO is loaded. In our SCORM Engine implementation, we preload all of the data (CMI and sequencing) when the player is launched and then use a background process to periodically commit dirty data as the learner progresses through the course. It can get a bit tricky to ensure that all data is properly persisted when dealing with the combinations of possible window launching and exit scenarios, but it's certainly possible. You will want to avoid any requests to the server from within a SCORM API call as SCOs will often flood the LMS with big batches of calls. Making server requests within those calls can seriously degrade the learner's experience and place a performance burden on the server.
Mike
The way we approached this problem was to queue the CMI data in the API when the SCO is launched. We first navigate to a launch page that loads the CMI data into the API's queue, and then the laucnch page actually launches the SCO. When the SCO calls intialize, we just move the data into the CMI.