The Weekly Source Code 40 - TweetSharp and Introducing Tweet Sandwich
I 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.
What Settings Do I Need to Save
OK. Now, I put the Settings in the Properties dialog for the project.
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.
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.
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.
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.
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.
About Newsletter
it should be http://blog.fohjin.com
Great article though, again!
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
"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)
I did some mild reading about flowdocuments / fixeddocuments / XPS, but I am clueless on WPF.
Teach me Scott, teach me!
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... :)
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.
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.
For printing you can try PrintForm from VBPowerPack instead of XAML ...
http://www.growlforwindows.com/towl.png
tweet# sure made it easy
Comments are closed.