Silverlight Tutorial – Using Yahoo Pipes to Access Cross-Domain JSON

If you’ve ever sat down to create a web application in Flash or Silverlight you might have run into cross-domain access restrictions. For security reasons, client side technologies won’t let you access data on a different domain if the remote server doesn’t explicitly allow it. Many APIs grant cross-domain access, however there are times there is no way to access the service directly. That’s where this tutorial comes in. Back in 2007 Yahoo! released Pipes, which allows developers to combine and filter web-based information which is then re-hosted by Yahoo. The great thing about this is that Pipes allows cross domain access.

The app we’re going to create today is a simple Silverlight app that displays the most recent 25 “Hot” Reddit posts. You can play with the app below. We’ve dealt with Silverlight and cross-domain issues before. The previous tutorial used Pipes to access Twitter’s XML. This tutorial, on the other hand, will being using Pipes to access Reddit’s JSON API.

?

Create The Pipe

The first thing we’re going to have to do is create a pipe. You’ll have to have a Yahoo account to do this, and everything is free. Once you’re logged in, you’ll see a large “Create Pipe” button at the top of the main Pipes site.

Create Pipe

After you create the pipe, you’ll see a blank canvas.

Blank Pipe

Every pipe starts with a data source. For this pipe, drag a “Fetch Data” module onto the canvas. For the URL, enter the address of the desired JSON data. In our case, we’ll be using reddit.com/.json. Reddit’s API is pretty simple -basically you stick “.json” at the end of any regular page to receive a JSON feed of the same data.

Fetch Data Module

When you connect this module to the Pipe Output module, you should be able to see it in the Debugger window.

Pipe Output

There are tons of other things we can do with this data, but for the purposes of this tutorial, our Pipe is done. All we needed was for Yahoo to give us cross-domain access to this data. We can now save our Pipe.

Save Pipe

I named this pipe “Reddit”.

Pipe Name

Run The Pipe

The last thing we need to do is run the pipe.

Run Pipe

After you click the “Run Pipe…” link, you’ll be taken to the pipe’s page where you can do basic administrative stuff. The thing we want to do is get a link to the JSON output for this pipe. To do this, all we need to do is click the “Get as JSON” link.

Get as JSON

The link initially provided by Yahoo doesn’t support cross-domain calls. Thanks to a post by Jonas Follesø I found out all that’s needed is a simple change to the URL. So take the link that Yahoo provides:

http://pipes.yahoo.com/pipes/pipe.run?_id=…&_render=json

and change it to:

http://pipes.yahooapis.com/pipes/pipe.run?_id=…&_render=json

Create the Silverlight Project

Now that we have the data, it’s time to start building our application. The first thing we need to do is create a new Silverlight 4 application.

New Silverlight Project

When it asks whether or not to create a website for the application, simply use the defaults.

New Silverlight Website

Now we need a class to hold the information obtained from Reddit’s API. Let’s call the class, “RedditEntry”.

namespace RedditViewer
{
  public class RedditEntry
  {
    /// <summary>
    /// Gets or sets the title of the reddit post.
    /// </summary>
    public string Title { get; set; }

    /// <summary>
    /// Gets or sets the thumbnail image.
    /// </summary>
    public string Thumbnail { get; set; }

    /// <summary>
    /// Gets or sets the url to the Reddit comments page.
    /// </summary>
    public string Permalink { get; set; }

    /// <summary>
    /// Gets or sets the url to actual contents.
    /// </summary>
    public string Link { get; set; }

    /// <summary>
    /// Gets or sets the number of comments.
    /// </summary>
    public int NumComments { get; set; }
  }
}

Because we’re going to be using the DataContractJsonSerializer class to deserialize our JSON data, we need to add some markup to this class so .NET knows how to parse the incoming data. The first thing we need to do is add a reference to System.Runtime.Serialization.

Add Reference System.Runtime.Serialization

Now we need to add attributes to support the DataContractJsonSerializer.

using System.Runtime.Serialization;

namespace RedditViewer
{
  [DataContract]
  public class RedditEntry
  {
    /// <summary>
    /// Gets or sets the title of the reddit post.
    /// </summary>
    [DataMember(Name="title")]
    public string Title { get; set; }

