Custom Calendar Providers for Outlook 2003

 

Stephen Toub
Microsoft Corporation

May 2004

Applies to:
    Microsoft® Office Outlook® 2003

Summary:   Stephen Toub walks you through customizing native Outlook integration with the Lists Web service from Microsoft Windows SharePoint Services to build a custom Web application that actually serves up custom data instead of that from an events list. (21 printed pages)

Download odc_OLOutlookCalendarSample.exe.

Download the odc_OLOutlookCalendarSample.exe sample file. (163 KB)

Contents

Introduction
Outlook and SharePoint Event Lists
A Plug-In Model
Configuring the Providers
Communicating with Outlook
Trust me, I'm a SharePoint Events List!
The StsSync Protocol
Security
Future Work

Introduction

Most data sets in the world have some inherently elegant way of being expressed. Frequencies over time are naturally rendered as histograms. Percentages are routinely conveyed by statisticians through pie charts. More relevant to those of us who spend the majority of our days in meetings, time-based data is often expressed in the form of a calendar. In fact, calendars make our lives easier by organizing activities into timelines, allowing us to view event-based data from any number of angles.

Unfortunately for a developer, the acquisition of new event-based data sources often translates into writing new calendar controls to display that data. And while this is certainly a manageable if not resource-consuming task, many individuals and organizations already have installed on their home and work computers one of the most full-featured and robust calendaring application ever to be written. I'm of course referring to Outlook. Microsoft® Office Outlook® 2003 has simply stunning calendaring capabilities. With Outlook, I can view weekly highlights, monthly summaries, and daily snapshots. I can view the calendar in standard form or as an organized list of events. I can view my personal calendar side-by-side with that of my teammates, gaining the ability to drag-and-drop scheduled items between them. I can even display a Microsoft Windows® SharePoint™ Services event list as a calendar within Outlook.

With this feature set, it'd be a shame to have to reinvent the wheel every time someone required the ability to display a new data source. For example, why can't I use Outlook's calendaring capabilities to display my group's server downtime schedule as culled from the servers' event logs? Why can't I use it to display my Windows Scheduled Tasks? Why can't I use it to display recent posts from my favorite RSS feeds? And why can't I use it to display the recording schedule from my Windows XP Media Center? In fact (drum roll please), I can.

Outlook and SharePoint Event Lists

Outlook provides an object model for accessing and modifying most of the data it displays. Using this object model, I can write a macro or a COM add-in or even an external application that creates event entries for every item in my external data source. For example, the following Microsoft Visual C#® method adds an event to my calendar (assuming I've added a reference to the Microsoft.Office.Interop.Outlook.dll interop assembly):

public static void CreateEvent(string subject, string body)
{
   Application app = new ApplicationClass();
   try
   {
      NameSpace ns = app.GetNamespace("MAPI");
      MAPIFolder calendarFolder =
         ns.GetDefaultFolder(OlDefaultFolders.olFolderCalendar);

      AppointmentItem newEvent = (AppointmentItem)
         calendarFolder.Items.Add(OlItemType.olAppointmentItem);
      newEvent.Subject = subject;
      newEvent.Start = DateTime.Now.AddHours(1);
      newEvent.End = newEvent.Start.AddHours(1);
      newEvent.Body = body;
      newEvent.Save();
   }
   finally
   {
      app.Quit();
   }
}

For some scenarios, writing applications to access Outlook in this way is perfectly acceptable, and it certainly provides for a great deal of flexibility. In fact, you can do programmatically almost anything you can do through the user interface (UI), and then some. However, for my purposes, it does have some downsides. First, it requires me as the developer to write code that synchronizes all events in the back-end data store with the programmatically created events already on the Outlook calendar (those created on a previous run). But more importantly, for large organizations, deploying to the client and keeping Outlook add-ons such as this up-to-date can be a nightmare. In addition, having code on the client implies that the client component either needs direct access to the back-end data source or a proxy component that the client can connect to in order to access the back-end data source. It'd be easier if I had an application running on a central server at which someone could point Outlook to retrieve all of the event information. It'd be even easier if Outlook was natively able to access and process this information and to display it on the calendar. Multiple clients running Outlook could then easily connect to this central server with nothing more than Outlook 2003 installed.

