My first experience with the .NET Framework was co-writing a custom build system in C# a few years ago. The build system ran as an add-in for Visual Studio 2003 and 2005. It enabled the developer to port a single code/content base to any number of Java ME and Brew handsets.

The core build system was pretty straight forward, but we ran in to some problems with the .NET Process object. Each step of the build system was executed as either an internal command, or an external process. The external processes were kicked off using the .NET Process object. These external processes could kick off a single executable file. Or, they could run a batch file, potentially spawning several child processes.

Under normal operation this was completely fine. However, canceling a build half way through exposed some limitations of the .NET Process object.

It turns out that Win32 doesn't maintain a parent/child hierarchy of processes. Killing the parent process wont automatically kill the child processes. So, if you create a .NET Process object, and that object creates a child process, killing the parent process just orphans the child process.

Process Groups

Visual Studio 2003 ships with a command-line tool called VCSPAWN.EXE. This tool was designed to solve the above problem for console applications. It works by exploiting the fact that all console processes run in a console group. When a message is sent to the group, all processes in the group receive the message. Under the hood VCSPAWN.EXE ensures that each process it spawns belongs to the same group. Terminate VCSPAWN.EXE and you terminate any processes it has spawned.

This is a good solution but VCSPAWN.EXE does not ship with Visual Studio 2005 or 2008.  Also, it does not handle GUI applications as far as I know.

Job Objects

Windows 2000 (and up) provide Job Kernel Objects. Job Kernel Objects offer a much better solution to the above problem, allowing you to group several processes together. Terminate the Job Object and all processes assigned to the Job Object are terminated also. This works for child processes because child processes are automatically assigned to the same job as the parent process.

There's one problem. The .NET Framework does not expose Job Kernel Objects as standard so we have to use Interoperation (specifically P/Invoke) to use this functionality. Below is some code that shows how to use a Job Object to run a .NET Process object. The idea is simple. Create your process in a suspended state. Assign it to a Job Object. Then, to terminate the process and all child processes, just terminate the Job Object.

class JobObjectProcess
{
    protected Process _process;
    protected IntPtr _jobHandle;
 
    public JobObjectProcess(ProcessStartInfo startInfo)
    {
        _process = new Process();
        _process.StartInfo = startInfo;
 
        CreateJobObject();
    }
 
    private void CreateJobObject()
    {
        win32.SECURITY_ATTRIBUTES SA = new win32.SECURITY_ATTRIBUTES();
        SA.nLength = 12;
        SA.SecurityDescriptor = IntPtr.Zero;
        SA.bInheritHandle = true;
        _jobHandle = win32.CreateJobObject(ref SA, "");
    }
 
    public bool Start()
    {
        bool started = _process.Start();
 
        if (started)
        {
            bool success = win32.AssignProcessToJobObject(_jobHandle, _process.Handle);
            if (!success)
            {
                string error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
                throw new ApplicationException(error);
            }
        }
 
        return started;
    }
 
    public void Kill()
    {
        bool success = win32.TerminateJobObject(_jobHandle, 0);
        if (!success)
        {
            string error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
            throw new ApplicationException(error);
        }
 
        if (_jobHandle != IntPtr.Zero)
        {
            if (0 == win32.CloseHandle(_jobHandle))
            {
                string error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
                throw new ApplicationException(error);
            }
            _jobHandle = IntPtr.Zero;
        }
    }
}

There's a problem with the code above. It starts the .NET Process object before it assigns it to the Job Object. For processes that don't exit quickly this is fine. But for processes that terminate quickly we could still end up with orphaned child processes. So why not just start the process suspended like I mentioned earlier? Well, the .NET Framework Process object doesn't expose a way to do this.

The only way around this is to use interoperation to call CreateProcess to create a process in a suspended state. The good news is this works fine. On Windows XP.

Vista

