Code Samples, Tips, Tricks and |
All Content Copyright © 2000 New Vision Software All rights reserved |
| |
Instance |
Create Your
Own "Super
Collections" in VB by Bryan Stafford - New Vision Software Have you ever wanted a collection that would allow you to see if a particular key exists without having to trap an error? How about the ability to rename a key without having to first remove the item and then add it back to the collection with the new key? Maybe being able to get the key for a particular item by passing the index would be nice? Or, have you ever needed to be able to use case sensitive key names? What about a collection that starts with an index other than one? These are but a few of the areas where VB’s collection object falls short. I’m sure you are saying to yourself that all of these
things are already possible if you write your own collection class. However, you then loose the ability to iterate the
collection using the For Each…Next syntax unless, of
course, you wrap the VB collection object and delegate the method for your class
to the _NewEnum method of the VB collection.
Unfortunately, this gets you right back where you started, trying to fix
the shortcomings of the VB collection but having to use the VB
collection to do it. In this article I will show you a relatively easily method
for implementing the IEnumVARIANT interface in your VB classes. This allows you to give any VB class the
ability to use the For Each…Next syntax for iterating through items
without having to delegate to VB’s collection.
Once you are able to ditch VB’s collection object, the sky is the limit
when it comes to the amount of power
and flexibility you want to offer in your collections. First, we need to talk about exactly what goes on under the hood when you use a For Each…Next loop to enumerate the objects in a collection. For a collection to work with the For Each...Next syntax, it must have a NewEnum method on it's interface. You can specify a NewEnum method in any VB class by creating a public property with the name "NewEnum" and setting it's procedure ID to (-4) via the procedure attributes dialog in the VB IDE. When your code hits the For Each line in the loop structure, VB calls the NewEnum method of the collection object. This method must return a pointer to an IEnumVARIANT interface. The IEnumVARIANT interface is usually implemented in a separate object created specifically for the enumeration. Once the behind the scenes looping structure has a pointer to the interface, it repeatedly calls the Next method of the interface until this method returns S_FALSE (1) indicating there are no more items to enumerate. Pretty simple so far.
So, why can’t you just implement the IEnumVARIANT interface in your
collection objects and let the enumerator call the Next method on your
class? Good question! There are two reasons you can’t do
this with VB straight out of the box. First, the standard definition of the IEnumVARIANT interface uses syntax
that VB cannot implement. This is why you get the dreaded “Not a valid interface
for Implements” error if you attempt to implement this interface in a VB
class. To get around this, we need
to write a typelib that redefines the syntax of the parameters to the methods in
the interface. This is easily
accomplished using Interface Definition Language (IDL) and the MIDL (Make IDL)
compiler that ships with Visual Studio. (See
"IDL typelib basics" for more info) Second, you would need to be able to return a value
from the Next method of the IEnumVARIANT interface which is declared as a Sub in
VB. You might be asking, “How do
you return a value from a Sub?” Well, VB’s subs are really functions
under the hood. It’s just that VB
handles the return value behind the scenes.
This is because all method calls in VB are implemented via COM and one of
the rules of COM is that all methods must return an HRESULT. VB handles
the return for you, returning info about the success or failure of the call and
because of this, you can't get access to the return value. Things are beginning to look a little bit on the impossible
side, eh? Fortunately,
functions in VB BAS modules allow you direct access to their return value. This still leaves the problem of how to
get the code that is enumerating the collection to call a function in a BAS
module instead of the method on the interface in your collection class. To do this, we use a routine from Bruce
McKinney’s book “Hardcore Visual Basic” that replaces a pointer in a VB
class’ Vtable with a pointer to a function in a BAS module. So, we simply replace the pointer in the
Vtable and the Next method is now delegated to a function in a BAS
module. Now we have a collection class that is a bit hacked up in
the sense that one of the methods is actually residing in a BAS module. The main problem with this approach is
knowing which instance of the class is associated with the current Next
method call from within the Next method since it is now in a totally separate module. Fortunately,
a method call also includes a pointer to the current instance of the class
object (in this case an IEnumVARIANT object) which VB normally hides and references
internally (see "Under the
hood in VB method calls"). Unfortunately, the IEnumVARIANT interface doesn’t
have any methods that we can call to return the current item being enumerated. So how the heck do we get a reference to
our enumerator class in which the IEnumVARIANT interface is implemented. The answer is, we don’t! Instead, we pull a fast one using some
more dirty, underhanded IDL trickery in our typelib. We simply add a method to the interface that allows us to
callback into our class. A bit of
an underhanded hack but it is what
makes this whole scheme work. Here
is the IDL for the interface: interface IEnumVARIANTReDef : IUnknown { HRESULT Next([in] LONG cElements, [in, out] VARIANT* aVariants, [in] LONG lpcElementsFetched); HRESULT Skip([in] LONG cElements); HRESULT Reset(); HRESULT Clone([in, out] IEnumVARIANTReDef** lppIEnum); HRESULT GetItems([in] LONG cElements, [in, out] VARIANT* aVariants, [in] LONG lpcElementsFetched, [in, out] LONG* lRetVal);
}; Notice the GetItems method. If you look at the standard definition of the IEnumVARIANT interface, you will see
that it isn’t there! When you
implement our redefined interface in your class, you get the extra method call. Now, it’s as simple as calling any VB
object to get the data from the correct instance of our enumeration object as
evidenced from the call in the IEnumVARIANT_Next function in the
BAS module:
this.GetItems nDummy, vTmp, nDummy, lRet It doesn’t get any easier than that! The benefits of implementing your own collection objects are unlimited. You can even change the way objects are returned in a For Each...Next loop if you have a special need. Download the fully commented CSuperCollection sample project and kick the VB collection object habit once and for all! Download the CSuperCollection sample project (27KB)
|