qt8gt0bxhw|20009F4EEE83|RyanMain|subtext_Content|Text|0xfbff300c00000000b700000001000700
If you have a website that maintains a list of events for users, it is a great idea to allow users to selectively add those events to their own calendar. Using automation from a website with something like Outlook is a bad idea. It would be blocked by the browser's security and your users might use something else for their calendar. Fortunately, many main-stream (most?) calendar applications, such as Outlook, Windows Calendar on Vista, and a whole lot more, support the iCalendar specification. This makes adding items from your applications a breeze.
The iCalendar Specification
The iCalendar spec (see
RFC 2445) was created to extend an earlier created vCalendar spec. This spec basically provides a universally accepted way to represent a calendar item in a file, or really, as text. An iCalendar can represent some single generic calendar item, or an Event, To-Do, Journal item, and can even include free/busy schedule information. We are going to focus here on including only a single event in our iCalendar data, although it can also represent multiple items all within the same iCalendar. When expressed as a file, the iCalendar data will typically have an "ics" extension, but really it is just text. It also has a MIME type of "text/calendar"
Scenario
For our scenario, we'll assume we have a website with events on it. For any of these events, we want to allow the user to be able to selectively add an event to their own local calendar. On the display of an event on our website, we'll provide a link that the user can click to add the event to their calendar. Here's a sample of a website that I've used this on to give you an idea:
The user clicks that link and we get some
jazz hands and it is added to the user's calendar. Let's take a look at how it all works.
The iCalendar Class
As I mentioned before, the iCalendar data is just text. The thing about it is, it is
ugly text. I don't like ugly text. I want to write it once and then forget it ever existed. So, I created a class that does all the ugly text for me to use whenever I need. If you're really wanting to see more of this ugly text then you and the rest of your zombie friends can go nicely over to the RFC and read all you want.
Here's my class:
using System;
using System.Text;
using System.Collections.Generic;
namespace RF.Events
{
public enum PriorityLevel : int
{
Normal = 5,
High = 1,
Low = 9
}
public class Event
{
private const string _DATEFORMAT = "yyyyMMdd\\THHmmss\\Z";
public Event() { }
public DateTime StartTime;
public DateTime EndTime;
public string Location = string.Empty;
public string Title = string.Empty;
public string Description = string.Empty;
public bool UseAlarm = true;
public PriorityLevel Priority = PriorityLevel.Normal;
public string Category = string.Empty;
public override string ToString()
{
return Output;
}
public string Output
{
get
{
StringBuilder sb = new StringBuilder();
sb.Append("BEGIN:VEVENT\n\n");
sb.Append("DTSTART:");
sb.Append(StartTime.ToUniversalTime().ToString(_DATEFORMAT));
sb.Append("\nDTEND:");
sb.Append(EndTime.ToUniversalTime().ToString(_DATEFORMAT));
sb.Append("\nLOCATION:");
sb.Append(Location);
sb.Append("\nCATEGORIES:");
sb.Append(Category);
sb.Append("\nTRANSP:OPAQUE\n");
sb.Append("SEQUENCE:0\n");
sb.AppendFormat("UID:RFCALITEM{0}\n", DateTime.Now.Ticks);
sb.Append("DTSTAMP:");
sb.Append(StartTime.ToUniversalTime().ToString(_DATEFORMAT));
sb.Append("\nDESCRIPTION:");
sb.Append(Description);
sb.Append("\nSUMMARY:");
sb.Append(Title);
sb.Append("\n\nPRIORITY:");
sb.Append((int)Priority);
sb.Append("\nCLASS:PUBLIC\n");
if (UseAlarm)
{
sb.Append("BEGIN:VALARM\n");
sb.Append("TRIGGER:PT15M\n");
sb.Append("ACTION:DISPLAY\n");
sb.Append("DESCRIPTION:Reminder\n");
sb.Append("PRIORITY:5\n");
sb.Append("END:VALARM\n");
}
sb.Append("END:VEVENT\n");
return sb.ToString();
}
}
}
public class iCalendar
{
public iCalendar()
{
this.Events = new List<Event>();
}
public List<Event> Events;
public override string ToString()
{
return this.Output;
}
public string Output
{
get
{
StringBuilder sb = new StringBuilder();
sb.Append("BEGIN:VCALENDAR\n");
sb.Append("PRODID:-//RyanFarley.com//iCalendar Sample MIMEDIR//EN\n");
sb.Append("VERSION:2.0\n");
sb.Append("METHOD:PUBLISH\n");
foreach (Event ev in Events)
sb.Append(ev.Output);
sb.Append("END:VCALENDAR");
return sb.ToString();
}
}
}
}
So, first question. Why the separation of the Event item from the rest of the iCalendar item? We'll get to that. For now, here's how we'll use that class. Let's add it to a handler to send back the iCalendar data in the response. We'll call this handler from the link we provide to the user (Typically we would do something like pass an "event ID" of some kind to it and grab the associated data from a database or something. We'll ignore all that stuff in this example to keep it simple).
Here's our handler code:
<%@ WebHandler Language="C#" Class="CalendarItem" %>
using System;
using System.Web;
using System.Text;
using RF.Events;
public class CalendarItem : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
// Set up the Event item first. This would typically come
// from a database or some object in the application
Event ev = new Event();
ev.Title = "Some Event";
ev.Description = "This is a test event. Yeye.\\nKTHXBYE";
ev.Location = "At ryanfarley.com";
ev.UseAlarm = true;
ev.StartTime = DateTime.Parse("11/1/2008 8:00:00 AM");
ev.EndTime = DateTime.Parse("11/1/2008 10:00:00 AM");
ev.Priority = PriorityLevel.Normal;
ev.Category = "Test Category";
// Now add the event to an iCalendar
iCalendar ical = new iCalendar();
ical.Events.Add(ev);
// Set the content-type of the response and file name
// so Windows knows how to handle the response.
context.Response.ContentType = "text/calendar";
context.Response.AddHeader("content-disposition", "inline;filename=CalendarEvent.ics");
// Write the bytes of the iCalendar item output to the resonse stream
context.Response.BinaryWrite(new System.Text.ASCIIEncoding().GetBytes(ical.Output));
context.Response.End();
}
public bool IsReusable
{
get { return false; }
}
}
If we add that as a hyperlink on a webpage, like this:
The user can click it (this is where we get the jazz hands) and they are prompted with this:
Of course, here they click OK and they are presented with some sort of dialog from whatever app is associated with ics files. On my machine that is Outlook, so I get this:
Cool. The user clicks the Save & Close and it now appears on their calendar. We're not just limited to do this from a webpage either. We could just as easily save the
iCalendar.Output to a file and Process.Start it to open it for the user if we're in a Windows app. Or attach it to an e-mail to a user, similar to services like WebEx, GotoMeeting, and other services that send you an e-mail with an iCalendar file attached.
Adding Multiple Events to Create an iCalendar Feed
Now, back to that separation of the Event from the iCalendar class. Why is that? The iCalendar spec indicates that an iCalendar can have one, or multiple events in it. If you've ever added an iCalendar feed to Outlook from your Google Calendar or TripIt, etc, this feed is not an RSS feed or something like that. It is a big long iCalendar file containing multiple events in it. We can create the same with the following code:
// Create the iCalendar
iCalendar ical = new iCalendar();
// Add the first event and add it to the iCalendar
Event ev = new Event();
ev.Title = "Some Event #1";
ev.Description = "This is a test event #1. Yeye.\\nKTHXBYE";
ev.Location = "At ryanfarley.com";
ev.UseAlarm = true;
ev.StartTime = DateTime.Parse("11/1/2008 8:00:00 AM");
ev.EndTime = DateTime.Parse("11/1/2008 10:00:00 AM");
ev.Priority = PriorityLevel.Normal;
ev.Category = "Test Category";
ical.Events.Add(ev);
// Add the second event and add it to the iCalendar
ev = new Event();
ev.Title = "Some Event #2";
ev.Description = "This is a test event #2. Yeye.\\nKTHXBYE";
ev.Location = "At ryanfarley.com";
ev.UseAlarm = true;
ev.StartTime = DateTime.Parse("11/5/2008 8:00:00 AM");
ev.EndTime = DateTime.Parse("11/5/2008 10:00:00 AM");
ev.Priority = PriorityLevel.High;
ev.Category = "Test Category";
ical.Events.Add(ev);
// ...you get the idea
// Now out iCalendar contains multiple events.
// We can output it all to the response stream.
// Set the content-type of the response and file name
// so Windows knows how to handle the response.
context.Response.ContentType = "text/calendar";
context.Response.AddHeader("content-disposition", "inline;filename=CalendarEvent.ics");
// Write the bytes of the iCalendar item output to the resonse stream
context.Response.BinaryWrite(new System.Text.ASCIIEncoding().GetBytes(ical.Output));
context.Response.End();
What happens now when the user clicks our link? They'll get prompted just as before, but when they click OK to add it, it will add the iCalendar feed as a second calendar in their calendar application. In Outlook it will show as a separate calendar which I can view side-by-side with my existing one or overlay on top of my existing one. In this case we'd also want to include the appropriate 304 response status in case the calendar item hasn't changed so we keep things nice.
So that is it. Not bad work and we've got a reusable class we can use for single item, or complete calendar feeds. Have fun.