It doesn't work on Windows Vista due to the enhanced security features. Luckily, there is also a way around this. In order to get this to work on Vista I had to replace the CreateProcess call with a call to CreateProcessAsUser. This call will create a process under the security token that is specified as a parameter. This token must be a primary token. I used the DuplicateTokenEx API method to convert the token of the current user to a primary token. The process is then created in a suspended state and assigned to a Job Object. Finally the main process thread is resumed. For more information on how CreateProcessAsUser is being used in this context, see this article.

If you want to know more about Job Kernel Objects, Jeffrey Richter has written several articles on this subject. You can find out more here and here.

Below is the C# source code for the custom JobObjectProcess object. Calling Kill on the JobObjectProcess will terminate the parent process and any child processes it has spawned.

[Update: For a much more complete solution that uses Job Objects in managed code, check the JobObjectWrapper up on CodePlex.]

class JobObjectProcess
{
    private IntPtr _jobHandle;
 
    public JobObjectProcess()
    {
    }
 
    public bool Start(string commandLinePath)
    {
        IntPtr Token = new IntPtr(0);
        IntPtr DupedToken = new IntPtr(0);
        bool ret;
 
        SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
        sa.bInheritHandle = false;
        sa.Length = Marshal.SizeOf(sa);
        sa.lpSecurityDescriptor = (IntPtr)0;
 
        Token = WindowsIdentity.GetCurrent().Token;
        const uint GENERIC_ALL = 0x10000000;
        const int SecurityImpersonation = 2;
        const int TokenType = 1;
        ret = DuplicateTokenEx(Token, GENERIC_ALL, ref sa, SecurityImpersonation, TokenType, ref DupedToken);
 
        STARTUPINFO si = new STARTUPINFO();
        si.cb = Marshal.SizeOf(si);
        si.lpDesktop = "";
 
        PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
        const int CREATE_SUSPENDED = 0x00000004;
        ret = CreateProcessAsUser(DupedToken, null, commandLinePath, ref sa, ref sa, false, CREATE_SUSPENDED, (IntPtr)0, "c:/", ref si, out pi);
 
        SECURITY_ATTRIBUTES saJob = new SECURITY_ATTRIBUTES();
        saJob.Length = 12;
        saJob.lpSecurityDescriptor = IntPtr.Zero;
        saJob.bInheritHandle = true;
        _jobHandle = CreateJobObject(ref saJob, "");
 
        bool success = AssignProcessToJobObject(_jobHandle, pi.hProcess);
        int nSuspendCount = ResumeThread(pi.hThread);
 
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
        CloseHandle(DupedToken);
 
        return true;
    }
 
    public bool Kill()
    {
        bool success = TerminateJobObject(_jobHandle, 0);
        CloseHandle(_jobHandle);
        return success;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
        public int cb;
        public String lpReserved;
        public String lpDesktop;
        public String lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public uint dwProcessId;
        public uint dwThreadId;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
        public int Length;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }
 
