Friday, January 11, 2008

Listening to Calendar Events with Outlook 2007 and VSTO

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).

Solution

I 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);
}
}
}

64 comments:

Anonymous said...

Thanks so much for this! I would have never figured it out.

Adrian Brown said...

I've been looking up other people's posts for help for years. I'm glad I've helped at least one person!

Anonymous said...

Hi Adrian, thank you so much for posting this code. I have finally managed to get Stephen Toub code "Custom Calendar Providers for Outlook 2003" to work but I have one doubt, which I need your help for. I need to be able delete an event from Outlook calendar when an event from my webservice is deleted. Would I be able to use your code to accomplish this???

Anonymous said...

Hi Adrian, I was reading through your code "CalendarMonitor" class, you have a variable called "MAPIFolderEvents_12_Event", can you tell me what this is as it's not been declared anywhere in your code????

Adrian Brown said...

The code here only listens to events of a Calendar in Outlook (it won't delete, add, move anything itself), specifically 2007. I believe there are some small differences between 2007 and 2003, but you should still be able to use this code as a basis.

As for the MAPIFolderEvents_12_Event reference, this is an interface implemented by the MAPIFolder object. From memory, I think you have to explicitly cast the MAPIFolder object to MAPIFolderEvents_12_Event to get access to the events. You should find this defined in the Outlook COM interop library that is referenced as part of the Visual Studio Outlook AddIn Project.

Anonymous said...

Hi Adrian, I'm glad to see I'm not the only one that had this issue. I was able to accomplish removing an appointment from the db when the user deleted an appointment from the calendar by returning all the items in the Deleted Items folder and then looping through those items to return the AppointmentItem for each item in the folder by using the following code.


Outlook.MAPIFolder deletedItemsFolder =
Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderDeletedItems);

Outlook.Items deletedAppointments = deletedItemsFolder.Items;

