Scott Hanselman

The Weekly Source Code 40 - TweetSharp and Introducing Tweet Sandwich

April 03, 2009 Comment on this post [24] Posted in Open Source | Source Code | Windows Client | WPF | XML
Sponsored By

imageI just was at Quiznos hanging out with @QuiznosNick. He's a former Technology Executive who bought a Quiznos Franchise for his retirement. He's a major geek, and while chatting he wonder how he could take orders over Twitter. I wanted to see how easy it'd be to write as a real app. I could use PowerShell or Curl, but it's all in good fun, right?

For no reason at all, here is the thought process and code as I write it. I shall call it Tweet Sandwich.

Step 0 - Make it Pretty

Ok, WPF will do fine. Of course, before I do any work or planning or anything, I'll need an icon. ;) Search the web, find a nice, free non-commercial icon of a sandwich. Make it transparent with Paint.NET, make a 32x32 and a 16x16 at 24 bit color and paste into Visual Studio. Name it app.ico. Waste of time, perhaps, but it motivates me personally to do the pretty stuff first.

Take the Window1, call it Main and setup some controls. Grab a Free Twitter Bird PNG and a picture of a Sandwich, more Paint.NET and I've got a Main Form.

I tell you, being able to use Paint.NET and the clipboard, and a good understanding of how transparency works in Windows is an important skill to have. I'm no artist, but I can hack together a picture of a bird and a sandwich with the best of them.

Tweet Sandwich

What Settings Do I Need to Save

OK. Now, I put the Settings in the Properties dialog for the project.

image

Then I'll put in some small databinding code to make the text boxes in the form fill with the data from the settings.

<Window x:Class="TakeOrdersOverTwitterWPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Tweet Sandwich" Height="459" Width="454"
xmlns:local="clr-namespace:TakeOrdersOverTwitterWPF.Properties">
<Window.Resources>
<local:Settings x:Key="settings" />
</Window.Resources>
<Grid>
<GroupBox Header="Settings" Name="groupBox1">
<Grid DataContext="{StaticResource settings}" >
<TextBox Name="twitterUserName" Text="{Binding Path=Default.TwitterUserName}"/>
...etc...

The keys are the Settings Resource that maps to the Properties (Settings) for the app. Then the Binding to the TextBox. Then we save them when the app closes with Settings.Default.Save();

How Often Will I Check For Orders?

Now, I'll setup a Timer to check for orders every five minutes:

DispatcherTimer twitterTimer = null;

private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.twitterTimer = new DispatcherTimer(new TimeSpan(0, 5, 0), DispatcherPriority.Normal, CheckForOrders, this.Dispatcher);
}

private void CheckForOrders(object sender, EventArgs e)
{
...
}

Gotta GET the Twitter Feed now...check the Twitter API. Do I want RSS or an API? The Twitter API has a REST model, and I need to see the replies to QuiznosNick, so I'll need to add some options to my application. I'll need to authenticate as QuiznosNick and ask for his replies list. I need his username and password. I'll probably want to call this API, which will let me see replies since some time. Looks like I can use the Date, or a status id, which is a big number that keeps getting bigger.

statuses/replies

Returns the 20 most recent @replies (status containing @username) for the authenticating user.

URL: http://twitter.com/statuses/replies.format

Formats: xml, json, rss, atom

Method(s): GET

Parameters:

  • since_id.  Optional.  Returns only statuses with an ID greater than (that is, more recent than) the specified ID.  Ex: http://twitter.com/statuses/replies.xml?since_id=12345
  • max_id. Optional.  Returns only statuses with an ID less than (that is, older than) the specified ID.  Ex: http://twitter.com/statuses/replies.xml?max_id=54321
  • since.  Optional.  Narrows the returned results to just those replies created after the specified HTTP-formatted date, up to 24 hours old.  The same behavior is available by setting an If-Modified-Since header in your HTTP request.  Ex: http://twitter.com/statuses/replies.xml?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
  • page.  Optional. Retrieves the 20 next most recent replies.  Ex:http://twitter.com/statuses/replies.xml?page=3

Returns: list of status elements

How Will I Call Twitter?

I could just make a call to Twitter using WebClient and Basic Auth, but since I'll only be paid in Sandwiches, I'll use TweetSharp. It's a smidge overkill for one API call, but it'll let me figure out if TweetSharp is fun or not. I could have also used LinqToTwitter, so try them both out and make your own judgment.

