Developing Add-ins for Office calls for some careful handling of references, particularly with references to objects that have an underlying COM reference.
My previous blog entry Listening to Calendar Events with Outlook 2007 and VSTO talked about listening to Appointment events, but my code didn't handle references very well. This left some references to Outlook objects hanging and Outlook was showing values that were old.
I've since update this with some better handling of these references. In particular, the explicit releasing of COM objects via the Marshal.ReleaseComObject() method.
More Reading:
Friday, October 10, 2008
VSTO, Memory Leaks & Reference Management
Thursday, June 26, 2008
Microsoft Office Developer Show Episode 4 (Harry Miller)
Its not everyday you get mentioned in an MSDN video blog:
http://blogs.msdn.com/vsto/archive/2008/06/11/microsoft-office-developer-show-episode-4-harry-miller.aspx
Monday, January 21, 2008
Outlook 2007: How to determine if you are the owner of an Appointment
There doesn't appear to be an easy way to find out if the current user is the owner of an Appointment. Maybe I've something extremely simple, but I just couldn't find that property called IsOwner.
I basically want to prevent functionality from being available if the user has opened an Appointment that isn't in one of their own calendars (ie: readonly and from someone elses calendar). After looking through the AppointmentItem and MAPIFolder properties, I finally found something that was useful: MAPIFolder.Store. This property returns a Store object except when the Folder is a shared folder (ie: someone elses). Cool.
So, I've now created my IsOwner method:
public static bool IsOwner(AppointmentItem anAppointment)
{
bool result = false;
if (anAppointment.Parent is MAPIFolder)
{
//
// A Store of null indicates a shared folder (ie: not the current user's).
//
result = (((MAPIFolder)anAppointment.Parent).Store != null);
}
return result;
}
This, of course, makes the assumption that if the AppointmentItem is not in a shared folder, it is in a folder that is owner by the current user. I'll need to confirm this, but for now it allows me to move forward.
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
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);
}
}
}
Wednesday, January 09, 2008
Maintaining Custom Toolbar Positions in Outlook 2007
NOTE: This applies to Visual Studio 2008 and Outlook 2007.
There are a few articles floating around have examples on saving toolbar position information at the end of a session and restoring that position the next time Outlook is loaded. One at MSDN applies to Visual Studio 2008 and .NET Framework 3.5:
How to: Maintain Position Information for Custom Toolbars between Outlook Sessions
I tried this code with Outlook 2007 and it throws an exception on shutdown when it tries to save the toolbar position information. Reading the article again, I noticed that it says it applies to Outlook 2003.
I just assumed that it would work with Outlook 2007, and why would they release an article for Visual Studio 2008 that works with an old version of Outlook?
The exception that is being thrown when the CommandBar instance is accessed is:
System.Runtime.InteropServices.COMException: Exception from HRESULT: 0x800A01A8
Look up this HRESULT value and you can see that it means "Object Required".
To me, this means that the CommandBar has already been cleaned up and is no longer accessible. ThisAddIn_Shutdown is supposedly the first user code to be run before a shutdown occurs, so what do you do?
I have settled on listening to the Close event of the main Explorer window (the one the CommandBar is added to). This is called before ThisAddIn_Shutdown and the CommandBar is still accessible.
Example:
...
private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
Outlook.Explorer explorer = this.Application.ActiveExplorer();
if (explorer != null)
{
((ExplorerEvents_10_Event)explorer).Close += new ExplorerEvents_10_CloseEventHandler(ThisAddIn_Close);
commandBar = explorer.CommandBars.Add(TOOLBARNAME,
Office.MsoBarPosition.msoBarFloating, false, true);
...
}
}
private void ThisAddIn_Close()
{
SaveCommandBarSettings();
}
...
Friday, November 30, 2007
Accessing the Current Principal in VSTO Add-In
I recently tried to get access to the Current Principal within an Outlook Add-In. Accessing the Identity was returning a blank name and an unauthorised user. Wierd.
It turns out the the VSTOLoader will load each Add-In into its own AppDomain. As such, you need to set the principal policy explicitly before you access the current principal for the first time.
Example:
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
string userName = Thread.CurrentPrincipal.Identity.Name;
As it running within an isolated environment, I am sure there are other issues you need to be aware of.
See the AppDomain Class in the MSDN Library for more information on application domains.
Thursday, November 29, 2007
Accessing Custom Resource Mailbox Properties With VSTO
In Exchange 2007 you can create custom resource properties to annotate your resources. For example, you may want to create a custom property called Vehicle and add this to all resources that represent vehicles in your Exchange organisation. The TechNet article, How to Create or Remove Custom Resource Properties, describes how this is done.
Getting access to these custom resource properties via Visual Studio Tools for Office (VSTO) is relatively straight forward, as long as you know exactly what you need to do! The problem is finding this with the documentation and newsgroup postings out there.
Anyway, access to this type of information is not available via properties on the Outlook Object Model. However, in VSTO 3 (for Outlook 2007), most Outlook objects have a PropertyAccessor object that allows you to get access to ALL the properties on the underlying MAPI object, not just the ones exposed by the COM interface. The GetProperty method takes a string identifying a property on the object and returns the value as an object. This string identifier consists of a namespace and some sort of property tag value. There are several namespaces that can be used, but the one used here is http://schemas.microsoft.com/mapi/proptag/. More information on referencing properties by namespace can be found in the MSDN article outrageously called Referencing Properties by Namespace.
Finding out the property tag value is next to impossible looking at the MSDN documentation. They simply don't document it, which I found incredibly frustrating. That is, until I found a copy of Outlook Spy. This is an excellent tool that allows you to browse the object model and also get access to the MAPI objects, including the elusive property tags!
Getting back to custom resource properties, I used Outlook Spy to examine one of my vehicle-type recipients. Under the AddressEntity property is a MAPIObject. If you "browse" this property, a window is displayed listing all of the properties of that MAPI object, including the property tag value, the type and its current value. On the right of the window is a field called DASL. This field is the value that you need to pass into the PropertyAccessor.GetProperty() method to extract its value. In my case, the property listing the custom resource properties was using the DASL http://schemas.microsoft.com/mapi/proptag/0x0806101E.
So, once you have this, the code is simple. The following example assumes you already have a Recipient object which is an Equipment resource with the custom property Vehicle.
public static bool IsVehicleResource(Recipient aRecipient)
{
bool result = false;
if (aRecipient.Type == (int)OlMeetingRecipientType.olResource)
{
try
{
//
// Get resource info property via MAPI property accessor.
// NOTE: this will throw an exception if it doesn't exist (ie: not a resource)
//
object resourceInfo = aRecipient.AddressEntry.PropertyAccessor.GetProperty(
"http://schemas.microsoft.com/mapi/proptag/0x0806101E");
//
// Split comma separated info into parts and look for information identifying
// the resource as Equipment and Vehicle.
//
string[] resourceDetails = resourceInfo.ToString().Split(',');
result = (resourceDetails.Contains("Equipment") && resourceDetails.Contains("Vehicle"));
}
catch (System.Exception ex)
{
// TODO: error handling
}
}
return result;
}
And that is it!
For more information on using the PropertyAccessor object, have a look at the MSDN documentation online.