foreach (AppointmentItem outlookAppointment in deletedAppointments)
{
if (outlookAppointment != null)
{
object objAppointmentID = GetUserProperty(outlookAppointment, APPOINTMENT_ID_KEY);
if (objAppointmentID == null) { return; }
int appointmentID = int.Parse(objAppointmentID.ToString());

Appointment appointment = Appointment.GetAppointment(appointmentID);

appointment.Delete();

If you see any issues with doing it this way please respond so I can fix them.

Thanks

Adrian Brown said...

The only issue I can see with this method is you are assuming that all deleted items go to the deleted items folder.

Appointments can be deleted directly (Shift-Del is the shortcut I think) and this won't get picked up by that code.

Cheers,
Adrian.

Anonymous said...

Hi Adrian,
this post is really helped me a lot. This is great code i was looking for. i have small issue in my outlook add-in, when user changes the time of the appointment item through the calender dragging, it fires item change event. I want to cancel Item Change event in certain conditions. Is it possible to cancel Item Change change event.

Thanks & Regards,
Sandeep

Adrian Brown said...

Sandeep,

Good question, but I don't know. From what I have seen, it looks unlikely.

Cheers,
Adrian.

Sandeep said...

hi Adrian,
Your code works great if i make any change in appointment or click on delete Appointment. If i send meeting invitation and and then cancel meeting it doesn't capture this event. I think BeforeItemMove should capture this event, but it doesn't. Can you please tell me how to capture cancel meeting event?

Thanks & Regards,
Sandeep

WAQ said...

Hi Adrian,

A very useful post.

Just a question for you see if you can help. How we can trap the item before delete event in outlook 2000 or if there is any other work around we can use by implementing some other events?

Thanks in advance

Adrian Brown said...

I'm sorry WAQ, I don't know with Outlook 2000.

Alex said...
This comment has been removed by the author.
Alex said...

Hi Adrian, loving the code you've posted although for some reason the MAPIFolderEvents_12_Event.BeforeItemMove event doesn't seem to fire on a custom calendar. Any ideas why this might be?

Thanks in advance,

Alex

EagleSigma said...

Great Post Adrian! Thanks to you I was able to actually get my application running the way I wanted.

Just one thing is bugging me: If I create or edit an appointment using Outlook 2007 everthing works as expected and I'm able to retrieve the "e.Value.Organizer" value. However, if a user does the same using Outlook web access 2003 - Premium or Basic, that value comes back with the vale "Error."

Any guidedance or suggestions woould be greatly appreciated. Thanks again.

Adrian Brown said...

Hi Alex,

The code posted won't work on a custom calendar because it is only listening to events on the default calendar.

You'll have to loop through all of the MAPIFolders looking for anything that is a calendar.

Cheers,
Adrian.

Adrian Brown said...

Hi eaglesigma,

Yeah, I haven't really played around much with OWA. It wasn't a requirement for me, so I deliberately chose to ignore it. :o)

Sorry, and good luck!
Adrian.

Alex said...

Hi Adrian, I've actually modified the code you posted to take in an array of MAPIFolders when constructing the CalendarMonitor object so it it is listening to events on the my calendar. The item events fire but not the folder event as mentioned before...?

Adrian Brown said...

Alex,

Hmmm... can't see why the folder event does not fire. I don't think I've tried it with a custom calendar myself and haven't really looked at this Outlook stuff for quite a while now.

I'm really sorry but I don't have the time to look into this right away.

If you have any luck getting it to work or find out why it doesn't, please let me know. Otherwise I'll try and get to it sometime over the next week or so (which is probably way too late for you).

Cheers,
Adrian.

William said...

Hello Adrian,

The modify event seems to be firing too early.

Here is the issue:

Sometimes when the calendar has multiple updates the entry shows up
on the calendar view but when I
access the properties, such as Start or Subject, it does not have the new values- it has the previous values that were present before the modify event fired.

The tricky part is that on the calendar view, the tooltip balloon shows the correct and latest data-but if you double click the item to open it, it still has the the old data. Somehow, the tooltip control is able to access the latest data before the item actually receives the changes.

The problem is that my code checks for the Start time and Subject to decide how to process the item; so when I get the Modify event and my code launches, it doesn't do anything because the two fields are still the same.

With 5 to 10 minutes eventually all the items in the calendar receive the latest data. However, at that point no event is triggered to let me process the modified entries.

What can I use in my Modify event that will force the public folder to give me the latest properties for the item?
Thanks you for your help.

William

Adrian Brown said...

Hi William,

I have feeling that I found some issues with my code after I posted this. It had to do with holding on to a reference of something, causing Outlook to display old data.

I'll dig it out and see if I can find out for you.

Cheers,
Adrian.

William said...

Many, ,any thanks for checking Adrian!

William

Adrian Brown said...

William,

I've update the code here to reflect some changes I made with regards to the way I handle references.

Also see this this link for some additional resources:

http://adriandev.blogspot.com/2008/10/vsto-memory-leaks-reference-management.html

William said...

Hello Adrian,

Thanks for updating your code.

I'm getting the following error messages when I try to run the code:


1. The best overloaded method match for 'OutlookAddIn2.CalendarMonitor.CalendarMonitor(Microsoft.Office.Interop.Outlook.Explorer)' has some invalid arguments

And

2. Argument '1': cannot convert from 'Microsoft.Office.Interop.Outlook.NameSpace' to 'Microsoft.Office.Interop.Outlook.Explorer

Sorry for my lack of knowlege but this error I have no clue about.

William

Adrian Brown said...

Sorry William, when I didn't update ALL the code! Try passing in this.Application.ActiveExplorer() to the CalendarMonitor constructor.

Cheers,
Adrian.

EagleSigma said...

Thank you Adrian!

EagleSigma said...

The updated code works beautifully Adrian. The old data issue is history. Thank you so much for your time and effort.


William

Anonymous said...

The code for the delete event handler does not work for me, but the other two yes, any clkue?

EagleSigma said...

Adrian,

Thanks to your insight and willingness to share I was able to get my project working properly. I was going crazy looking for an explanation of why my code was behaving so erratically, until you pointed out the leaking problems with COM objects and the need for carefull house cleaning. That turned out to be dead-on for my particular situation. I just wanted to say thanks man! We're incredibly lucky to have someone like you show us around.

William

Zilli said...

Well said.

Anonymous said...

Hi Adrian,
Thanks a lot for sharing your knowledge...I just copied your code as such and executed...

Appointment added and modified works fine. But when I delete the appoitment from the default calendar folder, nothing happens?

Is there any specific reason why the Appointment_Deleting is not firing..

Once again, thank you so much for your sample code which saves my time a lot...

Anonymous said...

Hi Adrian,
Thanks a lot for sharing your knowledge...I just copied your code as such and executed...

Appointment added and modified works fine. But when I delete the appoitment from the default calendar folder, nothing happens?

Is there any specific reason why the Appointment_Deleting is not firing..

Once again, thank you so much for your sample code which saves my time a lot...

Adrian Brown said...

Hi,

Sorry I have taken so long to reply - very busy on my current project (which has nothing to do with Outlook).

I'm not sure why the Appointment_Deleting doesn't fire. I haven't looked at this code for a while but it did work at the time. :o)

I'll have a look into it if I have a chance.

Cheers,
Adrian.

Chris said...

Thanks for the tip on adding the MAPI folder and items collection to lists. This properly prevented them from going out of scope. I added references to them in my private member variables in my Outlook add-in, but this didn't seem to do the trick.

zainu said...

Hi Adrian,
I am here again :),Appointment_Deleting is not working on default calendar,other 2 are working,any idea.

