Product Spotlight
(8 votes)

Silvester - A Silverlight Twitter Widget

15 comments   /   posted by Emil Stoychev on Jun 25, 2008

Are you a fan of Twitter? Personally I'm, but I'm also a fan of Silverlight. Twitter has a couple of Flash and HTML badges (a.k.a widgets) you can get and put on your blog to let your visitors know what you are up to in the moment. However, Twitter does not have a Silverlight widget. What negligence! :) If you are like me and want not a Flash, but a Silverlight widget on your blog go ahead and read on how you can build one by yourself or just copy the text below to use it.


Copy this HTML snippet, replace the twitterUser init parameter with your Twitter username and put it in your blog to let your visitors see your tweets.

UPDATE:
In this version you can also specify color scheme though the initParams parameter. The default values are as follows:
<param name="initParams" value="twitterUser=silverlightshow,
fontSize=9,
linkColor=#FFFFFF,
textColor1=#C5C5C5,
textColor2=#FFFFFF,
backgroundColor=#000000,
tweetBackgroundColor=#343434,
tweetSelectedBackgroundColor=#515151"
/>

Color scheme parameters: 

key description
fontSize font size
linkColor link color
textColor1 text color
textColor2 time gone, loading text, refresh button text
backgroundColor background color
tweetBackgroundColor background color of the status items
tweetSelectedBackgroundColor background color of the selected status

Download Source Code
 

Why Silvester?

Because it's funny. Remember the Warner Bros' Tweety right? Not sure if it has something to do with Twitter, but it does sounds the same way to me. So in the cartoon there is also a cat named Sylvester that want to eat the bird (Tweety). Thinking about Twitter, Tweety, Sylvester and Silverlight we came up with Silvester :)

Ok, enough laughing, let's start building the widget.

The UI

The Silvester's UI is pretty much simple. Basically what we need is a ListBox control that contains the tweets (user statuses).

 Tweet 
Structure of a tweet - user profile image, username, text, time gone

To arrange the elements I use a two StackPanels - one with Horizontal orientation to position the Image and the tweet details and one with Vertical orientation to position the username, the status and the time.

   1: "{StaticResource Status}" >
   2:     
   3:         Source="{Binding User.ProfileImageUrl}"
   4:         Style="{StaticResource UserProfileImage}" />
   5:     "{StaticResource StatusData}">
   6:         
   7:             Content="{Binding User.Name}"
   8:             NavigateUri="{Binding User.Url}"
   9:             Style="{StaticResource UserName}" />
  10:         
  11:             Text="{Binding Text}"
  12:             TextStyle="{StaticResource LinkLabelText}"
  13:             LinkStyle="{StaticResource LinkLabelLink}"
  14:             Style="{StaticResource StatusText}" />
  15:         
  16:             Text="{Binding TimeGone}"
  17:             Style="{StaticResource StatusTimeGone}" />
  18:     
  19: 

Fine, but what is this LinkLabel control? This is a custom control that represents a rich TextBlock with the ability to contain hyperlinks. For more information about it please read my previous article.

The UI is not complicated; however there are some elements, like the scrollbars, that need special attention. I won't get any deeper on this topic here, but we plan to write another article that will help you understand how you can easily customize elements using the States and Parts Model with VisualStateManager. If you are interested in the States and Parts Model you can read the great 4 parts tutorials by Karen Corby and Animating ListBoxItems - the VisualStateManager.

The Code

The main task of Silvester is to get the user timeline, i.e. the recent user posts. We add one more feature - refresh the timeline on a predefined interval of time.

Twitter service is available through a public API. However, to make calls from Silverlight to their API they have to explicitly add your domain to allow this (read more on the configuring a web service to enable Silverlight callers here). Of course, that's not an option for us - we want just to put the widget on our blog and have everything work, without having to ask Twitter to add our domain to their clientaccesspolicy.xml file (even if you ask them, like we did, they won't mind).