In fact, this is possible (you knew that's where I was going, didn't you). With Outlook 2003, Microsoft introduced functionality that allows Outlook to talk to a server running Windows SharePoint Services. Outlook is able to link to and consume various lists published on a SharePoint site, such as event and contact lists, a feat made possible through a Web service that lives in Windows SharePoint Services. Outlook can download event information from an events list on a SharePoint site and display it as a calendar within the Outlook application, side-by-side with the user's default calendar as well as calendars from other Exchange users.

So what if I want to display my own event-based data, rather than that which is stored in a SharePoint list? I see two options that take advantage of this interaction between Outlook and Windows SharePoint Services. Just as Outlook provides an object model for its calendaring capabilities, so does Windows SharePoint Services. The first solution is to write an application that continually updates a SharePoint events list with data from the back-end data source. Outlook could then connect to this list. This requires that I have a server running Windows SharePoint Services and that I have an application running frequently or constantly to update the list, either using the Windows SharePoint Services object model (if the application is running on the Windows SharePoint Services server) or using one of its Web services (if the application is running remotely from the Windows SharePoint Services server).

An easier solution, and one that I believe has many advantages, is to build my own Web application that looks to Outlook similar to Windows SharePoint Services but that actually serves up my custom data instead of that from an events list. This doesn't require Windows SharePoint Services to be installed, is easily maintainable, provides for great extensibility, and best of all requires nothing to be installed on the client besides Outlook 2003 itself. On the server, all I need is ASP.NET (and thus the "server" can be the same computer as the "client", even if the computer isn't running Microsoft Windows Server® 2003, which is required for Windows SharePoint Services) and a little bit of custom code to shim the Lists Web service (available from Lists.asmx) from Windows SharePoint Services.

A Plug-In Model

Let's set aside for a moment the details of how my custom ASP.NET application is going to communicate with Outlook and instead focus on the data I'm publishing and how I'm exposing it to the rest of the system. With all of the work I'm putting into this system, it'd be a shame to hardcode the logic surrounding the extraction of the event-based data from my data source. For this system to be useful, it needs to be able to expose events from multiple data sources in a convenient manner, one that is easy on both the users of the system as well as the developers who want to publish new data sources.

Plug-in models are perfect for situations like these. I can write a general framework that handles the communication with Outlook and throw at it a bunch of different event providers that each understands how to communicate with some back-end data source, exposing it as a list of events. The framework can then take these events from the various providers and render it to Outlook as a calendar.