best regards

zainu said...

Hi Adrian,
When i run ur programme and modify an appointment,the event fires twice.Can u please check it out on your machine.

Best regards

kaks said...

Hello Adrian!
I am having the same problems as zainu concerning the Appointment Delete.. Any clue/hint on why this is ? Thanks for posting the code !!

Alex said...
This comment has been removed by a blog administrator.
SAMir said...

Hi Adrian,
An outlook appointment can be modify through drag n drop or by simple modification. now i want to Differentiate between these two. is it possible to do tha same?

Alex said...

Some days ago I added new contact in my address book in MS Outlook,and it was damaged.But I didn't know what happened.Fortunately myself helped good tool-cannot view outlook inbox mail.It was found in one old forum.Application has free status as far as I know,besides that utility can help to extract all files with *.eml, *.vcf and *.txt extensions from *.ost file.

Paul said...

In reply to the people who are experiencing problems with the delete event. I had the same problem and discovered that the solution was not to call: Marshal.ReleaseComObject(folder);
folder = null;
in the HookupDefaultCalendarEvents method and in the Explorer_BeforeFolderSwitch event.

If anyone can explain this feel free.

Regards

Bijaya Kumar said...

Hi Buddy,
Its a very good article it works fine for outlook 2007 but not working for outlook 2003. Can you please tell me what are the procedure for outlook 2003 i m in urgent need. Thanks...

Adrian Brown said...

Sorry Bijaya, I am not familiar with VSTO for Office 2003.

Alexis said...

In past my uncle had big problems with mails.And he solved their with help-outlook express repairing tool,tool fixed all data free of charge and helped to him and me too how analyze the file of dbx format and prepare temporary files for further extraction with Outlook Express repair tools.Utility helped me too very easy and effectively.

Bijaya Kumar said...

Hi My Outlook Delete event is not firing as well as for modified item it running twice any suggestions Please.

Thanks !!!!

Mordon said...

Hi all,

Anybody found a solution regarding Outlook 2003 and the missing BeforeItemMove-event?

