Walk-through: Silverlight Flickr Client in IronRuby

13 Aug 2008

Yesterday I spoke about IronRuby and Silverlight to the .NET Developers Association, or "NETDA", on (Microsoft's) campus tonight. In this post, I'll show one of the apps I built, a Flickr client.

photoviewer

Here's the live app: http://jimmy.schementi.com/silverlight/photoviewer

Pre-requisites

This walk-through will work for both Mac and Windows, however the app seems to have problems in Safari currently. Firefox or IE will work fine.

For starters, you'll need install Silverlight 2 Beta 2, as well as download the Silverlight Dynamic Languages SDK (Beta 2), or "sdl-sdk" for short. Unzip sdl-sdk.zip to a folder named "sdl-sdk" anywhere on your file system.

You'll also need photoviewer-start.zip, which contains images and libraries that this app depends on. (You can also grab photoviewer-final.zip if you want to cheat)

Creating a new project

To create a Silverlight app, there's a script included in the SDK called "sl", which takes two arguments: the language (ruby, python, or jscript), and the name of your application. A folder will be created with the name of your app in your current directory, and a default app generated inside it. Enough talking, let's get to it ... open cmd.exe/Terminal.app/whatever and type:
$ cd path/to/sdl-sdk
$ script/sl ruby photoviewer
(Note: if you're on Windows, make sure to use backslashes "\" instead of forward slashes "/" in the path names)

This should say something along the lines of "Your ruby Silverlight application was created in photoviewer". It does for you too? Awesome, let's move on. So, what got generated? Inside the new "photoviewer" folder you'll find:

index.html - This hosts the Ruby Silverlight app. If you're curious as to what's going on here, there's documentation inline, so read it!

ruby/app.rb - Entry point to the Silverlight app. This generated file just renders app.xaml and sets some text from Ruby.

ruby/app.xaml - XAML UI for the app. Actually, we're not going to use XAML for this app, so we'll delete this later.

ruby/silverlight.rb - Defined "SilverlightApplication" and makes existing Silverlight API's friendlier to Ruby I gave you a newer version of this file in photoviewer-start.zip, so you'll have to overwrite this file with that.

stylesheets/error.css - In the unfortunate event of you writing bad Ruby code, this stylesheet will format the in-browser error message all pretty-like for you.

javascripts/error.js - In case you turn Ruby error reporting off, this file will still catch any errors and not cause your users to see some ugly alert box.

Running your newly created Ruby app

Enough talk, does this thing work? We'll, let's try ...
$ cd photoviewer
$ script/server /b:index.html
(Note "/b" is the same on Windows/Mac, always a forward slash since its part of the argument and not the path)

This will start a Silverlight development web server called Chiron, and launch your default browser at index.html. Using the "/w" flag just starts the server and doesn't open your browser. Anyway, you should see this (in your default browser, of course):

hello-silverlight

Adding external libraries

Before we can start coding, this app depends on some external libraries, so let's put them in our project now. Since I'm such a good guy, I put all the external dependencies in that photoviewer-start.zip file I had you download already. Yep, I'm awesome, you're welcome. Anyway, take the contents of that zip and place it in your project. Your OS may complain about overwriting silverlight.rb, but it's ok, overwrite it. Did it? Good. So, what did you just add to your app?

images/loading.gif - loading indicator gif thingy.

ruby/System.Json.dll - JSON parser that ships in the Silverlight SDK.

ruby/json.rb - requires and monkey patches System.Json to be more ruby-esk.

ruby/silverlight.rb - This app will depend on a more up-to-date version of silverlight.rb, so you can replace the generated one with this.

lightbox/ - A javascript library for showing images prettily. Love it.

Writing some freakin' code! UI, that is

As I hinted at before, this application with have a HTML UI with IronRuby driving it. Yes, you can use Silverlight without all the pretty graphics. ;) Anyway, to accomplish this, we need to make the Silverlight canvas virtually invisible. Open index.html and on line 28 change the width and height attributes from "100%" to "1":
<object data="data:application/x-silverlight," type="application/x-silverlight-2-b2" width="1" height="1">
By changing the width/height to "1", we're making the control basically invisible, but still actually visible so it still loads. Clever hack, huh?

Now let's give our app a UI. Type this after the "</object>" tag at the bottom of index.html:
<div class="search"> 
  <form id="search" action="javascript:void(0)"> 
    <input type="text" id="keyword" /> 
    <input type="submit" id="submit_search" value="search" /> 
    <img src="images/loading.gif" id="images_loading" /> 
  </form> 
  <div id="search_results"> 
    <div id="search_images"></div> 
    <div id="search_links"></div> 
  </div> 
</div> 
<div class="clear"></div> 
This is basic search box, submit button, and a place to render the images found. Let's style it ... open up stylesheets/screen.css. First comes first; delete the #silverlightControlHost style since we don't want our 1x1 control taking up 100% of the page.
#silverlightControlHost {
  height: 100%;
}
Now let's style our plain UI we just made:
body { 
  font-family: "Trebuchet MS" Verdana sans-serif; 
  border: 0px; padding: 0px; margin: 0px; 
} 

