On November 5th I gave a talk at the Twin Cities .NET User Group entitled ‘Using web service APIs in your applications’. I started out with a very brief overview of how REST APIs work and then talked about the current state of using REST APIs in .NET. I followed that up with a demonstration of a new OSS project I’m working on for making accessing REST APIs much easier. The project is called RestSharp but in the talk I refer to it as Stillwater as that was my working name for it prior to coming up with the final name.
Next I put RestSharp to use by writing a Twitter search bot that runs as an Azure worker role searching for mentions of a search term and then creating FogBugz tasks from the results. Then I demonstrated using Twilio to receive phone calls (utilizing ASP.NET MVC) and create FogBugz cases from those incoming phone calls. And lastly I demonstrated how to use RestSharp with Twilio’s REST API to initiate an outgoing phone call from a .NET app.
This was my first formal technical presentation (I gave one at jQuery Conference, but that was much more informal) and you can tell early on in the video. Stick with it however, things smooth out as they go along. Also, the audio is a little tinny with some minor background noise, but it’s not unbearable.
I’ll be posting more about RestSharp/Stillwater soon as I get closer to launching it. You can follow @RestSharp on Twitter, follow the project on GitHub or visit the official site (nothing there yet though).
Watch the Video (49:25)
Download (right click, save as): MP4 (103MB) | WMV (135MB)
I’m at the jQuery Conference in Cambridge, MA this weekend. Today was day one of two and it was a great start to the conference. Stephen Walther from the ASP.NET team at Microsoft gave a talk on building apps with ASP.NET and jQuery. The talk got very interesting when he mentioned that he was running unreleased bits from a recent internal build. There were two things that were previously unannounced that I found interesting.
The first thing Stephen demo’d was using the jQuery Validation plugin with MVC. It’s been publicized that Microsoft is planning to ship the Validation plug-in similar to how jQuery core is included. What hasn’t been shown publically (to my knowledge) were any code examples. The one that caught my eye the most was this snippet which was placed just under the opening <body> tag in the view (from memory, exact syntax may vary):
<% Html.EnableClientValidation(); %>
After the talk Stephen let me view source on the outputted page. This helper method outputs the JSON structures needed for the validation plugin to work. Interestingly, it outputs them at the end of the page just before the closing body tag. Does this remind you of anything? Reminds me of Webforms. Declare something one place in your code and have it do things somewhere else. The other thing I don’t like about this syntax is the name of the method. I don’t think that’s a very descriptive term for what it’s actually doing. I’m working on limited information here though so it might be doing more than just outputting the JSON data. It’s still only pre-Preview 2 and MVC 1 went through 5 previews and a couple RCs with lots of refinements along the way so it could very well change.
The second interesting thing that was discussed was a change in the way GET requests for JSON are going to be handled. The reasons for this have been covered extensively. If you want to allow JSON to be returned via a GET request, you’re going to have to be more explicit about it before you risk shooting yourself in the foot. While the sentiment behind the decision is good, I’m not a fan of the current way you have to specify it (again, from memory):
public ActionResult GetJson() {
return new JsonResult(data, JavascriptBehavior.AllowGet);
}
What I deduce from this is that by default JsonResult won’t return data if the request is a GET without explicitly allowing it. This change would break existing code since it would need to be updated to allow the GETs. If the default is going to be changed though, why add the parameter? Why not just check to make sure the proper AcceptVerbs attribute values are present? Again it’s early and could change even before the next preview, but it’s a curious change.
From my experience it’s rare when MS demos something at a conference that wasn’t already covered in some other form (usually blog posts) so it was an exciting afternoon to get a little advance preview of the changes coming in MVC 2. Even though these two features aren’t implemented the way I would like, the MVC team has proven that they’re listening to feedback and willing to make changes along the way if better ideas are presented. That sort of openness into the process is reassuring and I’m appreciative of the team’s efforts.
Tomorrow there will be an open spaces meeting for ASP.NET folks that use jQuery. I’m looking forward to sharing some ideas and helping some others out if they have any questions that I can answer. If anything interesting comes out of it, I’ll be sure to pass it on. Also, be sure to follow me on Twitter if you’re interested in my thoughts as the conference goes on tomorrow.
Occasionally I come across a situation where it would be interesting to know if a certain Twitter user is following some other Twitter user. I couldn’t find the right combo of search terms to find such a thing via Google or Bing, so I thought I’d just make one. Thanks to TweetSharp, it was incredibly easy. Here’s the ASP.NET MVC Controller code:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Check() {
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Check(string follower, string target) {
string response = FluentTwitter.CreateRequest()
.Friendships().Verify(follower).IsFriendsWith(target)
.Request();
response = Regex.Replace(response, @"<\/?friends>", "");
bool following = false;
bool.TryParse(response, out following);
return Json(new { result = following });
}
The view is a simple Spark file with some jQuery that posts to the Check() method above and displays the appropriate result based on the JSON returned.
Check it out and let me know what you think!
If you’re familiar with ASP.NET MVC this is probably not news to you. If you’re coming from Webforms, you might find this tip helpful.
One of my favorite things about MVC is how easy it is to build custom ActionResults. I previously wrote about building one for returning RSS feeds and this post expands on that idea a little bit.
When I originally set up the ManagedAssembly.com RSS feeds, I added some caching so that the feed would only be generated every 30 minutes resulting in a snapshot of the current set of popular stories. Unfortunately since I hosted the feeds directly, I have very little useful info about how much they’re being used. FeedBurner (when it works) is much better at that so I wanted to switch the feeds over to that but without changing the URLs so I don’t break any existing subscriptions. I had recently read in a post to Twitter by Scott Watermasyk that Graffiti CMS supports FeedBurner by sniffing the user agent and serving the live feed to FeedBurner but at the same URL, redirects real visitors to FeedBurner. This was the perfect solution to my problem, so here’s how I went about implementing it.
First I added a PermanentRedirectResult class that inherits from ActionResult to handle generating the 301 redirect. The built-in RedirectResult uses Response.Redirect which only is capable of issuing a 302 redirect (until ASP.NET 4.0 is out).
public class PermanentRedirectResult : ActionResult
{
private string _url;
public PermanentRedirectResult(string url) {
_url = url;
}
public override void ExecuteResult(ControllerContext context) {
context.HttpContext.Response.StatusCode = 301;
context.HttpContext.Response.RedirectLocation = _url;
}
}
Then in my ControllerBase class that all my controllers inherit from, I added a helper method to simplify calling the result:
public abstract class ControllerBase : Controller
{
public PermanentRedirectResult PermanentRedirect(string url) {
return new PermanentRedirectResult(url);
}
}
Then in the action method I do a simple check for FeedBurner and if it’s not found, issue the redirect. Otherwise return the live feed.
public ActionResult Popular() {
bool isBot = Request.UserAgent.Contains("FeedBurner");
if (!isBot) {
return PermanentRedirect(Settings.Feed.PopularFeedUrl);
}
// *snip* build and return live feed
}
Nice and straightforward and doesn’t break any existing subscriptions. Now with FeedBurner’s stats I can tell exactly how few of you are subscribing to the feed :)
In Part 1 I showed how the server-side tweet retrieval and caching mechanism works for the .NET Twitter Stream on Managed Assembly. In this post, I’ll show you how the UI pulls the items and updates the list live.
Twitter streams tend to show the newest items at the top of the page which I don’t think is ideal for reading a live stream of tweets. I decided to build an interface that showed the newest tweets at the bottom of a list so that it would read more like a chat window. I wanted the list to automatically scroll to keep up with the new items, but if you scrolled up to start reading older items autoscoll would be disabled so you wouldn’t be constantly losing your place.
When a visitor hits the page a call is made to the /Refresh method described in Part 1. On the first request the last 100 cached tweets on the server are returned along with the ID of the most recent tweet, which is stored in a hidden field. On subsequent requests the value from the hidden field is included in the request to /Refresh so that only new tweets are returned and appended to the list. This minimizes the amount of traffic sent back and forth and the amount of work needed to append items to the list (just loop through the returned items and append the generated HTML).
Let’s start with the HTML needed:
<div id="statusContainer">
<table id="statuses" style="table-layout: fixed; overflow: hidden;">
</table>
</div>
<p>
Updates automatically every 60 seconds.
Auto scroll is
<span id="autoScrollOn">ON</span>
<span id="autoScrollOff" style="display: none;">OFF</span>
</p>
<%=Html.Hidden("lid", "") %>
There’s a couple inline styles but bare with me. There’s a certain amount of “get-it-done”-ness to the code (particularly the HTML generation later) but it’s not important, this is just an example that happens to be running great live :-)
There’s a lot of CSS involved, but the important style definitions to make the autoscrolling work are as follows:
#autoScrollOff { color: #f00; }
#autoScrollOn { color: #0f0; }
#statusContainer
{
height: 525px;
overflow: auto;
border: 1px solid #ccc;
margin: 5px 0;
}
#statuses
{
width: 100%;
border: none;
border-collapse: collapse;
}
There are two functions that make it happen. The first calls for the JSON and schedules itself to be called again in 60 seconds:
function getUpdates() {
$.getJSON('<%=Url.Action("Refresh")%>', { lid: $("#lid").val() }, refreshList);
window.setTimeout(getUpdates, 60000);
}
The other function is called when the JSON is retrieved (again, this should use client-side templating or something better than just string concats, but it doesn’t so get over it):
function refreshList(json) {
if (json.max_id != 0)
$("#lid").val(json.max_id);
$.each(json.results.reverse(), function(i, result) {
var html = "<tr class=\"status new\"><td class=\"avatar\"><a target=\"_blank\" href=\"http://twitter.com/" + result.UserName + "\"><img height=\"48\" width=\"48\" src=\"" + result.ProfileImageUrl + "\" align=\"left\" /></a></td>";
html += "<td><p><span class=\"user\"><a target=\"_blank\" href=\"http://twitter.com/" + result.UserName + "\">" + result.UserName + "</a></span> ";
html += result.Text + "</p><p class=\"meta\">" + result.RelativeTime + " from " + result.ClientLink;
if (result.IsReply) {
html += " <a href=\"http://twitter.com/" + result.InReplyToUser + "/statuses/" + result.InReplyToId + "\">in reply to " + result.InReplyToUser + "</a>"
}
html += "</p></td></tr>";
$("#statuses").append(html);
});
if (autoScroll) $("#statusContainer").scrollTo(99999);
}
We store the max ID returned to use for the next call. The tweet list is reversed to put new ones at the bottom (this could be handled server-side too), the HTML is built and appended to the table. At the end of the function we check to see if autoScroll is set and if so, scroll to a big number using the jQuery ScrollTo plugin.
To start things in motion, we call the getUpdates() function in document.ready() and attach the handlers to manage autoScroll state:
var autoScroll = true;
$(function() {
getUpdates();
$("#statusContainer").scroll(function() {
autoScroll = this.scrollTop + this.clientHeight == this.scrollHeight;
if (autoScroll) {
$("#autoScrollOn").show();
$("#autoScrollOff").hide();
}
else {
$("#autoScrollOff").show();
$("#autoScrollOn").hide();
}
});
});
autoScroll is defined outside of document.ready() so that’s available in all the calls to getUpdates().
There’s a similar method for retrieving the list of users currently being followed for the stream. Since the time I originally wrote this code I’ve started reading JavaScript: The Good Parts by Douglas Crockford so I’m a little ashamed of the JavaScript, but it does work well and that’s all I need for now.
Back when I launched ManagedAssembly.com around MIX I put together a page to show real-time Twitter search results for ‘MIX09’. On the heels of the dvplrs.com launch this week and the lack of .NET representation, I decided to update my live Twitter page to show live updates of notable people in the .NET community. This is how I built it. You can see the finished product over at ManagedAssembly.com/Twitter.
Since I decided to use the standard Twitter API instead of the Search API, I had to build a server-side cache of updates so that I wasn’t hitting the API on every view of the page which would quickly exhaust the API rate limits. Using a database was overkill so I decided to use ASP.NET’s built-in cache mechanism to store new updates. When the client makes a request for the latest updates, the items will be served out of the cache. The cache is updated every so often with new items.
The workflow when the client makes a request for updates (via jQuery) is like so:
- Grab the date of the last cache update from the cache. If the value doesn’t exist or it’s older than the cache expiration threshold, it’s time to get more statuses from Twitter.
- When new updates are pulled from Twitter, store the highest ID in the cache as well so that for future requests we can get only the new tweets.
- Pull the currently cached items and add the new items from the Twitter API call to it.
- When updates are sent to the browser, the client-side code stores the latest ID. For future requests this ID is passed back to the server which returns items from the cached list that are greater than the locally-stored ID.
What this gives us is the ability to only call Twitter once a minute no matter how many times the site is hit. It also allows for each visitor to only get back tweets that are new to them (which is different for every visitor depending on when the first hit the page) whenever they make a request. Since the tweets are being pulled from memory on almost every request there’s no delay while the API call is being made. If the application restarts and we lose the cache it won’t really matter because we just grab all the updates it will send and repopulate the cache. It also means that the amount of data being sent to the client via JSON is the bare minimum needed. That makes updating the UI simple (just append the new ones without having to process the full list) and keeps the bandwidth requirements low.
That was a lot of words, so time for some code. We start by determining if we need to get new items from Twitter and if so, grabbing them. This uses the CacheManager from a previous post.
CacheManager cache = new CacheManager(new ShortTermProvider());
IEnumerable<TwitterStatus> statuses = new List<TwitterStatus>();
long maxId = 0;
DateTime lastFetch = cache.Get<DateTime>("last_fetch");
if (lastFetch < DateTime.Now.AddMinutes(-1)) {
long lastCacheId = cache.Get<long>("last_id");
if (lastCacheId == 0)
lastCacheId = 1; // TweetSharp/twitter returns null if you use Since(0)
var request = FluentTwitter.CreateRequest().AuthenticateAs("UserGoesHere", "passwordgoeshere");
request.Configuration.UseGzipCompression();
request.Statuses().OnFriendsTimeline().Since(lastCacheId).AsJson();
statuses = request.Request().AsStatuses();
}
The first time this is hit lastFetch doesn’t exist in cache and the cache manager returns the default for DateTime (DateTime.MinValue) which I’m pretty sure is always more than a minute ago. We’ll update the fetch time in cache later. Once we’ve determined that the cache is empty or expired, we grab the last_id value from cache (which is 0 if it doesn’t exist). We then use TweetSharp to get the latest updates and store them in a list.
We also make sure that statuses isn’t null because if you’ve exceed the Twitter API rate limit, TweetSharp returns a null collection. Once we have the updates we have to process them since the text doesn’t include links for @mentions, #hashtags or links. When deciding how to approach this part I contacted Jon Galloway and he pointed me to the code in Witty which handles creating links, which I borrowed heavily from (thanks Jon!). MA also uses SubSonic 2.2 for the DAL and I used one of the methods provided in that library for determining if a word is a URL.
if (statuses != null) {
foreach (var status in statuses) {
string raw = status.Text;
string[] words = Regex.Split(raw, @"([ \(\)\{\}\[\]])");
StringBuilder output = new StringBuilder();
foreach (string word in words) {
if (word.StartsWith("#")) {
string hashtag = String.Empty;
Match foundHashtag = Regex.Match(word, @"#(\w+)(?<suffix>.*)");
if (foundHashtag.Success) {
hashtag = foundHashtag.Groups[1].Captures[0].Value;
output.Append(string.Format(@"#<a href=""http://search.twitter.com/search?q=%23{0}"" target=""_blank"">{0}</a>", hashtag));
}
}
else if (word.StartsWith("@")) {
string userName = String.Empty;
Match foundUserName = Regex.Match(word, @"@(\w+)(?<suffix>.*)");
if (foundUserName.Success) {
userName = foundUserName.Groups[1].Captures[0].Value;
output.Append(string.Format(@"@<a href=""http://twitter.com/{0}"" target=""_blank"">{0}</a>", userName));
}
}
else if (SubSonic.Sugar.Validation.IsURL(word)) {
output.Append(string.Format(@"<a href=""{0}"" target=""_blank"">{0}</a>", word));
}
else {
output.Append(word);
}
}
status.Text = output.ToString();
}
cache.Store("last_fetch", DateTime.Now);
}
OK now that we’ve got the latest updates from the API and added the links, it’s time to store them in the cache.
var cachedStatuses = cache.Get<List<TwitterStatus>>("cachedStatuses");
if (cachedStatuses == null) {
cachedStatuses = new List<TwitterStatus>();
}
if (statuses != null) {
cachedStatuses.AddRange(statuses);
}
if (cachedStatuses.Count > 0) {
maxId = cachedStatuses.Max(s => s.Id);
cache.Store("last_id", maxId);
}
cache.Store("cachedStatuses", cachedStatuses);
Last but not least we build a JSON return value for all the tweets in the cache with an ID higher than the one sent by the client (lid is a parameter passed from the client).
long lastClientId = lid ?? 1;
var data = from s in cachedStatuses
where s.Id > lastClientId
orderby s.Id descending
select new {
Id = s.Id,
ProfileImageUrl = s.User.ProfileImageUrl,
Text = s.Text,
Source = s.Source,
UserName = s.User.ScreenName,
RelativeTime = s.CreatedDate.ToRelativeTime(false),
ClientLink = s.Source,
IsReply = s.InReplyToStatusId != 0,
InReplyToId = s.InReplyToStatusId,
InReplyToUser = s.InReplyToScreenName
};
return Json(new { results = data.Take(100), max_id = maxId });
In Part 2 I’ll cover building the UI.
In Part 1, I demonstrated getting started with Twilio and ASP.NET MVC. In this post, I’ll go over the remaining controller action implementations.
So far we’ve got up to the point where we play the current greeting for a caller. Coaches and umpires use the hotline to report any issues the League Director needs to be made aware of. In Part 1 we handle generating the response needed to play the greeting and then beep to let the caller know they can leave a message. When a message is left by a caller, a POST request is made to our /RecordVoicemail action:
public ActionResult RecordVoicemail(string CallGuid, string RecordingUrl, string Caller) {
try {
var msg = new MailMessage();
msg.To.Add(Settings.VoicemailEmailToAddress);
msg.From = new MailAddress(Settings.VoicemailEmailFromAddress);
msg.Subject = "New voicemail received from " + Caller;
var client = new WebClient();
string filename = string.Format("{0}-{1}.wav", Caller, DateTime.Now.ToString("MMddyyyhhmmss"));
msg.Attachments.Add(new Attachment(client.OpenRead(RecordingUrl), filename));
msg.Body = "Received " + DateTime.Now;
var smtp = new SmtpClient();
smtp.Send(msg);
}
catch (SmtpException ex) {
Log(CallGuid, "Could not send voicemail notification email: " + ex.Message);
}
return new EmptyResult();
}
This method downloads the message from Twilio’s servers, attaches it to an email and sends it off to the Director. He can listen to these messages on his BlackBerry and take care of any issues reported.
If there’s a problem we log the issue to a file. The other methods have logging as well, but I’ve excluded them from my examples for brevity.
Lastly, the League Director needs to be able to record a new greeting from his phone in case he’s out at the fields and the weather forces a cancelation. While the greeting is being played, the Director can enter the PIN followed by # to record a new greeting. Once the PIN has been entered, we send the required Twilio response to prompt for the recording:
public ActionResult Greeting(string CallGuid, string Digits) {
var doc = new XDocument();
var response = new XElement("Response");
if (Digits != Settings.PIN) {
response.Add(Verb("Say", "Invalid pin number. Please try again."));
response.Add(Verb("Gather", "",
new { action = ActionUrl.Greeting,
method = "POST",
finishOnKey = "#"
}));
}
else {
response.Add(Verb("Say", "Record your greeting after the tone. Hang up to save the greeting or press a key to start over."));
response.Add(Verb("Record", "",
new { maxLength = 120,
action = ActionUrl.RecordGreeting,
method = "POST"
}));
}
doc.Add(response);
return new XmlResult(doc);
}
When the PIN has been entered, a POST request is sent to this method. We use parameter binding again to get the digits entered. These are checked against the settings file. If the PIN is invalid, we notify the caller and ask for the PIN again. This will loop until the right PIN is entered (or the caller hangs up).
Once the correct PIN is entered, we return some short instructions and set up the Record verb. Once the new greeting is recorded, a POST is sent to our RecordGreeting controller action:
public ActionResult RecordGreeting(string CallGuid, string RecordingUrl, string Digits) {
if (Digits != "hangup") {
var doc = new XDocument();
var response = new XElement("Response");
response.Add(Verb("Say", "Record your greeting after the tone. Hang up to save the greeting or press a key to start over."));
response.Add(Verb("Record", "",
new { maxLength = 120,
action = ActionUrl.RecordGreeting,
method = "POST"
}));
doc.Add(response);
return new XmlResult(doc);
}
Settings.GreetingUrl = RecordingUrl;
return new EmptyResult();
}
Because it can sometimes take a few tries to record the greeting properly, pressing a number during recording will restart the recording. Once the Director is satisfied with the updated greeting, he hangs up and the new greeting URL is saved to the settings.
My first implementation of the recording restart was to redirect back to /Greeting and including the correct PIN as a query string parameter which was bound to the Digits method parameter. While this worked, I didn’t like passing the PIN in a request so I just rebuild the response XML and return it instead.
That’s all there is too it. Twilio offers a lot more options including a complete REST API for accessing recordings, provisioning phone numbers, etc. So far I’m very impressed with the service and didn’t run into any major hangups (ba-dum-dum).
Download the Sample MVC Project
In an effort to give myself more things to write about I’m going to start writing about code I’ve written recently.
I’ve been interested in writing something using Twilio since it became available late last year. Twilio is a service that provides an API for building telephony applications. I’ve had a lot of ideas over the years for integrating phone-related features into applications and Twilio makes it really easy.
I used to run a softball league and the past few years I was using GrandCentral (now Google Voice) for a weather hotline. On days when the weather wasn’t conducive to playing softball the players and coaches would call into the hotline to see if we were going to play or not. I no longer run the league and didn’t want to donate my precious Google Voice number to the league so I started looking for an alternative. Most of the services out there are too expensive if you don’t use them a lot and there’s only a couple weeks a year when the league needs the hotline.
If you’re not familiar with how Twilio works be sure to read up on it. In short, when your number gets an incoming call, the Twilio service makes a request to a specified URL, to which your app responds with a set of commands in XML.
The hotline has a few simple requirements:
- When a call is received, play a greeting. This can either be text-to-speech (if there’s no recorded greeting) or the currently recorded greeting.
- After the greeting is played, allow callers to leave a message (coaches use this to report issues to the league) which is emailed to the League Director.
- Allow the League Director to record a new greeting by calling the number and entering a secret PIN.
I created a new MVC site, removed all the default cruft and created a simple route (/{action}) to allow me to use the following URLs:
- / (root) – Entry point for every incoming phone call.
- /Greeting – Invoked when the PIN is entered and prompts caller to record new greeting
- /RecordGreeting – Handles the completion of the recording. If a digit is pressed while recording a new greeting, it restarts recording. If a hang up is detected, it saves the URL of the greeting audio file to the settings file.
- /RecordVoicemail – After a voicemail is left, this action method handles downloading it from the URL Twilio provides and emails the .mp3 recording to the League Director.
Because we’ll be returning a lot of XML, I created an XmlResult to take an XDocument and output it:
public class XmlResult : ActionResult
{
private XDocument _doc;
public XmlResult(XDocument doc) {
_doc = doc;
}
public override void ExecuteResult(ControllerContext context) {
context.HttpContext.Response.ContentType = "text/xml";
_doc.Save(context.HttpContext.Response.Output);
}
}
I also have a Settings class for managing the application settings:
public class Settings
{
private static XDocument doc =
XDocument.Load(HttpContext.Current.Server.MapPath("~/App_Data/settings.xml"));
private static string Get(string key) {
return doc.Descendants(key).FirstOrDefault().Value;
}
private static void Set(string key, string value) {
doc.Root.SetElementValue(key, value);
doc.Save(HttpContext.Current.Server.MapPath("~/App_Data/settings.xml"));
}
public static string GreetingUrl {
get {
return Get("greetingUrl");
}
set {
Set("greetingUrl", value);
}
}
public static string PIN {
get {
return Get("pin");
}
}
public static string VoicemailEmailFromAddress {
get {
return Get("voicemailEmailFromAddress");
}
}
public static string VoicemailEmailToAddress {
get {
return Get("voicemailEmailToAddress");
}
}
}
The settings file looks like so (obviously this isn’t the most secure solution in the world, but this is only an example):
<?xml version="1.0" encoding="utf-8" ?>
<settings>
<greetingUrl></greetingUrl>
<pin>1234</pin>
<voicemailEmailToAddress>test@example.com</voicemailEmailToAddress>
<voicemailEmailFromAddress>voicemail@example.com</voicemailEmailFromAddress>
</settings>
We’re ready to start implementing our action methods. I’ll start with the first one hit when a call is received.
public ActionResult Index(string CallGuid, string Caller, string CallStatus) {
var doc = new XDocument();
var response = new XElement("Response");
var gather = Verb("Gather", "", new { action = ActionUrl.Greeting,
method = "POST",
finishOnKey = "#" });
// say current greeting
if (string.IsNullOrEmpty(Settings.GreetingUrl)) {
gather.Add(Verb("Say", "Thank you for calling the league hot line. Please leave a message."));
}
else {
gather.Add(Verb("Play", Settings.GreetingUrl));
}
response.Add(gather);
response.Add(Verb("Record", "", new { maxLength = 120,
action = ActionUrl.RecordVoicemail,
method = "POST" }));
doc.Add(response);
return new XmlResult(doc);
}
This method builds the required Twilio response XML. The first thing we add is a Gather verb which listens for digits being pressed. When the # key is pressed, a POST request is sent off to /Greeting (generated using the ActionUrl helper class I wrote to make sure the complete URL is returned). To keep listening for input while the greeting is being played, we nest the Say or Play verb inside the Gather verb. Lastly a Record verb is added which lets the caller leave a message which is posted to /RecordVoicemail.
Twilio passes some standard parameters (like CallGuid, Caller and CallStatus) with every request. ASP.NET MVC makes it really easy to get these values using parameter binding.
I’ve written a helper method that makes it easy to generate XElements in the proper form. The only thing it doesn’t really support very well is nesting, but it’s easy enough to get around that and you could easily add params parameter to accept an array of child elements.
private XElement Verb(string verb, string value) {
return Verb(verb, value, null);
}
private XElement Verb(string verb, string value, object paramObject) {
var element = new XElement(verb, value);
foreach (var item in paramObject.ToDictionary()) {
element.Add(new XAttribute(item.Key, item.Value));
}
return element;
}
The last parameter lets you pass in an anonymous object to set attributes on the XElement, similar to ASP.NET MVC’s use of anonymous objects. ToDictionary() is an extension method which has been posted in numerous places.
Since this is getting long, I think I’ll stop here and do a Part 2 with the remaining method implementations.
I had a hunch I’d be too tired to write up a day-by-day recap of my first MIX conference which turned out to be exactly right. You’ve probably heard by now all the stuff that was announced, so I’ll try to keep this an opinion piece instead of a news article.
The Good
Prior to attending I had read that the most valuable part of conferences like MIX was not the presentations, but the people. This was absolutely and completely true. There’s no substitute for having conversations with brilliant people and there were a ton of brilliant people at the conference. I had good conversations with Phil Haack, Jeff Atwood and the Stack Overflow devs Jarrod and Geoff, Joel Spolsky (possibly the highlight of my career), Rob Conery, Jon Galloway, Scott Hanselman, Dave Ward from Encosia, Justin Etheredge, some guys from Woot and many more. Learning the how and why of how they do things is educational and inspiring.
Thankfully, Microsoft has realized the importance of these types of interactions and does a great job of facilitating them. They set up a room called “3rd Place” with couches, snacks, free wifi and power strips. It was a great setup and worth the price of admission alone. If you’re at MIX next year and none of the sessions at any given time really inspire you, go hang out at 3rd Place and strike up conversations instead. You can always watch the session video later.
One conversation I’m particularly enthused about is the meeting I had with Rob Conery of SubSonic fame. SubSonic has gone a little stale and needs some freshening up. We discussed some issues getting in the way of progress and some ideas for going forward. I’m going to try to get it kick started again, which I will blog about when that happens. I love SubSonic and am excited to start contributing to it’s future.
I also showed off Managed Assembly when I had a chance and got some good feedback on the site both design-wise and vision-wise. Now that it’s up and running, we’ll see how it goes for a little bit before I make any significant changes.
MIX also does a good job of keeping a good balance of content for designers and developers. I think all devs should have some basic understanding of good design principles and MIX does a good job of exposing that without overdoing it for devs. Bill Buxton and Deborah Adler (she designed the new Target prescription packaging) gave insightful keynotes that had some good design insights for both designers and developers.
The Not-As-Good
One reason 3rd Place was more appealing to me than the sessions is a matter of timing. ASP.NET MVC went final, but there was very little new content that wasn’t already covered at PDC or in blog posts since. Rob Conery’s session stood out because it was the only one that had more stuff in it that I hadn’t seen than stuff that I had seen. It was great. The video has been posted so you should watch it. Almost everything else that was new was tied to Silverlight. Which brings me to…
The ‘meh’
Silverlight 3. Meh. It’s nice. It does cool stuff. It now works out of the browser, which has huge potential. Adobe AIR now has some serious competition for building Twitter clients (ba-dum-dum). I did learn some things about Silverlight I didn’t know (like support for threading), but overall, I just don’t care that much about it yet.
The Bad
IE8. OK, IE8 isn’t so bad, but until IE6 is dead, it’s completely irrelevant. Slices? Yawn. Accelerators? Yawn. I don’t want to write browser-specific code anymore. The IE intro video was pretty funny though.
Was it worth the trip?
Absolutely. If you’re a MS web dev or a designer that works with them, you should be at MIX. You’ll learn a lot from other people and the sessions. You’ll have a chance to talk directly to the people who build the tools that are the basis for your profession and give them feedback and ideas from the real world. You’ll have fun because it’s in Vegas. Try to make it next year. You won’t regret it.
My two favorite programming-related sites right now are StackOverflow and Hacker News. StackOverflow suffers from a lot of “off-topic” questions that are really fit for a discussion group more than a question/answer site. Hacker News is a discussion and social news site for hackers and startups (with a programming bent) hosted on an extremely well-built web app. While the app itself is nice, the community there is even better.
With a site like Hacker News, the value is the community. They act as a filter. I rarely visit a link that comes across the HN front-page news feed without first reading the comments to see if its worth my time. There’s also value in building a place where you can converse with the same group of people for each new topic that comes up. This sounds obvious, but a common question that comes up on HN is “Does Hacker News steal the discussion from the original site/blog/etc that was submitted?” I don’t see a discussion on Hacker News with people who’s viewpoints I’ve become familiar with the same as a discussion with random people in blog comments. I think this is a big part of what keeps people coming back to HN: quality discussion with people you share a collective context with.
Hacker News also succeeds because of the moderation policies, which I won’t cover in any detail since Atwood did plenty of that recently.
Another site I frequent is DotNetKicks.com to get the latest .NET news. Unfortunately, DNK was not built around facilitating discussions which is apparent from the extremely low number of contents posted there. The ‘Shoutbox’ shows hints of “life” (in a discussion sense), but in recent times it’s gone nearly dormant (possible defection to DotNetShoutOut?).
DNK also suffers from an antiquated ranking system that was borrowed from Digg. Stories need a set number of votes to be published to the home page and there they sit until the next story with enough votes knocks them down a notch and eventually off the home page. It’s really easy to game (although, most social news sites are) and there’s not a culture of moderation that can handle the gaming.
I wondered how I could combine the good parts from these three sites into a site targeted to passionate .NET devs where they could get the latest information, take part in quality discussion and be interesting enough to keep them coming back. Building it myself sounded fun, and so ManagedAssembly.com was born.
Managed Assembly was borne out of my desire to combine the best of the aforementioned sites into a site I would actually want to use every day. It borrows heavily from these sites in the following ways:
Hacker News
It won’t take you long to see the inspiration of Managed Assembly in Hacker News. I started with their UI, jQuery-ed it up a little bit, changed some colors, made some other minor changes and off we go. I did this because I think Paul Graham built Hacker News for the right reasons, to build a community for the sake of the community, not for something else. DNK and other link sharing sites fail at building quality communities because they’re designed to generate page views and ad revenue. Paul took his own advice and built HN the way you would build a social news/discussion site if you were doing it for charity. There are no ads. There’s no frills. It just works.
There will be no ads for registered users on Managed Assembly as long as I can sustain it without it. Freeloaders (those not contributing to the community) and RSS feeds may get ads, but not for awhile.
The other big way MA is borrowing from HN is philosophically. HN believes in the ‘Broken Windows Theory’. HN kills off-topic posts with no remorse. Submission titles are cleaned up. Basically, they keep the house in order. Managed Assembly will do the same.
Stack Overflow
While the rep system on Managed Assembly works pretty closely to how Hacker News does, I want it to be more like the Stack Overflow system when it grows up. Users with more rep will get more capabilities. This has not been hashed out yet.
What I’d really like to borrow from Stack Overflow is the user base. I’ve got some ideas in this area, so stay tuned. We’re also going OpenID-only, following SO’s lead.
DotNetSomethingOrOtherLinkSharingSite.com
The only thing we’re copying from DNK and DNSO(?) is the types of links I want to see submitted: anything interesting to the intermediate-to-advanced .NET developer. This includes general-interest programming subjects. Topics that are too basic will be killed at the discretion of the mod(s).
New Stuff
The site isn’t completely a rip off of the others mentioned. I want to add a live aspect to it for events like MIX and PDC. My first attempt at this is a live Twitter tracker for MIX09 content. I wanted to implement a chat so you could have a place to go to discuss the keynotes as they happen, but I ran out of time before MIX. Chat in some form will be integrated in the future.
I also want to integrate Twitter, both into the Live section and other parts of the site. I find Twitter invaluable for keeping up with the latest .NET happenings as they happen, and that gels nicely with the MA MO.
There Will Be Bugs
The site was made by a single developer (me) in ASP.NET MVC. Since I’ve been putting this together in my spare time, I haven’t had the chance to stress test it the way I would like, so you will find bugs. Email me and let me know about them. I’ve also set up UserVoice for handling feature requests.
Check it out, let me know what you think and let’s build a community.