Why do some classes have an "I" in front of their name? - vba

I'm working with some legacy code in Visual Basic 98, and there are several classes with an "I" in front of their name. Most of the classes don't have this name, however.
Here's the contents of the IXMLSerializable.cls file.
' Serialization XML:
' Create and receive complete split data through XML node
Public Property Let SerializationXML(ByVal p_sXML As String)
End Property
Public Property Get SerializationXML() As String
End Property
Public Property Get SerializationXMLElement() As IXMLDOMElement
End Property

Note that VBA supports interfaces, just as C#/VB.NET do (almost). Interfaces are the only way to provide inheritance mechanisms in VBA.
By convention interfaces start their name with the capital letter I.
Here is an example interface declaration that states an object must define a name property
[File: IHasName.cls, Instancing: PublicNotCreatable]
Option Explicit
Public Property Get Name() As String
End Property
As you can see there is no implementation required.
Now to create an object that uses the interface to advertise that it contains a name property. Of course, the point is that there are multiple classes that use the one interface.
[File: Person.cls, Instancing: Private]
Option Explicit
Implements IHasName
Private m_name As String
Private Sub Class_Initialize()
m_name = "<Empty>"
End Sub
' Local property
Public Property Get Name() as String
Name = m_name
End Property
Public Property Let Name(ByVal x As String)
m_name = x
End Property
' This is the interface implementation that relies on local the property `Name`
Private Property Get IHasName_Name() As String
IHasName_Name = Name
End Property
As a convenience in the UI once you include the Implements statement you can choose the interface properties from the top
And to consume the above code use the following test, which calls a function that can take any object that implements IHasName.
[File: Module1.bas]
Option Explicit
Public Sub TestInterface()
Dim target As New Person
target.Name = "John"
GenReport target
' This prints the name "John".
End Sub
Public Function GenReport(ByVal obj As IHasName)
Debug.Print obj.Name
End Function

The I stands for Interface, like specified in the Microsoft Official Documentation:
IXMLDOMElement Members.
The following tables show the properties, methods, and events.
In C++, this interface inherits from IXMLDOMNode.
That was a pretty common convention and by doing so, you immediately know that it represent an Interface, without looking at the code.
Hope this helps.

I stands for interface. VBA and older Visual Basic dialects up to VB 6.0 are said to be object oriented but a have a very poor support for it. For example, there is no class inheritance. Nevertheless, you can declare and implement interfaces in VBA/VB6; however, there is no Interface keyword as there is a Class keyword. Instead, you just declare a class with empty Subs, Functions and Properties.
Example. In a Class named IComparable, declare a Function CompareTo:
Public Function CompareTo(ByVal other As Object) As Long
'Must return -1, 0 or +1, if current object is less than, equal to or greater than obj.
'Must be empty here.
End Function
Now you can declare classes that implement this interface. E.g. a Class named clsDocument:
Implements IComparer
public Name as String
Private Function IComparable_CompareTo(other As Variant) As Long
IComparable_CompareTo = StrComp(Name, other.Name, vbTextCompare)
End Function
Now, this lets you create search and sorting algorithms that you can apply to different class types that implement this method. Example of a class called Document
Option Explicit
Implements IComparable
Public Name As String
Public FileDate As Date
Public Function IComparable_CompareTo(ByVal other As Object) As Long
Dim doc As Document, comp As Long
Set doc = other
comp = StrComp(Me.Name, doc.Name, vbTextCompare)
If comp = 0 Then
If Me.FileDate < doc.FileDate Then
IComparable_CompareTo = -1
ElseIf Me.FileDate > doc.FileDate Then
IComparable_CompareTo = + 1
Else
IComparable_CompareTo = 0
End If
Else
IComparable_CompareTo = comp
End If
End Function
Here an example of a QuickSort for VBA. It assumes that you pass it an array of IComparables:
Public Sub QuickSort(ByRef a() As IComparable)
'Sorts a unidimensional array of IComparable's in ascending order very quickly.
Dim l As Long, u As Long
l = LBound(a)
u = UBound(a)
If u > l Then
QS a, l, u
End If
End Sub
Private Sub QS(ByRef a() As IComparable, ByVal Low As Long, ByVal HI As Long)
'Very fast sort: n Log n comparisons
Dim i As Long, j As Long, w As IComparable, x As IComparable
i = Low: j = HI
Set x = a((Low + HI) \ 2)
Do
While a(i).CompareTo(x) = -1: i = i + 1: Wend
While a(j).CompareTo(x) = 1: j = j - 1: Wend
If i <= j Then
Set w = a(i): Set a(i) = a(j): Set a(j) = w
i = i + 1: j = j - 1
End If
Loop Until i > j
If Low < j Then QS a, Low, j
If HI > i Then QS a, i, HI
End Sub

