2

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.

Scott Baker
  • 10,013
  • 17
  • 56
  • 102
  • Marshal.ReleaseComObject() is very ugly manual memory management. The kind that a C programmer has to write and forever gets wrong. Garbage collection was invented to stop getting this wrong all the time. Much the same here, you are calling it on objects that you did not create. Code unwittingly will continue to use a destroyed object, kaboom. Stop helping. Learn how the garbage collector works and learn to trust it, [read this](http://stackoverflow.com/a/17131389/17034). – Hans Passant Sep 17 '15 at 07:39
  • `When you work with recurring appointment items, you should release any prior references, obtain new references to the recurring appointment item before you access or modify the item, and release these references as soon as you are finished and have saved the changes. This practice applies to the recurring AppointmentItem object, and any Exception or RecurrencePattern object.` - just trying to follow that guidance. Believe me.. all this `Marshal.ReleaseComObject()` stuff isn't by choice. – Scott Baker Sep 17 '15 at 08:01
  • @HansPassant - I've read your very excellent post, but I'm still a bit fuzzy. Could you pick a method from above and re-write it properly as an answer? – Scott Baker Sep 17 '15 at 20:01
  • 1
    Just remove all the ReleaseComObject() calls. Observe memory usage of Outlook while you test your addin with realistic data. If you really, really have a problem then just count operations and call GC.Collect() at some value. That code isn't critical. – Hans Passant Sep 17 '15 at 20:08
  • My friend, you need to re-post this as an answer so I can accept it. After removing **all** the `ReleaseComObject()` calls it not only fixed this issue, but another one as well. – Scott Baker Sep 17 '15 at 21:00
  • It is much, much better when this answer comes from you. I don't get this wrong often enough and you have the real world experience. Tell us what went wrong and what got fixed, show us some memory usage statistics, share the email you are thinking of sending to the book author :) – Hans Passant Sep 17 '15 at 21:15

1 Answers1

1

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.

acelent
  • 7,965
  • 21
  • 39