    /// <summary>
    /// Gets or sets the thumbnail image.
    /// </summary>
    [DataMember(Name="thumbnail")]
    public string Thumbnail { get; set; }

    /// <summary>
    /// Gets or sets the url to the Reddit comments page.
    /// </summary>
    [DataMember(Name="permalink")]
    public string Permalink { get; set; }

    /// <summary>
    /// Gets or sets the url to actual contents.
    /// </summary>
    [DataMember(Name="url")]
    public string Link { get; set; }

    /// <summary>
    /// Gets or sets the number of comments.
    /// </summary>
    [DataMember(Name="num_comments")]
    public int NumComments { get; set; }
  }
}

I got the names of the fields by looking at the JSON output in the pipe editor.

Highlighted Pipe Output

Request and Parse the JSON Data

Now that we’ve got an object to hold our data, we can go ahead and create a function to request and parse the output.

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;

namespace RedditViewer
{
  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();

      RequestRedditEntries();
    }

    private void RequestRedditEntries()
    {
      // Create a Uri with the address to the Yahoo Pipe.
      Uri pipeUri = new Uri(
        @"http://pipes.yahooapis.com/pipes/pipe.run?_id=…&_render=json");

      // Create a WebClient to request the information.
      WebClient webClient = new WebClient();
      webClient.DownloadStringCompleted += webClient_DownloadStringCompleted;
      webClient.DownloadStringAsync(pipeUri);
    }

    void webClient_DownloadStringCompleted(object sender,
      DownloadStringCompletedEventArgs e)
    {
      // Show the results in a MessageBox.
      // TODO: parse results.
      MessageBox.Show(e.Result);
    }
  }
}

I created a function, RequestRedditEntries, that is initially called when the page loads. This function creates a WebClient that will go out and download the JSON from the Yahoo Pipe. When the request is complete it will call the event handler webClient_DownloadStringCompleted. At the moment, this handler simply displays the results in a MessageBox. If everything went well, you should see a huge alert filled with JSON data. If something went wrong, the properties inside DownloadStringCompletedEventArgs can tell you what happened.

Let’s now parse the output.

void webClient_DownloadStringCompleted(object sender,
  DownloadStringCompletedEventArgs e)
{
  List<RedditEntry> entries = new List<RedditEntry>();

  // Initialize the deserializer.
  DataContractJsonSerializer jsonSerializer =
    new DataContractJsonSerializer(typeof(RedditEntry));

  try
  {
    // Dig through the JSON and find the array of results.
    // The tree is: response -> value -> items -> [0] -> data -> children[]
    JsonObject jsonResponse = (JsonObject)JsonObject.Load(new StringReader(e.Result));
    JsonObject jsonValue = (JsonObject)jsonResponse["value"];
    JsonObject jsonItems = (JsonObject)((JsonArray)jsonValue["items"])[0];
    JsonObject jsonData = (JsonObject)jsonItems["data"];
    JsonArray jsonChildren = (JsonArray)jsonData["children"];

    // Iterate through every child.
    foreach (JsonObject child in jsonChildren)
    {
      // Get the data for the reddit post.
      JsonObject data = (JsonObject)child["data"];

      // Create a memory stream of the JSON text
      // to be passed to the deserializer.
      using (MemoryStream memStream =
        new MemoryStream(UTF8Encoding.UTF8.GetBytes(data.ToString())))
      {
        // Add the entry to the collection.
        entries.Add((RedditEntry)jsonSerializer.ReadObject(memStream));
      }
    }
  }
  catch (Exception ex)
  {
    MessageBox.Show("Failed to parse JSON: " + ex.ToString());
  }
}

There’s a lot going on here, so let’s break it down. First, you’ll have to add a reference to System.Json and System.ServiceModel.Web. The top of this function is simply there to dig through the JSON output and find the array of Reddit entries. I downloaded a tool called JSON Viewer to help see the structure of the JSON.

JSON Viewer

If you want to make this a little cleaner, Yahoo Pipes has lots of ways to massage the data to make it a little more straight forward to parse. Once we get down to the actual array of Reddit entries, I begin invoking the DataContractJsonSerializer. This object can only work on streams so I need to wrap the JSON for a single entry in a MemoryStream. I surrounded the entire thing in a try-catch since there’s about a million things that can go wrong when requesting data from a web service.

