I've recently being trying to listen to events from a user's calendars so that I can track when appointments are added, removed and modified, and ran into a couple of problems.
Problem 1: Events intermittently stop firing
I was listening to events on the MAPIFolder object and the Calendar's Items collection and they would work for a while and at some point would stop firing.
I eventually traced this down to what I believe is a COM reference counting issue. I wasn't storing a reference to these objects, so I guess there was no implicit AddRef by the interop library. As such, there was no need for the runtime to keep the object around.
Hanging on to these references appears to be resolving this issue, so I stand by my assumptions.
Problem 2: How do we know what has been deleted?
The events I was hooking up to were on the ItemsEvents_Event interface off the MAPIFolder.Items property. There are thee events on this interface:
- ItemAdd
- ItemChange
- ItemRemove
The first two have a single parameter which will be the
AppointmentItem that is either being added or changed. Great! The last one doesn't have any parameters - all it tells you is that an item has been removed, but doesn't give you any indication what has been deleted. Besides that, its actually too late anyway, the item has already been deleted (you cannot cancel it).
So, I am now hooking up to the
BeforeItemMove event on the
MAPIFolderEvents_12_Event interface off the
MAPIFolder object. This gives me a reference to the
AppointmentItem being moved, the folder in which it is being moved to, and also allows me to cancel it.
To detect that it is being deleted, just check the folder it is being moved to is
null (hard-delete such as
Shift-Del) or the deleted items folder (soft-delete).
SolutionI have created a class that will monitor a user's default calendar and fires three events:
- AppointmentAdded - gives you the AppointmentItem instance added.
- AppointmentModified - gives you the AppointmentItem instance changed.
- AppointmentDeleting - gives you the AppointmentItem that is about to be deleted and allows you to cancel the operation if needed.
Sample usage:The following code is for use within the
ThisAddIn class. It outputs to console when an item is added, modified, or about to be deleted. It also displays a dialog box confirming an item should be deleted.
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
CalendarMonitor monitor = new CalendarMonitor(this.Application.ActiveExplorer());
monitor.AppointmentAdded +=
new EventHandler<EventArgs<AppointmentItem>>(monitor_AppointmentAdded);
monitor.AppointmentModified +=
new EventHandler<EventArgs<AppointmentItem>>(monitor_AppointmentModified);
monitor.AppointmentDeleting +=
new EventHandler<CancelEventArgs<AppointmentItem>>(monitor_AppointmentDeleting);
}
private void monitor_AppointmentAdded(object sender, EventArgs<AppointmentItem> e)
{
Debug.Print("Appointment Added: {0}", e.Value.GlobalAppointmentID);
}
private void monitor_AppointmentModified(object sender, EventArgs<AppointmentItem> e)
{
Debug.Print("Appointment Modified: {0}", e.Value.GlobalAppointmentID);
}
private void monitor_AppointmentDeleting(object sender, CancelEventArgs<AppointmentItem> e)
{
Debug.Print("Appointment Deleting: {0}", e.Value.GlobalAppointmentID);
DialogResult dr = MessageBox.Show("Delete appointment?", "Confirm",
MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (dr == DialogResult.No)
{
e.Cancel = true;
}
}
There are two other classes used:
EventArgs and CancelEventArgs. These generics are used by the events to pass strongly typed appointments.
EventArgs.cs:
using System;
using System.Collections.Generic;
namespace OutlookAddIn2
{
public class EventArgs<T> : EventArgs
{
private T _value;
public EventArgs(T aValue)
{
_value = aValue;
}
public T Value
{
get { return _value; }
set { _value = value; }
}
}
}
CancelEventArgs.cs:using System;
using System.Collections.Generic;
namespace OutlookAddIn2
{
public class CancelEventArgs<T> : EventArgs<T>
{
private bool _cancel;
public CancelEventArgs(T aValue)
: base(aValue)
{
}
public bool Cancel
{
get { return _cancel; }
set { _cancel = value; }
}
}
}
CalendarMonitor.cs:using System;
using System.Collections.Generic;
using Microsoft.Office.Interop.Outlook;
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace OutlookAddIn2
{
public class CalendarMonitor
{
private Explorer _explorer;
private List<string> _folderPaths;
private List<MAPIFolder> _calendarFolders;
private List<Items> _calendarItems;
private MAPIFolder _deletedItemsFolder;
public event EventHandler<EventArgs<AppointmentItem>> AppointmentAdded;
public event EventHandler<EventArgs<AppointmentItem>> AppointmentModified;
public event EventHandler<CancelEventArgs<AppointmentItem>> AppointmentDeleting;
public CalendarMonitor(Explorer anExplorer)
{
_folderPaths = new List<string>();
_calendarFolders = new List<MAPIFolder>();
_calendarItems = new List<Items>();
_explorer = anExplorer;
_explorer.BeforeFolderSwitch +=
new ExplorerEvents_10_BeforeFolderSwitchEventHandler(Explorer_BeforeFolderSwitch);
NameSpace session = _explorer.Session;
try
{
_deletedItemsFolder = session.GetDefaultFolder(OlDefaultFolders.olFolderDeletedItems);
HookupDefaultCalendarEvents(session);
}
finally
{
Marshal.ReleaseComObject(session);
session = null;
}
}
private void HookupDefaultCalendarEvents(NameSpace aSession)
{
MAPIFolder folder = aSession.GetDefaultFolder(OlDefaultFolders.olFolderCalendar);
if (folder != null)
{
try
{
HookupCalendarEvents(folder);
}
finally
{
Marshal.ReleaseComObject(folder);
folder = null;
}
}
}
private void Explorer_BeforeFolderSwitch(object aNewFolder, ref bool Cancel)
{
MAPIFolder folder = (aNewFolder as MAPIFolder);
//
// Hookup events to any other Calendar folder opened.
//
if (folder != null)
{
try
{
if (folder.DefaultItemType == OlItemType.olAppointmentItem)
{
HookupCalendarEvents(folder);
}
}
finally
{
Marshal.ReleaseComObject(folder);
folder = null;
}
}
}
private void HookupCalendarEvents(MAPIFolder aCalendarFolder)
{
if (aCalendarFolder.DefaultItemType != OlItemType.olAppointmentItem)
{
throw new ArgumentException("The MAPIFolder must use " +
"AppointmentItems as the default type.");
}
//
// Ignore other user's calendars.
//
if ((_folderPaths.Contains(aCalendarFolder.FolderPath) == false)
&& (IsUsersCalendar(aCalendarFolder)))
{
Items items = aCalendarFolder.Items;
//
// Store folder path to prevent double ups on our listeners.
//
_folderPaths.Add(aCalendarFolder.FolderPath);
//
// Store a reference to the folder and to the items collection so that it remains alive for
// as long as we want. This keeps the ref count up on the underlying COM object and prevents
// it from being intermittently released (then the events don't get fired).
//
_calendarFolders.Add(aCalendarFolder);
_calendarItems.Add(items);
//
// Add listeners for the events we need.
//
((MAPIFolderEvents_12_Event)aCalendarFolder).BeforeItemMove +=
new MAPIFolderEvents_12_BeforeItemMoveEventHandler(Calendar_BeforeItemMove);
items.ItemChange += new ItemsEvents_ItemChangeEventHandler(CalendarItems_ItemChange);
items.ItemAdd += new ItemsEvents_ItemAddEventHandler(CalendarItems_ItemAdd);
}
}
private void CalendarItems_ItemAdd(object anItem)
{
AppointmentItem appointment = (anItem as AppointmentItem);
if (appointment != null)
{
try
{
if (this.AppointmentAdded != null)
{
this.AppointmentAdded(this, new EventArgs<AppointmentItem>(appointment));
}
}
finally
{
Marshal.ReleaseComObject(appointment);
appointment = null;
}
}
}
private void CalendarItems_ItemChange(object anItem)
{
AppointmentItem appointment = (anItem as AppointmentItem);
if (appointment != null)
{
try
{
if (this.AppointmentModified != null)
{
this.AppointmentModified(this, new EventArgs<AppointmentItem>(appointment));
}
}
finally
{
Marshal.ReleaseComObject(appointment);
appointment = null;
}
}
}
private void Calendar_BeforeItemMove(object anItem, MAPIFolder aMoveToFolder, ref bool Cancel)
{
if ((aMoveToFolder == null) || (IsDeletedItemsFolder(aMoveToFolder)))
{
AppointmentItem appointment = (anItem as AppointmentItem);
if (appointment != null)
{
try
{
if (this.AppointmentDeleting != null)
{
//
// Listeners to the AppointmentDeleting event can cancel the move operation if moving
// to the deleted items folder.
//
CancelEventArgs<AppointmentItem> args = new CancelEventArgs<AppointmentItem>(appointment);
this.AppointmentDeleting(this, args);
Cancel = args.Cancel;
}
}
finally
{
Marshal.ReleaseComObject(appointment);
appointment = null;
}
}
}
}
private bool IsUsersCalendar(MAPIFolder aFolder)
{
//
// This is based purely on my observations so far - a better way?
//
return (aFolder.Store != null);
}
private bool IsDeletedItemsFolder(MAPIFolder aFolder)
{
return (aFolder.EntryID == _deletedItemsFolder.EntryID);
}
}
}