Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Lingering COM Objects Caused by ActiveX Control Event Handlers

0.00/5 (No votes)
14 Mar 2006 1  
This article describes how, under some circumstances, an ActiveX control event handler can cause COM objects to linger.

Sample Image

Introduction

This article demonstrates a subtle problem that can occur when using ActiveX controls in a managed application.

I first ran into this problem while using a third party ActiveX control in one of my own C# programs. Occasionally, when I attempted to access one of my unmanaged resources via a COM object, I found that it was in a locked state. Based on former experience with the control, the components, and several unmanaged VB6 programs that used them, I knew that the existing unmanaged code was unlikely to be the cause of the problem. In fact, I wrote several test programs in VB6 to try and reproduce the problem, but the problem did not show up until I moved to a C# Windows Forms application.

After careful investigation, I found that extra references where being held on the COM objects by the .NET runtime callable wrappers (RCW) for the objects. This occurred even though I never directly accessed the COM object in the managed .NET code.

The demo project included with this article demonstrates when the problem occurs, and some workarounds.

Background

This article assumes that you have basic familiarity with ActiveX controls. In addition, it is assumed that you understand how to create and work with a simple C# Windows Forms application that uses an ActiveX control. Although the ActiveX control included with the demo project is written in C++ using ATL, the underlying code is kept as simple as possible.

The Demo Project

The project included with this article gives a simple example that demonstrates the problem. The project consists of three pieces:

  1. An unmanaged DLL that houses an ActiveX control and a COM component. (InfoSource)
  2. A managed C# Windows Forms application that makes use of the ActiveX control. (ClientApp)
  3. A container solution to build the two projects. (BuildAll)

The managed C# program, ClientApp, uses the ActiveX control CDataViewer. Internally, CDataViewer uses a CDataInfo COM object to manage the underlying data. To keep the demo program straightforward, the ActiveX control simply exposes an edit control, while the underlying CDataInfo object represents the contents of a text file. The CDataViewer control and the CDataInfo object are housed in the InfoSource DLL project.

When the viewer is opened, it creates an instance of a CDataInfo COM object with the file name passed in by the user. The CDataInfo object attempts to open the file for exclusive access. If the open fails, the viewer returns an error and a message is displayed.

To keep the logic simple, the viewer does not attempt to read the existing file contents when opened. This logic could be added, but it does not affect the concepts I'm demonstrating here.

Whenever the user types into the edit control, the CDataInfo object is updated to reflect the current state of the viewer. This update is performed in memory to avoid constantly writing the data to disk. In addition, whenever a change occurs in the control, a DataChanged event is fired off to any client that has connected to the ActiveX control's event source. The argument to the event handler routine, newVal, is a reference to the internal CDataInfo object. This is an important point to remember.

__interface _IDataViewerEvents
{
    [id(1), helpstring("method DataChanged")] 
            HRESULT DataChanged([in] IDispatch* newVal);
};

When the viewer is closed, it releases its reference on the underlying CDataInfo object. When the reference count goes to zero and the CDataInfo object is destroyed, it flushes its contents to disk and the file is closed.

To build the project, you'll need Visual Studio 2005. Open the BuildAll project and rebuild the entire solution. This will automatically build and register the COM component, the ActiveX control, and the C# application. You can then run the ClientApp application.

This project was built and tested on Windows XP Service Pack 2.

Demonstrating the Problem

To see the problem in action, start up the client application and select "Open" to open a file. Type something into the edit control and click on the "Close" button. Then, quickly attempt to open the same file again. Every so often, you'll get an error as shown in the figure below. You won't see the error every time, so try a few times if you don't see it immediately. Remember to type into the edit control each time, or the ActiveX control data change events will not be fired.

Sample Image

When this error occurs, it's an indication that the underlying file could not be reopened. The question is, why can't it be opened? What's locking the file?

Tracking Down the Problem

By using the Sysinternals.com "Process Explorer" tool, I quickly realized that the file was not always closed when the "Close" button was pressed. Sometimes the file would remain open for several seconds after "Close". Turning to Process Explorer once again, it was apparent that the file was not closed until the application went through an additional .NET garbage collection. Clearly, the .NET runtime was creating a reference to the underlying CDataInfo object and not releasing it until the next garbage collection. I verified this by adding debug code to force a garbage collection after every close. With the forced garbage collection code in place, the file locking problem went away.

Armed with this information, I tried to figure out what could be causing the reference to the underlying data object. The object was not used or referenced anywhere in the managed code. Eventually, I focused in on the data changed event. This was the only place where the C# code could interact with the underlying CDataInfo object. I still had serious doubts though; I had not hooked up an event handler for the data changed event.