    [DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    public extern static bool CloseHandle(IntPtr handle);
 
    [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
    public extern static bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
        ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment,
        String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
 
    [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
    public extern static bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess,
        ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType,
        int ImpersonationLevel, ref IntPtr DuplicateTokenHandle);
 
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern IntPtr CreateJobObject(ref SECURITY_ATTRIBUTES JobAttributes,
        [MarshalAs(UnmanagedType.LPTStr)]string strName);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern bool TerminateJobObject(IntPtr hJob, uint exitCode);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern int ResumeThread(IntPtr hThread);
}

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 recently put together a custom theme for this blog. I wanted to take an idea from concept to completion to see what issues occur along the way. This involved designing, developing and testing the custom theme.

Design and Development Process

The problem with one person performing the role of designer, developer and tester on a single project is that concessions tend to be made with each role. It's well known that programmers should not be the ones to black box test their own code (I'm not talking about TDD here) because the test coverage will tend to be biased or subject to blind spots. If a developer is designing the visuals for the site, then he or she is probably going to restrict the creative process by over burdening the design with worries about the implementation phase.

Since I had to perform the role of designer, developer and tester for this project, I wanted to cleanly separate each role. This helped me keep the focus solely on each step as I progressed through the project, thereby avoiding the issues above.

To design the custom theme, I used Adobe Illustrator CS3. An alternative to this is Expression Design, which I'll be looking at soon. I used Visual Studio 2008 to write the CSS and code. Finally, I used a VM with several browsers installed to test the CSS.

Design Phase

I began the design by loading Illustrator CS3 with a new Web Document. Using the Rectangle Tool, I very quickly plotted out areas for the common features of a blog template, including a header, menu bar, side bar, content area and footer. I decided on a default color for each area in addition to setting up some gradients to jazz things up a little. Below is a screen grab showing the result of this step.

Theme, with common areas, such as header, side bar, footer, and so on.

So far, the design is really simple. Regardless, at this point I'm not considering any CSS implications. I'm not asking myself, can this even be implemented with CSS? From a development point of view, this is not a good approach to take. But we're not in the development phase yet. We're only working on a design, and we don't want to restrict the design with implementation issues too soon.

With that in mind, I decided to complicate things a little. I wanted to make the side bar appear as though it's sitting on a translucent layer that exposes the area underneath it. This is very easy to implement in Illustrator CS3, but alarm bells started going off in my head. Is this going to cause me CSS headaches in the development phase? Quite possibly, but lets ignore that for now. We're still in the design phase.

The screen grab below shows the design with the new translucent side bar.

Theme, with common areas, but the side bar appears translucent.

At this point I was happy with the first draft of the design. It's quite basic, but all of the required areas are in place and I've satisfied the designer in me with the translucent side bar.

Before proceeding to the development phase I briefly investigated my options with regard to implementing the design with CSS. I put together a strategy I knew would work with CSS. This involved breaking the design into a series of horizontal strips using divs and associating styles with these divs to recreate the design in the browser. Any problems with this strategy means staying in the design phase until they're resolved. The strategy should also take into consideration potential CSS browser compatibility issues. See "CSS and Browser Compatibility Problems" for my take on this subject.

Development Phase

I kicked off the development phase once I was happy with the design and implementation strategy. I set up a new theme folder and added a skeleton site.master file along with some empty CSS files.

I wanted the implementation of my design to adapt to changes in the browser window size. This means I couldn't just import a static image of my design into the web page and slot text into the various areas. I needed to slice up the static image of my design into a series of gradient strips that could be used with CSS styles to 'tile' back in the gradients. The gradient slice images (including the site header logo image) weighed in at a total size of only 9.4k, which is smaller than the heading banners on most sites.

I created an images folder in my theme folder for the gradient images. These images are thin repeatable sections of the my design background which are used to recreate the gradient fills in the different areas of my design.

The strategy I decided on involved recreating the design with markup and CSS as shown below.

Theme, with common areas, broken into 4 horizontal strips.

The design is broken up into four distinct horizontal strips. Within each strip is a container div that holds a content area section and a side bar (or navigation bar) section. This approach allows me to apply CSS styles that recreate the gradients in each of the areas, thus providing the illusion of a translucent side bar. This approach also means that if the content area of an individual strip grows horizontally, so too does the corresponding side bar area.

Once the overall layout was in place, I added markup to the site.master file for the various controls of the side bar and the content area. The page you're looking at was the result of this effort.

As I progressed through each of the development steps, I tested the updated CSS with Firefox 2 and IE 7. Periodically, I did briefly test with IE 5, 5.5 and 6 just to make sure there were no major issues that would mean going back to the drawing board.

Testing Phase and Testing Environment

It is not possible to install multiple versions of IE on the same machine. At least not using the standard issue install files. Enter Multiple IE. While not perfect (the browsers will crash from time to time for no apparent reason), it enables you to set up a single VM with all versions of Internet Explorer, and whatever other browsers you want.

For testing my custom theme, I used a VM running Windows XP with Multiple IE installed in addition to Firefox 2, Opera, and a few others.

During this phase of the project, I made some small CSS refactorings to avoid CSS browser compatibility issues. One CSS modification involved setting the style of the body tag to the background color of the menu strip for IE 6. For some reason, the list items in the menu bar were not drawing with the appropriate background color. Instead of drawing with the background color of the containing div (which was apparently collapsing due to floating the contents of the div), it was falling right back to the body style background color. This modification resulted in the CSS working fine in all the browsers tested.

In addition to this I added a conditional comment to set the width of the site master page on IE 6, 5.5 and 5. I considered IE 4.0 and decided not to go there. I didn't use any CSS hacks.

There were no major issues during this phase. The only modifications made were purely to fix small CSS problems. This is down to both the approach used to get to this point, but also the fact that the design is quite simple. With a more complicated design, we could end up having to back-track to the development phase to fix problems.

Conclusion

Although the custom theme I've described above is quite simple, the process used to create it works quite well with more complicated designs. The key point to remember is not to over burden the design phase with implementation fears. The catch is that while doing this you must also try to produce a design that is not impractical. Finding the right balance between the two will help produce the most interesting designs that are reasonable to implement.


On the surface, developing with CSS is relatively easy. But then the reality of browser compatibility issues strikes and things become difficult for all the wrong reasons.

This forces the developer to choose one of four options.