So what we do to get over this problem? We create a proxy service, hosted on our domain, which we will call in order to reach Twitter.

To achieve our goals we will follow several steps:

  1. Create a proxy service to get the user timeline from the Twitter API
  2. Reference the service from the Silverlight project
  3. Create a timer to update the timeline every minute

Proxy Service to Get the User Timeline

Before creating the service let's see what kind of business object we need. In the UI we display a few fields - user profile image, username (with a hyperlink to the Twitter account), status and time gone. To reflect the XML structure of the user timeline that we receive from the Twitter API we create 2 business objects - Status and User. Let's first take a look at the XML structure.

   1: "array">
   2:     
   3:         Tue Jun 24 16:11:02 +0000 2008
   4:         842567566
   5:         Tip: Asynchronous Silverlight - Execute on the UI thread http://tinyurl.com/5o2rp9
   6:         web
   7:         false
   8:         
   9:         
  10:         false
  11:         
  12:             14341499
  13:             silverlightshow
  14:             silverlightshow
  15:             Sofia, Bulgaria
  16:             SilverlightShow.net - Silverlight articles, tutorials, showcase, videos
  17:             http://s3.amazonaws.com/twitter_production/profile_images/52594162/estoychev_bigger_normal.png
  18:             http://www.silverlightshow.net/
  19:             <protected>falseprotected>
  20:             29
  21:         
  22:     
  23: ...

We display only a part of this information so the business objects will reflect only those elements that need to be displayed.

Status

   1: [Serializable]
   2: public class Status
   3: {
   4:     public string CreatedAt
   5:     {
   6:         get;
   7:         set;
   8:     }
   9: 
  10:     public string Text
  11:     {
  12:         get;
  13:         set;
  14:     }
  15: 
  16:     public User User
  17:     {
  18:         get;
  19:         set;
  20:     }
  21: 
  22:     public string TimeGone
  23:     {
  24:         get
  25:         {
  26:             string[] values = this.CreatedAt.Split( ' ' );
  27:             string timeValue = string.Format( "{0} {1}, {2} {3}", values[ 1 ], values[ 2 ], values[ 5 ], values[ 3 ] );
  28:             DateTime parsedDate = DateTime.Parse( timeValue );
  29: 
  30:             DateTime relativeTo = DateTime.Now;
  31: 
  32:                 // time difference in seconds
  33:             double delta = relativeTo.Subtract( parsedDate ).TotalSeconds + DateTime.UtcNow.Subtract( relativeTo ).TotalSeconds;
  34: 
  35:             if ( delta < 60 )
  36:             {
  37:                 return "less than a minute ago";
  38:             }
  39:             else if ( delta < 120 )
  40:             {
  41:                 return "about a minute ago";
  42:             }
  43:             else if ( delta < ( 60 * 60 ) )
  44:             {
  45:                 return ( int )( delta / 60 ) + " minutes ago";
  46:             }
  47:             else if ( delta < ( 120 * 60 ) )
  48:             {
  49:                 return "about an hour ago";
  50:             }
  51:             else if ( delta < ( 24 * 60 * 60 ) )
  52:             {
  53:                 return string.Format( "about {0} hours ago", ( int )( delta / 3600 ) );
  54:             }
  55:             else if ( delta < ( 48 * 60 * 60 ) )
  56:             {
  57:                 return "1 day ago";
  58:             }
  59:             else
  60:             {
  61:                 return ( int )( delta / 86400 ) + " days ago";
  62:             }
  63:         }
  64: 
  65:         set
  66:         {
  67: 
  68:         }
  69:     }
  70: }

User

   1: [Serializable]
   2: public class User
   3: {
   4:     public string Name
   5:     {
   6:         get;
   7:         set;
   8:     }
   9: 
  10:     public string ProfileImageUrl
  11:     {
  12:         get;
  13:         set;
  14:     }
  15:
  16:     public string Url
  17:     {
  18:         get;
  19:         set;
  20:     }
  21: }