div.clear { 
  clear: both; 
} 

/* main search box */ 
.search { 
  padding: 20px; 
  margin: 20px; 
  border: 10px solid gray; 
  background-color: #ccc; 
} 

form#search #images_loading { 
  width: 18px;  
  height: 15px; 
  display: none; 
} 


/* search results */ 
#search_results { 
  display: none; 
} 

/* search images */ 
#search_images { 
  padding-top: 10px; 
} 
#search_images .image, .image a, .image a img { 
  float: left;  
  padding: 0px;  
  margin: 0px;  
  border: 0px; 
} 
#search_images .image a:link,  
#search_images .image a:visited { 
  background-color: white; 
  padding: 5px; 
  margin: 5px; 
  background-color: white; 
  border: 1px solid gray; 
} 
#search_images .image a:hover { 
  background-color: #ff9966; 
} 

/* search links */ 
#search_links { 
  clear: both; 
  padding-top: 10px; 
} 
#search_links a { 
  border: 1px solid #003344; 
  margin: 2px; 
  padding: 0px 5px; 
  color: #003344; 
  background-color: white; 
  text-decoration: none; 
} 
#search_links a:hover, 
#search_links a.active { 
  color: white; 
  background-color: #003344; 
  border: 1px solid white; 
} 
#search_links a.active { 
  cursor: default; 
}

Refresh your browser and you'll see this:
html

IronRuby loves the DOM

Awesome, now we have a UI, but it does nothing ... Ruby enters stage left. Open up ruby/app.rb and let's start hacking.
The point of this app is to type a keyword, hit search, and see images from Flickr that have something to do with the keyword. So, I propose the first step would be to get Ruby handle that search button press. Agree? Good. Replace the entire body of the App class with the following:

def initialize 
  document.submit_search.onclick do |s, e| 
    puts "Search button pressed!"
  end
end
That will print "Search button pressed!" at the bottom of the page every time you press the search button. Duh, that's what the code says! =P

search

Ruby loves Flickr

Now that we know how to hook a button click with Ruby, let's make it talk to Flickr and get the data about our search back. First off, we need to know what to say to Flickr. So, let's redefine initialize to do the following:
def initialize 
  @url = "http://api.flickr.com/services/rest" 
  @options = { 
    :method => "flickr.photos.search", 
    :format => "json", 
    :nojsoncallback => "1", 
    :api_key => "6dba7971b2abf352b9dcd48a2e5a5921", 
    :sort => "relevance", 
    :per_page => "30" 
  } 
  document.submit_search.onclick do |s, e| 
    create(document.keyword.value, 1) 
  end 
end 
We're going to talk to Flickr using REST, because we're sane. The @options hash simply collects the various parts that the REST call requires; we'll build a function to make this into a URL later. The important part is that we're calling flickr.photos.search and asking for the response to be JSON. The onclick event calls this cryptically-name create method which doesn't exist yet, so let's write it.
def create(keyword, page) 
  @options[:tags] = keyword 
  @options[:page] = page 
  request 
end 

def request 
  make_url 
  request = Net::WebClient.new 
  request.download_string_completed do |sender, args| 
    @response = args.result 
    show 
  end 
  document.images_loading.style[:display] = "inline" 
  request.download_string_async Uri.new(@url) 