We now have a collection of Reddit entries and are ready to move on to the user interface.

Build the User Interface

The user interface is not the primary part of this tutorial, so I’m going to just stick all of the XAML right here. I’ll explain it briefly afterwards.

<UserControl x:Class="RedditViewer.MainPage"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:my="clr-namespace:RedditViewer"
            mc:Ignorable="d"
            Width="400"
            Height="600">
  <UserControl.Resources>
    <my:CommentLinkConverter x:Key="commentLinkConverter" />
  </UserControl.Resources>
  <Grid x:Name="LayoutRoot"
       Background="White">
    <ListBox Name="_redditEntries"
            ScrollViewer.HorizontalScrollBarVisibility="Hidden">
      <ListBox.ItemTemplate>
        <DataTemplate>
          <Border BorderThickness="0,0,0,1"
                 BorderBrush="LightGray"
                 Padding="5">
            <Grid>
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
              </Grid.ColumnDefinitions>

              <!– Thumbnail Image –>
              <Border BorderThickness="1"
                     BorderBrush="LightGray"
                     Width="70"
                     Height="70">
                <Image Width="70"
                      Height="70"
                      Stretch="Uniform">
                  <Image.Source>
                    <BitmapImage UriSource="{Binding Thumbnail}" />
                  </Image.Source>
                </Image>
              </Border>

              <Grid Margin="10,0,0,0"
                   Grid.Column="1">
                <Grid.RowDefinitions>
                  <RowDefinition Height="Auto" />
                  <RowDefinition Height="*" />
                </Grid.RowDefinitions>

                <!– Title –>
                <HyperlinkButton VerticalAlignment="Top"
                                NavigateUri="{Binding Link}"
                                TargetName="_blank"
                                ToolTipService.ToolTip="{Binding Link}"
                                Foreground="Blue">
                  <TextBlock Text="{Binding Title}"
                            Width="285"
                            TextWrapping="Wrap" />
                </HyperlinkButton>

                <!– Comments –>
                <HyperlinkButton NavigateUri="{Binding Permalink, Converter={StaticResource commentLinkConverter}}"
                                VerticalAlignment="Bottom"
                                ToolTipService.ToolTip="{Binding Permalink, Converter={StaticResource commentLinkConverter}}"
                                Margin="0,10,0,0"
                                TargetName="_blank"
                                Grid.Row="1">
                  <StackPanel Orientation="Horizontal">
                    <Image Source="comments.png" />
                    <TextBlock Text="{Binding NumComments}"
                              Margin="5,0,0,0" />
                    <TextBlock Text=" Comments" />
                  </StackPanel>
                </HyperlinkButton>
              </Grid>
            </Grid>
          </Border>
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
  </Grid>
</UserControl>

The entire UI is basically created as an ItemTemplate within a ListBox. I created an Image to hold the thumbnail and a couple of HyperlinkButtons to allow the user to click through to the content. I had to create a binding converter to convert the comments link since Reddit returns the link as a relative URL.

public class CommentLinkConverter : IValueConverter
{
  public object Convert(object value, Type targetType,
    object parameter, System.Globalization.CultureInfo culture)
  {
    string commentLink = value as string;
    if (commentLink != null)
    {
      commentLink = @"http://www.reddit.com" + commentLink;
    }

    return commentLink;
  }

  public object ConvertBack(object value, Type targetType,
    object parameter, System.Globalization.CultureInfo culture)
  {
    throw new NotImplementedException();
  }
}

Other than the binding converter, everything else is a pretty straight forward user interface. The last thing to remember is to set the ItemsSource of the ListBox to the collection of RedditEntries we parsed earlier.

_redditEntries.ItemsSource = entries;

And there you have it. We’ve created a fully functional Silverlight app for viewing the main page of Reddit. Since Reddit doesn’t support cross-domain access we used Yahoo Pipes to provide it for us. The entire source of this application can be downloaded below. If you have any questions, feel free to leave them or head on over to our forums.

Leave a Reply

Your email address will not be published. Required fields are marked *