Search code examples
c#comninjectcom-interopvbide

Cleaning up CommandBar buttons


I have an issue with my COM add-in that has been dragging for months, and I can't figure out why.

The IDTExtensibility2 implementation has been peer reviewed by Carlos Quintero (the guy behind MZ-Tools) already, and deemed correct.

Per his recommendations the OnBeginShutdown implementation sets a flag that's checked in OnDisconnection, to ensure ShutdownAddIn only runs once (some VBE host applications don't call OnBeginShutdown, that's why):

public void OnBeginShutdown(ref Array custom)
{
    _isBeginShutdownExecuted = true;
    ShutdownAddIn();
}

My add-in uses Ninject for DI/IoC, and my ShutdownAddIn method boils down to calling Dispose on the Ninject IKernel instance, and then releasing all COM objects with Marshal.ReleaseComObject:

private void ShutdownAddIn()
{
    if (_kernel != null)
    {
        _kernel.Dispose();
        _kernel = null;
    }
    _ide.Release();
    _isInitialized = false;
}

I cannot think of an earlier time to run this code. Yet, when Dispose runs on my commandbar and menu wrappers, I'm getting an InvalidCastException in StopEvents when the commandbar/menus try to dismantle their controls:

public void HandleEvents()
{
    // register the unmanaged click events
    ((Microsoft.Office.Core.CommandBarButton)Target).Click += Target_Click;
}

public void StopEvents()
{
    // unregister the unmanaged click events
    ((Microsoft.Office.Core.CommandBarButton)Target).Click -= Target_Click;
}

public event EventHandler<CommandBarButtonClickEventArgs> Click;
private void Target_Click(Microsoft.Office.Core.CommandBarButton ctrl, ref bool cancelDefault)
{
    // handle the unmanaged click events and fire a managed event for managed code to handle
    var handler = Click;
    if (handler == null)
    {
        return;
    }
    var args = new CommandBarButtonClickEventArgs(new CommandBarButton(ctrl));
    handler.Invoke(this, args);
    cancelDefault = args.Cancel;
}

The InvalidCastException says that it can't cast to IConnectionPoint - and what I've found is that the reason for this is because when this code runs, my Target (the wrapped __ComObject) is gone already, and I'm left with an invalid pointer and a lingering reference to a COM object that no longer exists.

If I catch all exceptions thrown during my teardown process (I have more exceptions stemming from the same root problem, when I try to Delete the buttons and menus), the host application closes but the host process remains - and then I have to kill it from Task Manager. This behavior is consistent with a memory leak caused by not-removed click handlers I think.


Is there a more robust way I can deal with adding/removing event handlers for a Microsoft.Office.Core.CommandBarButton wrapper? Why would my wrapped COM objects be already "gone" when OnBeginShutdown runs, if I haven't released them yet?


Solution

  • I may be wrong, but I don't think InvalidCastException is because some COM object is gone, you would receive “COM object that has been separated from its underlying RCW cannot be used” in that case. InvalidCastException means what it means, that a type cannot be converted to another type, and that can happen not only in the obvious case that the types full names are different, but also I have seen it in edge cases such as

    1) The type full names are the same, but come from different assemblies or even from the same assembly that somehow was loaded twice from different locations. Example: case 1 mentioned in Isolating .NET-based add-ins for the VBA editor with COM Shims

    2) The type full names are the same but have been loaded in different CLRs (2.0 / 4.0) in the same process. Example: The strange case of System.InvalidCastException (“Unable to cast COM object of type ‘System.__ComObject’ to class type System.Windows.Forms.UserControl”) showing toolwindow

    I'd suggest to get the full type name/assembly names/CLR of the types being cast. Adding a temporary reference to Microsoft.VisualBasic reference allows you to use Microsoft.VisualBasic.Information.TypeName(object) to get the actual type behind a __ComObject.