end 
I had a feeling I'd need to reuse create, so it just adds those arguments to the @options we built in initialize since they're also needed to be part of the URI, and call this request method. This is where the "talking" happens.

First, it makes the URL from the @options (we'll define that method soon enough). Then it news-up an instance of System::Windows::Net::WebClient, part of Silverlight, to actually make the request to Flickr. The images_loading.style[:display] = "inline" causes the loading indicator to start spinning. When the response comes back, I call show, which presumable should show the results in some way. For now, we'll just print the response, and stop our loading indicator.
def show 
  puts @response 
  document.images_loading.style[:display] = "none" 
end
Oh, and now would be a good time to implement make_url ;)
def make_url 
  first, separator = true, '?' 
  @options.each do |key, value| 
    separator = "&" unless first 
    @url += "#{separator}#{key}=#{value}" 
    first = false 
  end 
end 
Nothing’s really special here, just puts the "?", "&", and "=" in the right places. Anyway, we're not ready to give her another run. Save app.rb, refresh your browser, type a search-term, click search, and you'll see the magic unfold:

json-dump

Let's make this gunk pretty

So, what to do with all this Flickr data ... hmmm ... thoughts? Somewhere in that muck are some pretty pictures, so let's squeeze them out ... aka parse the JSON. At the top of app.rb, add the following:
require 'json'
That'll load that json.rb file we copied in the beginning. It simply monkey patches System::Json::JsonValue to give easier access to the data. Here's a loot at json.rb, just for fun.
require 'System.Json.dll' 
include System 

module System::Json 

  class JsonValue 
    def [](index) 
      item = self.get_Item(index.to_clr_string) 
      type = item.get_JsonType 
      return item.to_string.to_s.to_f if type == JsonType.Number 
      return item.to_string.to_s.split("\"").last if type == JsonType.String 
      return System::Boolean.parse(item) if type == JsonType.Boolean 
      item 
    end 
  
    def inspect 
      to_string.to_s 
    end 
  end 

end
This just adds the [] method to JsonValue so you can access JSON values like foo['bar'] where the JSON was foo = {'bar' : 'baz'}. Useful. Anyway, back to writing code ourselves. In app.rb, let's redefine the show method to parse our JSON and do something useful with it.
def show 
  @flickr = System::Json::JsonValue.parse(@response) 
  render 
end 

def render 
  @render = Render.new(@flickr, @options[:tags], @options[:page]) 
  document.search_images[:innerHTML] = @render.generate_photos 
  document.search_links[:innerHTML] = @render.generate_pages 
  @render.hook_page_events('search_links') 
  document.images_loading.style[:display] = "none" 
  document.search_results.style[:display] = "block" 
end 
So, now show just calls System::Json::JsonValue.parse on our Flickr response, and then I've written a new method called render which delegates the rendering to a new class called Render (defining that is next), and stops the loading indicator. Let's make a new file called render.rb and require it in app.rb:
require 'render'
Open render.rb and let's first tackle rendering the photos:
class Render 
  def initialize(flickr, tags, current_page) 
    @flickr = flickr 
    @tags = tags 
    @current_page = current_page 
  end 

  def generate_photos 
    if @flickr['stat'] == "ok" && @flickr['photos']['total'].to_i > 0 
      tag(:div, :class => 'images') do 
        @flickr['photos']['photo'].collect do |p|  
          photo(p) 
        end.join 
      end 
    else 
      "No images found!" 
    end 
  end 

  def photo(p) 
    source = "http://farm#{p['farm'].to_i}.static.flickr.com/#{p['server']}/#{p['id']}_#{p['secret']}" 
    thumb = "#{source}_s.jpg" 
    img = "#{source}.jpg" 
    tag(:div, :class => 'image') do 
      tag(:a, :href  => "#{img}") do 
        tag(:img, :src => "#{thumb}") 
      end 
    end 
  end 

  def generate_pages 
    "" 
  end 

  def hook_page_events(div) 
  end 