Here's how you would get the replies for an authenticated user using TweetSharp. I might switch this over to DirectMessages, which is less fun, but more secure, if things become a problem.

TwitterClientInfo info = new TwitterClientInfo() { ClientName = "TweetSandwich", ClientVersion = "1.0" };
var twitter = FluentTwitter.CreateRequest(info)
.AuthenticateAs(twitterUserName.Text, Password.Password)
.Statuses()
.Replies().AsXml();

string result = twitter.Request();

At this point, "result" has the XML I want in it.

Text Visualizer (3) 

The general structure of the nodes I'll need is:

statuses
status
created_at
id
text
user
id
name
location

I want "all status's greater than the lastid, and from those, extract the text, user id, name and location, sorting by created_at descending." In LINQ to XML, that might be:

XDocument doc = XDocument.Parse(result);
var statuses = (from d in doc.Descendants("status")
where int.Parse(d.Element("id").Value) > lastOrderNum
where d.Element("text").Value.Contains(orderString.Text)
select new
{
tweetid = int.Parse(d.Element("id").Value),
name = d.Element("user").Element("name").Value,
location = d.Element("user").Element("location").Value,
tweet = d.Element("text").Value,
dateTime = d.Element("created_at").Value.ParseTwitterDateTime()
}).OrderByDescending(t => t.dateTime);

However, TweetSharp has an object model that will deserialize JSON so I don't even need to do this. I can use their objects and still use LINQ, which makes this even cleaner. I can avoid all the strings and the dataType conversions as it's all hidden. Not to mention the hacky ParseTwitterDateTime extension method I got from Wally that he got from Tim Heuer.

TwitterClientInfo info = new TwitterClientInfo() { ClientName = "TweetSandwich", ClientVersion = "1.0" };
var replies = FluentTwitter.CreateRequest(info)
.AuthenticateAs(twitterUserName.Text, Password.Password)
.Statuses()
.Replies()
.Since(lastOrderNum)
.AsJson();

IEnumerable<TwitterStatus> statuses = replies.Request().AsStatuses();

var statusesFiltered = from d in statuses
where d.Id > lastOrderNum
where d.Text.IndexOf(orderString.Text, StringComparison.OrdinalIgnoreCase) != -1
orderby d.CreatedDate descending
select d;

Printing Orders

Now I just need to print them out. Every Quiznos has the same computer and the same printer that they got from the corporate office. I don't care though, I'll just print out an order on whatever default printer they have.

Printing is hard, and I only allocated a few hours to do this, but Printing a "Visual Object" in WPF is easy.

PrintDialog dlg = new PrintDialog();
dlg.PrintVisual(orderCanvas, "Whatever");

I could just make a Canvas with a bunch of controls to represent the last tweeted order, and print that. 

Tweet Sandwich (2)

However, the last time I did anything with Printing it was VB6 and it was hard. How hard it is now to make a real document today in WPF?  I figured I'd find out by trying.

I thought I'd use these FlowDocuments and make one that can show a Tweet. I went File | Add New Item | FlowDocument and call it SandwichOrder.xaml.

Add New Item - TakeOrdersOverTwitterWPF

I made a FlowDocument as a Resource that looked like this I could do it manually, do it in Word and save it, or use an HTML to XAML converter as others have.

<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
FontSize="24" FontFamily="Georgia">
<Paragraph>
<TextBlock Text="{Binding Path=User.ScreenName}"/> in
<TextBlock Text="{Binding Path=User.Location}"/>
says on <TextBlock Text="{Binding Path=CreatedDate}"/>:
</Paragraph>
<Paragraph FontFamily="Arial">
<TextBlock Text="{Binding Path=Text}"/>
</Paragraph>
</FlowDocument>

I figured I want to DataBind to it, using the TweetSharp TwitterStatus object. Dynamically creating a document from a resource template, binding to it and printing it seems like a core scenario. Googling around, though found a lot of people having trouble with a few basic things that I was hitting into also.

NOTE: I might be doing this wrong, so I need to ask a WPF expert at Microsoft to see if I'm wrong about some things I think are too hard.

Dynamically Creating a FlowDocument, Data Binding and Printing It

First, I wrote this:

