I recently updated my photos page to use SilverLight. I'll go back to the page and do something more with it, but I wanted to keep this first version simple to use an an example. I'm only using SilverLight 1.0 - so no C# or embedded .Net framework; just XAML controlled by Javascript. To get started, you'll need to install the SilverLight 1.0 SDK (and have a version of Visual Studio 2005).
Once installed, you can create a new project using the SilverLight Javascript Application template, found under the Visual C# / SilverLight group. This will create a demo project that has a button tied to an alert box which you can run immediately. I'm going to focus on the Scene.xaml and Scene.xaml.js files, but I encourage you to explore the other files and settings to see how everything works.
Before we can begin, we need some data from flickr. Flickr provides an API with a REST interface, and I use a simple generic handler (ashx) to proxy the request. Browsers do not allow embedded elements and Javascript to make cross domain calls, so this proxy is needed. (It also provides a great point to cache the request, though I don't show that code here).
<%@ WebHandler Language="C#" Class="PhotoList" %>
using System;
using System.Web;
using System.Net;
public class PhotoList : IHttpHandler {
public void ProcessRequest (HttpContext context) {
WebClient web = new WebClient();
String result = web.DownloadString(
"http://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos...");
context.Response.ContentType = "text/xml";
context.Response.Write(result);
}
public bool IsReusable {get {return true;} }
}
I've shortened the flickr URL, checkout the API docs for the full link if you decide to try this example out. To see the results of this call, just take a look at the live version (Update: I've changed this now to use a web service, so the prior link is a static XML file for reference). Now to the XAML - if you know me, or have seen my WPF presentation - you'll know that I tend to start with the code, then move to markup once I understand what goes on behind the scenes first. So here is my XAML:
<Canvas xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Loaded="CanvasLoaded">
<Canvas x:Name="Body" />
</Canvas>
Honestly, I have nothing against XAML. I love it, and think it hellakewl. I just want to understand the framework first and I get a better feel for that in code. So all this file does is hold a canvas object for adding images to later. The Loaded attribute is the SilverLight equivalent of onLoad in Javascript.
var slPlugin;
var photoTags;
var thumbsize = 75;
function CanvasLoaded(sender, args) {
slPlugin = sender.getHost();
slPlugin.content.onResize = Resized;
var downloader = slPlugin.createObject("downloader");
downloader.addEventListener("completed", DownloadedList);
downloader.open("GET", "PhotoList.ashx");
downloader.send();
}
This method saves a reference to the SilverLight plugin instance, then attaches a handler to the onResized event which fires when the browser resizes the element containing the plugin. The downloader object is a SilverLight object for downloading resources at runtime and runs asynchronously. You can register progress event listners (great for loading bars) and a onCompeted listener as shown here.
function DownloadedList(sender, args) {
// get the response
var xml = sender.responseText;
// create appropiate XML document
if (window.ActiveXObject) {
// IE 6 (and 7)
doc = new ActiveXObject("Microsoft.XMLDOM");
if (!doc.loadXML(xml)){
// handle parse error
throw doc.parseError.reason;
}
}
else {
// Firefox and others
var parser = new DOMParser();
doc = parser.parseFromString(xml, "text/xml");
}
photoTags = doc.getElementsByTagName("photo");
Resized(sender, args);
}
This method simple converts the downloaded string to an XML Dom object, and then saves the list of photos. I call my Resized event handler here to draw the downloaded image data (note you must implement your own "paint" routines here, nothing magical is happening by me calling Resized other than I'm not duplicating code =D).
function Resized(sender, args) {
if(!photoTags) return;
width = slPlugin.content.ActualWidth;
cols = Math.floor(width / thumbsize);
document.getElementById('SilverlightPlugIn').height =
Math.floor(photoTags.length / cols * thumbsize + thumbsize * 3 );
DrawImages();
}
The resize method figures out how many columns we can display with our current width, and then adjusts the height of the div containing the plugin to account for the number of rows needed. If we changed the height of plugin content we would still be clipped by the containing element.
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 < photoTags.length; i++) {
farm_id=photoTags[i].attributes.getNamedItem("farm").value
server_id=photoTags[i].attributes.getNamedItem("server").value
img_id=photoTags[i].attributes.getNamedItem("id").value
secret_id=photoTags[i].attributes.getNamedItem("secret").value
title=photoTags[i].attributes.getNamedItem("title").value
src="http://farm"+farm_id+".static.flickr.com/"+server_id+"/"+img_id+"_"+secret_id+"_m.jpg"
_left = i % cols * 75;
_top = Math.floor(i / cols) * 75
var img = slPlugin.content.createFromXaml('<Image Source="' + src + '" MouseEnter="imageMouseEnter" ' +
' MouseLeave="imageMouseLeave" Height="140" Width="140" Stretch="UniformToFill" ' +
' MouseLeftButtonDown="imageClick" ' + ' />');
img["Canvas.Top"] = _top;
img["Canvas.Left"] = _left;
img.Tag = "http://flickr.com/photos/scoregasm/" + img_id + "/";
Body.children.Add(img);
}
}
After some calculations, findName finds the instance of our Canvas object. findName can be used from any UI object, so it's common to use sender.findName() in event handlers. The flickr API doesn't actually return a URL to an image, you must construct one (or make a second API call) - you'll find working in the flickr API tons of these "developer friendly features." createFromXaml is the real magic here - this method takes a snippet of XAML and returns the resulting object. Since it's the only method available in 1.0 (createObject only supports downloader in 1.0) it's good that is very flexible - you can see I've setup some attributes in the string and later added some attached properties after creation. The Tag property works just as it does in WPF - an object you can use to store data related to the element, in my case I store a link to the photo's page on flickr. Here are the event handlers:
function imageMouseEnter(sender, args) {
sender.Width = sender.Width * 1.75;
sender.Height = sender.Height * 1.75;
sender["Canvas.ZIndex"] = 1;
}
function imageMouseLeave(sender, args) {
sender.Width = sender.Width / 1.75;
sender.Height = sender.Height / 1.75;
sender["Canvas.ZIndex"] = 0;
}
function imageClick(sender, args) {
location.href = sender.Tag;
}
Enter and Leave change the image's size for effect - the click handler shows that we are running traditional Javascript and have all the old methods available to us, like location.href.
Last note - if you use flickr you may notice the URLs on the flickr website don't always work in SilverLight. The site URLs are redirected to the farm URLs created above and the SilverLight downloader object doesn't handle the redirection so no image is loaded. The XAML Image tag will follow the redirect if in a precompiled XAML file, but not when used in createFromXaml (which internally uses a downloader object). Yes, this is 1.0 we are working with.
Posted By Mike On Saturday, October 27, 2007
Filed under asp.net silverlight flickr |
Comments (4)
Gwynn
-
Wednesday, November 07, 2007
3:10:59 AM
I love how intuitive the UI is. As a user I immediately understand what's going on when browsing the photos and how to use it. Nice Job!
sloan
-
Monday, November 26, 2007
6:52:12 PM
For future readers (and beginners like I am), there is a small issue with one javascript function listing.
The lines which follow the sentence:
"If we changed the height of plugin content we would still be clipped by the containing element."
and which start out like this:
var thumbsize = 75;
var width = slPlugin.content.ActualWidth;
is missing the
"function DrawImages() {"
line (which needs to preceed the var declarations)
As In:
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 < photoTags.length; i++) {
farm_id=photoTags[i].attributes.getNamedItem("farm").value
server_id=photoTags[i].attributes.getNamedItem("server").value
img_id=photoTags[i].attributes.getNamedItem("id").value
secret_id=photoTags[i].attributes.getNamedItem("secret").value
title=photoTags[i].attributes.getNamedItem("title").value
src="http://farm"+farm_id+".static.flickr.com/"+server_id+"/"+img_id+"_"+secret_id+"_m.jpg"
_left = i % cols * 75;
_top = Math.floor(i / cols) * 75
var img = slPlugin.content.createFromXaml('<Image Source="' + src + '" MouseEnter="imageMouseEnter" ' +
' MouseLeave="imageMouseLeave" Height="140" Width="140" Stretch="UniformToFill" ' +
' MouseLeftButtonDown="imageClick" ' + ' />');
img["Canvas.Top"] = _top;
img["Canvas.Left"] = _left;
img.Tag = "http://flickr.com/photos/scoregasm/" + img_id + "/";
Body.children.Add(img);
}
}
/*
Thanks for the articles....It has gotten started on the right path.
That small little syntax issue took me a bit to figure out....thus my followup post.
*/
Mike
-
Monday, November 26, 2007
11:00:01 PM
Doh! It looks like I need to unit test my blog posts, lol. Thanks for the catch sloan, I've updated the article to fix it.