It's finally time to make the proxy service. Let's first see the code and then I'll give some explanations:

   1: [System.Web.Script.Services.ScriptService]
   2: public class TwitterWebService : System.Web.Services.WebService
   3: {
   4:     private const string UserTimelineUri = "http://twitter.com/statuses/user_timeline/{0}.xml";
   5:     private const string StatusElementName = "status";
   6:     private const string CreatedAtElementName = "created_at";
   7:     private const string TextElementName = "text";
   8:     private const string ProfileImageUrlElementName = "profile_image_url";
   9:     private const string NameElementName = "name";
  10:     private const string UserElementName = "user";
  11:     private const string UserUrlElementName = "url";
  12:     public const int RequestRateLimit = 70;
  13: 
  14:     [WebMethod]
  15:     public List GetUserTimeline( string twitterUser, string userName, string password )
  16:     {
  17:         if ( string.IsNullOrEmpty( twitterUser ) )
  18:         {
  19:             throw new ArgumentNullException( "twitterUser", "twitterUser parameter is mandatory" );
  20:         }
  21: 
  22:         WebRequest rq = HttpWebRequest.Create( string.Format( UserTimelineUri, twitterUser ) );
  23: 
  24:         if ( !string.IsNullOrEmpty( userName ) && !string.IsNullOrEmpty( password ) )
  25:         {
  26:             rq.Credentials = new NetworkCredential( userName, password );
  27:         }
  28: 
  29:         HttpWebResponse res = rq.GetResponse() as HttpWebResponse;
  30: 
  31:         // rate limit exceeded
  32:         if ( res.StatusCode == HttpStatusCode.BadRequest )
  33:         {
  34:             throw new ApplicationException( "Rate limit exceeded" );
  35:         }
  36: 
  37:         XDocument xmlStatusData = XDocument.Load( XmlReader.Create( res.GetResponseStream() ) );
  38:         List data = ( from status in xmlStatusData.Descendants( StatusElementName )
  39:                               select new Status
  40:                               {
  41:                                   CreatedAt = status.Element( CreatedAtElementName ).Value.Trim(),
  42:                                   Text = status.Element( TextElementName ).Value.Trim(),
  43:                                   User = new User()
  44:                                   {
  45:                                       ProfileImageUrl = status.Element( UserElementName ).Element( ProfileImageUrlElementName ).Value.Trim(),
  46:                                       Name = status.Element( UserElementName ).Element( NameElementName ).Value.Trim(),
  47:                                       Url = status.Element( UserElementName ).Element( UserUrlElementName ).Value.Trim()
  48:                                   }
  49:                               } ).ToList();
  50:         return data;
  51:     }
  52: }

The web service contains only one method - GetUserTimeline. Basically it just creates a new WebRequest and loads the underlying response stream into an XDocument. Then a new generic list of type Status is created and initialized using LINQ to XML. Nothing special here - it is a normal web service you all have used/built. A couple of constants are defined just before the web method to define the API method address and to simplify the XML work.

Reference the Service

Add a reference to the service we've just created by using the Add Reference context menu item of the Silverlight project. Get the desired user's timeline:

   1: private void RefreshTimeline( object sender, EventArgs e )
   2: {
   3:     TwitterWebServiceSoapClient twitterClient = new TwitterWebServiceSoapClient();
   4:     twitterClient.GetUserTimelineCompleted +=
   5:         new EventHandler( this.GetUserTimelineCompleted );
   6:     twitterClient.GetUserTimelineAsync( this.TwitterUser, null, null );
   7: }

On line 4 we pass a GetUserTimelineCompleted which is the target method to be called when the call completes. On line 5 this.TwitterUser is a property that specifies the username of the user whose timeline we want to get.

   1: private void GetUserTimelineCompleted( object sender, GetUserTimelineCompletedEventArgs e )
   2: {
   3:     StatusList.ItemsSource = e.Result;
   4: }

