June 30, 2009

Stretch Panel

I believe that Silverlight missing panel that will stretch children proportionally to their size. For example:


As you can see button with longer text appears longer, all buttons filling available client area. So I decide to implement StretchPanel, by inheriting from Panel and overriding MeasureOverride and ArrageOverride method.
First, we need Orientation. So I added Orientation dependency property to my controls (the same that StackPanel has). During the calculation we will need to operate either on Width or on Height of property of Size construct. And additionally in arrange method will need to move either on x or on y axis. Instead of checking Orientation value when I need to decide on which side to operate I created following delegates that are assigned in OnOrientationChanged method and then used elsewhere.
private delegate Point GetOffsetPointDelegate(double offset);
private delegate double GetSideDelegate(Size size);
private delegate void SetSideDelegate(ref Size size, double value);
We need 3 delegate types: to get side, to set side (notice ref since Size is value type), and to get offset point. We also need 2 delegates for each delegate type: to get primary side (width for horizontal) and secondary side (height for horizontal). Here is implementation of delegates along with Orientation dependency property:
private GetSideDelegate GetOtherSide;
private GetSideDelegate GetSide;
private SetSideDelegate SetOtherSide;
private SetSideDelegate SetSide;
private GetOffsetPointDelegate GetOffsetPoint;

public StretchPanel()
{
    SetDelegates(Orientation);
}

private void SetDelegates(Orientation value)
{
    GetSide = (value == Orientation.Horizontal) ? GetWidth : new GetSideDelegate(GetHeight);
    GetOtherSide = (value == Orientation.Horizontal) ? GetHeight : new GetSideDelegate(GetWidth);
    SetSide = (value == Orientation.Horizontal) ? SetWidth : new SetSideDelegate(SetHeight);
    SetOtherSide = (value == Orientation.Horizontal) ? SetHeight : new SetSideDelegate(SetWidth);
    GetOffsetPoint = (value == Orientation.Horizontal) ? GetXOffset : new GetOffsetPointDelegate(GetYOffset);
}

#region Orientation Property 

public static readonly DependencyProperty OrientationProperty =
    DependencyProperty.Register(
        "Orientation", typeof (Orientation), typeof (StretchPanel),
        new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));

public Orientation Orientation
{
    get { return (Orientation) GetValue(OrientationProperty); }
    set { SetValue(OrientationProperty, value); }
}

private static void OnOrientationChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ((StretchPanel) o).OnOrientationChanged((Orientation) e.NewValue, (Orientation) e.OldValue);
}

private void OnOrientationChanged(Orientation newValue, Orientation oldValue)
{
    SetDelegates(newValue);
    UpdateLayout();
}

#endregion

private static double GetWidth(Size size)
{
    return size.Width;
}

private static double GetHeight(Size size)
{
    return size.Height;
}

private static void SetWidth(ref Size size, double value)
{
    size.Width = value;
}

private static void SetHeight(ref Size size, double value)
{
    size.Height = value;
}

private static Point GetXOffset(double offset)
{
    return new Point(offset, 0);
}

private static Point GetYOffset(double offset)
{
    return new Point(0, offset);
}

All we need is to implement MeasureOverride and ArrageOverride methods.
protected override Size MeasureOverride(Size availableSize)
{
    var size = new Size();
    foreach (UIElement child in Children)
    {
        child.Measure(availableSize);
        SetSide(ref size, GetSide(size) + GetSide(child.DesiredSize));
        SetOtherSide(ref size, Math.Max(GetOtherSide(size), GetOtherSide(child.DesiredSize)));
    }
    _measureSize = size;
    return size;
}

protected override Size ArrangeOverride(Size finalSize)
{
    double offset = 0;
    foreach (UIElement child in Children)
    {
        double side = Math.Floor(GetSide(child.DesiredSize)/GetSide(_measureSize)*GetSide(finalSize));
        var final = new Size();
        SetSide(ref final, side);
        SetOtherSide(ref final, GetOtherSide(finalSize));
        child.Arrange(new Rect(GetOffsetPoint(offset), final));
        offset += side;
    }
    return finalSize;
}
In the measure method we measure all the children. And then sum up all desired size along primary axis and get the maximum value on the secondary axis. Then we save that value (_meassureSize) for later use and return it to caller. In arrange method we calculate side length as ration of desired side length on measure side length multiplied by final size side length. This will make child appear proportional to its content among others.
Markup for example above is following:
<Controls:StretchPanel xmlns:Controls="clr-namespace:Samples.Controls" 
                       Orientation="Horizontal" Height="30">
    <Button Content="Short Text" />
    <Button Content="Shorter" />
    <Button Content="Long Long Text" />
    <Button Content="Very Very Long Long Text" />
