excel 2003 vba.. writing arraylist - excel

I am trying to create ArrayList of Class like in Java, in Visual Basic Excel 2003.
Java
List<Employee> employees = new ArrayList<Employee>();
Employee employee = new Employee();
employee.setName("tom");
employees.add(employee);
VB
Dim resultList As New Collection
Dim Manager As Employee
Manager.Name = "df"
resultList.Add ("rr") 'correct
resultList.Add (Manager) 'error
But this gives the following error:
only user-define types defined in public object modules can be coerced
to or from a variant or passed to late-bound functions

There is no type information associated with a UDT so it can't be added to a collection as there is no way to reliably convert to/from a variant as the number & types of its members is unknown.
You can either replace the Employee Type with a Class or as you don't appear to be using a key, a typed array: arr() as Employee

Related

How can I create a proper Collection in VBA?

I am trying to convert a large 3 dimensioned Array into a series of class modules. I have each next class stored as an array in the previous class. It goes like Brand -> Products -> Lots.
I have successfully created this interaction and can access them by name like:
Sub test()
Dim MyBrand As Brand
Set MyBrand = New Brand
MyBrand.Name = "Company1"
MyBrand.AddProduct "Shoes"
MyBrand.Products("Shoes").AddLot "240502"
MsgBox MyBrand.Products("Shoes").Lots(0) 'Correctly Displays "240502"
End Sub
But then I wanted to create an object group that can save multiple Brand objects and access them like Brands("Company1").
If I used an array inside a class module, I'd end up with Brands.Brand("Company1").
If I used a Collection, I'd have to use indexes like Brands(1).
Is there a way to create a proper object group so that I can mimic the syntax of groups like Application.Workbooks and refer to members by Name?
A lot of the magic behind custom collections depends on hidden attributes that you cannot edit from within the VBE; you need to export (and remove from the project when prompted) the class module, edit its magic member attributes in Notepad/Notepad++, save changes, and then re-import the module into the project.
That's obviously tedious and error-prone, but there's a (much) better way.
In order to support this:
Set shoesProduct = MyBrand.Products("Shoes")
You can define Products as a Dictionary and call it a day, but then encapsulation as a concept is... well, taking a beating here (whether the internal collection is a Dictionary, a Collection, or a .NET ArrayList should typically be an implementation detail that the rest of the code doesn't need to care about).
I suspect the Brand class has too many responsibilities and "is" the product collection; best practices would be to have the Brand.Products property defined as follows:
Public Property Get Products() As Products
So you'll want to have a Products class (very much like the Workbook.Worksheets and Workbook.Sheets properties both return a Sheets collection object) that encapsulates a private, module-level VBA.Collection field (possibly keyed, but you can't access or iterate the keys of a collection).
The Products custom collection class needs an Item default property (the name Item is a convention); the implementation just pulls the item from the private encapsulated Collection:
'#DefaultMember
Public Property Get Item(ByVal Index As Variant) As Product
Set Item = ThePrivateCollection.Item(Index)
End Property
If you are using Rubberduck, this #DefaultMember annotation/comment is going to trigger an inspection result about the annotation and the corresponding hidden attribute(s) being "out of sync"; right-click that inspection result and pick "Adjust attribute values" to have Rubberduck generate the hidden code for you and deal with the annoying export/delete-edit-reimport cycle.
Otherwise, you'll want to manually edit the hidden VB_UserMemId member attribute that makes it the class' default member:
Public Property Get Item(ByVal Index As Variant) As Product
Attribute Item.VB_UserMemId = 0
Set Item = ThePrivateCollection.Item(Index)
End Property
And with that, MyBrand.Products("Shoes") becomes equivalent to MyBrand.Products.Item("Shoes").
Perhaps you want to iterate all the products in the collection, too?
For Each Product In MyBrand.Products
Debug.Print Product.Name
Next
In order to do this, you need a special "enumerator" member that forwards the enumerator from the encapsulated collection:
'#Enumerator
Public Property Get NewEnum() As IUnknown
Set NewEnum = ThePrivateCollection.[_NewEnum]
End Property
Again, Rubberduck annotations greatly simplify doing this, but everything Rubberduck does, you can also do manually if you like:
Public Property Get NewEnum() As IUnknown
Attribute NewEnum.VB_UserMemId = -4
Set NewEnum = ThePrivateCollection.[_NewEnum]
End Sub
And now For Each iteration works for your custom object collection!
If a Lot was more than just a String value (i.e. an actual object type), then the Product class could use a Lots custom collection too - but since a Lot is really just a String value (or is it?), then Product can simply encapsulate a Dictionary, and have a Lots property that exposes the Items array:
Public Property Get Lots() As Variant
Lots = ThePrivateLotsDictionary.Items
End Property
Note, that's simpler than using a Collection, because with a collection you'd need to iterate it and copy each item to an array in order to return the items without exposing the collection itself (exposing Lots() As Collection makes the AddLot member completely redundant).
As for the Brands collection itself, heed Tim Williams' advice and use a Dictionary data structure.
You can use a Scripting.Dictionary with Name as the key:
Sub test()
Dim MyBrand As Brand
Dim Brands As Object
Set Brands = CreateObject("scripting.dictionary")
Set MyBrand = New Brand
MyBrand.Name = "Company1"
MyBrand.AddProduct "Shoes"
MyBrand.Products("Shoes").AddLot "240502"
Brands.Add MyBrand.Name, MyBrand
MsgBox Brands("Company1").Products("Shoes").Lots(0)
End Sub