I searched the namespaces and on google with no luck so far. Bad luck my customer runs on office 2003 :(

Hope to hear from some one.

Kind regards,
Morten, Denmark

Bijaya Kumar said...

Hi Adrian,
Nice to learn from the particular topic. I have implemented exactly the same code in my outlook 2007 add in. I am facing some issues like:
1. In the ThisAddin monitor_AppointmentAdded and monitor_AppointmentModified events are fired multiple times. For Example If I am adding a meeting Appointment then the monitor_AppointmentAdded event fires once and after that monitor_AppointmentModified event fires multiple times. But in this case I need only monitor_AppointmentAdded should fire.
2. And the 2nd problem that i am facing is that the monitor_AppointmentDeleting event is not firing whenever I am deleting a meeting request.

Can you please give me a solution regarding the same problems so that I can implement this in my project.


Thanks a lot.

Adrian Brown said...

Hi Bijaya,

I'm sorry that you are having some issues with this code. Some others have pointed out issues with the deleted event as well.

However, I do not have the time nor inclination to fix this code so that it suits everyone's purposes.

I'm just trying to share some of my code and experiences with the community. This stuff was a learning experience for me too, and is the main reason I posted it. As such, it is by no means perfect.

Please take this code as it is, and if required, make changes, fixes, whatever - take it is a starting point, not a complete solution.

leSasch said...

Cool!
*upload beer to adrian* ;-)

Anonymous said...

I have a basic question: how to view the debug output?
Thanks very much!

Adrian Brown said...

You can view the debug output by running your add-in through Visual Studio.

If you have an external debug viewer (I think SysInternals has one) you might be able to monitor it without Visual Studio.

Anonymous said...

Genial post and this post helped me alot in my college assignement. Gratefulness you as your information.

Alex said...

I have heard about a lot of tools which work with address books. But the day before yesterday I observed in the Internet a such-and-such application - wab repair tool. Which marveled me,reason of it resolved all my old problems for seconds and absolutely free as far as I kept in mind. I have been glad...

Para said...

thank you bro you saved my life!

John EDC said...

Hi

I hope this forum is still active.
Thanks for the code - but I am having an issue.

I can't get the BeforItemMove event to fire - any Idea?

I am using Office 2010

Char said...

Has anyone solved this?

Im having trouble with change event firing up multiple times

any help would by great

Pandu said...

Hi Adrian,

I have to create an add in like
Suppose i am the owner of an appointment and send the appointment/meeting request to three people.In that three people one user id compulsory to attend(i.,e our customer).Once he accepts or Declines the appointment a mail will come to me only..whereas the other two users in the appointment would not know about the status.Is there any way we can automate through any add in that other tow users also came to know that our customer has accepted/declined the meeting.

Thanks & Regards,
Suresh K

Pandu said...

Hi Adrian,

I have to create an add in like
Suppose i am the owner of an appointment and send the appointment/meeting request to three people.In that three people one user id compulsory to attend(i.,e our customer).Once he accepts or Declines the appointment a mail will come to me only..whereas the other two users in the appointment would not know about the status.Is there any way we can automate through any add in that other tow users also came to know that our customer has accepted/declined the meeting.

Thanks & Regards,
Suresh K

MikeGledhill said...
This comment has been removed by the author.
MikeGledhill said...

Excellent code (even 5 years later!)

Many thanks !

John Nilsson said...

From what I've learnt ReleaseComObject is only meant as a safe guard against the possibility that the application exits without the GC having done it's job.

Besides, when Outlook exits it does it's own releasing of all COM objects.

It is possible that the problems discussed for which ReleaseComObject was the intended solution was in fact cause by the GC collecting references to eagerly. If the GC collects the object on which the handlers are attached they won't fire.

It's possible that adding a call to ReleaseComObject at the end of a scope was enough to avoid the GC and thus mitigating the problems observed.

Cesar Vega said...

I'm seeing that the "Modified" event is fired twice more times, after I have effectively modified an event. I can't see why, it just get fired like 8 seconds after I modify and save, then a couple seconds after, again.
Anyone else seeing this behavior?

Scott Baker said...

Adrian, I am using your code verbatim, but for some reason the Calendar_BeforeItemMove method is never getting called. I'm using it with Outlook 2013 - could this be the problem?

Mithun Mithun said...

Thanks for your informative articel .its very useful
dot net training center in chennai | dot net training institute in velachery | dot net training and placement in chennai