Dealing with Circular References
Why you get circular references in VB and two methods to prevent the problem.
VB and COM makes a lot of things easy to do, particularly using and dealing with objects: you just create them and normally they clear themselves up automatically once you're finished with them. However, one side effect of the way COM works is that it is possible to create objects which cannot terminate properly: this problem is known as a "Circular Reference".
The effects of a circular reference can range from using more memory than you ought to (possibly to the extent that you get an Out Of Memory error) to much harsher effects when you are experimenting with API calls.
How VB Determines When to Terminate an Object
Normally, you don't need to worry about object termination. If you create an object using the New operator, you know that VB when the object goes out of scope VB will try to call its _Terminate method (if it has one) and clear up any memory associated with it. You can also set any object reference you have to Nothing which will do the same thing. So how does VB know when there are no outstanding references to the object? After all, you can run code like this:
Dim m_c As New Collection Private Sub Form_Load() Dim c1 As New MyClass c1.Name = "This Will Terminate At The End Of Form_Load" Dim c2 As New MyClass c2.Name = c1.Name m_c.Add c2 End Sub ' c1 will terminate here, but c2 will not
The answer to this is provided by the fundamentals of COM objects. All VB objects are either explicitly created as COM objects (if they are exposed objects compiled into an ActiveX binary) or are created as such internally within VB. All COM objects must follow the COM contract, which means they must implement at least three methods:
The second two of these are of interest to how VB knows when to terminate an object. Whenever the AddRef method of a COM object is called, the object itself must increment an internal reference count by 1. Whenever Release is called, the object decrements the count. When the internal reference count of the object reaches zero then it can be terminated and any resources associated with it cleared up.
A C++ coder using a COM object has complete control over when AddRef and Release are called. This means that you can decide if a particular object always lives within the lifetime of another object, and decide not to call AddRef. This is considerably more flexibility than you have with VB. VB always calls AddRef and Release for you at the correct moments. That is fine, except it results in a problem called Circular References.
Creating a Circular Reference
A circular reference occurs when there are two objects which refer to each other. Say you have a class which manages a series of Worker objects that perform specific tasks, and the Worker objects need to notify their parent, or to interact with data held by the parent. What happens is this:
The only way that these objects can go out of scope is when the VB process finally shuts down. Then VB will terminate all the remaining objects it knows about, and the order in which that occurs is not known. Fine if the order doesn't affect the object VB is terminating, but not so good if you need to ensure if something terminates first (e.g. to close a handle).
The circular reference problem typically occurs when you're trying to create a strongly typed collection of objects for a control. When you modify elements of the object held in the collection typically this has to affect the control itself. However, if the child object has a reference to the control to notify the control about the change, then you have a circular reference.
Resolving Circular References
There are three ways you can go about preventing circular references. These are:
Whilst the event method is a possible solution, it is not covered here. The problem with using events is that the events have to be wired up at design time. If you don't know how many child objects you're going to have then it is all but impossible to design a solution to fix this.
The other two solutions are demonstrated in the example projects and are covered in turn.
Providing a "Dispose" Style Method
The problem with a circular reference is there is no event which automatically occurs when the owner of the Parent object releases its reference. Clearly, however, you can provide a method in the Parent object which goes through any child Worker objects and terminates the object reference. By doing this, the Worker objects release their references on the parent, so the additional reference count is removed, and then the Parent object can terminate as soon as it goes out of scope.
This method is demonstrated in the Fix 1 download. However, whilst it works ok, it has two distinct disadvantages:
Ideally there would be a way of coding these objects which could be used in a normal VB style by any user. Luckily there is, but you need to incorporate a few hacks.
Using Non-Counted References
This method is the equivalent of what a C++ coder would do when they knew they did not have to call AddRef or Release on an object reference because its scope could never go outside the bounds of the parent object. There are two ways you can go about doing this:
COM reference counting and the circular reference problem associated with it are one of the major bugbears with all COM related projects. You will note that the .NET Framework, like Java, does away with reference counting altogether and takes a completely different approach to managing objects in memory. That means you do have no choice but to consider the "Dispose" route if you need to be certain when objects terminate in .NET, but at least it is obvious that this will be a problem and there are certain constructs, such as the IDisposable interface and the using language features that make it more likely that these will be used correctly.
This article demonstrates how you get a circular reference and a number of techniques for removing the problem. Although all have a measure of danger, so do circular references with no attempt at resolution. If used judiciously, you should find the techniques presented here will stop these problems in their tracks.