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).
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:
- Create a proxy service to get the user timeline from the Twitter API
- Reference the service from the Silverlight project
- 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: }