Late Binding a UDT in a form module to pass as a parameter

I have an access database and I'm attempting to write some VBA to increase automation.
I have a module I've entitled Global Variables which I've successfully used to define global constants (file paths etc) and a module ReportCode which has two main subrouties, one to run a query with ADODB (scraping form params where needed - returning a recordset), and a second which takes this record set and writes the data out to an excel template.
Given I may want to have multiple queries write to multiple tabs I thought the best way was to define a ExportDocument object to contain common parameters and a OrgReport object, containing query and tab specific parameters - then gather multiple OrgReport objects in a collection.
I'd hope to then pass just these two parameters into the main subroutine. This turns out to be a pain in VBA (or at least compared to ruby!).
Here you can see how I've defined by custom objects
Option Private Module
' Define Custom Doc Object
Public Type ExportDocument
TeamName As String
TemplatePath As String
SaveName As String
SavePath As String
End Type
' Define Custom Report Object
Public Type OrgReport
Query As String
Fields As Variant
Sheet As String
StartCol As Integer
StartRow As Integer
Headers As Boolean
End Type
And here is the code in my form which then called an additional module which does the heavy lifting - I know that part works because it did before I tried to go all OOP on this...
Private Sub my_report_from_form_Click()
' Prep Query Inputs
Dim TeamX_Report As OrgReport
TeamX_Report.Query = "qry_TeamReporting Query"
TeamX_Report.Sheet = "RawData"
TeamX_Report.StartCol = 1
TeamX_Report.StartRow = 2
TeamX_Report.Headers = True
TeamX_Report.Fields = Nothing
' Prep Document Inputs
Dim Teamx_Doc As ExportDocument
Teamx_Doc.TeamName = "MyTeam"
Teamx_Doc.TemplatePath = strReportTemplatePath & "MyTeam.xltm"
Teamx_Doc.SaveName = ""
Teamx_Doc.SavePath = strReportSavePath & Teamx_Doc.TeamName
' Init and set collection for CHAIN reports
Dim TeamReports As New Collection
TeamReports .Add Item:=TeamX_Report, Key:=TeamX_Report.Query
Call export_data_dump(Teamx_Doc, TeamReports)
End Sub
This gives me the issue of:
Only public user defined types defined in public object modules can be
used as parameters or return types for public procedures of class
modules or as fields of public user defined types
Following advice here I changed
Dim Teamx_Doc As ExportDocument
to
Teamx_Doc = CreateObject("ExportDocument")
But alas now i get
Run-time error '429': ActiveX component can't create object VBA
All references to this problem seem to be related to calling code from the Word., Excel. or Outlook. codebases, so perhaps I'm just missing a prefix for my own module stored within my database?
Best lead I've found is this one, which seems to suggest there's deeper issues with what i'm trying to do, or that i may get around parts by calling Friend, though I'm lost to where and how.
Is there a way I can late bind my UDT Objects within my form code, stash one in a collection then pass both to a subroutine that will be able to grab params from the first 'Doc' object and then iterate through the second 'report' object?
VBA >_<
There's no reason I can see why this doesn't work:
Dim Teamx_Doc As ExportDocument
Especially if you're not getting an error on line
Dim TeamX_Report As OrgReport
I've used custom Public Types before - no need for CreateObject
Though the docs seem to say it's just fine, can you try removing the
Option Private Module
The error message is kinda misleading. You simply can't put variables with an User-defined Type into a collection.
Option 1: Use an array instead. This actually sounds like it would work well for what you want to do.
Option 2: Create a class module instead of an UDT for OrgReport. Then instantiate objects of that class, those you can add to a collection.
See
Excel VBA Collections and Custom Data Types
and
http://www.mrexcel.com/forum/excel-questions/16849-adding-user-defined-types-collection.html

