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

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.

Related

How to handle a function where the returned value type is not known at run-time (Object or Non-Object)

This question centers around the return value of a call to CallByName. I have a class called PropertyPtr which is meant to act as a generic pointer to an object property. It holds a reference to an Object, and the name of one of its properties. It exposes a Getter and Setter method.
PropertyPtr:
Option Explicit
Public Obj As Object
Public PropertyName As String
Public Sub Setter(Val As Variant)
If IsObject(Val) Then
CallByName Me.Obj, Me.PropertyName, VbSet, Val
Else
CallByName Me.Obj, Me.PropertyName, VbLet, Val
End If
End Sub
Public Function Getter() As Variant
If IsObject(CallByName(Me.Obj, Me.PropertyName, VbGet)) Then
Set Getter = CallByName(Me.Obj, Me.PropertyName, VbGet)
Else
Getter = CallByName(Me.Obj, Me.PropertyName, VbGet)
End If
End Function
In the Getter, my CallByName could return a object or not. But the only way I can see to test if the CallByName value will be an object is to end up running it twice - once to test inside an IsObject and then again to get a reference to the value. The only other way I could see doing this is trapping for an error. Then, you at least only SOMETIMES run the CallByName twice.
My question is: is there some other way to do this without running CallByName twice?
Okay, so if you really want to follow that route then I think you'll have to set an IsObj flag - probably at the point you set the property name.
However, I'd still maintain that using a Variant for either an Object or primitive type isn't a great idea, and the CallByName() function in this context comes with issues. My hesitations are that performance will be diminished and you'll have quite a task to keep the property strings aligned with the property names (should you update things in the future).
It is possible to implement a Mediator Pattern in VBA and I do feel you should consider this route. Below is a really basic example of how you could do it. I haven't bothered with an interface for the mediator, but I have created an interface for my participating classes (to cover the possibility that you're dealing with your own 'groups' of classes).
Mediator class (called cMediator):
Option Explicit
Private mSweets As Collection
Private Sub Class_Initialize()
Set mSweets = New Collection
End Sub
Public Sub RegisterSweet(sweet As ISweet)
Set sweet.Mediator = Me
mSweets.Add sweet
End Sub
Public Sub SendSugarLimit(limit As Long)
Dim sweet As ISweet
For Each sweet In mSweets
sweet.ReceiveSugarLimit limit
Next
End Sub
Public Sub ReceiveMeltingAlert(offender As String)
Dim sweet As ISweet
For Each sweet In mSweets
sweet.ReceiveEatNow offender
Next
End Sub
Participating classes Interface (called ISweet):
Option Explicit
Public Property Set Mediator(RHS As cMediator)
End Property
Public Sub ReceiveSugarLimit(g_perDay As Long)
End Sub
Public Sub ReceiveEatNow(offender As String)
End Sub
My two participating classes (cQtySweet and cWeightSweet):
Option Explicit
Implements ISweet
Public Name As String
Public SugarPerItem As Long
Public CanMelt As Boolean
Private pMediator As cMediator
Public Sub OhNoItsMelting()
pMediator.ReceiveMeltingAlert Name
End Sub
Private Property Set ISweet_Mediator(RHS As cMediator)
Set pMediator = RHS
End Property
Private Sub ISweet_ReceiveEatNow(offender As String)
If CanMelt Then Debug.Print offender & " is melting. Eat " & Name & "s now!"
End Sub
Private Sub ISweet_ReceiveSugarLimit(g_perDay As Long)
Dim max As Long
max = g_perDay / SugarPerItem
Debug.Print "Max " & Name & "s: " & max & "."
End Sub
Option Explicit
Implements ISweet
Public Name As String
Public SugarPer100g As Long
Public CanMelt As Boolean
Private pMediator As cMediator
Public Sub OhNoItsMelting()
pMediator.ReceiveMeltingAlert Name
End Sub
Private Property Set ISweet_Mediator(RHS As cMediator)
Set pMediator = RHS
End Property
Private Sub ISweet_ReceiveEatNow(offender As String)
If CanMelt Then Debug.Print offender & " is melting. Eat " & Name & " now!"
End Sub
Private Sub ISweet_ReceiveSugarLimit(g_perDay As Long)
Dim max As Long
max = g_perDay / (SugarPer100g / 100)
Debug.Print "Max " & Name & ": " & max & "g."
End Sub
Module Code:
Public Sub RunMe()
Dim m As cMediator
Dim qtySweet As cQtySweet
Dim weightSweet As cWeightSweet
Set m = New cMediator
Set qtySweet = New cQtySweet
With qtySweet
.Name = "Gobstopper"
.SugarPerItem = 5
.CanMelt = False
End With
m.RegisterSweet qtySweet
Set qtySweet = New cQtySweet
With qtySweet
.Name = "Wine Gum"
.SugarPerItem = 2
.CanMelt = True
End With
m.RegisterSweet qtySweet
Set weightSweet = New cWeightSweet
With weightSweet
.Name = "Sherbert"
.SugarPer100g = 80
.CanMelt = False
End With
m.RegisterSweet weightSweet
Set weightSweet = New cWeightSweet
With weightSweet
.Name = "Fudge"
.SugarPer100g = 50
.CanMelt = True
End With
m.RegisterSweet weightSweet
'Blasted government has reduced sugar allowance.
Debug.Print "New govt. limits..."
m.SendSugarLimit 200
'Phew what a scorcher - the fudge is melting in my pocket.
Debug.Print "Sweet alarm..."
weightSweet.OhNoItsMelting
End Sub
… and the output looks like this:
New govt. limits...
Max Gobstoppers: 40.
Max Wine Gums: 100.
Max Sherbert: 250g.
Max Fudge: 400g.
Sweet alarm...
Fudge is melting. Eat Wine Gums now!
Fudge is melting. Eat Fudge now!

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

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

