Something like a Public constant populated dictionary - vba

In VBA what's the best way to declare a Public constant collection/dictionary of elements with multiple properties like this?
Dim fruits as new dictionary
fruits.add "banana", array("yellow", "long", "curved")
fruits.add "watermelon", array("red", "big", "sferic")
fruits.add "blueberry", array("blue", "little", "sferic")
I could change the Dim fruits as new dictionaryintoPublic fruits as new dictionary moved on top (outside procedure) but how could I populate this dictionary once for multiple sub/functions that will use it?
I could put all the three "add" instructions in a dedicated sub called "fruits_populate()", and call this sub at the beginning in each sub/function where I use it but is there a better solution?

One solution would be to simulate a memoised getter:
Public Function FRUITS() As Dictionary
Static obj As Dictionary ' Static keeps the object between calls '
If obj Is Nothing Then
Set obj = New Dictionary
obj.add "banana", Array("yellow", "long", "curved")
obj.add "watermelon", Array("red", "big", "sferic")
obj.add "blueberry", Array("blue", "little", "sferic")
End If
Set FRUITS = obj
End Sub
Then to get an item:
Debug.Print FRUITS.Item("banana")(1)
Another way would be to implement a class module ClsFruits.cls :
Dim base As Dictionary
Private Sub Class_Initialize()
Set base = New Dictionary
base.add "banana", Array("yellow", "long", "curved")
base.add "watermelon", Array("red", "big", "sferic")
base.add "blueberry", Array("blue", "little", "sferic")
End Sub
Public Property Get Item(Key)
Item = base.Item(Key)
End Property
Then to get an item:
Dim fruits As New ClsFruits
Sub Test()
Debug.Print fruits.Item("banana")(1)
End Sub

Originally a comment, but it grew too long:
1) It is easy to declare a public dictionary (as you already know), but
2) It is not possible to initialize it with a literal -- you need to run some set-up code that runs before any other code. Workbook_Open() is a natural place for such code.
3) There is no way to lock it down as constant -- dictionaries are mutable, but
4) If you really want, you could define your own class of objects that refuse to update themselves.
To expand on point 2). It is probably still a good idea to write a dedicated sub such as fruits_populate(). Put that code in a public module, and then in the Workbook code module put:
Private Sub Workbook_Open()
fruits_populate
End Sub
The advantage of doing it this way is that if you get to the point of doing robust error-handling, you might want to be able to bounce back from loss of state (e.g. a runtime error causes the project to reset), in which case you might have reason to call fruits_populate() from some error-handling code.

You can create your own Fruits object (dictionary).
You can set the VB_PredeclaredId set to True to act as a global default instance (access it from anywhere in your program) and the Item as the default member so you don't have to call it each time.
To do this:
Create a Class Module
Export
Replace with below code
Import
Fruits Class:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Fruits"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Private m_fruits As Object
Public Property Get Item(ByVal Name As String) As Variant
Attribute Item.VB_UserMemId = 0
Item = m_fruits(Name)
End Property
Public Property Let Item(ByVal Name As String, ByVal Value As Variant)
m_fruits(Name) = Value
End Property
Public Sub Clear()
m_fruits.RemoveAll
End Sub
'For testing - can omit
Public Function Names() As Variant
Names = m_fruits.Keys
End Function
Private Sub Class_Initialize()
Set m_fruits = CreateObject("Scripting.Dictionary")
End Sub
A simple test:
Sub Test()
Fruits("Banana") = Array("yellow", "long", "curved")
Fruits("Watermelon") = Array("red", "big", "sferic")
Fruits("Blueberry") = Array("blue", "little", "sferic")
PrintFruits
Fruits.Clear
End Sub
Private Sub PrintFruits()
Dim d As Variant, idx As Integer
For Each d In Fruits.Names()
Debug.Print "Fruit: " & d
For idx = 0 To UBound(Fruits(d))
Debug.Print String(3, " ") & Fruits(d)(idx)
Next idx
Next d
End Sub
'Output:
'Fruit: Banana
'yellow
'long
'curved
'Fruit: Watermelon
'red
'big
'sferic
'Fruit: Blueberry
'blue
'little
'sferic

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!

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.