To start, I create a base class from which all "calendar providers" (as I'm calling them) derive:

public abstract class CalendarProvider
{
   internal string _name;
   internal Guid _id;

   public string Name { get { return _name; } }
   public Guid ID { get { return _id; } }

   internal void Initialize(string name, Guid id)
   {
      _name = name;
      _id = id;
   }
   public virtual void Configure(string initData){} 
   public abstract EventCollection GetEvents();
   public override string ToString() { return _name; }
}

This very simple class is the interface used by the calendar framework to access my data source's events. Given an instance of a CalendarProvider-derived class, one can call GetEvents to retrieve a collection of all events in the back-end data source.

For a new provider, the minimum I need to do is derive from CalendarProvider and implement the GetEvents method. The Name and ID properties expose values configured for this provider in the web.config file (to be discussed shortly). The Configure method is called by the framework with any initialization data provided for it in the web.config file. The CalendarProvider can use this configuration data in whatever way is appropriate to the specifics of the provider.

Let's look at an example. Below is a fully-functional CalendarProvider that gathers entries from an EventLog and publishes them as events that can be displayed on a calendar. The name of the event log I'm mining is provided to the class using the Configure method.

public class EventLogCalendarProvider : CalendarProvider
{
   private string _logName;
   public override void Configure(string logName) 
   { 
      _logName = logName; 
   }
   public override EventCollection GetEvents()
   {
      EventCollection events = new EventCollection();
      using (EventLog log = new EventLog(_logName))
      {
         foreach(EventLogEntry entry in log.Entries)
         {
            try
            {
               if (entry.EntryType == EventLogEntryType.Error)
               {
                  Event e = new Event();
                  e.Author = entry.UserName;
                  e.Description = entry.Message;
                  e.Location = entry.EntryType.ToString();
                  e.Start = entry.TimeGenerated.ToUniversalTime();
                  e.End = entry.TimeWritten.ToUniversalTime();
                  e.Title = entry.Source;
                  events.Add(e);
               }
            }
            finally { entry.Dispose(); }
         }
      }

      return events;
   }
}

That's all there is to it. When configured appropriately in the web.config file and linked to Outlook, all of the errors in the specified event log are viewable on a calendar within Outlook. Cool, huh? Here's another example. This CalendarProvider allows me to view on an Outlook calendar all of the items in an RSS feed whose endpoint is provided to the class using the Configure method:

public class RssCalendarProvider : CalendarProvider
{
   private string _url;      
   public override void Configure(string url) { _url = url; }
   public override EventCollection GetEvents()
   {
      EventCollection events = new EventCollection();
      if (_url != null)
      {
         RssItem [] items = RssFeedReader.GetFeedItems(_url);

         foreach(RssItem item in items)
         {
            Event e = new Event();
            e.Author = item.Creator;
            e.Location = item.Link;
            e.Description = HttpUtility.HtmlDecode(item.Description);
            if (e.Location.Length > 0) e.Description = 
               e.Location + Environment.NewLine + 
               Environment.NewLine + e.Description;
            e.Title = item.Title;
            e.Start = item.PubDate;
            e.End = e.Start.AddMinutes(1);
            events.Add(e);
         }
      }
      return events;
   }

   public override string ToString()
   {
      return base.ToString() + " (" + _url + ")";
   }
}

Again, that's all there is to it. The GetEvents method creates a collection in which to store the generated Events. Assuming a URL was provided to it through the Configure method, the code retrieves all items from the RSS feed and converts each into an Event which is then added to the EventsCollection. When it's done, the collection is returned. You should also notice that I override ToString in order to provide a more descriptive representation, one that includes the URL.

Most providers have a similar form to that shown above, creating an events collection to store all of the events, retrieving all items from the data source, converting each to an event, and storing that event to the collection which is returned at the end. As an example of retrieving items from a data source, here is the implementation of RssFeedReader.GetFeedItems. For any custom providers you write, this is obviously specific to your data source.

public static RssItem [] GetFeedItems(string url)
{
   XmlDocument rssDoc = new XmlDocument();
   using (WebClient client = new WebClient())
   {
      rssDoc.Load(new MemoryStream(client.DownloadData(url)));
   }

   XmlNamespaceManager ns = new XmlNamespaceManager(rssDoc.NameTable);
   ns.AddNamespace("dc", "http://purl.org/dc/elements/1.1/");

   XmlNodeList itemNodes = rssDoc.SelectNodes("/rss/channel/item", ns);
   ArrayList items = new ArrayList(itemNodes.Count);
   foreach(XmlNode node in itemNodes)      
   {
      XmlNode creator = node.SelectSingleNode("dc:creator", ns);
      XmlNode title = node.SelectSingleNode("title", ns);
      XmlNode link = node.SelectSingleNode("link", ns);
      XmlNode pubDate = node.SelectSingleNode("pubDate", ns);
      XmlNode description = node.SelectSingleNode("description", ns);

      RssItem item = new RssItem();
      if (creator != null) item.Creator = creator.Value;
      if (title != null) item.Title = title.InnerText;
      if (link != null) item.Link = link.InnerText;
      if (pubDate != null) item.PubDate = 
         DateTime.Parse(pubDate.InnerText).ToUniversalTime();
      if (description != null) item.Description = description.InnerXml;
      items.Add(item);
   }
   return (RssItem[])items.ToArray(typeof(RssItem));
}

To show off the kinds of things for which you can use this system, I've included a handful of CalendarProvider-derived class implementations that I think are both interesting and useful.

Table 1. CalendarProvider class implementations

Provider Description
EventLogCalendarProvider Allows error entries in an event log to be displayed as events on a calendar in Outlook. The name of the event log to be mined must be defined by the administrator in the configuration file.
RssCalendarProvider Allows items from an RSS feed to be displayed as events on a calendar in Outlook. The URL of the feed to be downloaded and parsed must be defined by the administrator in the configuration file.
NntpCalendarProvider Allows postings on an NNTP newsgroup to be displayed as events on a calendar in Outlook. The address of the newsgroup server, the name of the newsgroup, and the maximum number of items to be retrieved from the newsgroup must be defined by the administrator in the configuration file.
MsnMessengerCalendarProvider MSN Messenger 6.x has a logging feature that stores all instant messages to a log file. This CalendarProvider culls those logs and allows complete conversations to be displayed on a calendar in Outlook. The path to the directory containing the log files must be specified by the administrator in the configuration file.
SystemRestoreCalendarProvider Allows information about all of the system restore points on a computer to be displayed as events on a calendar in Outlook.
GroupingCalendarProvider Allows multiple CalendarProviders to be grouped together and displayed on one calendar in Outlook.

These examples are all functional, and they can serve as templates when writing your own providers.

Configuring the Providers

Now that you know how to write CalendarProviders, the next step is configuring them. The solution includes a custom configuration section handler that instantiates and configures various CalendarProviders based on data in the web.config file (note that currently the same instance of a provider is used for all requests for that provider). To use the custom configuration section handler, one adds the handler to the configuration/configSections node of the web.config file:

<configSections>
   <section name="outlookCalendar"
      type="Msdn.Outlook.Calendaring.CalendarProviderSettings, 
         OutlookCalendarProvider" 
      allowLocation="false" />
</configSections>

This indicates to the .NET Framework configuration system that any configuration section named "outlookCalendar" should be handled by the CalendarProviderSettings class, defined in the OutlookCalendarProvider assembly.

The outlookCalendar section contains a providers node which can have any number of provider nodes as children. Each of these provider nodes represents one CalendarProvider that should be available to Outlook. A provider node's attributes are used to appropriately configure the CalendarProvider as shown in the following table.

Table 2. Attributes used to configure the CalendarProvider

Attribute Description
name The friendly name of the provider. This is what is visible to users in Outlook as the name of the calendar.
id A GUID for this particular provider. Every provider node under the providers node must have a different ID. This is necessary for Outlook to identify uniquely and communicate with a specific provider.
type The fully-qualified type name of the CalendarProvider-derived class to be used for this provider. If the class is not defined in the OutlookCalendarProvider assembly, it must include the assembly name in a format suitable for being parsed and loaded with Type.GetType("type, assembly").
initData Custom string data that is passed to CalendarProvider.Configure. This is where, for example, one would specify the name of the event log for the EventLogCalendarProvider to use.

I can have as many providers as I desire, and I can use the same CalendarProvider class more than once to expose multiple data sources that are of the same type. For example, consider the following configuration section.

<outlookCalendar>
   <providers>
      <provider
         name="MSDN Magazine Current Issue"
         type="Msdn.Outlook.Calendaring.RssCalendarProvider"
         id="{3B60FC93-2769-40b0-A866-ED635B54B127}"
         initData="http://msdn.microsoft.com/msdnmag/rss/recent.xml"/>
      <provider 
         name="Application Event Log Calendar"
         type="Msdn.Outlook.Calendaring.EventLogCalendarProvider"
         id="{2FC380F2-FD3F-46af-BDB4-40BD727813CE}"
         initData="Application" />
      <provider 
         name="System Restore Points"
         type="Msdn.Outlook.Calendaring.SystemRestoreCalendarProvider"
         id="{7CB4C0A5-D6EA-48e1-92DE-852148F64149}" />
      <provider 
         name="MSDN RSS Feeds Related to this Article"
         type="Msdn.Outlook.Calendaring.GroupingCalendarProvider"
         id="{9DF3229F-31B5-4b42-B7DF-60242E1CFF04}" />
         <provider 
            name=".NET Framework MSDN RSS"
            type="Msdn.Outlook.Calendaring.RssCalendarProvider"
            initData="http://msdn.microsoft.com/netframework/rss.xml"/>
         <provider 
            name="Office MSDN Rss"
            type="Msdn.Outlook.Calendaring.RssCalendarProvider"
            initData="http://msdn.microsoft.com/office/rss.xml"/>
      </provider>
   </providers>
</outlookCalendar> 

This configures four top-level calendars available to Outlook. The "Application Event Log Calendar" provider exposes data from the Application Event Log, while "System Restore Points" exposes information on all available system restore points on the computer. The "MSDN RSS Feeds Related to this Article" provider combines two sub-providers, both of which expose a different RSS feed (one from the .NET Framework Developer Center on MSDN and the other from the Office Developer Center on MSDN). The ID attributes on the top-level provider nodes are used to uniquely identify a provider to Outlook; since these sub-providers do not need to be uniquely addressable (they are only ever accessed as a group), they do not have id attributes.

To make accessing this provider information easy, the static property CalendarProviderSettings.Providers returns an array of these providers, instantiated and configured. The static method CalendarProviderSettings.Lookup is used to find the correct instance of a provider given an ID. This comes into play heavily when I discuss our communications with Outlook. Speaking of which, I now showed you how to create custom providers and how to configure them. It's about time I get this data populated into Outlook.

Communicating with Outlook

For this solution to work, I need our Web application to mimic the Windows SharePoint Services' integration with Outlook. This means I must present to Outlook the same interface as is presented by Windows SharePoint Services, and I must let Outlook know about the presence of our calendar provider in the same way that Windows SharePoint Services informs Outlook about the existence of an event list.

The "Link to Outlook" button on a SharePoint event list Web page uses the stssync protocol to contact, using Microsoft JScript®, a client application used for synchronizing with server-side data. By default with the Microsoft Office System installed, Outlook is registered as the stssync protocol handler. You can verify this by creating a SharePoint.StssyncHandler COM object and querying it for the name of the handler using the GetStssyncAppName method.

Once contacted, Outlook prompts the user for whether it should add the specified event list as a calendar folder. When instructed to do so, it proceeds to sync with Windows SharePoint Services using the Lists Web service, available at http://server_name/_vti_bin/Lists.asmx. While the Lists Web service exposes a dozen methods, only a few of them are used by Outlook: GetList and GetListItemChanges (and GetAttachmentCollection if attachments to events are supported, but that is beyond the scope of this article and is not necessary for our implementation). Outlook starts the sync using the GetListItemChanges Web method to retrieve all items on the list. It then uses the GetList method to retrieve important information about the list itself, such as its title and last modified date.

Figure 1. Blog Posts on an Outlook Calendar

Trust me, I'm a SharePoint Events List!

To begin creating the Web service, I used the .NET Framework SDK tool, wsdl.exe, a very useful command-line tool that wraps the functionality exposed from the System.Web.Services.Description namespace. To create a service class based on the Lists.asmx WSDL, I use a command similar to:

% wsdl.exe /server 
/out:OutlookCalendar.cs http://server_name/_vti_bin/Lists.asmx?WSDL

By default, the wsdl.exe application creates a client proxy when supplied with WSDL from a file or from a URL. Using the /server flag forces it to instead generate an abstract class for an XML Web service implementation using ASP.NET based on the contract. A snippet from the generated class is shown below.

[WebServiceBinding(Name="ListsSoap",
   Namespace="http://schemas.microsoft.com/sharepoint/soap/")]
public abstract class Lists : WebService
{
   [WebMethod]
   [SoapDocumentMethod(
      "http://schemas.microsoft.com/sharepoint/soap/GetListItems",
   RequestNamespace=
      "http://schemas.microsoft.com/sharepoint/soap/",
   ResponseNamespace=
      "http://schemas.microsoft.com/sharepoint/soap/",
   Use=SoapBindingUse.Literal,
      ParameterStyle=SoapParameterStyle.Wrapped)]
   public abstract XmlNode GetListItems(string listName,
      string viewName, XmlNode query, XmlNode viewFields, 
      string rowLimit, XmlNode queryOptions);

   [WebMethod]
   [SoapDocumentMethod(
      "http://schemas.microsoft.com/sharepoint/soap/GetListCollection",
   RequestNamespace=
      "http://schemas.microsoft.com/sharepoint/soap/",
   ResponseNamespace=
      "http://schemas.microsoft.com/sharepoint/soap/",
   Use=SoapBindingUse.Literal,
      ParameterStyle=SoapParameterStyle.Wrapped)]
   public abstract XmlNode GetListCollection();
   ...
}

Since I don't want an abstract class, I need to remove all of the abstract keywords and supply boilerplate implementations for each of the unused methods. The implementations are empty bodies when the methods return void and return null when the methods return objects. Since these methods are never called, there's no point in providing any actual implementation. While I could have simply removed all of the unnecessary methods, I wanted to make sure that the WSDL automatically generated for our service by ASP.NET remained as true to the original WSDL as possible. This is, of course, not really important as Outlook has no need to request the WSDL for our service; it already knows the contract for communicating with Windows SharePoint Services. Even if it had to request the WSDL, it would be fairly trivial to store the actual WSDL as provided by Windows SharePoint Services and send it to Outlook when it requested our description. Behind-the-scenes redirects like this are easily achieved using HttpContext.Current.RewritePath in an event handler for HttpApplication.BeginRequest.

Now all that remains to be done for our base implementation is to implement the two important methods, GetList and GetListItemChanges. GetListItemChanges is the more important of the two, as it provides the event information I want to display. GetListItemChanges returns an XmlNode which means the results are not strongly-typed. Consequently, I get no IntelliSense help from the Microsoft Visual Studio® .NET integrated development environment (IDE), and I have to be very careful about how I form a response. Outlook displays a vague error message should the data I send down not conform to the expected schema, so the returned XML fragment must exactly mimic that which is returned from a real SharePoint list. For information about the sync errors, see the Windows SharePoint Services synchronization log file written to the users temporary directory after a sync (%temp%\wss-sync-log.htm). In fact, if you enable the "Enable Mail Logging" option in Outlook not only are the errors logged, but the entire SOAP response, as well information on each Outlook SyncItem, is also logged — a very handy tool when debugging your service.

To enable mail logging

  1. On the Tools menu, click Options.
  2. Click the Other tab and then click Advanced Options.
  3. Select the Enable mail logging (troubleshooting) box and then click OK.

The root node of the response is named "listitems" and specifies through attributes a variety of namespaces for the returned rowset, including "xmlns:rs" for "urn:schemas-microsoft-com:rowset", and "xmlns:z" for "#RowsetSchema". The listitems node contains an "rs:data" node as a child, whose sole attribute ItemCount specifies the number of events being returned. Each event then exists as a child of this node, created as a "z:row" with attributes containing specific event data.

The code that generates this response is available in the WssListResponse.cs file in the code associated with this article. Using WssListResponse, my Web method implementation is written as follows:

[WebMethod(BufferResponse=false)]
[SoapDocumentMethod(
   "http://schemas.microsoft.com/sharepoint/soap/GetListItemChanges",
   RequestNamespace="http://schemas.microsoft.com/sharepoint/soap/",
   ResponseNamespace="http://schemas.microsoft.com/sharepoint/soap/",
   Use=SoapBindingUse.Literal,
      ParameterStyle=SoapParameterStyle.Wrapped)]
public XmlNode GetListItemChanges(string listName,
   XmlNode viewFields,string since,
   XmlNode contains)
{
   WssListResponse responseXml = new WssListResponse();
   try
   {
      CalendarProvider prov = 
         CalendarProviderSettings.Lookup(new Guid(listName));
      if (prov == null) prov = NotFoundCalendarProvider.Instance;
      int counter = 0;
      foreach(Event e in prov.GetEvents())
      {
         responseXml.AddEvent(++counter, e.Title, e.Description, 
            e.Location, e.Author, e.Start, e.End);
      }
   }
   catch(Exception exc)
   {
      System.Diagnostics.Debug.WriteLine(exc);
   }
   return responseXml.FinishXml();
}

I create a new instance of my WssListResponse class. I then find the appropriate CalendarProvider (as configured in the web.config) based on the ID provided by Outlook, and I get all of its events. For every event given to me by the provider, I add its information to the list response generator. When complete, I call the generator's FinishXml method which formats all of the events into an XML fragment to be returned from the Web method. The implementation of WssListResponse.AddEvent looks like the following:

public void AddEvent(int id, string title, 
   string description, string location, 
   string author, DateTime start, DateTime end)
{
   XmlElement node = _doc.CreateElement(
      "z", "row", "#RowsetSchema");
   author = "1;#" + author;
   AddAttribute(node, null, "ows_ID", null, id.ToString());
   AddAttribute(node, null, "ows_Title", null, title);
   AddAttribute(node, null, "ows_Author", null, author);
   AddAttribute(node, null, "ows_GUID", null,
      Guid.NewGuid().ToString("B"));
   AddAttribute(node, null, "ows_EventDate", null,
      start.ToString("s") + "Z");
   AddAttribute(node, null, "ows_EndDate", null, 
      end.ToString("s") + "Z");
   AddAttribute(node, null, "ows_Description", null, description);
   AddAttribute(node, null, "ows_Location", null, location);
   AddAttribute(node, null, "ows_Duration", null, 
      (end - start).TotalSeconds.ToString());
   ...
   events.Add(node);
}

All it does is create an XmlElement and populate it with the appropriate data from our event. With that, our implementation of GetListItemChanges is complete.

GetList's implementation is easier. The XML fragment returned from this method requires a single List node attributed with information about the SharePoint list, in my case a faux entity I construct just to fit into the model. The WssListResponse.GetListDescription implementation demonstrates the creation of this node:

public static XmlNode GetListDescription(
   string title, string description, Guid id)
{
   XmlDocument doc = new XmlDocument();

   XmlNode list = doc.CreateElement(null, "List",  _wssns);
   doc.AppendChild(list);

   AddAttribute(list, null, "DocTemplateUrl", null, "");
   AddAttribute(list, null, "DefaultViewUrl", null, 
      "/Lists/" + id.ToString("N") + "/AllItems.aspx");
   AddAttribute(list, null, "ID", null, id.ToString("B"));
   AddAttribute(list, null, "Title", null, title);
   AddAttribute(list, null, "Description", null, description);
   AddAttribute(list, null, "Name", null, id.ToString("N"));
   AddAttribute(list, null, "BaseType", null, "0");
   AddAttribute(list, null, "ServerTemplate", null, "106");
   AddAttribute(list, null, "EnableAttachments", null, "TRUE");
   AddAttribute(list, null, "EnableModeration", null, "FALSE");
   AddAttribute(list, null, "EnableVersioning", null, "FALSE");
   AddAttribute(list, null, "Hidden", null, "FALSE");
   ...
   return doc;
}

The implementation of GetList is then very straightforward:

[WebMethod(BufferResponse=false)]
[SoapDocumentMethod(
   "http://schemas.microsoft.com/sharepoint/soap/GetList", 
   RequestNamespace="http://schemas.microsoft.com/sharepoint/soap/", 
   ResponseNamespace="http://schemas.microsoft.com/sharepoint/soap/", 
   Use=SoapBindingUse.Literal,
      ParameterStyle=SoapParameterStyle.Wrapped)]
public XmlNode GetList(string listName)
{
   Guid id = new Guid(listName);
   CalendarProvider provider = CalendarProviderSettings.Lookup(id);
   if (provider == null) provider = NotFoundCalendarProvider.Instance;
   return WssListResponse.GetListDescription(provider.Name, id);
}

Again, I'm using the listName (the ID) provided to me by Outlook to find the appropriate CalendarProvider to service this query. That CalendarProvider's information is then handed off to WssListResponse in order to create the description of my list.

The list service is now complete. All that remains to be done is to tell Outlook that my "list" exists. Outlook 2003 development began around the same time that Microsoft was touting its "Hailstorm" platform. While "Hailstorm"-branding no longer appears in Office, it does find its way into some of the documentation and still exists in some of the JScript code used by Windows SharePoint Services. If you view the source of an event list page rendered by Windows SharePoint Services and find the HTML that displays the "Link to Outlook" button, you'll see that it uses a JScript method named ExportHailstorm, a method whose sole purpose is to construct a URL that allows Outlook to connect to this SharePoint list. I need to generate a similar URL, one that uses the stssync protocol.

The StsSync Protocol

As mentioned previously, the stssync protocol enables you to add to Outlook (or for that matter to any other application that supports stssync) an Events list or a Contacts list that exists on a SharePoint site. For more information about the stssync protocol, see stssync Protocol in the Microsoft SharePoint Products and Technologies Software Development Kit (SDK). The link I create contains data that allows Outlook to connect to my list, including information such as the URL of the list, the friendly name of the list, and the folder type that Outlook should create (a calendar folder in my case). The Default.aspx page in the associated code download creates this link and allows a user to use it just as they would an event list's "Link To Outlook" button. By browsing to this page, Outlook displays a dialog similar to the following:

Figure 2. Adding a Calendar To Outlook

Clicking OK causes Outlook to synchronize with my list service and to display my calendar with data from the back-end data source.

Unfortunately, the stssync:// URL doesn't contain the actual location of the list service's ASMX file, but rather the URL of the SharePoint site on which it exists. Outlook knows the structure of a SharePoint site and knows to find all Web services in http://server_name/_vti_bin/. As such, when Outlook requests the service, it requests it at http://server_name/OutlookCalendarProvider/_vti_bin/Lists.asmx, whereas I actually want it to find the service at http://server_name/OutlookCalendarProvider/OutlookCalendar.asmx. As such, I implemented a very simple event handler for the HttpApplication.BeginRequest event that redirects all requests with a path including "_vti_bin" to my service:

protected void Application_BeginRequest(Object sender, EventArgs e)
{
   HttpContext ctx = HttpContext.Current;
   string path = ctx.Request.Url.AbsolutePath;

   if (path.IndexOf("_vti_bin") >= 0)
   {
      string newUrl = ctx.Request.ApplicationPath +
         "/OutlookCalendar.asmx";
      ctx.RewritePath(newUrl);
   }   
}

That's it! The system is fully-functional. I can create new calendar providers and register them in the web.config. After doing so, browsing to Default.aspx shows the new provider as an available option. Clicking the "Link to Outlook" button contacts Outlook allowing it to add the calendar as a provider. And every time Outlook syncs with the provider, it downloads all of the events from the back-end data source.

Security

There are some security implications that stem from this approach. If the solution only services requests from an instance of Outlook running on the same computer, the Web server should be configured to block requests to the application from any external computers. If the application is running on a separate server from Outlook, the application should be configured to only accept requests over HTTPS given that the CalendarProvider could be serving sensitive data to the client. In addition, it should be configured to only accept requests from authenticated users and should impersonate those users. That way, you can configure ASP.NET itself to run under a low-privileged account such as ASPNET or NetworkService while still allowing authorized users to access resources such as event logs (or whatever other local data source is being accessed).

Future Work

While a good start, the implementation provided here is not perfect. For one thing, every request from Outlook retrieves all events from the data source, regardless of whether Outlook already synchronized. If you take a look at the GetItemListChanges Web method, the name itself should tell you that it can return only updates to the list rather than the whole list. In fact, Outlook sends to this method as a parameter the last time it synchronized. If your back-end data source includes a change log that keeps track of when items were added, updated, and deleted, you can take advantage of this by only sending the updates back to Outlook. This requires a slight redesign of the CalendarProvider interface, but more importantly requires some adjustments to the SOAP response from GetItemListChanges. If you examine the WssListResponse.FinishXml method, you find that the code actually generates two rowsets that get sent to Outlook, and each of these rowsets contains a node for each event. When events are deleted on the server, this second rowset lists the IDs of all of the events that still exist (it's empty if there are no deletions). If the second rowset is not empty, when Outlook syncs it deletes any previously synchronized events that no longer exist. Then, for all events in the first rowset, if there are previously synchronized events with the same ID, they are discarded, and all of the events in the first rowset are added, thus updating any existing events and adding all new ones. Since my current design necessitates a full refresh on every synchronization, every event is listed in both rowsets. If you want to modify the system to support partial updates, you must reimplement FinishXml accordingly.

Another improvement could be made in how the system deals with requests for more information on individual events. If you open up one of the synchronized events in Outlook, you notice that at the bottom of the description field, Outlook adds a link that references the particular item on the Windows SharePoint Services server. Right now, the implementation on the server just handles these requests by redirecting to the Default.aspx page. However, it would be fairly straightforward to extend CalendarProvider so that it can process such requests. The application event's handler which currently does the redirecting could instead forward the information on to the correct CalendarProvider for it to handle.

At the beginning of the article, I also mentioned that I could use Outlook's object model to add events to the calendar. I can use the exact same CalendarProvider and derived classes to implement this client-side approach. All I need to do is write a framework that can create calendars using the object model and populate events on the calendars based on the events returned from CalendarProvider.GetEvents. Much of the code I already wrote you can reuse for this purpose without any changes. This approach has the advantage of not requiring Outlook to be connected to a network, whereas the approach described in this article requires Outlook to be online.

And, of course, there are an infinite number of data sources out there ripe to be harvested with a CalendarProvider implementation. All in all, this solution makes importing those new event-based data sets into Outlook extremely easy: write a new provider that derives from CalendarProvider, add it to the web.config, browse to the Default.aspx page, and click the auto-generated link for your provider. In no time you'll be up and running with your custom data viewable as a calendar in Microsoft Outlook 2003. Enjoy!

About the Author

Stephen Toub is the Technical Editor for MSDN Magazine for which he also writes the ".NET Matters" column. You can reach him at stoub@microsoft.com.

© Microsoft Corporation. All rights reserved.