Thursday, June 14, 2007 -
This is going to be a big post, but I'll do my best to be as succinct as I can. This wasn't easy - mostly since the controls I needed the most were not easy to understand, and Twitter likes to crash every 15 minutes (I'm not kidding, that's what I ran into - completely non-responsive every 15 minutes). No ranting though! Onward!
Jon Galloway and I were talking the other day about architectural approaches for Silverlight - in other words where would you use it, and why? We sort of came to the idea that these controls work best when they act as "mini apps" or, dare I even say this, "Applets". But all this is so new, and I'm sure that I'll be eating these words in another few months.
So to best demonstrate the "Applet" idea, I thought it would be fun to run up a very basic Twitter client. Now I need to preface this and say that I flat ran out of time to keep going with this thing, and I'm not a designer so my layout skills below leave a LOT to be desired. I've left a few things for later (thus the title Part 1) since the control set I was using was just too hard to figure out with the time I had, and I'm not a very good designer.
I have put the source up on Google Code however, and if you'd like to continue working with it, that would be very very cool:
To get this to work, you'll need to change your username/password in the App_Code/TwitterService.asmx.cs file, and also make sure you have all the love Silverlight Alpha 1.1 bits installed.
I Managed to snag "silverlight" as the project name :) and If you want commit access on this just let me know.
Silverlight can't, as of 1.1 Alpha, use the BrowserHttpRequest to talk to anything but the server from whence it came. This is to (understandably) avoid cross-site scripting attacks. So to get this to work, I'm going to setup a web service which "proxies" the call to Twitter for me - in other words Silverlight is going to call back to it's own server, and the server is going to fetch the Twitters. There's a bit of overhead in this approach, but for now it's all I can do.
The calls to Twitter are done using a nice REST interface, so I can work with JSON or XML if I choose. In this instance I'm going to use XML and while normally I'd serialize the XML data into an object so I can work with it, C# 3.5 has some interesting changes in it that I'm not familiar with. So for this demo I'm going to shove it into a DataSet first, then into an object. This isn't optimal but I'm not too good at XmlReaders.
This is the XSD created using XSD.exe:
Not really optimal for what I want to do, since the information I want is spread out over two datasets for some odd reason. So this is where I'll start.
The call to twitter is simple enough - just a WebRequest with proper credentials:
//REST request to get Twitters string twittersUrl = "http://twitter.com/statuses/friends_timeline.xml"; //create a WebRequest and add in your Twitter User Info WebRequest request = HttpWebRequest.Create(twittersUrl);
//this is for demo only - this info should be passed in request.Credentials = new NetworkCredential("robconery", "PASSWORD"); //pull back the response Stream stream = request.GetResponse().GetResponseStream(); StreamReader sr = new StreamReader(stream); //store the XML in a variable string twitterXML = sr.ReadToEnd(); sr.Close(); StringReader stream = new StringReader(twitterXML); XmlTextReader rdr = new XmlTextReader(stream); //throw the XML into a DataSet DataSet ds = new DataSet(); ds.ReadXml(rdr);
Now I know I can use an XMLReader - it just takes a lot of code and for now this is easier to read (and I'm not good with them). Now I didn't like the way Twitter handed me back the data, so I added a little extra code here to put the result set into an object (called Twitter, with very basic properties like "UserName", "TwitterText", etc - the code's here if you want to see it) , then add that object to a List<>:
//I only want to work with one object //so let's load this up into a workable object //to send out to our client List<Twitter> TwitterList = new List<Twitter>(); //loop the first table - this has the user info foreach (DataRow dr in ds.Tables[0].Rows) { Twitter t = new Twitter(); t.UserID = Convert.ToInt32(dr["id"]); t.Text = dr["text"].ToString(); t.DatePosted = dr["created_at"].ToString(); t.StatusID = Convert.ToInt16(dr["status_id"]); TwitterList.Add(t); } //now loop through again - loading up the twitter text foreach (Twitter t in TwitterList) { DataRow[] rows = ds.Tables[1].Select("status_id=" + t.StatusID.ToString()); t.UserName = rows[0]["screen_name"].ToString(); t.ImageUrl = rows[0]["profile_image_url"].ToString(); //check the image for non-jpg bits if (!t.ImageUrl.ToLower().Contains(".jpg")) { t.ImageUrl = "nogifs.jpg"; } }
And from my Web Service I return this List<Twitter>.
So just like the Login Sample, I created a Web Site, and then in the same solution I created a new Silverlight Project, and then linked them by right-clicking on my Web Site project and selecting "Add Silverlight Link".
An important note here: I wrote a post about the intricacies of working with Silverlight and Web Services and you can read it here. When you set up your web site, be sure to do it using Local IIS and not File Directory. If you use Local IIS, your site will launch without Cassini and a dynamic port, which causes problems when calling a web service from Silverlight.
So the first thing I want to do is use the existing control set I used for the Login demo, the one I extended from agLayoutDemo - so I've added a reference to this dll (which I renamed from agLayoutDemo to AgControls).
Now I want to do two things with this example- Read and Post Twitters. So to encapsulate everything I'm going to create two Silverlight Controls in my project and expose them both on a Silverlight Page in the same way you'd expose a User Control on a Web Page.
The way you structure projects like this is to have a set of controls that can use/consume each other, then put those controls on a main page, where you can then program them. In this case I want to have two controls, TwitterPost and TwitterStatus, and they will be displayed on using my main "Default.xaml" page, which will handle the layout and logic of these controls. Default.xaml will also be pulled over to my website automatically since I created a Silverlight link:
Image Gotcha: Silverlight doesn't like PNG or GIF. You might notice that there is a file in there called "nogifs.jpg" - this is because the Image control in Silverlight (as of 1.1 Alpha) does not like GIFs. Nor does it like PNG's - the only thing I could find that it likes was JPEGs and BMPs. Ugh.
Given this limitation, and the fact that Twitter icons can be PNG's I needed to have an image to show in place of a GIF - so that's what "nogifs" is all about. More on that later.
Another Image Gotcha: Silverlight has problems with remote image sources (i.e. those starting with "http"). If you work with Images long enough you'll bang your head against the table when trying to set the source. If you're working in a Silverlight project and set the source of an Image to http://mydomain.com/myimage.jpg, the Image will show up with nothing in it if you hit Debug/Run. You can try adding by code, doing whatever you want but it won't work. If you drag the image into your project and use a relative reference (myimage.jpg) it will work fine.
Workaround: If you compile everything and run your sample from your web site (as opposed to Debug/Run from the Silverlight project), this problem goes away. I think Silverlight thinks it's offline or something. I don't get it - but it's Alpha so lotsa slack here.
The XAML for TwitterStatus is pretty straightforward, and I added in some tricky fun for mouseovers that expand the text out. It's too long to list out here, but you can see the code here. I also added four properties to the control to allow for setting various text bits and so on, and then added in an event handler for the "Loaded" event so that these values could get set properly (again, see the code here).
The XAML for the TwitterPost page uses the TextBox control that I used for the login page. This a GREAT first effort for a TextBox, but unfortunately it fell a little short for what I wanted in that it didn't wrap the text. Oh well - I can live with that (it's ALPHA). So this is a simple page that has a TextBox control.
This is the page that put's it all together. I want to use the neat layout controls from the AgControls toolkit, and I'll just summarize that I was beating my head against a wall for a very long time since I could NOT get the stuff to layout or show up properly. And then I looked a little harder at the demo stuff and realized that I needed the XAML page to inheret from AgControls.Controls.Page. Bingo, in business. Here's the XAML for the page:
<Canvas x:Name="parentCanvas" xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:app="clr-namespace:Twitter.Controls;assembly=ClientBin/Twitter.dll" xmlns:agCtl="clr-namespace:AgControls.Controls;assembly=ClientBin/AgControls.dll" Loaded="Page_Loaded" x:Class="Twitter.Default;assembly=ClientBin/Twitter.dll" > <agCtl:Grid RowDefinitions="auto,auto,auto,auto" ColumnDefinitions="auto,auto" Width="500" x:Name="TwitterGrid"> <agCtl:Label GridRow="0" GridColumn="0" Text="Send a Twitter"/> <agCtl:TextBox GridRow="1" GridColumn="0" Background="White" BorderThickness="2" Width="500" x:Name="newMessage"/> <agCtl:StackPanel GridRow="2" GridColumn="0" Width="100" Height="70" IsHorizontal="true" > <agCtl:Button FontSize="12" Margin="2" Width="100" Height="50" Text="Send" x:Name="btnTweet"/> <agCtl:Button FontSize="12" Margin="2" Width="100" Height="50" Text="Reload" x:Name="btnReload"/> </agCtl:StackPanel> <agCtl:StackPanel GridRow="3" GridColumn="0" Name="TwitterStack" Width="500" Height="500" /> </agCtl:Grid> </Canvas>
Notice here I'm using the controls mostly for layout, and the StackPanel called "TwitterStack" will hold all the Twitters for me once they are returned from the Web Service.
On the loadup of the page, I call the web service and load up the control set:
void LoadTwitters() { //load up the control that will be holding onto the twitters StackPanel TwitterStack = (StackPanel)this.FindName("TwitterStack"); //clear it out to enable refreshing TwitterStack.Children.Clear(); try { //call out web service localhost.TwitterService svc = new Twitter.localhost.TwitterService(); localhost.Twitter[] twitters = svc.GetTwitters(); foreach (localhost.Twitter t in twitters) { //add in a Twitter Status control for //each twitter Twitter.Controls.TwitterStatus status = new Twitter.Controls.TwitterStatus(); status.TwitterText = t.Text; status.ImageUrl = t.ImageUrl; status.PostDate = t.DatePosted; status.UserName = t.UserName; status.Height = 70; status.Width = 500; //make sure JPG's ONLY! if (!status.ImageUrl.Contains(".jpg")) { status.ImageUrl = "nogifs.jpg"; } //add to the stack panel TwitterStack.Children.Add(status); } } catch (Exception x) { //Show the issue to the user Twitter.Controls.TwitterStatus status = new Twitter.Controls.TwitterStatus(); status.TwitterText = x.Message; status.UserName = "Error Loading"; status.PostDate = DateTime.Now.ToLongDateString() ; status.Height = 60; status.Width = 500; Size s = new Size(); s.Height = 500; s.Width = 60; //weird thing that was bombing the StackPanel... //not sure why I need to do this... status.LayoutStorage.DesiredSize = s; TwitterStack.Children.Add(status); } }
And this is how it looks:
You'll quickly notice that the icons, if you're a friend of any of these people, are not synched up. I stole Phil Haack's Gravatar as my replacement. I really hope that Image supports GIF and other web image formats soon...
The web service to send a Twitter is very simple: just send a POST like so:
[WebMethod]
public string SendTwitter(string message)
{
//make sure the string is only 160 chars in length
if (message.Length > 160)
message = message.Substring(0, 160);
String result = "SUCCESS";
//need to post using "status"
String strPost = "status="+message;
StreamWriter myWriter = null;
HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://twitter.com/statuses/update.xml");
req.Method = "POST";
req.Credentials = new NetworkCredential("robconery", "PASSWORD");
req.ContentLength = strPost.Length;
req.ContentType = "application/x-www-form-urlencoded";
try
{
myWriter = new StreamWriter(req.GetRequestStream());
myWriter.Write(strPost);
}
catch (Exception x)
{
result = "ERROR: "+x.Message;
}
finally
{
myWriter.Close();
}
return result;
}
I hooked up the Send button event to pull the text from the input field in the control, then send it back to my "SendTwitter()" Web service above:
void SendTwitter() { string message = newMessage.Text; //clear out the control newMessage.Text = ""; //send the message off try { localhost.TwitterService svc = new Twitter.localhost.TwitterService(); string result=svc.SendTwitter(message); } catch (Exception x) { newMessage.Text = x.Message; } //reload LoadTwitters(); }
And this is what happens when I do this!:
In closing I want to point out that I'm not a designer, and I'll do my best to make a Part 2 that looks a lot better. Also, I'll add in some security so that a user can store their user name and password in IsolatedStorage. If you want to have at the code - please do!
I stayed away from ATLAS because of all the XML configuration you needed to do. XAML looks a lot cleaner than the ATLAS XML but I was just wondering how you feel its going so far. Drawbacks? Etc.
The b*tch with working with alpha is...well its alpha and no documentation, etc.
It's rough being the guinea pig :)
Oh, and maybe you'd consider just throing your twitter .net api up on google code? figured it may be useful one day.
brain fart.
The Twitter stuff was pretty simple (aside from the service crashing) and really, the above is all there is to it :).