3 Dimentional Dictionary

I'm trying make to a 3 Dimension Dictionary to store the data in the form of tools(material)(part)(attribute), and I have managed to create the Dictionary like this:
Dim Tools As New Dictionary(Of String, Dictionary(Of String, Dictionary(Of String, Decimal)))
And what I basically want to do is have some subs that manage that for me instead of dealing with that mess, and I want it to be like this like this:
Add_Attribute("Iron", "Pickaxe Head", "Durability", 204)
Get_Attribute("Stone", "Pickaxe Head", "Mining Speed")
Any answers would be greatly be appreciated.
My comment was not worded properly.
Create a class with add/get attributes function that accepts 3 parameters.
Concatenate the parameters and use it as dictionary key.
Option Explicit
Dim oDict As Dictionary
Public Function Add_Attribute(psParam1 As String, psParam2 As String, psParam3 As String, psValue As String)
Dim sKey As String
sKey = BuildKey(psParam1, psParam2, psParam3)
If oDict.Exists(sKey) Then
oDict.Item(sKey) = psValue
Else
oDict.Add sKey, psValue
End If
End Function
Public Function Get_Attribute(psParam1 As String, psParam2 As String, psParam3 As String) As String
Dim sKey As String
sKey = BuildKey(psParam1, psParam2, psParam3)
If oDict.Exists(sKey) Then
Get_Attribute = oDict.Item(sKey)
Else
Get_Attribute = ""
End If
End Function
Private Sub Class_Initialize()
Set oDict = New Dictionary
End Sub
Private Function BuildKey(psParam1 As String, psParam2 As String, psParam3 As String) As String
BuildKey = Join(Array(psParam1, psParam2, psParam3), "#")
End Function
Private Sub Class_Terminate()
Set oDict = Nothing
End Sub
Jules' answer of a custom class and concatenation of the three strings as a key will work very nicely for you and is a neat solution to your problem.
I'm posting another answer here for anyone who wants more of a dot notation style of solution. So one of the lines in your example could look something like:
mTools("Pickaxe Head").Attr("Durability").Material("Iron") = 204
I'm guessing you're deriving the values from a comboxbox or something similar, so working with strings might serve you fine. However, if you wished, you could go one stage further and create objects for the Attributes and Material parameters to achieve true dot notation (I didn't do the Parts parameter but you could do that one too):
mTools("Pickaxe Head").Durability.OnIron = 204
From a development point of view, the time consuming part would be to create all the parameter objects and keys, but if you are intending to manipulate the data anything more than trivially, it could make your life easier further down the track.
For your own project, are you certain that the data is genuinely 3 dimensional? Perhaps it's just the variable names that you've picked, but it seems as though you have one main object, ie the part (Pickaxe Head) which has some attributes (Durability and Mining Speed) which themselves have values based on the material they're operating on (Stone and Iron). Structurally, could it look like this?:
In terms of the code for this solution, create three classes. I've called them clsKeys, clsMaterials and clsPart.
For your clsKeys, the code is simply your field names:
Public Durability As String
Public MiningSpeed As String
Public Iron As String
Public Stone As String
For clsPart, the code contains the object names and a means of accessing them by string:
Public Name As String
Public Durability As New clsMaterials
Public MiningSpeed As New clsMaterials
Private mProperties As New Collection
Public Property Get Attr(field As String) As clsMaterials
Set Attr = mProperties(field)
End Property
Private Sub Class_Initialize()
With Keys
mProperties.Add Durability, .Durability
mProperties.Add MiningSpeed, .MiningSpeed
End With
End Sub
clsMaterials is similar:
Public OnStone As Integer
Public OnIron As Integer
Private mProperties As New Collection
Public Property Let Material(field As String, value As Variant)
mProperties.Remove field
mProperties.Add value, field
End Property
Public Property Get Material(field As String) As Variant
Material = mProperties(field)
End Property
Private Sub Class_Initialize()
With Keys
mProperties.Add OnStone, .Stone
mProperties.Add OnIron, .Iron
End With
End Sub
These classes can take as many objects as you like. You'll note I've instantiated the objects in the declaration which isn't best form but I've done it in the interest of space.
Finally, in a Module you need 3 routines: one to create the field keys, one to populate the data and one to retrieve it.
For the keys:
Option Explicit
Public Keys As clsKeys
Private mTools As Collection
Sub CreateKeys()
Set Keys = New clsKeys
With Keys
.Durability = "Durability"
.MiningSpeed = "Mining Speed"
.Iron = "Iron"
.Stone = "Stone"
End With
End Sub
For data population:
Sub PopulateData()
Dim oPart As clsPart
Set mTools = New Collection
Set oPart = New clsPart
With oPart
.Name = "Pickaxe Head"
'You could use dot notation
.Durability.OnIron = 204
.Durability.OnStone = 100
'Or plain strings
.Attr("Mining Speed").Material("Stone") = 50
.Attr("Mining Speed").Material("Iron") = 200
mTools.Add oPart, .Name
End With
End Sub
and for data retrieval:
Sub RetrieveValue()
Dim oPart As clsPart
Dim v As Variant
Set oPart = mTools("Pickaxe Head")
With oPart
'Using dot notation
v = oPart.Durability.OnIron
Debug.Print v
'Using plain strings
v = oPart.Attr("Durability").Material("Stone")
Debug.Print v
End With
'Or even without assigning the oPart variable
v = mTools("Pickaxe Head").Attr("Mining Speed").Material("Iron")
Debug.Print v
End Sub

Clean Initialization of Scripting.Dictionary

I'm using VBA in an application called ProcessBook and I'm looking to store some static information in a dictionary.
I'm trying to initialize this dictionary cleanly and without needing to call a separate procedure, but none of the methods I try seem to be expected.
Here's what I've tried so far:
Dim myDict As New Dictionary(Of String, String) (("key1", "item1"), ("key2", "item2"))
Dim myDict As Variant
Set myDict = New Dictionary( ("key1", "item1"), ("key2", "item2") )
And basically a bunch of guessing involving forms like that.
So far the best I've been able to do is something like:
With myDict
.add "key1", "item1"
.add "key2", "item2"
End With
But this barks at me when it's not inside of a routine, which I'd like to avoid.
I believe the (Of type, type) From { } syntax is beyond VBA 6.5
Does anyone know of some clean ways to initialize a scripting.dictionary in VBA 6.5?
By 6.5 presumably you mean VBA? If so there is no way to manipulate/initialise an object instance outside of a routine.
The only way is to encapsulate it within a class (cDict) and use the constuctor:
Private mDict As Scripting.Dictionary
Private Sub Class_Initialize()
Set mDict = New Scripting.Dictionary
With mDict
.Add "key1", "item1"
.Add "key2", "item2"
End With
End Sub
Public Property Get Dict() As Scripting.Dictionary
Set Dict = mDict
End Property
Then in the client module;
Private Dict As New cDict
Sub foo()
MsgBox Dict.Dict.Item("key1")
Dict.Dict.Add "key3", "item3"
MsgBox Dict.Dict.Item("key3")
End Sub
(Of type relates to VB.Net Generics)
VBA is really a PROCEDURAL language more then an Object Oriented Language so the answer is to use a procedure.
How to do it...
Option Explicit
Option Compare Text
Sub test()
Dim dic As New Scripting.Dictionary
Add dic, Array("Darren", 123, "Alex", 321)
MsgBox dic.Count
End Sub
Public Sub Add(dic As Dictionary, values As Variant)
If Not IsArray(values) Then
Err.Raise -1, "Add", "Values must be varient Array"
ElseIf UBound(values) Mod 2 = 0 Then
Err.Raise -1, "Add", "Even number of values required"
End If
Dim iQ As Long
For iQ = 0 To UBound(values) Step 2
dic.Add values(iQ), values(iQ + 1)
Next
End Sub

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.