A friend of mine introduced me to Twitter recently. To be honest, at first I really didn't see the point in using it.

I decided to have a look at what other people were using it for and it dawned on me that Twitter is actually a really cool, simple way to provide updates to anyone who's interested in what you do.

After signing up for an account, I noticed that the site provides JavaScript that allows you to add your twitter updates to your blog. I added this to the footer section of my site master file and reloaded the page. Everything worked fine.

If you're using Twitter you might have noticed that over the last few days the site was down at several points. If you are using the Twitter JavaScript code to pull down updates directly to your blog while Twitter is down, your blog will appear to be at least partially down also.

I wanted to avoid this problem. This sounds like a job for a custom web control.

Options

Before diving in, I wanted to weigh up the pros and cons of using a web control to add Twitter updates to my blog. Below are the pros and cons with each approach as I see it.

Twitter JavaScript Approach.

Pros.

  • Since the JavaScript will be processed on the client browser, the request for Twitter updates wont be routed through your host, saving you bandwidth.
  • It's very easy to add the JavaScript to your site.

Cons.

  • If Twitter.com experiences problems, so do you. This means you've just added an extra source of potential problems to your site that is entirely out of your control.
  • You can't control or filter the content sent to the client without editing the JavaScript.

Custom Twitter Control Approach.

Pros.

  • You have complete control over the content sent to your site. For example, you can add a custom profanity filter.
  • Most importantly, you can store and cache the content received from Twitter.com. If Twitter.com is down, it's not a problem, just output the last updates made to the cache, or the store. You've just removed an external source of problems and you've taken control of the content on your site.
  • You don't have to make a request to Twitter.com for every request to your site. For example, you can set up the control to only issue 24 requests per day if you want. This means your site is at most an hour behind your twitter updates, but it's not eating into your bandwidth too much. In fact, this may also help Twitter.com deal with the scaling issues that are causing them problems at the moment.

Cons.

  • Performing a request to Twitter.com for every request made to your blog costs you additional bandwidth and (for a popular blog) means you're leaning hard on the servers at Twitter.com. For my site that's probably not a big deal. I'm new in town and no one knows me yet. Regardless, as I mentioned above, there are ways around this when using a custom web control so this is really a very small con.

After reviewing the points above I decided to go with the custom Twitter control approach. The con regarding bandwidth can easily be dealt with. The pros speak for themselves.

Implementation

I noticed a BlogEngine.NET custom Twitter control up on CodePlex. After looking at it I decided to proceed with my own control. The CodePlex Twitter control issues a request every time it is rendered, so in effect it provides no benefit over the JavaScript approach. I wanted more control over how the content is retrieved, stored, cached and updated.

The custom twitter control I implemented is pretty simple. It exposes the following properties.

  • Username, the username of the Twitter account.
  • Password, the password of the Twitter account.
  • CacheFlag, a flag to determine if content is stored and accessed from the ASP.NET cache.
  • CacheLifetime, the duration to cache updates for before Twitter is asked for more updates.
  • StoreFlag, a flag to determine if content is stored on file once it has been received from Twitter. This ultimately helps avoid Twitter.com downtime problems. Currently, an XML file is used for storage, but this can easily be replaced with database storage.
  • StoreFilename, the path to the XML used to store updates from Twitter.com. The default is twitter.xml, and it will be stored in the default BlogEngine.NET data folder (~/App_Data).
  • TimeOutInSeconds, the Twitter request time out. The default is 4 seconds. After that the control gives up and tries to get updates from the store.

By storing the updates from Twitter.com, we always have something to show on our blog pages. It doesn't matter if Twitter.com goes down briefly or if the ASP.NET cache has been cleared. By caching the content read from our storage, we avoid having to hit the XML update file (or a database) for each request made to the blog site.

Currently, the code only supports storage in an XML file. It would be easy to update the code to include a StoreConnectionString property and store updates in a database instead. It would also be easy to update it to expose links within the Twitter updates, as Scott Pio has done with the CodePlex Twitter control.

The formatting code is based on the JavaScript code provided by Twitter.com. For more information on the Twitter API, visit the Twitter API page.

Usage

This custom web control was written for BlogEngine.NET, but it only relies on BlogEngine.NET for the default data folder. You can add it to any ASP.NET 2.0 site with some minor modifications.

