SilverLight: A simple flickr photo viewer

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)

Submit this story to DotNetKicks   

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.

net application development - Tuesday, September 23, 2008 7:10:55 AM

Very nice information... great post...

Leave a comment



Your name:
 

Your email (not shown):
 
Will display your Gravatar image.

Your website (optional):



About Me

Michael C. Neel, born 1976 in Houston, TX and now live in Knoxvile, TN. Software developer, currently .Net focused. Board member and President of ETNUG, and organizes CodeStock, East Tennessee's annual developers conference. .Net speaker, a Microsoft ASP.NET MVP and ASPInsider. Founder of FuncWorks, LLC and Feel The Func podcast.

Proud father of two amazing girls, Rachel and Hannah.

 Subscribe to ViNull.com |  Comments

Follow me on Twitter | Contact Me

Related Posts

SilverLight Interop with Flash/Flex (flashlight?)

While the marketers and academics focus on the battle between silverlight and flash/flex, here in Mike's Mad Scientist Labs we've been focused on making ... Read more

SilverLight: Using Web Services with SilverLight 1.0

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, ... Read more

ASP.NET: Creating a UserControl with Child Content

I love ASP.NET User Controls, aka “ascx” files.  These little guys are great for reusable content and dividing up the components of a website.  ... Read more

SilverLight 2.0: Setting the Background of a Button

I know, this hardly seems a topic worthy of a blog post.  To create a Button in SilverLight like the first in the picture is just: <Button Content="Stock ... Read more

ASP.NET SEO Interview on Polymorphic Podcast

Craig Shoemaker just posted the latest episode of the Polymorphic Podcast: ASP.NET SEO - Interview with Michael Neel.  Yes, I've now appeared in a ... Read more

FeelTheFunc Podcast

CodeStock
Are you going?

ASPInsiders Member

ETNUG Member