private void PrintOrder(TwitterStatus t)
{
var streamInfo = Application.GetResourceStream(new Uri("resources/SandwichOrder.xaml",UriKind.Relative));
FlowDocument doc = XamlReader.Load(streamInfo.Stream) as FlowDocument;
doc.DataContext = t;
PrintDialog dlg = new PrintDialog();
dlg.PrintDocument(((IDocumentPaginatorSource)doc).DocumentPaginator,"Tweeted Sandwich Order");
}

I felt OK about it, but not awesome. First, it was too hard to get my FlowDocument out of the Embedded Resource. I thought I could do something like App.Resources["SandwichOrder.xaml"]. I also wanted to do lines one and two in all one like like: var doc = FlowDocument.FromResource("SandwichOrder.xaml").

Finally, the weird interface cast in the PrintDocument line was totally counter intuitive. Seemed like PrintDocument should have an overload that takes a FlowDocument.

Then I tried to print. When I printed, the data binding didn't happen. I just got the basic text. More Googling showed there's a threading issue and the binding happens on another thread?

Now I had to add what appears to be the WPF equivalent of "DoEvents" - that big dispatcher call that releases the thread to do pending stuff. This CAN'T be right. I MUST be doing something wrong, so I'll update this post as I learn.

private void PrintOrder(TwitterStatus t)
{
var streamInfo = Application.GetResourceStream(new Uri("resources/SandwichOrder.xaml",UriKind.Relative));
FlowDocument doc = XamlReader.Load(streamInfo.Stream) as FlowDocument;
doc.DataContext = t;
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.SystemIdle, new DispatcherOperationCallback(delegate { return null; }), null);
PrintDialog dlg = new PrintDialog();
dlg.PrintDocument(((IDocumentPaginatorSource)doc).DocumentPaginator,"Tweeted Sandwich Order");
}

After this printing and databinding worked, except the TextBlocks I was using didn't wrap, so the orders got clipped. I tried using a <Run> but they don't support DataBinding. I ended up having to add a BindableRun class as more Googling showed more confusion. Folks have created Bindable Tables also, it seems and this BindableRun pattern seems common. I need to check on why this isn't built in.

Now my FlowDocument looks like this:

<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:bt="clr-namespace:TakeOrdersOverTwitterWPF.BindableText;assembly=TakeOrdersOverTwitterWPF"
FontSize="24" FontFamily="Arial">
<Paragraph FontSize="48">Twitter Order</Paragraph>
<Paragraph>
<bt:BindableRun BoundText="{Binding Path=User.Name}" />
(<bt:BindableRun BoundText="{Binding Path=User.ScreenName}" />) in
<bt:BindableRun BoundText="{Binding Path=User.Location}" /> says on
<bt:BindableRun BoundText="{Binding Path=CreatedDate}" /> :
</Paragraph>
<Paragraph FontFamily="Arial">
<bt:BindableRun BoundText="{Binding Text}" />
</Paragraph>
</FlowDocument>

And it prints exactly as it should. So, for printing, to recap, I:

  • Made a Template FlowDocument and embedded it as a Resource
    • Had to use a BindableRun
  • Pulled it out of the resuorce, DataBinding, did a weird dispatcher hack, printed it.

Too much Googling on that one. It wasn't nearly as obvious to print as it was to do the Graphical UI.

image

This app, modified, could be used to waste dead trees by printing out tweets that contain words. Mark Nijhof is using TweetSharp to tweet what he blogs. It could also make the computer beep when a sandwich order arrives. Oh! There's an idea!

Tomorrow at lunch, I'll present Tweet Sandwich, the first automated Twitter-based Sandwich Ordering System to @QuiznosNick and hope I earned a free sandwich or ten. ;)

Download the Source from SkyDrive

Please offer feedback, as I'm sure there's lots of ways this can be cleaner. For example, a bound listbox of Replies/Orders (starts to look like a real Twitter Client, then.)

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
April 03, 2009 11:46
Good article. First?
April 03, 2009 12:33
Absolute classic app! i can see subway, pizza hut, dominos all clambering at your door for this! Thank you for the post.
April 03, 2009 12:40
Cool!
April 03, 2009 12:45
IF you get a an order for frank sandwich does it count as dog fooding :)
April 03, 2009 14:55
The link to Mark Nijhof's blog doesn't work (http://blog.fohjin.com/blog)
it should be http://blog.fohjin.com

Great article though, again!
April 03, 2009 15:32
The zipped file seems to be corrupted, anyone else having this problem or is it just me?
April 03, 2009 16:17
Interesting thing about the above is that I'd not have thought of binding data to the document but would probably have taken the order as XML (or at least angle delimited data - though as in this case one would be using in nice objects in memory its probably XML) and generated the printable page using XSLT - would have similar problems though, pulling the template from wherever to combine with data.

Printing does seem far too hard!

One last thought (analyst head on) you probably need two things: first is a must and that is to positively confirm the order back to the sender after printing i.e. I have received and processed your order; second is that I'd probably want to have someone actively ack the order on the computer before printing - though that rather depends on the practicalities of same in your average Quiznos (can't really comment having been in exactly one precisely twice).