</Controls:StretchPanel>
Download source code

June 29, 2009

Custom Picasa Web Albums photo viewer with JQuery and Google Data API

In this post I would like to show how to implement a custom AJAX Photo viewer that will show albums and pictures from someone's Picasa Web Albums using Google Data API. I would use jQuery as JavaScript library to simplify DOM manipulation and remote requests. Actually my original viewer page was implemented in plain JavaScript but it was total mess. So I decided to rewrite it in much better way: with clean mark up, emphasis on CSS classes instead of direct CSS manipulation (most of page's code is CSS J) and minimum code. You can see final result here. Just check the source of the page to see implementation, since there is no single line of code server-side written. We would use JSON-in-script aka JSONP to make cross-domain requests to Picasa to avoid use of server-side proxy.

Idea

The idea is simple. We just get list of one's albums using Google Data API web service. Originally the data returned in xml format (Atom or RSS), but we can instruct web service to return data serialized in JSONformat using alt=json-in-script query parameter. The URL of the request would look likehttp://picasaweb.google.com/data/feed/api/user/username?alt=json-in-script. Just change the username to user whose albums you want to display on your page. You can simply mash-up albums of multiple users. The data will be parsed to JavaScript object that is easy to work with. We are interested in feed.entry array containing album entries. Album title, thumbnail picture, and URL are then extracted and populated to album container:



Picture 1. Album View
When one clicks to album title we pull album's photo entries and fill another container with thumbnails, using the same method:

Picture 2. Photos View
Finally, when user clicks on picture we provide him/her with a preview:

Picture 3. Preview photos
Pretty simple steps, aren't they?

Markup

As I promised the markup is very clean and simple:
<body    <ul class="album"id="split">
        <
liid="albums">
            <
h1>Albums</h1>
            <
div></div>
        </
li>
        <
liid="photos">
            <
h1>Photos</h1>
            <
div></div>
        </
li>
        <
liid="preview"> <h1>Preview</h1>
            <
div></div>
        </
li>
    </
ul</body>
I think it is quite self-explaining. The empty divs are containers which we will populate with albums, photos and preview picture respectively. Class "album" states that initially only albums container will be shown. I achieve this with CSS.

Cascading Style Sheets

Yes, here they will be really cascading not simply style sheets.
First, I want my photo viewer to fill area of entire page. The containers will scroll individually not whole page. So I set my page's height to 100%. And my container's overflow to auto.
<body>
    <ul class="album" id="split">
        <li id="albums">
            <h1>Albums</h1>
            <div></div>
        </li>
        <li id="photos">
            <h1>Photos</h1>
            <div></div>
        </li>
        <li id="preview">
            <h1>Preview</h1>
            <div></div>
        </li>
    </ul>
</body>
But actually that would not work alone, since 100% height containers does not use overflow, so we would need to set explicit height of the div containers with script. I would show how to in the script section.
Now comes the main style trick: simply changing the class of top UL element will change the user interface. Complex style manipulations are avoided in script by using cascading styles.
body,html
{
    margin:0px;
    padding:0px;
    height:100%;
    overflow:hidden;
}
/*-- Header --*/
li > h1
{
    background:#D5E4F2;
    font-weight:bold;
    font-size:medium;
    margin:0px;
    padding:0.2em 1em 0.2em 1em;
    text-align:center;
}
/*-- Anchors --*/
a,a:active,a:visited
{
    color:Black;
    text-decoration:none;
}
a:hover
{
    color:Navy;
    text-decoration:underline;
}
/*-- Containers --*/
ul#split
{
    margin:0.4%;
    padding:0px;
    height:99%;
    width:99%;
}
ul#split > li
{
    height:100%;
    list-style:none;
    border:solid 1px gray;
}
ul#split > li > div
{
    overflow:auto;
}
/*-- Preview State --*/
li#albums
{
    width:210px;
    float:left;
}
li#photos
{
    width:210px;
    float:left;
}
li#preview
{
    padding:0px;
}
/*-- Album State --*/
ul.album#split > li#albums
{
    width:100%;
}
ul.album#split > li#photos
{
    display:none;
}
ul.album#split > li#preview
{
    display:none;
}
/*-- Photos State --*/
ul.photos#split > li#albums
{
    width:210px;
}
ul.photos#split > li#photos
{
    display:block;
    width:auto;
    float:none;
}
ul.photos#split > li#preview
{
    display:none;
}
 
Here the CSS for elements inside these containers.
 
