I have an code length issue I would like to hear your expertise about.
I have a class, called MyClass, with several Properties, P1, P2,...P16.
The values I want to put in the properties are in an array, called MyArray.
Right now, what I am doing, and it is working fine is:
MyClass.P1 = MyArray(0)
...
MyClass.P16 = MyArray(15)
It takes a lot of lines, and the code is not very readable.
I would like to be able to loop through the variables, like
For i = 0 to 15
array_of_variables(0) = MyArray(0)
Next
However, I have no idea on how to create this 'array_of_variables'.
I have tried creating a property of the class as an Array, but that is not correct VBA :(.
Do you have any thoughts on how to achieve this?
Thanks a lot,
Maxime
You could use a collection if you wanted a dirty way to do it, setup your class with a single collection of properties with predefined keys(names) and also use this same format with the collection your going to pass to the class and look through each name finding the match between the 2 collections and transferring the values. i guess you could do the same thing with an array but you would have to make sure the data in them is aligned, unlike collections that have names to identify them arrays only have indexes.
try this
place the following in your "MyClass" Class Module
Public Properties As Variant
and here follows a possible exploitation of that
Option Explicit
Sub main()
Dim MyClassInstance As MyClass
Dim i As Long
Set MyClassInstance = New MyClass
With MyClassInstance
.Properties = Array(1, 2, 3, 4, 5, 6)
For i = LBound(.Properties) To UBound(.Properties)
MsgBox .Properties(i)
Next i
End With
End Sub
Thanks to your suggestions, I came up with something mixing them all.
In the class I created the property like this:
Property Let Target(index, Value)
Select Case index
Case 0:
P1 = Value
...
Case 15
P16 = Value
End Select
End Property
This way, I can loop like this:
For i = 0 to 15
MyClass.Target(i) = MyArray(i)
Next
Related
I have a code like below to find a category property with named "Model" and after that i will get the Model's name.
But as you can see "parent" property shows the upper level of parameter. My parameters in parameter groups and its like cascaded. And i don't know in which level they are currently i'm using below code but it's not sufficient because if i have a parameter very in lower levels i had to write this elseif conditions.
Is there any quick solution to make it easier and wise way?
Public Class ParameterInfoClass
Public Shared Sub GetSubvar(ByVal ParameterGroups As IScrNamedObjectList)
Dim ParameterGroup As IScrParameterGroup
Dim nParameterGroup As Integer
Dim ParameterClass As String
nParameterGroup = ParameterGroups.count
For i As Integer = 0 To nParameterGroup - 1
ParameterGroup = ParameterGroups.item(i)
If ParameterGroup.parent.category.name = "Model" Then
ParameterClass = ParameterGroup.parent.name
ElseIf ParameterGroup.parent.parent.category.name = "Model" Then
ParameterClass = ParameterGroup.parent.parent.name
ElseIf ParameterGroup.parent.parent.parent.category.name = "Model" Then
ParameterClass = ParameterGroup.parent.parent.parent.name
'...
'This should be continue like this because i don't know in which level i will find the category name as "Model"
'.
End If
DataGridView1.Rows.Add(ParameterClass, ParameterGroup.name)
Next
End Sub
End Class
It would be great to make this section with correct solution. I thought like do-while loops can be one option but i dont know how to apply because focus is in here to look upper levels of parameter to find "Model" category after that i'm writing that Model's name.
For i As Integer = 0 To nParameterGroup - 1
ParameterGroup = ParameterGroups.item(i)
If ParameterGroup.parent.category.name = "Model" Then
ParameterClass = ParameterGroup.parent.name
ElseIf ParameterGroup.parent.parent.category.name = "Model" Then
ParameterClass = ParameterGroup.parent.parent.name
ElseIf ParameterGroup.parent.parent.parent.category.name = "Model" Then
ParameterClass = ParameterGroup.parent.parent.parent.name
'...
'This should be continue like this because i don't know in which level i will find the category name as "Model"
'.
End If
I don't have a lot of time to dig into the details, but recursion is something I love so I'm giving you a hint (I would help more but that's the time I have right now).
Instead of iterating through every possible parent level, you should create a simple recursive function which will look if it finds the "Model", then either return it or else look for it's parent by calling itself to look for it.
You would have to use this new function instead of your If ElseIf ElseIf... potentially infinite function.
I drafted something which kinda looks like what I mean:
Private Function GetParameterClass(rootClassName As rootClass) As String
If ParameterGroup.category.name = "Model" Then
Return ParameterGroup.category.name
End If
If ParameterGroup.parent IsNot Nothing Then
Return GetParameterClass(ParameterGroup.parent)
End If
Return ""
End Function
By calling a similar function, you will iterate recursively through every parent until it finds none, and the first time it finds "Model", it'll stop the recursion and return it.
Sorry for not being more precise, as I have to get back to work myself! I'll look on your thread this evening when I can, just in case. Have fun!
EDIT:
I'm not familiar with the class you're working with, so there's a good amount of guesswork in this edit. Here's how I would try to solve your issue:
Public Class ParameterInfoClass
Public Shared Sub GetSubvar(ByVal ParameterGroups As IScrNamedObjectList)
For Each parameterGroup As IScrParameterGroup In ParameterGroups
Dim parameterClass As String = GetParameterClassName(parameterGroup)
If parameterName <> "" Then
DataGridView1.Rows.Add(parameterClass, parameterGroup.Name)
End If
Next
End Sub
Private Shared Function GetParameterClassName(parameterGroup As IScrParameterGroup) As String
If parameterGroup.category.name = "Model" Then
Return parameterGroup.name
End If
If parameterGroup.parent IsNot Nothing Then
Return GetParameterClass(parameterGroup.parent)
End If
Return ""
End Function
End Class
The main idea behind GetParameterClassName is that it'll either find the parameterGroup.category.name = "Model", or else it'll return an empty string. I replaced the way you were planning to iterate with a For Each loop, which should work well with most lists, but you might need to ajust that part is IScrNamedObjectList is not or doesn't contains a list or array or something.
Whenever a GetParameterClassName is found, then the parameterClass is not empty, so we can add these informations to the DataGridView1.
You can ask your questions in the comments if you have any, and I'll be happy to oblige. I love recursion!
Is it possible to either:
Declare an array as a constant
OR
Use a workaround to declare an array that is protected from adding, deleting or changing elements, and therefore functionally constant during the life of a macro?
Of course I could do this:
Const myConstant1 As Integer = 2
Const myConstant2 As Integer = 13
Const myConstant3 As Integer = 17
Const myConstant4 ...and so on
...but it loses the elegance of working with arrays. I could also load the constants into an array, and reload them each time I use them, but any failure to reload the array with those constant values before use could expose the code to a "constant" value that has changed.
Any workable answer is welcome but the ideal answer is one that can be setup once and not require any changes/maintenance when other code is modified.
You could use a function to return the array and use the function as an array.
Function ContantArray()
ContantArray = Array(2, 13, 17)
End Function
How about making it a function? Such as:
Public Function myConstant(ByVal idx As Integer) As Integer
myConstant = Array(2, 13, 17, 23)(idx - 1)
End Function
Sub Test()
Debug.Print myConstant(1)
Debug.Print myConstant(2)
Debug.Print myConstant(3)
Debug.Print myConstant(4)
End Sub
Nobody can change it, resize it, or edit its content... Moreover, you can define your constants on just one line!
I declared a String constant of "1,2,3,4,5" and then used Split to create a new array, like so:
Public Const myArray = "1,2,3,4,5"
Public Sub createArray()
Dim i As Integer
A = Split(myArray, ",")
For i = LBound(A) To UBound(A)
Debug.Print A(i)
Next i
End Sub
When I tried to use ReDim or ReDim Preserve on A it did not let me. The downfall of this method is that you can still edit the values of the array, even if you can't change the size.
If the specific VBA environment is Excel-VBA then a nice syntax is available from the Excel Application's Evaluate method which can be shortened to just square brackets.
Look at this
Sub XlSerialization1()
Dim v
v = [{1,2;"foo",4.5}]
Debug.Assert v(1, 1) = 1
Debug.Assert v(1, 2) = 2
Debug.Assert v(2, 1) = "foo"
Debug.Assert v(2, 2) = 4.5
'* write all cells in one line
Sheet1.Cells(1, 1).Resize(2, 2).Value2 = v
End Sub
If you don't need a new instance each time you can use a Static local variable to avoid multiple objects creation and initialization:
Private Function MyConstants()
Static constants As Variant
If IsEmpty(constants) Then
constants = Array(2, 13, 17)
End If
MyConstants = constants
End Function
Can an array be declared as a constant? No.
Workarounds - Simplest one I can think of is to define a constant with delim and then use Split function to create an array.
Const myConstant = "2,13,17"
Sub Test()
i = Split(myConstant, ",")
For j = LBound(i) To UBound(i)
Debug.Print i(j)
Next
End Sub
Is this too simplistic?
PUBLIC CONST MyArray = "1,2,3,4"
then later in a module:
Dim Arr as Variant
SET Arr = split(MyArray,",")
I know this is an old question, but these archives are often scanned for many years after being posted, so I don't see a problem with adding things long after the origin date.
How about creating a class, with a read-only property returning the 'array' value? You can specify a parameter using the same syntax as an array index, and defining only a GET property effectively makes it read-only. Define the constant values inside the class and it will work just like a constant array, even though the actual construction is different.
No - arrays can't be declared as constant but you can use a workaround.
You can create a function that returns the array you want
http://www.vbaexpress.com/forum/showthread.php?1233-Solved-Declare-a-Constant-Array
Using above information, I came to following working solution for comparing short text information of a month, independent from Excel using German language:
Const MONATE = ",Jän,Feb,März,Apr,Mai,Jun,Jul,Aug,Sep,Okt,Nov,Dez"
.. and later in the code:
If StringToCompare = Split(MONATE, ",")(Month(dt)) Then
NOTE: as the Split-Array starts with index 0 I added the comma in the beginning.
Don't know when this changed, but in Excel 365, this works (or, at least, does not generate a compiler error):
Const table1Defs As Variant = Array("value 1", 42, Range("A1:D20"))
I have a class module called MyClass, with a public integer in it:
Public i as Integer
When I try to use this variable in a For loop like so:
Dim MyInstance as MyClass: Set MyInstance = New MyClass
For MyInstance.i = 1 To 10
Debug.Print "Hello"
Next
I get the error: Variable required. Can't assign to this expression
I have consulted the help page but cannot see how it applies to my case. The relevant fragment is: "You tried to use a nonvariable as a loop counter in a For...Next construction. Use a variable as the counter." But i is a variable after all, and not a Let Property function or any other expression.
What is wrong with the code?
EDIT: I should point out that the reason I want my iterator to be part of the class is that I have multiple instances of the class, serving different purposes in my project, and there are multiple nested For loops for each instance of the class. Therefore it is worth having the iterators belong to their respective objects, say:
For Client.i = 1 To Client.Count
For Order.i = 1 To Order.Count
For Item.i = 1 To Item.Count
etc.
I have settled for the following workaround but am still not entirely satisfied with it:
For ciii = 1 To Client.Count
Client.i = ciii ' Client.i is later used in the code
For oiii = 1 To Order.Count
Order.i = oiii
For iiii = 1 To Item.Count
Item.i = iiii
You cannot use MyInstance.i as the increment counter but you can use it as the terminator; e.g. For i = 1 To MyInstance.i.
MyClass class
Option Explicit
Public pi As Long
Public Property Get i() As Long
i = pi
End Property
Public Property Let i(Value As Long)
pi = Value
End Property
test sub procedure in Module1
Sub test()
Dim MyInstance As MyClass, i As Long
Set MyInstance = New MyClass
MyInstance.i = 10
For i = 1 To MyInstance.i
Debug.Print "Hello"
Next
End Sub
If you want a publicly accessible loop variable stick it at the top of a standard module i.e. declare the Public i at the top of a standard module.
Note that this would mean you need to re-write your standard module code as, as per point two, you are treating i as if it is a property/method of the class.
So, standard module code would be:
Public i As Long
Sub ........
For i = 1 To 10
Debug.Print "Hello"
Next i
End Sub ......
If you want it to somehow be a property/method then you need to define Getters and Setters (potentially) in the class. And then re-write your module code accordingly. Especially if you are planning on looping using i, you will need an incrementor method in the class.
And yes, I have changed i to Long as there are no advantages, in this case I believe, of having it declared as Integer. A Long is a safer bet for avoiding potential overflow.
If you need a workaround so that you iterate through a property of the instance, you could create a method to increment it, change your loop to a Do While ... Loop and call that method before the loop call.
'Class Module
Option Explicit
Public i As Integer
Public Sub increment_i()
i = i + 1
End Sub
Private Sub Class_Initialize()
i = 0
End Sub
'Module
Sub loop_myclass()
Dim instance As MyClass: Set instance = New MyClass
Do While instance.i <= 10
'Instance property dependent code here
Debug.Print instance.i
instance.increment_i
Loop
End Sub
OK, I found the answer. There is a Microsoft help page on For…Next loop regarding VB, but I think it pertains to VBA as well.
It says:
If the scope of counter isn't local to the procedure, a compile-time
warning occurs.
So there's not much to discuss here, it's just the way MS wants it to be. Though I'd think that if the scope is greater than the procedure it shouldn't cause any problems, but apparently it does.
As always, this may be something of a newb question but here goes:
I have a Class with 15 properties. Each class represents information about an item of stock (how many there are, how many recently shipped etc). Each time a class is initialized by passing it a stock code, it gathers all the data from other sources and stores it as properties of the class.
I want to be able to initialize n number of classes, dependent on the length of a list (never more than 200). I want to name those classes by their stock code, so that I can call up the information later on and add to it. The only problem is I don't know how to use a variable to name a class. I don't really want to write out 200 classes long hand because I'm sure there is a better way to do it than Diming: Stock1 As C_ICODE, Stock2 As C_ICODE, Stock3 As C_ICODE etc and initializing them in order, until input (from ActiveCell) = "" or it hits the maximum list length of 200. I would like to create as many class instances as there are stock codes if possible, and generate them something like this:
PseudoCode:
For Each xlCell In xlRange
strIN = xlCell.Value
Dim ICode(strIN) As New C_ICODE
ICode(strIN).lIcode = strIN
Next
Letting classname.lIcode = strIN provides the class with all the user input it needs and then it carries out various functions and subroutines to get the other 14 properties.
I would be very grateful if someone could let me know if this sort of thing is possible in VBA, and if so, how I could go about it? Definitely struggling to find relevant information.
You can use Dictionary object:
Dim ICode As Object
Set ICode = CreateObject("Scripting.Dictionary")
For Each xlCell In xlRange
strIN = xlCell.Value
ICode.Add strIN, New C_ICODE
ICode(strIN).lIcode = strIN
Next
I just did a quick test of this and it seems as though it might work for you. You can create an array to hold multiple instances of your class.
Sub thing()
Dim cArray(1 To 10) As Class1
Dim x As Long
For x = 1 To UBound(cArray)
Set cArray(x) = New Class1
Next
' Assume the class has a property Let/Get for SomeProperty:
For x = 1 To UBound(cArray)
cArray(x).SomeProperty = x * 10
Next
For x = 1 To UBound(cArray)
Debug.Print cArray(x).SomeProperty
Next
End Sub
I have defined a variable with an own type, say
Dim point As DataPoint
Public Type DataPoint
list as Collection
name as String
number as Integer
End Type
and I want to delete all values of the variable point at once. If it was a class, I would just use Set point = New DataPoint, or set Set point = Nothing, but how can I proceed if it's a type?
You can benefit from the fact that functions in VB have an implicit variable that holds the result, and that contains the default type value by default.
public function GetBlankPoint() as DataPoint
end function
Usage:
point = GetBlankPoint()
The standard way is to reset each member to its default value individually. This is one limitation of user-defined types compared to objects.
At the risk of stating the obvious:
With point
Set .list = Nothing
.name = ""
.number = 0
End With
Alternatively, you can create a "blank" variable and assign it to your variable each time you want to "clear" it.
Dim point As DataPoint
Dim blank As DataPoint
With point
Set .list = New Collection
.list.Add "carrots"
.name = "joe"
.number = 12
End With
point = blank
' point members are now reset to default values
EDIT: Damn! Beaten by JFC :D
Here is an alternative to achieve that in 1 line ;)
Dim point As DataPoint
Dim emptyPoint As DataPoint
Public Type DataPoint
list As Collection
name As String
number As Integer
End Type
Sub Sample()
'~~> Fill the point
Debug.Print ">"; point.name
Debug.Print ">"; point.number
point.name = "a"
point.number = 25
Debug.Print ">>"; point.name
Debug.Print ">>"; point.number
'~~> Empty the point
point = emptyPoint
Debug.Print ">>>"; point.name
Debug.Print ">>>"; point.number
End Sub
SNAPSHOT
One-liner:
Function resetDataPoint() As DataPoint: End Function
Usage:
point = resetDataPoint()
Another option is to use the reserved word "Empty" such as:
.number= Empty
The only issue is that you will need to change the number from integer to variant.
Using classes in VBA is usually a good practice in case it is not a single purpose solution or the class do not contain too many private attributes because if you want to adhere on OOP rules and keep your class safe, you should declare all the Let and Get properties for all private attributes of class. This is too much coding in case you have more than 50 private attributes. Another negative side of using classes in excel is fact, that VBA do not fully support the OOP. There is no polymorfism, overloading, etc.) Even you want to use an inheritance, you have to declare all the attributes and methods from the original class in the inherited class.
So in this case I would prefer the solution suggested by Jean-François Corbett or GSeng, i.e. to assign an empty variable of the same UDT as the variable you want to clear or to use a function which to me seems little bit more elegant solution because it will not reserve permanent memory for the emtpy variable of your UDT type.
For that is better to use classes, you can declare a class module with the name of your type, then declare all of your members as public, then automatically you can set to nothing and new for create and delete instances.
syntax will be somthing like this after you create the class module and named like your type:
'
Public List as Collection
Public Name as String
Public Number as Long
Private Sub Class_Initialize()
'Here you can assign default values for the public members that you created if you want
End Sub