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)));
        }
    }
}

I've been putting this off for quite a while. Anyway, after researching various hosting possibilities I finally settled on DiscountASP.net. I'm running BlogEngine.NET version 1.3.1.0 on IIS 7.0 in Integrated Mode.

The first thing I did when I set up my blog was decide on a visual theme for the site. BlogEngine.NET comes with several themes and more can be downloaded, but I wanted to make my own unique theme. Rather than start from scratch, I decided to create a new theme based on the default Standard theme by Mads Kristensen. [Update: I've since updated my blog with an entirely custom theme, but who's counting.]

CSS

Since all of the browsers seem to have their own defaults for the various tag attributes (margin, padding, line-height, etc) I immediately added Eric Meyer's reset.css file to the head element of my theme's site.master file.

<head runat="server" profile="http://gmpg.org/xfn/11">
    <link rel="stylesheet" href="reset.css" type="text/css" />
    <link rel="stylesheet" href="style.css" type="text/css" />
    <link rel="shortcut icon" href="~/pics/blogengine.ico" type="image/x-icon"/>
</head> 

This effectively resets all the browser defaults so that my blog will look pretty much the same in all browsers. That's the idea anyway. Once I had that in place, I began to set up defaults appropriate to my site.

Accessibility

I've noticed that quite a few of the high profile blogs I read do not adapt well to changes in the size of text content. For most users this isn't a problem, but if you're using a low screen resolution and/or have the default text size of your browser cranked right up, you're going to have problems viewing the site content. My monitor is a 30 inch Dell, running at 2560x1600 so I wont see these problems unless I go looking for them.

As an example, check the screen grab below. This shows a heading from the side bar of a blog with no attempt made to make it adaptable to changes in the size or wrapping of the text content. As you can see, the text completely overruns the boundary of the image container beneath it.

_no_adapt

However, by splitting the image into three parts and replacing the heading container with three nested divs (one for each bitmap) we end up with a header container that adapts to the size and wrapping of the text it contains. This also requires creating a new css rule for each of the three divs to determine how the image sections will be drawn.

_adapt 

This approach can be applied to all text containers where bitmaps are used to spice things up.

The subject of accessibility is far greater than what I've described here. The two standards to look out for are WCAG and Section 508.

Nesting Divs Programatically

After applying the approach above to my side bar headings, I began to notice that the markup for the page had become quite cluttered. What was originally this..

<div class="box">
    <h1><%=Resources.labels.recentPosts %></h1>
</div>

..was now this..

<div class="boxmiddle">
    <div class="boxupper">
        <div class="boxlower">
            <h1><%=Resources.labels.recentPosts%></h1>
        </div>
    </div>
</div> 

If we only use this container once off it's not so bad. But if many occurrences are spread throughout a file, it can make modifications tedious. There are a number of ways to avoid this problem. The approach I chose was to write a small helper method to produce the nested divs programmatically rather than declaratively. Although the output to the browser ends up the same, the production markup looks like this..

<% using (HtmlHelper.NestedDivs(3, new String[]{ "boxmiddle", "boxupper", "boxlower" })) { %>
    <h1><%=Resources.labels.recentPosts %></h1>
<% } %> 

It works by exploiting the fact that the using block automatically calls the Dispose method of objects created by the using block. Calling the NestedDivs helper method creates the opening div tags. Then you add the markup you wish to contain within the divs. The closing brace on the using statement effectively calls Dispose, which is used to terminate the div tags.

Incidentally, I noticed this technique being used in the new ASP.NET MVC source code release. This code release contains a class called SimpleForm that is the base class for the standard MVC form. It's usage is similar.

Enough about Me, what do you think of Me?

Once I was reasonably happy with the overall look of the site, I decided to add an About Me page. I signed up for a gravatar so I could assign my self a consistent icon that appears on my blog and also anyone else's blog if I post a comment etc.

I also signed up for a feedburner account and hooked it up to my site. I need to research feeds a little more.

What's Next?

There are a ton of things still to play with. All of which require a little research. I'll probably add some amazon referral links to books that have come in handy over the years (I wont endorse anything I can't personally recommend). I want to investigate several other modifications to the site, but I'll be keeping the focus on the articles. I have several brewing in my head at the moment.

Any modifications I make that may be of interest, I'll most likely blog about.

Until next time...