private 

  def tag(name, options, &block) 
    output = "" 
    output << "<#{name}" 
    keyvalue = options.collect do |key, value|  
      "#{key}=\"#{value}\"" 
    end 
    output << " #{keyvalue.join(" ")}" if keyvalue.size > 0 
    if block  
      output << ">" 
      output << yield  
      output << "</#{name}>" 
    else 
      output << " />" 
    end 
    output 
  end 
end
A bit of code here, but it's all straight-forward, except for all these "tag" calls. The private tag method does most of the work here, but generating HTML based on a name and options; it makes writing HTML more ruby-esk. You'll also notice that generate_pages and hook_page_events aren't implemented yet. You can not implement them now, refresh your browser, do a search, and you'll at least get the images back:

images

Paginating the Flickrnation

Flickr only sends you one page of your request. When we created the options for the Flickr request, there was a :per_page => "30" entry, saying that I want pages of 30 images ... duh. When we call create(document.keyword.value, 1) on the search button click, that second argument asks Flickr for the 1st page of the request. So, to get any page we want, you just give it to the create function, and it'll give is that page. Told you we'd need to reuse it!
So, to implement generate_pages, we just need to render links for the pages, and to implement hook_page_events, we need to hook each one of those page links with an event that calls create for the given page. Simple enough ...
def generate_pages 
  render = "" 
  if @flickr['photos']['total'].to_i > 0 
    num_pages = @flickr['photos']['pages'].to_i > 10 ? 10 : @flickr['photos']['pages'].to_i 
    num_pages.times { |i| render += page(i + 1) } if num_pages > 1 
  end 
  render 
end 

def page(i) 
  tag(:a, :href => 'javascript:void(0)', :id => "#{i}") { "#{i}" } 
end 

def hook_page_events(div) 
  $app.document.get_element_by_id(div.to_s.to_clr_string).children.each do |child| 
    if child.id.to_s.to_i == @current_page 
      child.css_class = "active"  
    else 
      child.onclick { |s, args| $app.create(@tags, child.id.to_s.to_i) } 
    end 
  end 
end
Do the whole save/refresh/search song-and-dance and you'll have pretty paging action.

pagination

Ruby, you play nice with Javascript, ya hear!

This is pretty awesome ... ya know, Ruby in the browser and all. But truth be told, I like Javascript too. Especially since there are a ton of Javascript libraries out there.

That being said, I'd like to spruce up the "clicking on an image". Right now it opens in a completely new page, and breaks the back-button since your search is wiped away. It's be awesome to have some cool visual effect when clicking on the image, and even having it link to the real Flickr image page. Only if there was such a library ...

http://www.huddletogether.com/projects/lightbox2/

Like I said before, Lightbox is awesome. So, to pretty up zooming in on an image, we'll use Lightbox. First, let's go back to index.html and add some references to Lightbox:
<script type="text/javascript" src="lightbox/js/prototype.js"></script> 
<script type="text/javascript" src="lightbox/js/scriptaculous.js?load=effects"></script> 
<script type="text/javascript" src="lightbox/js/lightbox.js"></script> 
<link rel="stylesheet" href="lightbox/css/lightbox.css" type="text/css" media="screen" />
Lightbox uses special properties on the anchor tag to store information about the image you are viewing, like if it's part of a collection, or what the title of the image should be, etc. Let's go back into our Render#photo method and add a :title and :rel values to our :a tag.

So, the entire tag(:a) call turns into this:
tag(:a, {
  :href  => "#{img}",  
  :title => "&lt;a href=&quot;http://www.flickr.com/photos/#{p['owner']}/#{p['id']}&quot; target=&quot;_blank&quot;&gt;#{p['title']}&lt;/a&gt;", 
  :rel   => "lightbox[#{@tags}]" 
}) do 
  tag(:img, :src => "#{thumb}") 
end
Lastly, let's initialize Lightbox when we show the response. Put the following after the render call in the show method in app.rb:
if document.overlay && document.lightbox 
  document.overlay.parent.remove_child document.overlay 
  document.lightbox.parent.remove_child document.lightbox 
end
HtmlPage.window.eval("initLightbox()") 
That last line isn't the prettiest, but our images are all pretty.

And we're done!

Congrats for getting this far! Now waste time searching for "boobs" or other dirty things on Flickr.

lightbox
comments powered by Disqus