Dynamicaly referencing class based on a string variable

I'm fairly new to vba. I have a vba script that basically connects to my database using the ADODB class and returns a recordset with a bunch of information in. One column is entitled Class and it will contain the name of a Class that i'm looking to use in vb, the Class I want to use will change based on the value in the row. Is there a way I can declare and instantiate an object of this class based on the string I receive from each field under the column class. The purpose being that I need information for each row based on the class it contains without having to account for all possibilities.
for example
Dim Class as string
Class = Array(1, 4) ' Array(1, 4) contains Maths Class
Dim x as Class
Set x = New Class
if for example the particular Class is Math that would equate to
Dim x as Math
Set x = New Math
Is there a Function that could return the Class based on the string value? Thanks

Visual basic string issues

Every time i try to attribute any type of string to this i get Object reference not set to an instance of an object. I have tried every combination of possible way to handle the string, convert it to a string again and all the fuzz. It's very frustrating and i guess it's some kind of base principle of the structure/class usage and the string array or whatnot (which is also very dumb)
Private Class movie
Public name As String
Public actors As String
Public year As Integer
Public country As String
Public votes As Integer
End Class
Private movies() As movie
If File.Exists(OpenFileDialog1.FileName) Then
lblPath.Text = OpenFileDialog1.FileName
Dim iFile As New StreamReader(lblPath.Text)
While Not iFile.EndOfStream
current = iFile.ReadLine
movies(i).name = "sasasasa"
lbMovies.Items.Add(movies(i).name)
i = i + 1
End While
End If
these are the code parts where i use it
You are creating an empty array of movie objects, as was pointed out previously. Consequently movies(i) is Nothing. When you try to access a member (movies(i).name) the appropriate exception is generated. Note that your code does not even reach the assignment operator = but fails prior to that. In other words, this has nothing to do with strings altogether; you will get the same error if you write movies(i).votes = 42 instead. To fix your code you will first have to create a movie object, populate it and append it to your array.

VBA Passing Object into another Objects Collection

I have a set of owners, who each have their own set of opportunities.
I have two class modules, ClmOpportunity which has a bunch of properties, and ClmOwner which has a single name property and a Collection storing ClmOpportunity Objects:
Public name As Variant
Private opps As New collection
Public Function addOpportunity(opp As ClmOpportunity)
opp.ID = opps.Count + 1
opps.Add opp, opps.Count + 1
End Function
These owner objects are also being stored in a collection in my main module. When I try to use the function addOpportunity as shown below:
Dim item As New ClmOpportunity
item.name = "test"
owners.item(overallOwner).addOpportunity (item)
I get the error:
"object doesn't support this property or method"
I am quite new to VBA and I don't understand why this is, I am passing in a ClmOpportunity, so it should be fine right?
Any help would be greatly appreciated!
You don't use parentheses if there's no return value...
owners.item(overallOwner).addOpportunity item
...then you'll get a "type mismatch" error because a collection expects a string value as a key, so you'll need to adjust your addOpportunity function (which should probably be a Sub if you don't intend adding a returned value)

Resources