Adding a custom class collection to another custom class collection

Ok to start off, I read through this.
It is close although it doesn't answer my specific question. This talks about taking smaller collections and adding items to a larger main collection. Then destroying the smaller collection.
I have two definitions under Class Modules.
TimeDet
Option Explicit
Public recDate As String
Public recQty As String
Public recDieNo As String
Public recCatID As String
Public recCatName As String
Public recGroupID As String
Public recGroupName As String
TimeRec
Option Explicit
Private objTimeRec As Collection
Private Sub Class_Initialize()
Set objTimeRec = New Collection
End Sub
Private Sub Class_Terminate()
Set objTimeRec = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Set NewEnum = objTimeRec.[_NewEnum]
End Property
Public Sub Add(obj As TimeDet)
objTimeRec.Add obj
End Sub
Public Sub Remove(Index As Variant)
objTimeRec.Remove Index
End Sub
Public Property Get Item(Index As Variant) As TimeDet
Set Item = objTimeRec.Item(Index)
End Property
Property Get Count() As Long
Count = objTimeRec.Count
End Property
Public Sub Clear()
Set objTimeRec = New Collection
End Sub
Public Sub FillFromArray(Arr As Variant)
Dim i As Long, obj As TimeDet
For i = 1 To UBound(Arr)
Set obj = New TimeDet
obj.recDate = Arr(i, 1)
obj.recQty = Arr(i, 2)
obj.recDieNo = Arr(i, 3)
obj.recCatID = Arr(i, 4)
obj.recCatName = Arr(i, 5)
obj.recGroupID = Arr(i, 6)
obj.recGroupName = Arr(i, 7)
Me.Add obj
Next
End Sub
Then in the code I am using it this way:
Sub Test()
Dim RecSet1 As TimeRec, Record As TimeDet
Dim fSet1 As TimeRec, fRecord As TimeDet
Dim repArray() As Variant
Dim startDT As Date, endDT As Date, dieNo As String
repArray() = Sheet4.Range("A2:G" & Sheet4.Range("A2").End(xlDown).Row)
Set RecSet1 = New TimeRec
Set fSet1 = New TimeRec
RecSet1.FillFromArray (repArray())
startDT = "1-1-2015"
endDT = "1-1-2016"
dieNo = "16185"
For Each Record In RecSet1
If Record.recDate <= endDT And Record.recDate >= startDT And Record.recDieNo = dieNo Then
fSet1.Add (Record)
End If
Next
End Sub
I am getting an error when I try to add the Record object to the fSet1 object.
"Object doesn't support this method or property"
The Record object is Type TimeDet which as you can see up in the class module my Add method is expecting type TimeDet.
Either I am missing something very simple and have blinders on, or this is a bigger issue.
The array has 200,000 records roughly. I am attempting to create a smaller subset of filtered data. Maybe I am approaching this from the wrong way.
Your error is not at Add but at For Each
Most likely you copied your TimeRec Class. In VBA, you can't create enumerable classes inside the VBE (VBA IDE). There's a different way of creating Enumerable classes.
Open a notepad, copy all your class code and then add this attribute to NewEnum property Attribute NewEnum.VB_UserMemId = -4
Then import the class.
This is always hidden in VBA code, but can be seen in text editors.
Also add this attribute to Item property, it will make it default and allows syntax like ClassName(1)
Attribute Item.VB_UserMemId = 0
So , your code in text editor/ notepad should be:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Class1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit
Private objTimeRec As Collection
Private Sub Class_Initialize()
Set objTimeRec = New Collection
End Sub
Private Sub Class_Terminate()
Set objTimeRec = Nothing
End Sub
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = objTimeRec.[_NewEnum]
End Property
Public Sub Add(obj As Class2)
objTimeRec.Add obj
End Sub
Public Sub Remove(Index As Variant)
objTimeRec.Remove Index
End Sub
Public Property Get Item(Index As Variant) As Class2
Attribute Item.VB_UserMemId = 0
Set Item = objTimeRec.Item(Index)
End Property
Property Get Count() As Long
Count = objTimeRec.Count
End Property
Public Sub Clear()
Set objTimeRec = New Collection
End Sub
The answer to this particular problem was to remove the parenthesis form my Add method. That being said, the attribute info being hidden was really good info and would have probably contributed to the problem after I figured out that removing the parenthesis fixed it.

