Ajax made Simple
Most developers have heard of Ajax by now. Unless you've worked with it, you probably think it's another complicated form of alphabet soup. It actually turns out to be quite easy to use from AppStudio.
First, let's explain what it is (and isn't). It stands for Asynchronous JavaScript And XML. The good news is that there's no need to be asynchronous or to use XML - and that AppStudio takes care of the JavaScript.
Ajax makes it easy to exchange data with your server. Think of it as a function you can call that is running on another system. You can pass it parameters and it can return values. Since it's running on your server, it can do things that you cannot do on your device. It can access files and databases on the server, or get data from other web sites without worrying about Same Origin Policies.
You will need to use a server with PHP enabled to run these samples. The nsbapp.com site does not have PHP enabled.
A Simple Exchange of Data with the server
Let's look at some code. We will have our AppStudio app send the server a string, which the server will return reversed. Here is what the app looks like:
The only code in this app is the "Reverse" button. The Ajax function sends the contents of the top textarea to a PHP script that is running on the server. If it returns successfully, we display what came back. Otherwise, we show the error:
Dim req Function btnAjax_onclick() req=Ajax("ajax.php/?myText=" & encodeURIComponent(txtSend.value)) If req.status=200 Then 'success txtResponse.value=req.responseText Else 'failure txtResponse.value="Error: " & req.err.message End If End Function
The PHP script is even easier. It gets the input that is named myText, reverses it, and uses the echo statement to send it back.
<?php // Get the data from the client. $myText = $_GET['myText']; // Send back the text, reversed echo "Data received from Device (reversed): " . strrev($myText); ?>
Here is the result:
A few notes:
- The php script needs to be on the same folder on the server as the AppStudio app.
- Ajax is based on XMLHttpPost.
- A handy way to return data from your PHP script is in JSON format. PHP even has a handy json_encode() function.
- PHP is not enabled on the nsbapp.com server.
Using POST to Send more Data to the Server
Let's extend what we are doing a bit and talk about using the POST method.
We discussed the simplest Ajax call, which sends data to the server by adding it to the end of the URL, a method called, rather misleadingly, GET. But what if we want to send more data? GET has a limit, some say, of around 2000 characters. For more than that, we'll use the also badly named POST method. POST has an upload limit on most servers of 8 megabytes. If you need more, server settings can be adjusted.
Let's look at some code. You'll see it looks very much like our last post:
Function btnAjax_onclick() req=Ajax("ajaxPost.php","POST",txtSend.value) If req.status=200 Then 'success htmResponse.innerHTML=req.responseText Else 'failure htmResponse.innerHTML="Error: " & req.err End If End Function
The difference is a couple of extra parameters. We add "POST" and the data we want to send as a separate parameter. Before we talk about what is going to happen at the server end, let's see what the app looks like:
OK, here is the PHP code on the server which we are calling:
<?php // uncomment the next line to see runtime error messages from this script // ini_set('display_errors','1'); // Get the data from the client. $myText = file_get_contents('php://input'); // Send back the text, reversed echo "Data received from Device (reversed):<br>" . strrev($myText); //write the data out to a file on the server //make sure permissions are all OK! $myFile = "AjaxPost/ajaxPost.txt"; echo "<p>Writing data to " . getcwd() . $myFile; $f = fopen($myFile, 'w') or die("can't open file"); fwrite($f, $myText); fclose($f); ?>
The file_get_contents('php://input') call gets the data, exactly as it was sent. We'll do two things with it: echo it back reversed and we will write it to a file on the server. The first part is easy: anything in an echo statement is sent back to our app:
echo "Data received from Device (reversed):" . strrev($myText);
The second part involves a bit more PHP, but is straightforward. It writes the data was received to a file.
$myFile = "AjaxPost/ajaxPost.txt"; echo "<p>Writing data to " . getcwd() . $myFile; $f = fopen($myFile, 'w') or die("can't open file"); fwrite($f, $myText); fclose($f);
What would you use this feature for?
- Upload a JSON copy of your database.
- Upload some pictures in Base64 format.
- Create a .htm file which can be accessed as a web page.
- Lots more ideas!
A tip on debugging PHP scripts Since PHP scripts run on the server and not on your screen, you don't get to see the error messages. This can make debugging difficult. If you put this statement at the top of your PHP script, the error messages will be echoed to your app:
ini_set('display_errors','1');
The error messages come back as HTML formatted text. The best way to look at them is to assign them to the innerText property of an HTMLview control.
Getting Data from Another Site
One of the basic security features of the modern web is the Same Origin Policy. It states that a web app cannot pull up just any data from another site. This becomes a problem if you need data on someone else's site.
Fortunately, they left an easy workaround. While a web app isn't allowed to access the files on another site, a PHP script is. We'll get our PHP script to do the dirty work for us and return the results to our app. The technique we will use is often called scraping.
Time to look at some code again. Here's the AppStudio part that calls the PHP script. It looks very much like the code in A Simple Exchange of Data with the server above. The main difference is that we're calling a different PHP script.
Function btnGetFile_onclick() req=Ajax("ajaxGetURL.php/?urlToGet=" & encodeURIComponent(txtURL.value)) If req.status=200 Then 'success htmResponse.innerHTML=req.responseText Else 'failure htmResponse.innerHTML=req.err End If htmResponse.refresh() End Function
Before we talk about what is going to happen at the server end, let's see what the app looks like:
And here is what the PHP script looks like:
<?php $ch = curl_init($_GET['urlToGet']); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $data = curl_exec($ch); curl_close($ch); echo $data ?>
Let's take apart this one line:
- $_GET('urlToGet') gets the parameter string that was attached to the URL in our AppStudio code.
- _file_get_contents get the contents of the file, in this case, the URL that was passed.
- echo sends the result back to our AppStudio program.
Pretty simple, isn't it?
(In this example, we're grabbing the robots.txt file from Microsoft's site, just to show we can get files from anywhere. Nearly all sites have a robots.txt file: it is used to guide search engines.)
There is a simpler way to do this, but it can run into security problems.
<?php echo file_get_contents($_GET['urlToGet']); ?>
How fast is it?
If your app is going to communicate with your server while it is running, you've probably wondered how much your app will slow down when it has to wait for the server.
In this blog post, we're going to make a program to test this. We'll construct a little program which sends data to the server as many times as it can. The server will record the information in a log file. Here is what our app will do:
Do Until SysInfo(10) > limit req=Ajax("ajaxLog.php/?myText=" + encodeURIComponent(Now)) counter=counter+1 Loop
SysInfo(10) is the current time in milliseconds. Limit is the time we want run to. You can run this test for up to about 5 seconds: after that, the system will think you are in a endless loop and shut you down.
On the server side, we need a PHP script to get the messages and write them out to disk. Here's the code:
<?php // read and append to file $myFile = "AjaxLog/log.txt"; $f = fopen($myFile, 'a') or die("can't open file"); fwrite($f, $_GET['myText'] . Chr(13) . Chr(10)); fclose($f); ?>
Now, let's run it on a few different devices and see how many accesses we can do per second:
Desktop (Ethernet) | 2745.0 |
iPhone 4 (WiFi) | 10.5 |
iPhone (3G) | 2.5 |
Nexus S (WiFi) | 5.0 |
Kindle Fire (WiFi) | 9.0 |
It's obvious that the speed that really depends on what kind of connection you have to the web. A hard wire to your router gets the best time. WiFi is much slower, and 3G is slower again. No real surprise here.
The speeds shown here probably are not what you will see if you run the same test. There are a number of factors that will affect the speed:
- Type of data connection
- Load on the network
- Number of hops the data has to make to get to the server
- Load on the server
(You can try this sample yourself - it is called AjaxLog in the Samples folder.)
Asynchronous Calls
In our last Ajax discussion, we discovered that it could take half a second for an Ajax call to complete (actually, due to network latency, it could even be a lot more than that.) How do we do Ajax calls and not make our app look slow?
Easy. We send the Ajax message off, then go do other stuff. When the response comes back, we respond to it. This is called Asynchronous operation.
We do this by adding the name of the function to be called on completion as the 4th parameter of our Ajax call:
req=Ajax("ajax.php/?myText=" + encodeURIComponent(txtSend.value), "", "", done)
Our callback function is called 'done'. After we execute the Ajax call, the program goes on to the next statement in the program immediately. It doesn't wait for any response. We can update the screen, or wait for the user to do something else. When the reply comes back, our 'done' function will be called:
Function done() If req.readyState<>4 Then Exit Function 'ignore progress reports If req.status=200 Then 'success txtResponse.value=req.responseText Else 'failure txtResponse.value="Error: " & req.err.message End If End Function
'done' will actually be called several times with progress reports on the call. We only care if it completes, so we wait for req.readyState to be 4. The rest of our function works just like when we were not asynchronous.
One of the uses of this technique that we see every day is to return partial results on a search field (as in Google). Each time a character is entered, it is sent to the server for a list of possible matches. As the results come back, the window is updated. Entering characters in the field is unaffected by the server communications that are going on at the same time.
Check out the AjaxAsync app in the Samples to see this in action.
Caching
Sometimes when you make the same request repeatedly, you'll get the same result, even through the data on the server has changed. The browser is "helping" you by delivering the same result from its cache instead of going out to the server again. To fix this, add a time stamp to the query string. This example puts the number of milliseconds into the string:
req=Ajax("ajax.php/?myText=" + "sdfsfd" + "&time=" + SysInfo(10), "", "", done)
Where to put your PHP files
For development, put the PHP files for your project right in your project folder. You can then use any text editor that you have installed on your system to edit them.
Next, add the file names to the manifest in Project Properties. When you do a Full Deploy, they will be copied to the server with the rest of your project.
The result is that your PHP files will be placed on the server, in the same folder as your app. Your app can call a PHP script by just giving the file name - no path name will be needed.
Using JSONP to get a Stock Quote
Working with Ajax, we have run into the Same Origin Policy over and over again. In this post, we'll show you another way to get around it. The Same Origin Policy restricts you from loading files from a server not your own. There is an exception: JavaScript (.js) files are allowed. There are sites on the web where you can request information and have it passed back in JSONP format. The "P" stands for padding: JSONP is JSON with padding. The results are returned as a function call, with the results as parameters. That function can then be called in your program.
Let's use this to get a stock quote from Yahoo. There are 3 parts to this: first, we have to make the request. Next, we handle the return. Finally, we execute the function.
Make the request
Dim YQI, URL, callback YQI = encodeURIComponent("select * from yahoo.finance.quotes where symbol in ('AAPL','GOOG','MSFT')") callback="requestComplete" URL = "http://query.yahooapis.com/v1/public/yql?q=" & YQI & _ "&format=json&env=http://datatables.org/alltables.env&callback=" & callback loadScript(URL)
Yahoo! has a very powerful API to look up all kinds of data. To make it easier, they have set up an interface using SQL like statements called YQI. In our code above, we start by creating our YQI request. The encodeURIComponent() function translates special characters and spaces so they can be sent in our Ajax call.
URL is then composed of our YQI request with the rest of the boilerplate that is needed. The most interesting part is the &callback part. It names the function that the return values will be wrapped in.
Having prepared everything, we can inject the script.
Inject the script
Yahoo! is going to return some code: it will be a call to the function we specified in callback, with the parameters all filled in. The following code will insert the code returned by Yahoo! into our program and execute it:
Function loadScript(JSONP) Dim head, script head=document.getElementsByTagName("head")[0] script=document.createElement("script") script.src= JSONP script.async=True head.appendChild(script) End Function
We do the usual checking to see if the call is complete. If it is, we will have gotten a text string back that looks something like this:
requestComplete({"query": {"count":3,"created":"2012-03-20T11:34:00Z","lang":"en-US","results": {"quote":...);
This is a valid function call, so when it is run, the function is executed.
Execute the function
The Eval() function causes requestComplete() to be called.
Function requestComplete(data) Dim quotes, i quotes=data.query.results.quote TextArea1.value="" For i=0 To Len(quotes)-1 TextArea1.value=TextArea1.value+quotes[i].symbol & quotes[i].Ask & vbCRLF Next End Function
The data that comes back is a fairly complex structure with a lot of data in it. You can copy it to the NotePad to see everything that is there. For now, we are just interested in the asking price of the stock. The results are returned in an array, with one element per stock.
Cool things you can get on the web
There are many web services that work in a similar fashion. One of the tip offs is if the calling string has an &callback argument. If it does, it probably can be used with this method.
Some of these services are free or limited, while others require an API key that you must obtain from the service.
Some places to go for services:
- The master list is here: Programmable Web
- Lots of stuff: Yahoo!
- Weather: Wunderground
- Elevation: Geonames
- Geographic Info: Geonames
Got more? Let us know at support@nsbasic.com.
Zip Code Lookup
Ever realize, when getting a user's address, that knowing the zip code makes asking for the city and state unnecessary? Here's some simple code to look up the city and state from a zip code:
zip="90210" req=Ajax("http://zip.elevenbasetwo.com?zip=" & zip) If req.status=200 Then 'success MsgBox req.responseText Else 'failure MsgBox "Error: " & req.err.message End If
The result is
{"country": "US", "state": "CA", "city": "BEVERLY HILLS"}
Cross Origin Resource Sharing
A lot of the discussion in this Tech Note has been ways to get around Same Origin Policies. Can we get rid of this restriction? We can, but there's a catch. To allow a PHP script on your server to give its results to apps which were not loaded from your server, add this line as the first line of your script.
header('Access-Control-Allow-Origin: *');
The catch is that now anyone with an internet connection can run your PHP script.
Using AJAX to send an email
Contributed by Graham Barlow, New Zealand
The AppStudio Code:
EmailText = EmailText & vbCRLF & "Date: " & FormatDateTime(Date,"dd/mm/yy") Url = "/ajaxEmailRegister.php/?EmailTo=gotnomates@farmmates.com" req=Ajax(Url,"POST",EmailText) If req.status=200 Then 'success MsgBox("Confirmation email sent.") Else 'failure MsgBox("Error sending email " & req.err) End If
...and the PHP script that gets called ("ajaxEmailRegister.php"):
<?php // Get the data from the client. $myText = file_get_contents('php://input'); $to = $_GET['EmailTo']; $subject = "New Mate registration confirmation"; $body = $myText; $headers = "From: gotnomates@farmmates.com"; mail($to, $subject, $body, $headers); ?>
Using AJAX to get info from a database on your server.
Since AppStudio runs on your device, it cannot directly access files or databases on another computer, such as a server. It need to communicate with an app running on the server, which gets the information from the database and returns it.
PHP has built in support for MySQL, Oracle and SQLite. If you have a database on your server, it's easy to query it and return the result to your app. Here's a very simple example:
<?php $host = 'localhost'; $databasename = 'myDatabaseName'; $username = 'myUsername'; $password = 'myPassword'; $con = mysql_connect($host, $username, $password) or die(mysql_error()); mysql_select_db($databasename) or die(mysql_error()); $query = file_get_contents('php://input'); $results = mysql_query($query); if (mysql_errno()) { header('HTTP/1.1 500 Internal Server Error'); echo $query . "\n"; echo mysql_error(); } else { $rows = array(); while($r = mysql_fetch_assoc($results)) { $rows[] = $r; } echo json_encode($rows); } ?>
Here is a link to an article showing how to do the same thing to Access:
http://phpmaster.com/using-an-access-database-with-php/
and Oracle
<http://www.php.net/manual/en/book.oci8.php>
You'll want to address security with this. There is no parsing of the incoming query string, so someone could conceivably put some malicious code in there ("SQL Injection") and you may want to determine that the person requesting the info is authorized to do so.
For a more detailed description, see this article: http://www.raywenderlich.com/2941/how-to-write-a-simple-phpmysql-web-service-for-an-ios-app.
Here's XKCD's comment on this:
Using AJAX to get save a picture to your server
See the sample "CameraToServer", contributed by Mike Miller jdmdude2009@gmail.com
This sample lets you get a picture from the camera or the Photo Library and display it on your screen and:
- Push it to a web server in base64 or PNG format using an AJAX POST
- Receive a response from the server and do something with it.
This is a mod of the original Camera Sample file. You must have the supplied PHP files in the same location as the AppStudio production files.
To control when a pic may be sent or that a pic is not sent multiple times by click happy users, btnSubmitImages is placed in different states of visiblility and/or usability by way of the SentFlag.
Note: PictureBox1 is in the negative left of the form. This is a trick that developers use to hide controls from the users but still have availability to them.
Image1 is used for preview ONLY because viewing PictureBox1 isn't very realistic for viewing because it exceeds the mobile screen size. (You can size the PictureBox to any size a camera is capable of, but beware, size does matter! The lag to insert an image into a db gets longer as the pics get larger. I also noticed phpmyadmin is not very responsive to large images AND I have had timeouts when loading pages with lots of images. I suspect some sort of slide presentation tool that controls the load of the images will be in order to view large quantities of photos (lot's of opensource for slide views)
(the source code of the program)
Dim e Dim SentFlag reader = new FileReader() img=new Image() SentFlag=0 Sub Main() pb=PictureBox1.getContext("2d") SetTimeout(window.scrollTo(0,window.innerHeight),100) btnSubmitImage.value="" End Sub Function txtGetPicture_onchange() 'This gets called after the picture is chosen. 'Next, let's read start reading it in. 'The _onload function will be called when that completes. reader.readAsDataURL(txtGetPicture.files[0]) e=event End Function Function reader_onload(e) 'read of the file is complete. 'Now, let's load it into an image. 'The _onload function for the image will be called on completion. img.src=e.target.result Image1.firstChild.src=e.target.result 'Phone Viewing ONLY SentFlag=0 btnSubmitImage.hidden=False btnSubmitImage.value="Send Image to Web" lblPicMsg.textContent="Now is the time" End Function Function img_onload() 'image is now loaded. Let's add it to the PictureBox2 and scale 'PictureBox1 is very large here and takes a while for the image 'to transfer to the web server (approx 1MB) 'still no solution it the iOS distortion. pb.drawImage(img,0,0,PictureBox1.width,PictureBox1.Height) End Function Function PostToWebServer() Dim devInfo Dim postData SentFlag=1 'Don't allow multiple submissions because the button 'is pushed more than once. 'Get divice information - not available devInfo="No device selected" 'Build POST postData = "filename=" & txtGetPicture.files[0].name postData = postData & "&deviceInfo=" & devInfo postData = postData & "&imgBase64=" & PictureBox1.toDataURL() 'Send POST to server req=Ajax("insertimage.php","POST",postData) If req.status=200 Then 'success btnSubmitImage.hidden=True 'Picture was pushed to server so hide button lblPicMsg.textContent = "Success!!!" htmResponse.innerHTML=req.responseText Else 'failure htmResponse.innerHTML="Error: " & req.err btnSubmitImage.hidden=False lblPicMsg.textContent = "Failure" SentFlag=0 End If End Function Function btnSubmitImage_onclick() If Len(PictureBox1.toDataURL())>0 And SentFlag=0 Then btnSubmitImage.value = "Sending" lblPicMsg.textContent = "Please Wait" 'Use the SetTimout function to call a procedure OUTSIDE 'this onclick event so that any events that need to 'occur on submission are not held up by the size of the 'image or the speed of the internet connection. '(i.e. the btn value change and the label caption change. SetTimeout(PostToWebServer,3000) 'POST TO WEB Else lblPicMsg.textContent = "Need to take a pic first" End If End Function
Using AJAX to get all kinds of stuff on the web
Many websites have information that can be retrieved using AJAX. The best list we have found is on Programmable Web.
Using Ajax with PhoneGap
When running as a PhoneGap app, your program is no longer tied to the server it was downloaded from. As a result, it doesn't know where to go on the web. You need to make two changes to any of the above samples to make them work as PhoneGap apps.
1. Declare the website you want to access in the PhoneGap configxml property (in Project Properties), by adding this line:
<access origin="https://www.nsbasic.com" subdomains="false" />
(substitute the URL to your site which has the script on it).
2. Use the full path to the script in your Ajax call:
req=Ajax("https://www.nsbasic.com/i/Ajax/ajax.php/?myText=" + encodeURIComponent(txtSend.value))
(Again, substitute the path to your script - don't use this one!)
Using Ajax with ASP.NET
Everything is pretty much the same on the AppStudio side. Here's how it looks on the server side. (Thanks to John Cody!)
1) Make sure this entry is in your asp.net webservice's web.config:
<webServices> <protocols> <add name="HttpPost"/> </protocols> </webServices>
2) ASP.NET VB code (i.e Service.asmx file):
Imports System.Web.Services Imports System.Web.Services.Protocols Imports System.ComponentModel <System.Web.Services.WebService(Namespace:="http://tempuri.org/")> _ <System.Web.Services.WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> <ToolboxItem(False)> _ Public Class WebService Inherits System.Web.Services.WebService <WebMethod()> _ Public Function HelloWorld(Token as string) As String 'the var Token has the string that was send from the AppStudio app Return "Hello World" End Function End Class
3) AppStudio code:
Dim req Function cmdSend_onclick() req = Ajax("http://mywebsite.com/Service.asmx/HelloWorld","POST","token=&Test123", send_return) End Function Function send_return() Dim Res If req.readyState <> 4 Then Exit Function 'ignore progress reports If req.status = 200 Then 'success Res = req.responseText 'Res has "Hello World" in it, but it is in XML/UTF-8 format and needs to be decoded Else 'some error End If End Function