Search code examples
comoutlookoutlook-addin

Outlook Add-In :: COM object that has been separated from its underlying RCW cannot be used


While I have found many instances of this question on SO, none of the solutions I have implemented have solved my problem; hopefully you can help me solve this riddle. Note: This is my first foray into the world of COM objects, so my ignorance is as deep as it is wide.

As a beginning, I am using Adrian Brown's Outlook Add-In code. I won't duplicate his CalendarMonitor class entirely; here are the relevant parts:

public class CalendarMonitor
{
    private ItemsEvents_ItemAddEventHandler itemAddEventHandler;
    public event EventHandler<EventArgs<AppointmentItem>> AppointmentAdded = delegate { };

    public CalendarMonitor(Explorer explorer)
    {
        _calendarItems = new List<Items>();
        HookupDefaultCalendarEvents(session);
    }

    private void HookupDefaultCalendarEvents(_NameSpace session)
    {
        var folder = session.GetDefaultFolder(OlDefaultFolders.olFolderCalendar);
        if (folder == null) return;

        try
        {
            HookupCalendarEvents(folder);
        }
        finally
        {
            Marshal.ReleaseComObject(folder);
            folder = null;
        }
    }

    private void HookupCalendarEvents(MAPIFolder calendarFolder)
    {
        var items = calendarFolder.Items;

        _calendarItems.Add(items);

        // Add listeners
        itemAddEventHandler = new ItemsEvents_ItemAddEventHandler(CalendarItems_ItemAdd);
        items.ItemAdd += itemAddEventHandler;
    }

    private void CalendarItems_ItemAdd(object obj)
    {
        var appointment = (obj as AppointmentItem);
        if (appointment == null) return;

        try
        {
            AppointmentAdded(this, new EventArgs<AppointmentItem>(appointment));
        }
        finally
        {
            Marshal.ReleaseComObject(appointment);
            appointment = null;
        }
    }

Bits not relevant to adding appointments have been redacted.

I instantiate the CalendarMonitor class when I spool up the Add-in, and do the work in the AppointmentAdded event, including adding a UserProperty to the AppointmentItem:

private void ThisAddIn_Startup(object sender, EventArgs e)
{
    _calendarMonitor = new CalendarMonitor(Application.ActiveExplorer());
    _calendarMonitor.AppointmentAdded += monitor_AppointmentAdded;
}

private async void monitor_AppointmentAdded(object sender, EventArgs<AppointmentItem> e)
{
    var item = e.Value;

    Debug.Print("Outlook Appointment Added: {0}", item.GlobalAppointmentID);

    try
    {
        var result = await GCalUtils.AddEventAsync(item);

        //store a reference to the GCal Event for later.
        AddUserProperty(item, Resources.GCalId, result.Id);

        Debug.Print("GCal Appointment Added: {0}", result.Id);
    }
    catch (GoogleApiException ex)
    {
        PrintToDebug(ex);
    }
    finally
    {
        Marshal.ReleaseComObject(item);
        item = null;
    }
}

The error is thrown here, where I try to add a UserProperty to the AppointmentItem. I have followed the best example I could find:

private void AddUserProperty(AppointmentItem item, string propertyName, object value)
{
    UserProperties userProperties = null;
    UserProperty userProperty = null;

    try
    {
        userProperties = item.UserProperties;
        userProperty = userProperties.Add(propertyName, OlUserPropertyType.olText);
        userProperty.Value = value;
        item.Save();
    }
    catch (Exception ex)
    {
        Debug.Print("Error setting User Properties:");
        PrintToDebug(ex);
    }
    finally
    {
        if (userProperty != null) Marshal.ReleaseComObject(userProperty);
        if (userProperties != null) Marshal.ReleaseComObject(userProperties);
        userProperty = null;
        userProperties = null;
    }
}

... but it chokes on when I try to add the UserProperty to the AppointmentItem. I get the ever-popular error: COM object that has been separated from its underlying RCW cannot be used. In all honesty, I have no idea what I'm doing; so I'm desperately seeking a Jedi Master to my Padawan.


Solution

  • The main problem here is using Marshal.ReleaseComObject for RCW's that are used in more than one place by the managed runtime.

    In fact, this code provoked the problem. Let's see class CalendarMonitor:

        private void CalendarItems_ItemAdd(object obj)
        {
            var appointment = (obj as AppointmentItem);
            if (appointment == null) return;
    
            try
            {
                AppointmentAdded(this, new EventArgs<AppointmentItem>(appointment));
            }
            finally
            {
                Marshal.ReleaseComObject(appointment);
    

    After the event returns, it tells the managed runtime to release the COM object (from the point of view of the whole managed runtime, but no further).

                appointment = null;
            }
        }
    

    Then, an async event is attached, which will actually return before using the appointment, right at the await line:

    private async void monitor_AppointmentAdded(object sender, EventArgs<AppointmentItem> e)
    {
        var item = e.Value;
    
        Debug.Print("Outlook Appointment Added: {0}", item.GlobalAppointmentID);
    
        try
        {
            var result = await GCalUtils.AddEventAsync(item);
    

    This method actually returns here. C#'s async code generation breaks async methods at await points, generating continuation passing style (CPS) anonymous methods for each block of code that handles an awaited result.

            //store a reference to the GCal Event for later.
            AddUserProperty(item, Resources.GCalId, result.Id);
    
            Debug.Print("GCal Appointment Added: {0}", result.Id);
        }
        catch (GoogleApiException ex)
        {
            PrintToDebug(ex);
        }
        finally
        {
            Marshal.ReleaseComObject(item);
    

    Look, it's releasing the COM object again. No problem, but not optimal at all. This is an indicator of not knowing what is going on by using ReleaseComObject, it's better to avoid it unless proven necessary.

            item = null;
        }
    }
    

    In essence the use of ReleaseComObject should be subject to a thorough review of the following points:

    • Do I need to actually make sure the managed environment releases the object right now instead of at an indeterminate time?

      Occasionally, some native objects need to be released to cause relevant side effects.

      For instance, under a distributed transaction to make sure the object commits, but if you find the need to do that, then perhaps you're developing a serviced component and you're not enlisting objects in manual transactions properly.

      Other times, you're iterating a huge set of objects, no matter how small each object is, and you may need to free them in order to not bring either your application or the remote application down. Sometimes, GC'ing more often, switching to 64-bit and/or adding RAM solves the problem in one way or the other.

    • Am I the sole owner of/pointer to the object from the managed environment's point of view?

      For instance, did I create it, or was the object provided indirectly by another object I created?

      Are there no further references to this object or its container in the managed environment?

    • Am I definitely not using the object after ReleaseComObject, in the code that follows it, or at any other time (e.g. by making sure not to store it in a field, or closure, even in the form of an iterator method or async method)?

      This is to avoid the dreaded disconnected RCW exception.