To add it to BlogEngine.NET, copy the source file to (~/AppCode/Controls/Twitter.cs). Then, add the control to your markup as follows:

<blog:Twitter ID="Twitter" runat="server" Username="YourTwitterUsername" Password="YourTwitterPassword" />

The code has only been briefly tested, so use it at your own risk. If you use it, modify it, enhance it or find it useful (or find bugs in it), please let me know.

I'll make any updates or fixes available here. Below is the source code for the custom Twitter web control.

[Update: The day after I uploaded this article, I found Mads Kristensen's Twitter Widget. It requires the latest build of BlogEngine.NET, because it sits on top of a new widget framework. It uses caching, but not storage as far as I can see. Regardless, you should definitely check it out.]

/*
    File:           Twitter.cs 
    About:          Custom Twitter Web Control.
    Written by:     Aidan Doolan (www.aidandoolan.com).
    Comments:       Needs to be tested fully. Use at your own risk.
    Updated:        28/05/2008, added a request timeout parameter.
*/
using System;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Diagnostics;
using System.Web.Caching;
using System.Xml.XPath;
using System.Text;
using System.Collections.Generic;
using System.Net;
using System.Xml;
using BlogEngine.Core;
 
namespace Controls
{
    public enum TwitterUpdateType
    {
        User,
        Friends,
        Public,
    }
 
    public class Twitter : Control
    {
        public const string TwitterCacheKey = "Controls.Twitter.Updates";
 
        private XmlDocument _twitterXmlDoc;
        private string _username;
        private string _password;
        private bool _cacheFlag;
        private int _cacheLifeTimeInSeconds;
        private bool _storeFlag;
        private string _storeFilename;
        private int _numberOfUpdates;
        private int _timeOutInSeconds;
        private TwitterUpdateType _updateType;
        public string Username { get { return _username; } set { _username = value; } }
        public string Password { get { return _password; } set { _password = value; } }
        public bool CacheFlag { get { return _cacheFlag; } set { _cacheFlag = value; } }
        public int CacheLifeTimeInSeconds { get { return _cacheLifeTimeInSeconds; } set { _cacheLifeTimeInSeconds = value; } }
        public bool StoreFlag { get { return _storeFlag; } set { _storeFlag = value; } }
        public string StoreFilename { get { return _storeFilename; } set { _storeFilename = value; } }
        public int NumberOfUpdates { get { return _numberOfUpdates; } set { _numberOfUpdates = value; } }
        public TwitterUpdateType UpdateType { get { return _updateType; } set { _updateType = value; } }
        public int TimeOutInSeconds { get { return _timeOutInSeconds; } set { _timeOutInSeconds = value; } }
 
        private string RequestUrl
        {
            get
            {
                return string.Format(@"http://twitter.com/statuses/{0}_timeline.xml?count={1}",
                    Enum.GetName(typeof(TwitterUpdateType), _updateType).ToLower(), _numberOfUpdates);
            }
        }
 
        public Twitter()
        {
            Username = String.Empty;
            Password = String.Empty;
            CacheFlag = true;
            CacheLifeTimeInSeconds = 3600;
            StoreFlag = true;
            StoreFilename = "twitter.xml";
            NumberOfUpdates = 5;
            TimeOutInSeconds = 4;
            UpdateType = TwitterUpdateType.User;
 
            _twitterXmlDoc = new XmlDocument();
        }
 
        public override void RenderControl(HtmlTextWriter writer)
        {
            try
            {
                Validate(_username != String.Empty, "Username must be set.");
                Validate(_password != String.Empty, "Password must be set.");
                Validate(_cacheFlag ? _cacheLifeTimeInSeconds > 0 : true, "Cache life Time must be greater than 0.");
                Validate(_storeFlag ? _storeFilename != String.Empty : true, "Store File name must be set.");
                Validate(_numberOfUpdates > 0 && _numberOfUpdates <= 20,
                    "Number of updates must be greater than 0 and less than or equal to 20.");
                Validate(_timeOutInSeconds > 0, "Twitter request time out must be greater than 0.");
 
                GetTwitterUpdates();
 
                writer.Write(RenderMarkup());
            }
            catch (Exception ex)
            {
                HttpContext.Current.Trace.Write("Twitter Control", ex.Message);
                writer.Write("Service unavailable. Please try again later.");
            }
        }
 
        protected static void Validate(bool assertion, string errorMessage)
        {
            if (!assertion)
            {
                throw new ApplicationException(errorMessage);
            }
        }
 
        public string RenderMarkup()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("<ul>");
 
            XmlNodeList xmlStatusList = _twitterXmlDoc.SelectNodes("statuses/status");
            foreach (XmlNode xmlStatus in xmlStatusList)
            {
                sb.Append(String.Format(    "<li><span>{0}</span> <a style=\"font-size:85%\"" +
                                            "href=\"http://twitter.com/{1}/statuses/{2}\">{3}</a></li>",
                                            xmlStatus["text"].InnerText, _username, xmlStatus["id"].InnerText, 
                                            RelativeTime(xmlStatus["created_at"].InnerText)));
            }
 
            sb.Append("</ul>");
            return sb.ToString();
        }
 
        private void GetTwitterUpdates()
        {
            if (_cacheFlag)
            {
                if (HttpContext.Current.Cache[TwitterCacheKey] == null)
                {
                    RequestTwitterUpdates();
                    HttpContext.Current.Cache.Insert(TwitterCacheKey, _twitterXmlDoc, null,
                        DateTime.UtcNow + new TimeSpan(0, 0, _cacheLifeTimeInSeconds), Cache.NoSlidingExpiration);
                }
                else
                {
                    _twitterXmlDoc = HttpContext.Current.Cache[TwitterCacheKey] as XmlDocument;
                }
            }
            else
            {
                RequestTwitterUpdates();
            }
        }
 
        private void RequestTwitterUpdates()
        {
            try
            {
                HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(RequestUrl);
                request.Headers.Add(HttpRequestHeader.Authorization,
                    "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(_username + ":" + _password)));
                request.Method = "GET";
                request.Timeout = _timeOutInSeconds * 1000;
                using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
                {
                    _twitterXmlDoc.Load(response.GetResponseStream());
                }
 
                if (_storeFlag)
                {
                    _twitterXmlDoc.Save(GetStoreFilePath());
                }
            }
            catch (Exception ex)
            {
                HttpContext.Current.Trace.Write("Twitter Control", ex.Message);
                if (_storeFlag)
                {
                    _twitterXmlDoc.Load(GetStoreFilePath());
                }
            }
        }
 
        private string GetStoreFilePath()
        {
            string p = BlogSettings.Instance.StorageLocation.Replace("~/", "");
            string folder = System.IO.Path.Combine(System.Web.HttpRuntime.AppDomainAppPath, p);
            return folder + Path.DirectorySeparatorChar + _storeFilename;
        }
 
        protected string RelativeTime(string createdAt)
        {
            string[] CreatedAtValues = createdAt.Split(' ');
            string converted = String.Format("{0} {1} {2} {3} GMT", CreatedAtValues[1], CreatedAtValues[2],
                CreatedAtValues[5], CreatedAtValues[3]);
 
            TimeSpan elapsedTime = DateTime.UtcNow - DateTime.Parse(converted).ToUniversalTime();
 
            if (elapsedTime.TotalMinutes < 1)
            {
                return "less than a minute ago";
            }
            else if (elapsedTime.TotalMinutes < 2)
            {
                return "about a minute ago";
            }
            else if (elapsedTime.TotalHours < 1)
            {
                return String.Format("{0} minutes ago", Convert.ToInt32(Math.Floor(elapsedTime.TotalMinutes)));
            }
            else if (elapsedTime.TotalHours < 2)
            {
                return "about an hour ago";
            }
            else if (elapsedTime.TotalDays < 1)
            {
                return String.Format("about {0} hours ago", Convert.ToInt32(Math.Floor(elapsedTime.TotalHours)));
            }
            else if (elapsedTime.TotalDays < 2)
            {
                return "1 day ago";
            }
            return String.Format("{0} days ago", Convert.ToInt32(Math.Floor(elapsedTime.TotalDays)));
        }
    }
}

Comments

5/28/2008 1:38:15 AM

Karsten Januszewski

Nice - I was just complaining about this! www.rhizohm.net/.../default.aspx#comments

Karsten Januszewski us