Related

In VBA how do you compare 2 instances of a User Defined Type (UDT)? I'm getting a Type mismatch

I have a module that defines a UDT as follows:
Private Type UserData
uName As String 'user name
uDate As Date 'date/time of last interaction
End Type
I have a simple test function that I'm trying to use to compare two different instances of the UDT as follows:
Sub TestCheck()
Dim testRec(1) As UserData
testRec(0).uName = "a"
testRec(0).uDate = Date
testRec(1) = testRec(0)
If testRec(1) = testRec(0) Then
Debug.Print "Records match"
Else
Debug.Print "Records don't match"
End If
End Sub
I get Compile error: Type mismatch on testRec(1) = testRec(0)
I really would rather not have to loop through each member of each instance in order to check for equivalency. Aren't UDTs supposed to act as variables? If I have to loop through each member of each instance to compare them, then it really doesn't save anything for me to use the UDTs. Is there a way to do the comparison without looping through the members?
For anyone who has the same question, based on Brian M Stafford's comments, the simple answer is no.
However, here's a simple function to get the job done:
Private Function UserDataEqual(ByRef varA As UserData, ByRef varB As UserData) As Boolean
If varA.uName = varB.uName _
And varA.uDate = varB.uDate Then
UserDataEqual = True
Else
UserDataEqual = False
End If
End Function
It would be used as follows:
Sub TestCheck()
Dim testRec(1) As UserData
testRec(0).uName = "a"
testRec(0).uDate = Date
testRec(1) = testRec(0)
If UserDataEqual(testRec(1), testRec(0)) Then
Debug.Print "Records match"
Else
Debug.Print "Records don't match"
End If
End Sub
Thanks for answering my questions Brian.
This type of activity is what Classes are for. Instead of a user defined type create a class with appropriate methods. Here we have defined a Class called UserData which has a predeclared Id so that we can use the class as a UserData Factory. In this example we have
UserData Class
' User Rubberduck annotations to set the predecalredId to True
'#PredeclaredId
Option Explicit
Public Enum UserDataType
udDate
udName
End Enum
Private Type Properties
UserData As Object
End Type
Private p As Properties
Public Function Make(ByVal ipName As String, ByVal ipDateAndTime As Date) As UserData
With New UserData
Set Make = .Self(ipName, ipDateAndTime)
End With
End Function
Public Function Self(ByVal ipName As String, ByVal ipDateAndTime As Date) As UserData
' Use late bound crreation of a scripting dictionary to avoid reference issues
Set p.UserData = CreateObject("Scripting.Dictionary")
With p.UserData
.Add udName, ipName
.Add udDate, ipDateAndTime
End With
Set Self = Me
End Function
Public Property Get Item(ByVal ipEnum As Long) As Variant
Item = p.UserData.Item(ipEnum)
End Property
Public Property Let Item(ByVal ipEnum As Long, ByVal ipValue As Variant)
p.UserData.Item(ipEnum) = ipValue
End Property
Public Function SameAs(ByVal ipUserData As UserData) As Boolean
SameAs = False
Dim myIndex As Long
For myIndex = 0 To p.UserData.Count - 1
If Me.Item(myIndex) <> ipUserData.Item(myIndex) Then Exit Function
Next
SameAs = True
End Function
This class makes the creation of user data types a bit easier as we can now just say UserData,Make( ,
So the text sub can become
Option Explicit
Public Sub TestCheck()
Dim testRec(1) As UserData
Set testRec(0) = UserData.Make("a", Date)
Set testRec(1) = UserData.Make("b", Date)
If testRec(1).SameAs(testRec(0)) Then
Debug.Print "Records match"
Else
Debug.Print "Records don't match"
End If
End Sub
As you can see. You can see. To change the UserData class for a different set of members you only have to change the enumeration (provided you keep to simple variables).
Simple answer probably what you were looking for (although won't work in your situation I'm afraid, I'll explain):
'#Description("Returns the count of bytes which match over length")
Public Declare PtrSafe Function RtlCompareMemory Lib "ntdll" ( _
ByRef a As Any, _
ByRef b As Any, _
ByVal Length As LongPtr _
) As LongPtr
Called like:
Debug.Assert RtlCompareMemory(a, b, LenB(a)) = LenB(a) 'checks every byte matches
'Or as a function ?UserDataMatch(a,b)
Public Function UserDataMatch(ByRef a As UserData, ByRef b As UserData) As Boolean
UserDataMatch = RtlCompareMemory(a, b, LenB(a)) = LenB(a)
End Function
The catch is this won't work for you because Strings are stored in the UDT as pointers to some variable length block of memory containing their actual values (and these pointers will never match in VBA). So my approach only works for fixed sized UDTs, e.g.:
Public Type GUID
Data1 As Long
Data2 As Integer
Data3 As Integer
Data4(0 To 7) As Byte
End Type
Public Type UDT
IID As GUID 'nested is fine
ClsID As GUID
RefCount As Long
MyClass As Object 'even classes and objects work - if the UDTs point to the same instance
End Type
But not for a UDT which contains a string or some other immutable reference type where the pointers can never be shared between two variables without low level hackery.

Is it possible to change the type of a variable or function in VBA during runtime?

I am riddling about if it is possible to conditionally switch the type of a function or variable between enum types.
Something like this:
Public Enum enmTest
eA = 1
eB = 2
eC = 3
End Enum
Public Enum enmDemo
eA = 10
eB = 50
eC = 100
End Enum
Public Function demoFunction() as enmDemo
Dim eDemo as enmDemo
ReDim eDemo as enmTest
ReDim demoFunction as enmDemo
End Function
'this does not work, but is there no way to make this work?
Public Sub test()
debug.print demoFunction().eA 'should be 1
End Sub
'this does not work, but is there no way to make this work?
Public Sub test2
Dim temp as Variant
temp = demoFunction()
debug.print temp.eB 'should be 2
End Sub
Basically the goal is to have a variable like Dim myVar which might be an enumA or enumB type. These enums might be identicall, except their values.
My guess is this won't work at no angle, because of the way VBA handles enums. But just to make sure I would like to get an explanation, as I only have a gut feeling after an hour of experimenting.
My current workaround, which hopefully demonstrates my goal:
Public Enum enmTest
eA = 1
eB = 2
eC = 3
End Enum
Public Enum enmDemo
eA = 10
eB = 50
eC = 100
End Enum
Public Function demo()
Debug.Print Str(getValues(1)(1)) 'prints 1
Debug.Print Str(getValues(2)(1)) 'prints 10
End Function
Public Function getArray(val1, val2, val3) as Variant
Dim result as Variant
ReDim result(1 to 3)
result(1) = val1
result(2) = val2
result(3) = val3
getArray = result
End Function
Public Function getValues(myInt as Integer) as Variant
If (myInt = 1) Then
getValues = getArray(enmDemo.eA, enmDemo.eB, enmDemo.eC)
Else
getValues = getArray(enmTest.eA, enmTest.eB, enmTest.eC)
End If
End Function
The best I can offer is a custom conversion Function for each Enum type. Although I would echo Dans comment: consider carefully why you want this.
' write one of these for each conversion you want
Function CastToDemo(ByRef v As enmTest) As enmDemo
Select Case v
Case enmTest.eA
CastToDemo = enmDemo.eA
Case enmTest.eB
CastToDemo = enmDemo.eB
Case enmTest.eC
CastToDemo = enmDemo.eC
End Select
End Function
' Use like this
Public Sub test()
Dim a As enmTest
Dim b As enmDemo
a = enmTest.eA
b = CastToDemo(a)
Debug.Print b
End Sub
I know we're half a year later now, but in case someone else finds this...
You could also achieve what you're looking for with classes and interfaces (using the implements keyword) instead of enumerations. It's a little more verbose than enumerations, but it's not as clunky as the conversion options, I think. If you have to use enums for some reason not included in the question, then this doesn't solve your problem. But, if you're just using the enum as a collection of named variable with numeric values, then this should do the trick:
In short, you define an interface (a class) with public read only members for eA, eB, and eC. This spells out what properties each interchangeable "enum" (class) must have.
interface:
' In a class module called IEnm
Public Property Get eA() As Long
End Property
Public Property Get eB() As Long
End Property
Public Property Get eC() As Long
End Property
Then you write another class for each specific "enum" that you're looking for - enmTest and enmDemo. These define the values of each property.
enmTest:
' In a class module called enmTest
Implements IEnm 'promises that this class defines each required property
Public Property Get IEnm_eA() As Long
IEnm_eA = 1
End Property
Public Property Get IEnm_eB() As Long
IEnm_eB = 2
End Property
Public Property Get IEnm_eC() As Long
IEnm_eC = 3
End Property
enmDemo:
' In a class module called enmDemo
Implements IEnm
Public Property Get IEnm_eA() As Long
IEnm_eA = 10
End Property
Public Property Get IEnm_eB() As Long
IEnm_eB = 50
End Property
Public Property Get IEnm_eC() As Long
IEnm_eC = 100
End Property
Here's a demo of how to use it.
Private actsLikeAnEnum As IEnm ' doesn't care if its enmTest, enmDemo,
' or enmSomethingElse
Public Function demoFunction() As IEnm ' you don't know what you'll get out
'Dim eDemo As enmDemo
'ReDim eDemo as enmTest
'ReDim demoFunction as enmDemo
Set actsLikeAnEnum = New enmTest
Set demoFunction = actsLikeAnEnum ' you could just return a new enmTest,
' but I wanted to show that the single IEnm typed variable (actsLikeAnEnum) can
' store both enmTest type objects and enmDemo type objects
End Function
Public Sub test()
Debug.Print demoFunction().eA 'prints 1
End Sub
Public Sub test2()
Dim temp As Variant
' since IEnm is an object, need to use the Set keyword
Set temp = demoFunction()
Debug.Print temp.eB 'prints 2
End Sub
'Or, if you want it to return 10 and 50....
Public Function demoFunctionTwo() As IEnm
Set actsLikeAnEnum = New enmDemo
Set demoFunctionTwo = actsLikeAnEnum
End Function
Public Sub test3()
Debug.Print demoFunctionTwo().eA 'prints 10
End Sub
Public Sub test4()
Dim temp As Variant
Set temp = demoFunctionTwo()
Debug.Print temp.eB 'prints 50
End Sub
You can set actsLikeAnEnum (which is an IEnm type object) to either a new enmDemo or an enmTest because they both implement IEnm. Then you can use actsLikeAnEnum without knowing whether there happens to be an enmDemo object or an enmTest object stored in the variable.

Performance Enum vs class?

i found few new style (for me) to "define" output from select query.
Private Enum Item
ID
Item
Description
End Enum
Private Class Item
Private ID as String
Private Item as String
Private Desc as String
End Class
I 'm thinking of using either one of them. by using class i does not need to re-cast the element type before i display. but Enum seems like easier to understand.
Anyone have some suggestion how to decide?
Enum members are numeric (usually integer, but can be long). But they are not variable and do not change at runtime. So your enum equates to:
Private Enum Item
ID = 0
Item = 1
Description = 2
End Enum
If you want Description to be a string, then a class is a better idea. Enums are used to reference or index something or limit/define a selection. Like:
Public Property Stooge As Stooges
Friend Enum Stooges
Larry
Moe
Curly
Shemp
CurlyJoe
End Enum
The Stooge Property must be one of those values. in code it will show you the text ("moe") but store and integer (1). users will be shown the text in drop downs etc.
You can associate a description with Enum constants:
Public Enum Stooges
<Description("Larry - Funny one")> Larry
<Description("Moe - 'Smart' One")> Moe
<Description("Curly - Sore One")> Curly
<Description("Shemp - One with bad haircut")> Shemp
<Description("CurlyJoe - Last one")> CurlyJoe
End Enum
To get the description for a single one:
Public Shared Function GetDescription(ByVal EnumConstant As [Enum]) As String
Dim fi As Reflection.FieldInfo =
EnumConstant.GetType().GetField(EnumConstant.ToString())
Dim attr() As DescriptionAttribute =
DirectCast(fi.GetCustomAttributes(GetType(DescriptionAttribute),
False), DescriptionAttribute())
If attr.Length > 0 Then
Return attr(0).Description
Else
Return EnumConstant.ToString() ' return enum name if no Descr
End If
End Function
Usage: str = enumHelper.GetDescription(Stooge.Moe) (enumHelper is the name of the calss where the static/shared function resides).
To get a String Array of all the descriptions:
Public Shared Function GetDescriptions(ByVal type As Type) As String()
Dim n As Integer = 0
Dim enumValues As Array
Try
enumValues = [Enum].GetValues(type)
Dim Descr(enumValues.Length - 1) As String
For Each value As [Enum] In enumValues
Descr(n) = GetDescription(value)
n += 1
Next
Return Descr
Catch ex As Exception
MessageBox.Show(ex.Message)
Return Nothing
End Try
End Function
Usage: Dim strEnum As String() = enumHelper.GetDescriptions(GetType(Stooges))
From your question, what you really mean is Struct vs Class. I would default to creating a class. The main reason to use a struct vs a class, is when you need value semantics -- assignment/parameters copies the bits, not a pointer. This is fairly rare in my experience. Unless you have a compelling reason (and you know the difference), go with a class.

Get the name of the object passed in a byref parameter vb.net

How can I get the name of the object that was passed byref into a method?
Example:
Dim myobject as object
sub mymethod(byref o as object)
debug.print(o.[RealName!!!!])
end sub
sub main()
mymethod(myobject)
'outputs "myobject" NOT "o"
end sub
I'm using this for logging. I use one method multiple times and it would be nice to log the name of the variable that I passed to it. Since I'm passing it byref, I should be able to get this name, right?
For minitech who provided the answer:
This would give you the parameter name in the method and it's type, but not the name of the variable that was passed byref.
using system.reflection
Dim mb As MethodBase = MethodInfo.GetCurrentMethod()
For Each pi As ParameterInfo In mb.GetParameters()
Debug.Print("Parameter: Type={0}, Name={1}", pi.ParameterType, pi.Name)
Next
If you put that in "mymethod" above you'd get "o" and "Object".
That's impossible. Names of variables are not stored in IL, only names of class members or namespace classes. Passing it by reference makes absolutely zero difference. You wouldn't even be able to get it to print out "o".
Besides, why would you ever want to do that?
Alternatively you could get the 'Type' of the object using reflection.
Example: (Use LinqPad to execute)
Sub Main
Dim myDate As DateTime = DateTime.Now
MyMethod(myDate)
Dim something As New Something
MyMethod(something)
End Sub
Public Class Something
Public Sub New
Me.MyProperty = "Hello"
End Sub
Public Property MyProperty As String
End Class
Sub MyMethod(Byref o As Object)
o.GetType().Name.Dump()
End Sub
Sorry to say, but this is your solution. I left (ByVal o As Object) in the method signature in case you're doing more with it.
Sub MyMethod(ByVal o As Object, ByVal name As String)
Debug.Print(name)
End Sub
Sub Main()
MyMethod(MyObject, "MyObject")
End Sub
Alternatively you could create an interface, but this would only allow you to use MyMethod with classes you design. You can probably do more to improve it, but as this code stands you can only set the RealName at creation.
Interface INamedObject
Public ReadOnly Property RealName As String
End Interface
Class MyClass
Implements INamedObject
Public Sub New(ByVal RealName As String)
_RealName = RealName
End Sub
Private ReadOnly Property RealName As String Implements INamedObject.RealName
Get
Return _RealName
End Get
End Property
Private _RealName As String
End Class
Module Main
Sub MyMethod(ByVal o As INamedObject)
Debug.Print(o.RealName)
End Sub
Sub Main()
Dim MyObject As New MyClass("MyObject")
MyMethod(MyObject)
End Sub
End Module
If your program is still in the same place relative to the code that made it, this may work:
' First get the Stack Trace, depth is how far up the calling tree you want to go
Dim stackTrace As String = Environment.StackTrace
Dim depth As Integer = 4
' Next parse out the location of the code
Dim delim As Char() = {vbCr, vbLf}
Dim traceLine As String() = stackTrace.Split(delim, StringSplitOptions.RemoveEmptyEntries)
Dim filePath As String = Regex.Replace(traceLine(depth), "^[^)]+\) in ", "")
filePath = Regex.Replace(filePath, ":line [0-9]+$", "")
Dim lineNumber As String = Regex.Replace(traceLine(depth), "^.*:line ", "")
' Now read the file
Dim program As String = __.GetStringFromFile(filePath, "")
' Next parse out the line from the class file
Dim codeLine As String() = program.Split(delim)
Dim originLine As String = codeLine(lineNumber * 2 - 2)
' Now get the name of the method doing the calling, it will be one level shallower
Dim methodLine As String = Regex.Replace(traceLine(depth - 1), "^ at ", "")
Dim methodName = Regex.Replace(methodLine, "\(.*\).*$", "")
methodName = Regex.Replace(methodName, "^.*\.", "")
' And parse out the variables from the method
Dim variables As String = Regex.Replace(originLine, "^.*" & methodName & "\(", "")
variables = Regex.Replace(variables, "\).*$", "")
You control the depth that this digs into the stack trace with the depth parameter. 4 works for my needs. You might need to use a 1 2 or 3.
This is the apparently how Visual Basic controls handle the problem.
They have a base control class that in addition to any other common properties these controls may have has a name property.
For Example:
Public MustInherit Class NamedBase
Public name As String
End Class
Public Class MyNamedType
Inherits NamedBase
public Value1 as string
public Value2 as Integer
End Class
dim x as New MyNamedType
x.name = "x"
x.Value1 = "Hello, This variable is name 'x'."
x.Value2 = 75
MySubroutine(x)
public sub MySubroutine(y as MyNamedType)
debug.print("My variable's name is: " & y.name)
end sub
The output in the intermediate window should be:
My variable's name is: x

Extend Collections Class VBA

I have created a sort function to allow a collection of instances of a custom object to be sorted based on one of the objects properties. Is it possible to extend the existing collections class in VBA? I do not believe inheritance is supported in VBA, so I am not sure how to go about this in the proper way. I could just create a new module and place the function in that module, but that doesn't seem like the best way of doing it.
Thanks for the responses. I ended up creating my own class which extends the Collections class in VBA. Below is the code if anyone is interested.
'Custom collections class is based on the Collections class, this class extendes that
'functionallity so that the sort method for a collection of objects is part of
'the class.
'One note on this class is that in order to make this work in VBA, the Attribute method has to be added
'manually. To do this, create the class, then export it out of the project. Open in a text editor and
'add this line Attribute Item.VB_UserMemId = 0 under the Item() function and this line
'Attribute NewEnum.VB_UserMemId = -4 under the NewEnum() function. Save and import back into project.
'This allows the Procedure Attribute to be recognized.
Option Explicit
Private pCollection As Collection
Private Sub Class_Initialize()
Set pCollection = New Collection
End Sub
Private Sub Class_Terminate()
Set pCollection = Nothing
End Sub
Function NewEnum() As IUnknown
Set NewEnum = pCollection.[_NewEnum]
End Function
Public Function Count() As Long
Count = pCollection.Count
End Function
Public Function item(key As Variant) As clsCustomCollection
item = pCollection(key)
End Function
'Implements a selection sort algorithm, could likely be improved, but meets the current need.
Public Sub SortByProperty(sortPropertyName As String, sortAscending As Boolean)
Dim item As Object
Dim i As Long
Dim j As Long
Dim minIndex As Long
Dim minValue As Variant
Dim testValue As Variant
Dim swapValues As Boolean
Dim sKey As String
For i = 1 To pCollection.Count - 1
Set item = pCollection(i)
minValue = CallByName(item, sortPropertyName, VbGet)
minIndex = i
For j = i + 1 To pCollection.Count
Set item = pCollection(j)
testValue = CallByName(item, sortPropertyName, VbGet)
If (sortAscending) Then
swapValues = (testValue < minValue)
Else
swapValues = (testValue > minValue)
End If
If (swapValues) Then
minValue = testValue
minIndex = j
End If
Set item = Nothing
Next j
If (minIndex <> i) Then
Set item = pCollection(minIndex)
pCollection.Remove minIndex
pCollection.Add item, , i
Set item = Nothing
End If
Set item = Nothing
Next i
End Sub
Public Sub Add(value As Variant, key As Variant)
pCollection.Add value, key
End Sub
Public Sub Remove(key As Variant)
pCollection.Remove key
End Sub
Public Sub Clear()
Set m_PrivateCollection = New Collection
End Sub
One popular option is to use an ADO disconnected recordset as a sort of hyperpowered collection/dictionary object, which has built-in support for Sort. Although you are using ADO, you don't need a database.
I would create a wrapper class that exposes the collection object's properties, substituting the sort function with your own.