After further testing, I concluded that it didn't matter that there was no explicit event handler. The Windows Forms application must be hooking up to the event source, even though my application didn't use the event. I verified this by explicitly adding an event handler with code to free the COM object. The event handler routine is shown below:

/// This routine is called whenever the data

/// is changed in the Active X viewer. This occurs whenever 

/// the user types into the edit control. 

///

/// Arguments:

/// sender - The ActiveX control object

/// e - The event object (which contains

///     the CDataInfo object reference)


private void CDataViewer_DataChanged(object sender, 
        AxInfoSource._IDataViewerEvents_DataChangedEvent e)
{
    // In this event handler,

    // e.newVal is the object pointer for 

    // the CDataInfo object from the ActiveX control.


    // If desired, use the object to get

    // its data contents (what the user typed)

    // string str = ((CDataInfo)e.newVal).DataContents;


    // Decrement the reference count

    // on the runtime callable wrapper.

    // If we don't do this, the object

    // will linger and the underlying file object 

    // will remain open until a garbage collection occurs.

    int i = System.Runtime.InteropServices.
              Marshal.ReleaseComObject(e.newVal);
}

With the ReleaseComObject call in place, the file was never locked after the "Close" operation. The additional references to the COM object in the managed code were eliminated. It was no longer necessary to wait for a garbage collection.

In the demo project, you can experiment with the different scenarios by using the checkboxes for the data-changed event handler. You can also force a .NET garbage collection by using the "Force GC" button.

The Technical Details

Once I understood the problem, I decided to investigate further to understand the cause. Since the problem was clearly occurring in the interop layer between the ActiveX control and the .NET form, I modified the projects to explicitly create the interop assemblies using Aximp.exe instead of using the Toolbox in Visual Studio.

Aximp.exe is a utility that generates interop assemblies for ActiveX controls. It is included with Visual Studio. One of the benefits of using Aximp.exe is that you have the option of generating the C# source code for the Windows Forms wrapper. The demo project makes use of Aximp.exe.

With the source code and a debugger, it's easy to see what's happening. Use the debugger to put a breakpoint in the functions CreateSink and RaiseOnDataChanged. These functions are members of the form's control wrapper class AxCDataViewer. This class is used by the managed application to interact with the ActiveX control CDataViewer. When you run the program with the debugger, you'll see that CreateSink is called immediately upon the form creation (in the method InitializeComponent). CreateSink hooks up to the COM event source so that the COM events can be forwarded to any .NET client. What's important to notice is that CreateSink is always called, whether or not any .NET clients are subscribing to the events.

The code to determine whether or not to forward the events to a .NET client is in the RaiseOnDataChanged function. You can experiment with the RaiseOnDataChanged logic by using the "Use Event Handler" checkbox.

Since the COM events are always handled, even when they are not forwarded to .NET clients, a runtime callable wrapper (RCW) is used to manage the CDataInfo COM object that is passed as an argument to the event handler routine. It's this RCW that lingers until the next garbage collection.

The figure below illustrates the concept:

Concept

The AxCDataViewer.CreateSink function sets up the event sink for the CDataViewer ActiveX control events. The COM event sink is always connected at the form initialization time.

The AxCDataViewer.RaiseOnDataChanged function forwards the COM events as .NET events if there are any subscribing clients.

Some Workarounds

Let's consider some options to work around the problem.

  1. Add an explicit event handler with code to release any COM objects that hold critical resources. Repeat this for every event handler that is causing a problem.
  2. Force a garbage collection when required. (Not necessarily recommended.)
  3. Redesign your unmanaged components to provide explicit methods to free resources, instead of waiting for the reference count to drop to zero.
  4. Redesign your ActiveX controls so the event handler routine arguments don't include COM components that lock resources until the final destruction. Better yet, don't pass COM components at all. Let the client request a component reference when necessary.
  5. Create your own custom ActiveX control wrapper class. You can start with the output source from Aximp.exe.

Obviously, options 3 and 4 are not always practical. If you are using a third party control without access to the source, code modification is impossible.

Conclusion

This article demonstrated how ActiveX events in Windows Forms applications can cause COM objects to linger while waiting for a .NET garbage collection.

Obviously, this situation does not apply to all ActiveX controls. However, if your ActiveX control exposes events to .NET clients, and those events pass along COM objects that lock critical resources, you might find yourself tracking down the same problem presented in this article. As mentioned above, you might encounter this problem even if you have not explicitly hooked up an event handler for the control.

Hopefully, the concepts presented in this article will help you to understand the problem and avoid it in your own code.

History

  • 03-Mar-2006 - Original.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here