After posting my first SilverLight example, Dave Campbell asked if this approach could work for getting SQL data into a SilverLight 1.0 app (remember, in 1.0 we are limited to client JavaScript). Yes, you could do it but it got me thinking there should be a better way. Wouldn't it be nice if we could get the response in JSON and avoid that ugly (and I use the term lightly) JavaScript XML parsing? I'd also like to use ASMX files instead of generic handlers (ASHX) and let .Net take over mapping request to response. Hey, while I'm making demands, I'd like a JavaScript wrapper to the service interface - I don't want to deal with encoding and parsing the call.
Good thing the guys on the ASP.NET AJAX Team were forward thinkers and did all of this for me already.
Okay, so to convert my previous photo viewer to Web Services I'll need to have the ASP.NET AJAX 1.0 Extensions installed on my server. I'm not going to go into the details of creating ASP.NET 2.0 Web Services (ASMX) here, but to take a service and add JavaScript/JSON support you only need to add ScriptService and ScriptMethod attributes to the class and methods of the Web Service. Here is the service to get photos in a set from flickr (you will also need to make sure your Web.Config is updated for ScriptService support):
<%@ WebService Language="C#" Class="PhotoService" %>
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Web.Script.Services;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Xml;
using System.Configuration;
[ScriptService]
[WebService(Namespace = "http://vinull.com/PhotoService")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class PhotoService : System.Web.Services.WebService {
private static String FlickrEndpoint = "http://api.flickr.com/services/rest/?";
[WebMethod]
[ScriptMethod]
public Photo[] GetPhotosInSet(String SetID) {
StringBuilder req = new StringBuilder(FlickrEndpoint);
req.Append("method=flickr.photosets.getPhotos");
req.Append("&api_key=");
req.Append(ConfigurationManager.AppSettings["FlickrApiKey"]);
req.Append("&photoset_id=");
req.Append(SetID);
WebClient web = new WebClient();
String result = web.DownloadString(req.ToString());
XmlDocument xml = new XmlDocument();
xml.LoadXml(result);
List<Photo> photos = new List<Photo>();
foreach(XmlElement elm in xml.GetElementsByTagName("photo")) {
Photo p = new Photo();
p.Title = elm.Attributes["title"].Value;
p.PhotoPage = "http://flickr.com/photos/scoregasm/" + elm.Attributes["id"].Value + "/";
StringBuilder url = new StringBuilder("http://farm");
url.Append(elm.Attributes["farm"].Value); url.Append(".static.flickr.com/");
url.Append(elm.Attributes["server"].Value); url.Append("/");
url.Append(elm.Attributes["id"].Value); url.Append("_");
url.Append(elm.Attributes["secret"].Value); url.Append("_m.jpg");
p.PhotoUrl = url.ToString();
photos.Add(p);
}
return photos.ToArray();
}
}
public class Photo {
public String PhotoPage;
public String PhotoUrl;
public String Title;
}
Most of the code is related to parsing the XML returned by flickr and crafting an image URL. I'm making a call to another service here, but this is C# code - you can do anything you want here including making a connection to a database server and sending back the results. I highly recommend making a custom class to hold the results - sending back DataSets/DataTables is not a good idea (you asking for compatibility issues).
When used with a ScriptService attribute, a Web Service will generate a JavaScript wrapper for invoking calls from script. This wrapper is located at Service.asmx/js - note: while developing I noticed this wrapper doesn't always update to reflect changes in the ASMX file and required an IISReset to clear out the old version. Since we are now using AJAX Extensions, we can use the ScriptManager tag to handle references to external JavaScript files:
<asp:ScriptManager ID="ScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Path="~/Photos/PhotoService.asmx/js" />
<asp:ScriptReference Path="~/Photos/Silverlight.js" />
<asp:ScriptReference Path="~/Photos/Default.aspx.js" />
<asp:ScriptReference Path="~/Photos/Scene.xaml.js" />
</Scripts>
</asp:ScriptManager>
<div id="SilverlightPlugInHost" style="width: 120%; height: 1000px;">
<script type="text/javascript">
createSilverlight();
</script>
</div>
Now for the cool stuff - all this work has given us a method to use in JavaScript to get the photos: PhotoService.GetPhotosInSet. This method will return an array of Photo object that have fields Title, PhotoUrl, and PhotoPage. The original Scene.xaml.js can be shortened up a bit:
var slPlugin;
var photos;
var thumbsize = 75;
function CanvasLoaded(sender, args) {
slPlugin = sender.getHost();
slPlugin.content.onResize = Resized;
PhotoService.GetPhotosInSet("72057594140080132", DownloadedList);
}
function DownloadedList(data, context) {
photos = data;
Resized(null, null);
}
function Resized(sender, args) {
if(!photos) return;
width = slPlugin.content.ActualWidth;
cols = Math.floor(width / thumbsize);
document.getElementById('SilverlightPlugIn').height =
Math.floor(photos.length / cols * thumbsize + thumbsize * 3 );
DrawImages();
}
var imgXaml = '<Image MouseEnter="imageMouseEnter" ' +
' MouseLeave="imageMouseLeave" Height="140" Width="140" Stretch="UniformToFill" ' +
' MouseLeftButtonDown="imageClick" />';
function DrawImages() {
var thumbsize = 75;
var width = slPlugin.content.ActualWidth;
var cols = Math.floor(width / thumbsize);
var height = slPlugin.content.ActualHeight;
var Body = slPlugin.content.findName("Body");
Body.children.Clear();
for(i = 0; i < photos.length; i++) {
_left = i % cols * 75;
_top = Math.floor(i / cols) * 75
var img = slPlugin.content.createFromXaml(imgXaml);
img.Source = photos[i].PhotoUrl;
img.Tag = photos[i].PhotoPage;
img["Canvas.Top"] = _top;
img["Canvas.Left"] = _left;
Body.children.Add(img);
}
}
(I left out the image event handlers as there is no change to them). I love this version for three reasons - one it's shorter (less code, less bugs!). Two, I no longer need to have separate code paths for IE and FireFox. Three, it's going to be very easy to add methods to my service as I add features to the photo viewer.