Thursday, February 16, 2012

WP7 PhoneGap Appointments PlugIn

I'm involved in a project that needs a web frontend and clients for iPhone, Androide and of course WindowsPhone. Using a framework like PhoneGap is definitly a good point to start. In a later post I will write about how this all ended up. In this post I will focus on one of my first tasks, building a PhoneGap PlugIn that reads the calendar appointments.

I started with a web research and found a helpfull blog from Jesse here, explaining how to build a PlugIn. What I was missing in his blog, was how to return a value from the plugin. And there where some additional stumbling blocks to build this first PlugIn.

So lets start. First I created a new PhoneGap project in Visual Studio with the standard template (Visual Studio and PhoneGap is already installed).

In the next step I build the PlugIn class written in C#. The PlugIn should read my appointments and return them to the html page. So I added a new class AppointmentProvider in the Plugins folder of my project. This class must be
- in the namespace WP7GapClassLib.PhoneGap.Commands
- inherit from the BaseCommand base class

The rest of the code is more or less normal WP7 C# code.

 using System;
 using System.Collections.Generic;
 using System.Runtime.Serialization;
 using Microsoft.Phone.UserData;
 
 // The namespace must point to the phonegap commands  
 namespace WP7GapClassLib.PhoneGap.Commands 
 {
   // Use the phonegap base class
   public class AppointmentProvider : BaseCommand 
   {
     // Start appointment search with parameters
     public void Get(string args)
     {
       // Check the availability of the args
       if (string.IsNullOrWhiteSpace(args))
         throw new ArgumentNullException();
  
       // Deserialize the args to the ListArgs class
       var arguments = JSON.JsonHelper.Deserialize<ListArgs>(args);
       var appts = new Appointments();
       appts.SearchCompleted += 
         new EventHandler<AppointmentsSearchEventArgs>(appointments_SearchCompleted);
  
       // Set the parameters
       var start = DateTime.Now;
       var end = start.AddDays(int.Parse(arguments.Days));
       var max = int.Parse(arguments.Number);
  
       // Start the asynchronous search
       appts.SearchAsync(start, end, max, null);
     }
  
     // Get the result and pass it to the javascript code
     private void appointments_SearchCompleted(object sender, AppointmentsSearchEventArgs e)
     {
       // Build the result
       var apmts = new List<AptmResult>();
       foreach (var item in e.Results)
       {
         apmts.Add(new AptmResult() { 
           Text = string.Format("{0} {1}-{2}<br>{3}", 
           item.StartTime.ToShortDateString(), 
           item.StartTime.ToShortTimeString(), 
           item.EndTime.ToShortTimeString(),
           string.IsNullOrWhiteSpace(item.Subject) ? "Privat" : item.Subject)
         });
       }
  
       var result = new PluginResult(PluginResult.Status.OK, apmts);
  
       // Return the result
       DispatchCommandResult(result);
     }
  
     // Argument class to be serialized
     public class ListArgs
     {
       [DataMember]
       public string Number; 
  
       [DataMember]
       public string Days;
     }
  
     // Argument class to be serialized
     public class AptmResult
     {
       [DataMember]
       public string Text;
     }
   }
 }

Wow, my first PhoneGap PlugIn is finished :)

Now I need some html to show the results and some JavaScript code to call the PlugIn. The html is easy, just a button and a list.

<div>
  <button onclick="getAppointments();">
    Get Appointments</button>
</div>
<div>
  <ul id="apmtResults">
  </ul>
</div>

Then I need the JavaScript code to call the PlugIn. I did all the explanation in the code, so you can hopefully follow on what I did. One point I did not get is why a reference to a local jQuery lib does not work... and now when I'm writing this, I realize that I should have made an entry in the GapSourceDictionary.xml file. So this problem is solved as well.

<!-- Local reference to the jQuery lib does not work!? -->
<script type="text/javascript" charset="utf-8" src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
  
<script type="text/javascript" charset="utf-8" src="phonegap-1.4.1.js"></script>
  
<script type="text/javascript">
  
  // Wait for PhoneGap to connect with the device
  document.addEventListener("deviceready", onDeviceReady, false);
 
  // PhoneGap is ready to be used!
  function onDeviceReady() {
    // Nothing to do for me
  }
  
  // APPOINTMENT PLUGIN
 
  // Call the AppointmentProvider plugin
  function getAppointments() {
    window.plugins.apmtProvider.showApmtProvider("10", "30");
  }
  
  // Create the class
  function ApmtProvider() {
    this.resultCallback = null;
  }
  
  ApmtProvider.prototype.showApmtProvider = function (number, days) {
    var args = {};
    if (number)
      args.Number = number;
  
    if (days)
       args.Days = days;
    PhoneGap.exec(onApmtOk, onApmtError, "AppointmentProvider", "Get", args);
  }
  
  // Create an instance of the class
  PhoneGap.addConstructor(function () {
    if (!window.plugins) {
      window.plugins = {};
    }
    window.plugins.apmtProvider = new ApmtProvider();
  });
  
  // Callback to handle the ok event
  function onApmtOk(result) {
    // Use jQuery to add the result to the div
    var apmts = [];
    // Clear the list first
    $("#apmtResults").empty();
    // Prepare the result by building an array
    $.each(result, function (i, item) {
      apmts.push('<li>' + item.Text + '</li>');
    });
    // Add the result array
    $("#apmtResults").append(apmts.join(''));
  }
  
  // Callback to handle the error event
  function onApmtError(result) {
    navigator.notification.alert(result, null, "Error");
  }
</script>  


We are done, but when I start the project it fails. First I need to know that I can't use the emulator, because there are no appointments. I need to run the project on a real device.

Still not working. It took me a while till I figured out, that in the WMAppManifest.xml file the entry
<Capability Name="ID_CAP_APPOINTMENTS"/>
was missing. After adding this everthing worked fine!

Please note that this is just a sample and error handling is missing ;-)