/*-- Album Elements --*/
div.album
{
    float:left;
    border:solid 1px gray;
    margin:10px;
}
div.album img
{
    border:ridge 2px #AAAAAA;
    width:160px;
    height:160px;
}
div.album > a
{
    display:block;
    border-bottom: double 3px white;
    text-align: center;
    background-color: Silver;
    padding: 2px;
}
/*-- Active Album --*/
div.album.active
{
    border:ridge 2px red;
}
div.album.active > a
{
  background-color: #FFCC66;
  border-bottom: double 3px red;
}
/*-- Photo Elements --*/
li#photos a > img
{
    border:ridge 2px #AAAAAA;
    margin:6px;
}
li#photos a.active > img
{
    border: double 4px red;
    margin:4px;
}
/*-- Preview Image --*/
li#preview img
{
    margin: 10px auto;
    border: ridge 2px #AAAAAA;
    max-width:98%;
    max-height:98%;
    position:relative;
    display:block;
}
/*-- Copyright --*/
div#copy
{
    position:absolute;
    bottom:3px;
    right:3px;
    border:solid 1px black;
    background:#D5E4F2;
    padding:3px;
}
An album element has a title and thumbnail on it. I set them to float in the container. Meanwhile a photo element is an anchor which has its standard inline display style forcing it to wrap on lines and fill the container. For a preview image I set max-height and max-height properties to percentages and margin auto. These scales the preview image properly while displaying it on the center of its container.

Script

Here we come to most interesting part. Before I start digging into data manipulations let me put the snippet of code that will fix the height of the containers after window size has been changed. Put this into$(document).ready handler and you will get the height of DIV container fill the client area of LI leaving the container title intact. As you can see, JQuery makes it possible to set resize function for all three containers in one move. Then we trigger window resize to apply these changes.
$(document).ready(function() {
    // Set explicit height of the containers
    // (100% height and overflow does not work together)
    $(window).resize(function() {
        $("ul#split > li > div").each(function() {
            $(this).height($(this).parent().innerHeight() - $(this).prev().outerHeight());
        });
    });
    $(window).trigger("resize");
});

Next thing we need to do in ready function is to download user’s album (in this case mine):
 
$.ajax({ url: 'http://picasaweb.google.com/data/feed/api/user/adlordy?alt=json-in-script',
    success: fnShowAlbums,
    dataType: 'jsonp'
});
The callback function looks like this:
 
// Callback function to show albums
function fnShowAlbums(data, status) {
    var albums = data.feed.entry;
    $.each(albums, function() {
        // Create album element
        $("<div class='album'><a>" + this.title.$t + "</a><img alt='" +
            this.title.$t + "' src='" + this.media$group.media$thumbnail[0].url + "'/></div>")
        .appendTo("li#albums > div").
        // Set anchor properties
        children("a").attr('href', this.link[0].href)
        .click(function() {
            // Change view state to photo
            $("div.album").removeClass('active');
            $(this).parent().addClass('active');
            $("ul#split").removeClass('album').addClass('photos');
            $(window).resize();
            
            // Get album photos
            $.ajax({ url: this.href,
                success: fnShowPhotos,
                dataType: 'jsonp'
            });
            return false;
        });
    });
}
The great thing about JSON is that you don’t need to deal with data parsing, it’s already a JavaScript object. You simply need traverse object property tree. In this case, we iterate data feed entry collection. For each entry we create div element with album class, fill it with album’s title, link, and thumbnail, then take a link and attach handler to it. Handler changes active album and page state by manipulating class names and then call web services for album’s photos.
// Callback function to show photos from album
function fnShowPhotos(data, status) {
    // Clear container
    $("li#photos > div > a").remove();
    
    var photos = data.feed.entry;
    $.each(photos, function() {
        // Get photo info
        var mg = this.media$group;
        var orig = mg.media$content[0];
        var desc = mg.media$title.$t + "\n" + mg.media$description.$t;
        
        // Create photo element
        $("<a class='photo' href='" + orig.url + "'><img src='" + mg.media$thumbnail[1].url + "' title='"+desc+"'/></a>")
        .appendTo("li#photos > div")
        .click(function() {
            // Change view state to preview (no class)
            $("ul#split").removeClass('photos');
            $("a.photo").removeClass('active');
            $(this).addClass('active');
            
            // Show photo in preview container
            $("li#preview > div").html("<img src='" + fnSmall(this.href) + "' />");
            return false;
        });
    });
}
This callback function fills the photos container with photos from selected album. It’s similar to previous function. The only difference is the is manipulates with photos instead of albums. fnSmall function creates URL of 800x600 thumbnail like this:
// Convert url to smaller image
function fnSmall(url) {
    var l = url.lastIndexOf("/");
    var pref = url.substring(0, l);
    var suf = url.substring(l + 1);
    return pref + "/s800/" + suf;
}
That’s all you need to do to show photos from your Picasa albums on you web site. Hope you liked this post and my photo previewer. See source code simply by clicking view source in your favorite browser, it’s all there.