Should I use two methods with String and Integer,or use TypeOf?

We have a series of collections of objects that all have two fields for sure, an integer "key" and a string "name". We have methods that return a particular instance based on the name or key...
Public ReadOnly Property Inflations(ByVal K as String) As InflationRow
' look for K in the names
End Property
Public ReadOnly Property Inflations(ByVal K as Integer) As InflationRow
' look for K in the keys
End Property
COM interop has the interesting side effect that only the first method with a given name is exported. So we added this...
Public ReadOnly Property Inflations(ByVal K as Object) As InflationRow
Return Inflations(K)
End Property
This leads to some confusion when reading the code, and multiple lines doing the same thing. So what if I replace all of this with...
Public ReadOnly Property Inflations(ByVal K as Object) As InflationRow
If TypeOf K Is String then
'do a string lookup on name
else
'try it on the key
end if
End Property
This does the same thing in the end, but seems much easier to read and keeps all the code in the same place. But...
Most of the calls into this code doesn't come from COM, but our own code. Will many calls to TypeOf in our .net code be significantly slower than allowing the runtime to make this decision through polymorphism? I really don't know enough about the runtime to even guess.
Test it and see! :-)
Option Strict On
Module Module1
Sub Main()
Dim irc As New InflationRowCollection
For i As Integer = 0 To 4999
irc.InflationList.Add(New InflationRow With {.IntProperty = i, .StrProperty = i.ToString})
Next i
Dim t1 As Date = Now
For i As Integer = 0 To 4999
Dim ir1 As InflationRow = irc.Inflations(i)
Dim ir2 As InflationRow = irc.Inflations(i.ToString)
Next i
Dim t2 As Date = Now
For i As Integer = 0 To 4999
Dim ir1 As InflationRow = irc.InflationsObj(i)
Dim ir2 As InflationRow = irc.InflationsObj(i.ToString)
Next i
Dim t3 As Date = Now
Console.WriteLine("Typed property: " & (t2 - t1).TotalSeconds & " sec" & vbCrLf & "Object property: " & (t3 - t2).TotalSeconds & " sec")
Console.ReadKey()
End Sub
End Module
Class InflationRow
Property IntProperty As Integer
Property StrProperty As String
End Class
Class InflationRowCollection
Property InflationList As New List(Of InflationRow)
ReadOnly Property InflationsObj(o As Object) As InflationRow 'use different name for testing, so we can compare
Get
If TypeOf o Is String Then
Return Inflations(DirectCast(o, String))
ElseIf TypeOf o Is Integer Then
Return Inflations(DirectCast(o, Integer))
Else
Throw New ArgumentException
End If
End Get
End Property
ReadOnly Property Inflations(k As String) As InflationRow
Get
For Each ir As InflationRow In InflationList
If ir.StrProperty = k Then Return ir
Next
Return Nothing
End Get
End Property
ReadOnly Property Inflations(k As Integer) As InflationRow
Get
For Each ir As InflationRow In InflationList
If ir.IntProperty = k Then Return ir
Next
Return Nothing
End Get
End Property
End Class

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.