First of all, I'm a big proponent of "not reinventing the wheel". There's some excellent code put out by
Yavor Georgiev of the Connected Framework team that I was able to use for much of the basis of this sample app.
This app uses the Twitter REST API to perform a search and return RSS like so:
http://search.twitter.com/search.rss?lang=en&rpp=100&q=searchterm
If you check http://search.twitter.com/crossdomain.xml you'll see that our kind friends at Twitter are enabling cross-domain access for us.
When the Silverlight Page class initializes, I check for a cookie called "search". If it isn't null, I execute my asynchronous MakeRequest call which then databinds the ListBox that is used to show the results. When you click on a result, we grab the Url and use the System.Windows.Browser.HtmlPage.Window.Navigate method to make the browser - not the Silverlight app - request the selected page. Having saved the search in a cookie, when the user clicks their browser's Back button, our Silverlight app reads the cookie, and re-issues the request, redisplaying the results of the last search. If we wanted, we could actually use the retrieved cookie value to load the previous results from IsolatedStorage, a technique which I've covered in a previous article. Since all of the business logic is included in the Page.Xaml.cs class, I present it here:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.ServiceModel.Syndication;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Xml;
namespace SLTwitterSearch
{
public partial class Page : UserControl
{
SynchronizationContext uiThread;
string searchUrl = "http://search.twitter.com/search.rss?lang=en&rpp=100&q=";
public Page()
{
InitializeComponent();
// Grab a reference to the UI thread
uiThread = SynchronizationContext.Current;
string cookval = GetCookie("search");
if(cookval!=null)
{
this.feedAddress.Text = cookval;
MakeRequest();
}
}
private string GetCookie(string key)
{
string[] cookies = HtmlPage.Document.Cookies.Split(';');
foreach (string cookie in cookies)
{
string[] keyValue = cookie.Split('=');
if (keyValue.Length == 2)
{
if (keyValue[0].ToString() == key)
return keyValue[1];
}
}
return null;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
itemsList.Visibility = Visibility.Collapsed;
MakeRequest();
}
void MakeRequest ()
{
// save this search in a cookie for later retrieval
this.SetCookie("search", feedAddress.Text);
// Begin HTTP request to get feed
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(new Uri(searchUrl + feedAddress.Text));
request.BeginGetResponse(new AsyncCallback(responseHandler), request);
}
void responseHandler(IAsyncResult asyncResult)
{
// Get HTTP response
HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(asyncResult);
// Load feed into SyndicationFeed object
XmlReader reader = XmlReader.Create(response.GetResponseStream());
SyndicationFeed feed = SyndicationFeed.Load(reader);
// invoke the databinding method on the UI thread
uiThread.Post(setItemsListDataContext, feed);
}
void setItemsListDataContext(object feed)
{
// Set up databinding
itemsList.DataContext = (feed as SyndicationFeed).Items;
itemsList.Visibility = Visibility.Visible;
}
private void itemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SyndicationItem itm = (sender as ListBox).SelectedItem as SyndicationItem;
// Twitter puts the Url to the Tweet in the "Id" field:
System.Windows.Browser.HtmlPage.Window.Navigate(new Uri(itm.Id.ToString()));
}
private void SetCookie(string key, string value)
{
// Expire in 7 days
DateTime expireDate = DateTime.Now + TimeSpan.FromDays(7);
string newCookie = key + "=" + value + ";expires=" + expireDate.ToString("R");
HtmlPage.Document.SetProperty("cookie", newCookie);
}
}
// Helper classes to clean up and shape received data for UI databinding
public class HtmlSanitizer : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// Remove HTML tags and empty newlines and spaces
string returnString = Regex.Replace(value as string, "<.*?>", "");
returnString = Regex.Replace(returnString, @"\n+\s+", "\n\n");
// Decode HTML entities
returnString = HttpUtility.HtmlDecode(returnString);
return returnString;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class LinkFormatter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// Get the first link - that's the link to the post
return ((Collection)value)[0].Uri;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}This is a pretty simple implementation; if you are interested in Twitter you can find some very interesting folks to follow on Twitter by using this little search app. You can download the complete
Visual Studio 2008 Silverlight Application and web here.
NOTE: Some Twitter API's require HTTP BASIC authentication. Unfortunately the "Authorization" HttpWebRequest header in Silverlight is on the "restricted" list, so if you want to use those methods, you'll need to implement a page or service on your hosting web server application to accept the username:password combo on the querystring (encrypted if you like) or as POST form parameters, then have your server-side proxy make the HttpWebRequest call using the full Framework (which is not "restricted") and return the results to your client-side Silverlight app.
See here for a detailed list. You will note that the help topic states that "many of these headers are set by the hosting browser". Unfortunately, if you use the browser-based HttpRequest object, it isn't smart enough to read the crossdomain.xml policy file on Twitter, so it becomes, effectively, useless.