In the callback just get the returned result and assign it to the ItemsSource property of the ListBox that contains the tweets.

Create a Timer to Update the Timeline

Twitter is obsessive. There are tweeters that updates their status every few minutes. To keep the widget up to date we need a mechanism to update the timeline every couple of minutes (by default every 3 minutes). To achieve this we can use a DispatcherTimer.

   1: private DispatcherTimer timelineTimer;
   2: private void RefreshTimeline()
   3: {
   4:    // first refresh
   5:    if ( this.timelineTimer == null )
   6:    {
   7:        this.timelineTimer = new DispatcherTimer();
   8:        this.timelineTimer.Tick += new EventHandler( RefreshTimeline );
   9:        this.timelineTimer.Interval = new TimeSpan( 0, 1, 0 );
  10:    }
  11: 
  12:    // manual refresh
  13:    else
  14:    {
  15:        this.timelineTimer.Stop();
  16:    }
  17: 
  18:    this.RefreshTimeline( this, EventArgs.Empty );
  19: }

A couple of changes are needed in the GetUserTimelineCompleted callback to make it work properly with the DispatcherTimer.

  1. If an error is encountered during the web service call the timer should be stopped.
  2. If the timeline is explicitly refreshed by the user the timer should be restarted.
   1: private void GetUserTimelineCompleted( object sender, GetUserTimelineCompletedEventArgs e )
   2: {
   3:     if ( e.Cancelled || e.Error != null )
   4:     {
   5:         this.timelineTimer.Stop();
   6:         // process the error
   7:         return;
   8:     }
   9: 
  10:     StatusList.ItemsSource = e.Result;
  11: 
  12:     // restart the timer if the user explicitly
  13:     // call the refresh action
  14:     if ( !this.timelineTimer.IsEnabled )
  15:     {
  16:         this.timelineTimer.Start();
  17:     }
  18: }

I've hidden the error processing and other code for clarification.

Known Issues

When you click on a link in the tweet you will notice that if the ListBoxItem is not selected then the link won't open. This is a known issue in the latest Silverlight 2 release (currently beta 2). There is an opened thread about that problem in the Silverlight.net forums.

Summary

Building a Twitter widget is a straight and relatively easy task - depends on what UI you want. You can download the full source code and examine it for yourself. If you have any questions or misunderstandings please comment below and I'll be glad to clarify the things for you. 

Related Articles

Animating ListBoxItems - the VisualStateManager by Ivan Dragoev

Silverlight LinkLabel control by Emil Stoychev

Asynchronous Silverlight - Execute on the UI thread by Emil Stoychev

States & Parts Model with VisualStateManager by Karen Corby



Comments

Comments RSS RSS
  • RE: Silvester - A Silverlight Twitter Widget  

    posted by Jeff Blankenburg on Jun 25, 2008 16:04

    This is a great resource for the Silverlight community.  Thanks for sharing this!  I've been looking for a great example of Silverlight and Twitter working together.

    Jeff Blankenburg - Microsoft Developer Evangelist

  • RE: Silvester - A Silverlight Twitter Widget  

    posted by timheuer on Jun 26, 2008 12:57

    i see that Twitter added you to their cross domain list...so you should be able to eliminate the proxy service now right?

  • RE: Silvester - A Silverlight Twitter Widget  

    posted by emil on Jun 26, 2008 14:21

    Tim, not exactly... they've added our domain in the crossdomain.xml file - the Flash format. Unfortunately, to enable Silverlight applications to access services hosted on domains containing only crossdomain.xml, the policy file should be configured to allow access from any domain as described in MSDN. For now we should stick to the proxy service.

  • RE: Silvester - A Silverlight Twitter Widget  

    posted by flowerpower on Jul 09, 2008 19:07

    Funny Silvester. haha. Made a geek's day a little lighter. haha