Ajax made Simple

From NSB App Studio
Jump to: navigation, search

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 AppStudio Server 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:

Ajax1.png

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 btnSendData_onclick()
  req=Ajax("https://www.nsbasic.com/i/Ajax/ajax.php/?myText=" + txtSend.value, done) 
End Function 

Function done()
  If req.status = 200 Then 'success
    txtResponse.value=req.responseText
  Else 'failure
    msg = "Error: Status = " & req.status
    If TypeName(req.statusText)="string" Then msg = msg & " " & req.statusText
    If TypeName(req.err)="string" Then msg = msg & " " & req.error
    MsgBox msg
  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:

Ajax2.png

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 AppStudio 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:

Ajaxpost.png

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.

First, check to see if the site you want to access has an API. If it does, it's the best solution. If not, you may need to create your own:

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:

Ajaxgeturl.png

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.

To abort a pending Ajax call (it took too long, the user wants to cancel, you no longer want the results, etc.):

req.abort()

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:

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 MS SQL, 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

and MS SQL

http://php.net/manual/en/function.mssql-connect.php

"I use IHttpHandler with VB.net. Basically asp.net without the user interface. I find it is very fast and works extremely well with SQL Server. You can write custom HTTP handlers to process specific, predefined types of HTTP requests in any Common Language Specification (CLS) compliant language. Executable code defined in the HttpHandler classes, rather than conventional ASP or ASP.NET Web pages, responds to these specific requests. HTTP handlers give you a means of interacting with the low-level request and response services of the IIS Web server and provide functionality much like ISAPI extensions but with a simpler programming model. https://msdn.microsoft.com/en-us/library/system.web.ihttphandler(v=vs.100).aspx " - JamesF

Security

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:

Exploits of a mom.png

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" />

Substitute the URL to your site which has the script on it. To allow everything (not really a good idea), do this:

  <access origin="*" />

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!)

3. Set jQuery's options to allow cross origin requests:

Sub Main()
  'the next two statements should be added to your Sub Main()
  $.support.cors = True
  $.mobile.allowCrossDomainPages = True
End Sub

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
<pre>