  1. Try to re-implement all or part of the CSS so that it avoids the compatibility problems.
  2. Dump the entire feature that is causing the compatibility problems.
  3. Use CSS hacks.
  4. Use conditional comments.

People tend to get religious about the above options. Some only use CSS hacks, others only use simplified CSS that works everywhere. A lot of people loath conditional comments. Whatever your religion, there are some undeniable facts.

Riddling CSS files with CSS hacks is bad for two reasons.

  1. It creates a minefield for future browser developers who have to ensure that new browsers don't break when they encounter the CSS hacks.
  2. It makes the CSS files difficult to read and update.

Riddling markup files with conditional comments is bad because it also makes the code difficult to read and update. But it won't create a minefield for future browser developers.

With the above points in mind I tend to proceed as follows.

1. I try to avoid browser compatibility issues by refactoring the CSS to a point where it works everywhere.

I recently had to sort out a problem where an unordered list being used in a horizontal menu was causing placement problems on IE 5.5. The list item tags were being set to display as inline in the CSS style. By changing a single line of CSS (from display: inline; to float: left;) I managed to avoid the problem and the code worked in IE 5, up to 7, Firefox 2, Safari and so on.

2. If I can't avoid a browser compatibility issue, I use two conditional comment statements in the markup, and style overrides in the CSS.

When I was setting up the CSS for this blog, I was unhappy with how the page resized in IE 5, 5.5 and 6. By adding two conditional comments to the markup, I was able to provide any number of CSS style overrides to correct the problems. Specifically I added two conditional comments that wrapped the entire page markup as follows.

<body>    
    <!--[if lte IE 6]> <div class="ie6-below">  <![endif]-->
             <form id="Form1" runat="Server">
                     ...
              </form>
    <!--[if lte IE 6]> </div>  <![endif]-->
</body>

Then, I added style overrides in the CSS file where I wanted to correct problems. CSS specificity guarantees that my overrides will take priority over the original styles. For example, below are two styles, one that applies to IE versions 6 and below, and the original style to be used everywhere else.

.container
{
    min-width: 1016px;
    max-width: 1160px;
    margin-left: auto;
    margin-right: auto;
}
 
.ie6-below .container
{
    width: 1116;
}

Once the two conditional comments are added to the markup you can override as many styles as you like in your CSS files. These overrides are explicitly defined and don't make the CSS files hard to read.

3. As a last resort, I'll use a CSS hack.

I've seen all of the above problems before with Java ME while leading development at Upstart Games. Java ME is a small simple language that should run everywhere. It doesn't. Not even close.

We solved all of our Java ME porting problems (porting to 700 handsets on some projects) using a middleware solution based on conditional code. The first version of this got a little out of hand because we relied too heavily on conditional code and hacks. The second version was a huge improvement. Conditional code was kept to an absolute minimum. There were no hacks.