Fun though!

Murph
April 03, 2009 16:34
Looks like you've got a typo on the front of the app. Should be:

"When I'm running, I'll check your Twitter stream every 5 minutes..." (note the 'y' is missing in every from your screen shots, and from the source I downloaded a few minutes ago)

(Sorry, OCD got the better of me this time)
April 03, 2009 16:35
Thanks for the great write-up on TweetSharp! One small point in context, is that you don't have to make a JSON call to be able to deserialize your responses into objects, you can call AsStatuses() with the XML response in the first example where you use LINQ to XML.
April 03, 2009 16:58
Scott I am getting redirected to the MSN Live login page whenever I access your blog. Might want to check into it.
April 03, 2009 17:03
Application.LoadComponent(Uri) does the embedded Resource/Page application loading... No need to manually invoke XamlReader.
April 03, 2009 17:13
I look forward to seeing more on printing. I'm looking for something very similar. I have to do certain receipts / tickets at work and the printing solutions seem terrible. Some of the reporting solutions have acceptable designers, but they aren't designed to take objects but instead work closer with ADO.NET (they don't seem to be aimed at programmers). I would love to be able to just pass an object / xml to a document and have it bind / print. Adobe seems to have a product that can take xml and print or make PDFs, but I think its priced in the six figure range :). We actually have in production systems that use the webbrowser control and string.format to print things out.

I did some mild reading about flowdocuments / fixeddocuments / XPS, but I am clueless on WPF.

Teach me Scott, teach me!
April 03, 2009 17:27
Scott,

Technically very cool...
However, it's been my experience that the "excellence" of a Quiznos sub degrades quickly over time (no one likes cold toast, right?).
So I think I'll pass on using it for Quiznos... :)

April 03, 2009 19:47
Thanks for finding the typo, I'll fix ASAP!

Asills! Wow, thanks for Application.LoadComponent(Uri). Not an intuitive name, but I'll try that and change the code.

Seamus - I wonder if that's the SkyDrive embed...I can't repro here?

Huey - Agreed. That's why I may change it to Direct Messages. That would be only people that @QuiznosNick *follows* on Twitter would DM, and hence, could order.
April 03, 2009 19:57
I am suffering from news-about-Twitter overload. It's mentioned in so many blogs, articles, podcasts that I can't escape it. I don't use twitter and I don't follow anyone. I think Twitter is a major source of information overload.
April 03, 2009 19:59
lol hilarious
April 03, 2009 20:33
Typo: "I'll check...ever 5 minutes.
April 04, 2009 0:08
Interesting idea. I was looking to do something with Twitter, although on the sending side.

One suggestion I would make would be to order the statuses by Twitter name then date/time. With only 140 characters for an order I could see ordering for a couple of people requiring multiple tweets. You may get things out of order chronologically, but an individual's order would all be together. With a 5 minute window the timing of chronological versus twitter name order will not be that big.
April 04, 2009 4:28
Wow. This goes down under my "things I really dislike (or nice way of saying hate)" category as Quiznos and Twitter.
April 05, 2009 23:50
Scott, I share your obsession for a nice icon. Nice to see I am not alone. All too often software developers do not appreciate the value and importance of a nice UI.
April 08, 2009 6:34
Y, I agree, a classic demo. And I like the style and the demo, very fashion.
April 08, 2009 8:05
Hello Scott,

For printing you can try PrintForm from VBPowerPack instead of XAML ...
April 10, 2009 0:03
nice. i stole your idea (and a fair bit of your code) and made a Twitter-to-Growl (Towl?) bridge:

http://www.growlforwindows.com/towl.png

tweet# sure made it easy

April 15, 2009 2:48
Yuck, I just flashed back to the creepy Quiznos ads where the oven asks the dude to "stick it in me"...

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.