Blog

Creating iCal files in ASP.NET MVC and C#


Recently, during a project I was working on for a client, I was tasked with adding events to a user's iCal in addition to being able to add events to Google Calendar. In researching this, I found that this would necessitate creating a .ics file for each event. Given that my client's site had hundreds if not thousands of events, that would be a lot of .ics files sitting around. There had to be a solution to this.

Fortunately, there is, and ASP.NET MVC and C# makes this surprisingly painless.

First, we need to know what goes into a iCal .ics file. This PasteBin page gives a very rough overview of a partial iCal format (the parts that I needed, as a bonus). It's at the bottom of the window, below the Google Calendar API and Yahoo Calendar API if you need those as well.

Now, while I had some of the spec, I needed a way to put it together into a .ics file. Now, there's some libraries out there, but I ran into this post at Balajiprasad's useful codes that had some sample code that I could adapt. He used a library for his solution. I opted for a simple Stringbuilder approach to start.


/// <summary>
/// Generates an iCalendar .ics link and returns it to the user.
/// </summary>
/// <param name="downloadFileName">Name of the download file to return.</param>
/// <param name="evt">The Event.</param>
/// <returns>The .ics file.</returns>
[System.Web.Mvc.HttpPost]
public ActionResult AddToICalendar(string downloadFileName, int eventId)
{
    // replace db with however you call your Entity Framework or however you get your data.
    // In this example, we have an Events collection in our model.
    using (
        var db =
            new ExampleEntities(
                ConfigurationManager.ConnectionStrings[YourConnectionString].ConnectionString))
    {
        // Alternatively, you may use db.Events.Find(eventId) if this fits better.
        var demoEvent = db.Events.Single(getEvent => getEvent.ID == eventId);

        var icalStringbuilder = new StringBuilder();

        icalStringbuilder.AppendLine("BEGIN:VCALENDAR");
        icalStringbuilder.AppendLine("PRODID:-//MyTestProject//EN");
        icalStringbuilder.AppendLine("VERSION:2.0");

        icalStringbuilder.AppendLine("BEGIN:VEVENT");
        icalStringbuilder.AppendLine("SUMMARY;LANGUAGE=en-us:" + demoEvent.EventName);
        icalStringbuilder.AppendLine("CLASS:PUBLIC");
        icalStringbuilder.AppendLine(string.Format("CREATED:{0:yyyyMMddTHHmmssZ}", DateTime.UtcNow));
        icalStringbuilder.AppendLine("DESCRIPTION:" + demoEvent.Description);
        icalStringbuilder.AppendLine(string.Format("DTSTART:{0:yyyyMMddTHHmmssZ}", demoEvent.StartDateTime));
        icalStringbuilder.AppendLine(string.Format("DTEND:{0:yyyyMMddTHHmmssZ}", demoEvent.EndDateTime));
        icalStringbuilder.AppendLine("SEQUENCE:0");
        icalStringbuilder.AppendLine("UID:" + Guid.NewGuid());
        icalStringbuilder.AppendLine(
            string.Format(
                "LOCATION:{0}\\, {1}\\, {2}\\, {3} {4}",
                evt.LocationName,
                evt.Address,
                evt.City,
                evt.State,
                evt.ZipCode).Trim());
        icalStringbuilder.AppendLine("END:VEVENT");
        icalStringbuilder.AppendLine("END:VCALENDAR");

        var bytes = Encoding.UTF8.GetBytes(icalStringbuilder.ToString());

        return this.File(bytes, "text/calendar", downloadFileName);
    }
}

And then, within the .cshtml file in question, we call it like this:


@using (Html.BeginForm("AddToICalendar", "Home", new { downloadFileName = "thisEvent.ics", eventId = Model.TheEvent.ID }))
{
    <input type="submit" value="Add to iCal" class="ical-button" id="button" /><br />
}

This will generate a form and will, when clicked, call the AddToICalendar function. It will then retrieve the event data and populate the fields in the generated .ics file with the relevant data. As a bonus, should you have the actual location data, someone on an iPhone or iPad can actually call up the map and get directions to go to wherever the event is located. (Now, how to call something other than Apple Maps as a default for this, I've yet to figure out and would prefer Google Maps or, better yet in my case, Waze).

The new Guid for the UID line will assign a new ID to this event. Now, if you had an event that would be updated, and wanted someone to just click on the same link and update their calendar that way, I would wager that you would have a different way of setting the UID earlier on and keeping it the same for the same event. I haven't had the opportunity to test this yet, but that's my understanding based on my research.

As an additional bonus, you can even download the .ics file on a Windows system and if you have Outlook, you can import that as an event into that calendar. I haven't tested this with other calendar applications or even Google Calendar. So in the end, this proved to not just be for iCal.

Also, if you wanted to keep the start and end date/times to a local time, you could remove the Z part in the string.Format strings for each, which would relegate it to be local to the user's time. Still, it's much better to use UTC-based time for everything in my opinion when feasible. As far as the formatting string, the {0:yyyyMMddTHHmmssZ} part would map to 20150601T123000Z if given a date/time of June 1st, 2015 at 12:30 UTC. Eliminate the Z part, and it becomes June 1st, 2015 at 12:30PM local time.

You'll also notice the double-backslash in front of the commas. For some reason, iCal needs those commas to be escaped, and a single backslash just won't do the job using this method. I've tested this without doing that in the generated iCal file, and it didn't work very well. On my iPhone, it would only put the LocationName before the first comma into the Location field on the Event entry in my iCal. With the proper escaping, it puts the entire address. I haven't tested what happens if you have a comma in one of those fields, so if you run into any problems you could easily put a .Replace(",", "\\,") onto the appropriate lines. The DESCRIPTION line, however, did not have this problem and did not seem to require the escaped commas, so it may just be for the LOCATION line. Have no worries about importing this into Outlook, as the backslashes won't show in the Location line on those events, at least not on my local Outlook with Office 365.

In closing, this should be a help to anyone who needs a basic iCal file to share events and reminders. I imagine the process would be similar if you wanted to make Reminders for iPhone/iPads the same way, and there are other fields that are available to put in these files too. This documentation at kanzaki.com should also be useful, however I don't really know if this is up to date. It's worth a shot though if you want to experiment. Happy coding.

Links

Archive