VBA using a Class as a property of another Class - class

I am having trouble using a Class that I created as a property in another Class. I have two classes, one called Vertex and the other called Edge. Two of the properties of Edge, Parent and Child, are Vertex objects. Here is a method where I try to use them.
Option Explicit
Public Sub OrgChart()
Dim Vertices As Collection
Dim Edges As Collection
Dim vParent As Vertex
Dim vChild As Vertex
Dim eEdge As Edge
Dim rEdgeRow As Range
Set rEdgeRow = ActiveSheet.Range("A1:C1")
Do While Len(rEdgeRow(1, 1).Value) > 0
Set vChild = New Vertex
Set vParent = New Vertex
Set eEdge = New Edge
vChild.Name = rEdgeRow(1, 1).Value
vChild.Dummy = False
vParent.Name = rEdgeRow(2, 1).Value
vParent.Dummy = False
eEdge.Parent = vParent
eEdge.Child = vChild
eEdge.Percent = rEdgeRow(3, 1).Value
Set rEdgeRow = rEdgeRow.Offset(1, 0)
Loop
End Sub
When I run this, I get the error,
"Run-time error '91': Object variable or With block variable not set"
The debugger indicates that it is breaking at the line
eEdge.Parent = vParent
I assume that the issue has to do with the eEdge.Parent not being initialized, but I tried using
Set eEdge.Parent = New Vertex
and I got the same issue.
If it will help, I can post the class code as well, though they are fairly simple, only containing Property Get and Property Let functions.
I apologize if I did anything wrong when posting this; this is my first time posting anything on SO.
Thanks,
Eric

I might not be understanding this right, but did you try:
Set eEdge.Parent = vParent
Set eEdge.Child = vChild
If they're objects, you need to use a "Set"
... Also, if this doesn't fix it, can you post the code for your Vertex class - You may need to dim the Parent and Child objects differently in its constructor...

Related

AS3 - Private Object. Make property as read only

I have a object property in my Class which is private and marked as read-only.
private var readOnlyObj:Object;
I can only access it with a get method:
public function get readOnly(){ return readOnlyObj }
I can access it by:
var objClass = new MyClass();
trace(objClass.readOnly)
And if i'll try to modify it:
objClass.readOnly = new Object();
I'll get an error:
Error# Property is read only.
Now my question is:
How do I set the properties of my readOnlyObj as read-only?
If I have set the object in the constructor:
readOnlyObj["property1"] = 0;
And modify that property by:
objClass.readOnly["property1"] = 2;
It is valid. I want set the property1 to a read-only property. Is this possible? Thank You!
You can do this by returning a duplicate of the original object and not the object itself.
The transform properties of DisplayObjects work like this: you can get the object property from a get function and can modify the object, but such modification has no effect until you pass the modified object back to the set function.
In your case, there's no way to give the object back (no setter) and by returning a copy (commonly called 'clone') from the getter, there is no way to modify the object property from outside, because the returned reference reference the newly created independent clone, essentially making the internal object constant.
What you are asking is not possible and only yield the answer "no" if on the other hand you asked about how to achieve that functionality then there would be a few answer possible.
First of all given your code and the problem at hand it is clear that you misunderstand the class scope. You set:
private var readOnlyObj:Object;
As read only while it's really not the object you want to protect, it's its properties. So readOnlyObj should really not even be visible and accessible.
Now that readOnlyObj is private and not accessible, you can put together a simple method to retrieve properties:
public function getProperty(name:String):*
{
if(readOnlyObj[name] != undefined)
{
return readOnlyObj[name];
}
return null;
}
It might also be useful to know how to put together a public setter that cannot be used externally.
Create an internal Boolean variable (only with true package), then internally set that variable to true before setting the property then set it back to false. Since externally that boolean cannot be set you end up with a public setter that cannot be used externally.
internal var allowSetter:Boolean;
public function set whatever(value:*):void
{
if(allowSetter)
{
//set property ect...
allowSetter = false;
}
}
You can't really do this, at least in the way you describe. You can of course make your readOnly object a custom class instance that only has read-only properties, but you can't freeze a dynamic Object instance.

Excel VBA, Custom Class: Function call raises "method not supported" error

I am writing my first classes.
One is cCRElist which is essentially a collection of cCRE instances (some specialized events).
I want there to be a sub or function inside cCRElist that will load all the CRE's from the worksheet into one big collection I can work with. I created the function and it worked OK when I called it from a normal code module, but then I tried to move the code into the class. Now I am having trouble calling the function LoadFromWorksheet(myWS as Worksheet).
The error is "object does not support this property or method". I have tried making it a sub, a function, making it public, not public, I have tried turning into a Property Let instead of a sub. Obviously I have a flimsy grasp on what that does. I have tried
Call CREList.LoadFromWorksheet(myWS)
and
CREList.LoadfromWorksheet myWS
Same error every time.
Here is the test code that uses the class and calls the function:
Sub TestClassObj()
Dim CRElist As cCRElist
Set CRElist = New cCRElist
Dim myWS As Worksheet
Set myWS = ThisWorkbook.ActiveSheet
CRElist.LoadFromWorksheet (myWS)
End Sub
Here is a snippet of the class cCRElist:
' **** CLASS cCRElist
Option Explicit
' This is a collection of CRE objects
Private pCRElist As Collection
Private Sub Class_Initialize()
Set pCRElist = New Collection
End Sub
Public Property Get CREs() As Collection
Set CREs = pCRElist
End Property
Public Property Set Add_CRE(aCRE As cCRE)
pCRElist.Add aCRE
End Property
Function LoadFromWorksheet(myWS As Worksheet)
Dim CRE As cCRE
Dim iFirst As Long
Dim iLast As Long
Dim i As Long
Set CRE = New cCRE
iFirst = gHeader_Row + 1
iLast = FindNewRow(myWS) - 1
' update data in CRE then add
For i = iFirst To iLast
If myWS.Cells(i, gCRE_Col) <> "" Then ' This is a CRE row
Set CRE = New cCRE
With CRE
.CRE_ID = myWS.Cells(i, gCRE_Col)
If Not IsDate(myWS.Cells(i, gCRE_ETA_Col)) Then
.ETA = "1/1/1900"
Else
.ETA = Trim(myWS.Cells(i, gCRE_ETA_Col))
End If
<... snipped ...>
End With
pCRElist.Add_CRE CRE
End If
Next
End Sub
' **** END OF CLASS cCRElist
Thanks for your expertise.
Here is what worked based on help I got in the comments. First, I did the "break in class module". In the test code, I changed the function call from:
CRElist.LoadFromWorksheet(myWS)
to
CRElist.LoadFromWorksheet myWS
Inside the class, i had to change
Set pCRElist.Add_CRE CRE
to
pCRElist.Add CRE
Then I was able to get rid of extraneous CLASS functions Add_CRE and Count.
Thanks for everyone's input. I couldn't figure out how to mark comments as accepted answers so I did this. Let me know if I need to do something differently.
Now it works!

VBA Class Method Chaining

I'm looking for a way to 'chain class methods', for example the Range object can do things like "Range.Borders.Color", I guess that the Borders part is it's own class which is being accessed by the Range class but I have no idea how to implement something similar with my own classes - Nor do I even know what this is called and after hours of searching I think I might slowly be un-learning VBA.
Can anybody either a) Provide code which I could look at to replicate or b) Tell me what this is called and maybe even nudge me in a helpful direction?
As I know asking for code without providing any makes me look like a dick, consider the following pseudo-code. I know it's horrific but it might help me make any sense:
main ------------------------------------------------------------------------
Dim obj as class1
set obj = new class1
obj.Target = Range("A1:B5")
obj.Borders.Add
'A1:B5 put into modRange then given borders
class1 ------------------------------------------------------------------------
Private modRange as range
Public Property Let Target(newTarget as Range)
set modRange = newTarget
End Property
Public Property Borders()
Public Sub Add()
'Code to add borders to modRange
End Sub
Public Sub Remove()
'Code to remove borders from modRange
End Sub
End Property
I know this is not how the actual code would look. but as I don't know the syntax this is the closest thing I can imagine. I guess the real thing would have class1 linking to other class modules. Maybe.
As a side note. If I did have a class called "Borders" (I probably wont) as part of this class 1 object, would it conflict with the Borders portion of the Range object as well as it has a similar name? Or will the Private scope save the day?
(The .Borders.Add/Remove is a bit ridiculous to have as a class I know, I'm really only after the syntax - Honest)
To have complex properties of an object, you need to create a new class and then create an instance of that class in the parent class. So if you want to have something like Class1.Borders.Add(), you'd first have to create a new CBorders class (I used to prepend C to my class names in VB6 / VBA to avoid name collisions). Something like:
'- in class CBorder
Private m_lColor As Long
Public Property Get Color() As Long
Color = m_lColor
End Property
Public Property Let Color(ByVal lNewColor As Long)
m_lColor = lNewColor
End Property
Public Sub Reset()
m_lColor = 0
End Sub
...
Then inside Class1, you'd have something like this:
Private m_oBorder As CBorder
Private Sub Class_Initialize()
...
Set m_oBorder = New CBorder
...
End Sub
Public Property Get Border() As CBorder
Set Border = m_oBorder
End Property
...
Then you can do this:
Dim obj As Class1
Set obj = New Class1
obj.Borders.Color = ...
...
Notice how the Borders property of Class1 is accessed as a member of the obj instance and then how the Color property of the CBorder class is used. Creating these values as properties is what lets you chain these calls together.
You'd need error checking and validation code as well - I left those out to keep the example short.
Another solution for this is to just return Me to make it chainable.
class module: CChaining
Using Functions for Target and Borders returning Me to enable chaining. Using Subs for Add and Remove to "finish" the chain.
Private modRange As Range, modRangeBorders As Object
Public Function Target(rng As Range)
Set modRange = rng
Set Target = Me
End Function
Public Function Borders()
Set modRangeBorders = modRange.Borders
Set Borders = Me
End Function
Public Sub Add()
modRangeBorders.LineStyle = xlContinuous
End Sub
Public Sub Remove()
modRangeBorders.LineStyle = xlNone
End Sub
Testing the class in a module
Sub testing()
Dim obj As New CChaining
obj.Target(Range("A1:B5")).Borders.Add
'now the target and property (Borders) is set and you could do this
'obj.Remove
End Sub
Pretty nice ... hmm, with this it would be possible to build a library like in other languages (javascript > jQuery) to make using Excel VBA much easier.

Assignment of objects in VB6

I am attempting to create two identical objects in VB6 by assignment statements; something like this...
Dim myobj1 As Class1
Dim myobj2 As Class1
Set myobj1 = New Class1
myobj1.myval = 1
Set myobj2 = myobj1
It has become apparent that this doesn't create two objects but rather two references to the same object, which isn't what I am after. Is there any way to create a second object in this sort of way, or do I have to copy the object one member at a time...
Set myobj2 = new Class1
myobj2.mem1 = myobj1.mem1
...
?
Edit 2 Scott Whitlock has updated his excellent answer and I have incorporated his changes into this now-working code snippet.
Private Type MyMemento
Value1 As Integer
Value2 As String
End Type
Private Memento As MyMemento
Public Property Let myval(ByVal newval As Integer)
Memento.Value1 = newval
End Property
Public Property Get myval() As Integer
myval = Memento.Value1
End Property
Friend Property Let SetMemento(new_memento As MyMemento)
Memento = new_memento
End Property
Public Function Copy() As Class1
Dim Result As Class1
Set Result = New Class1
Result.SetMemento = Memento
Set Copy = Result
End Function
One then performs the assignment in the code thus...
Set mysecondobj = myfirstobj.Copy
Like many modern languages, VB6 has value types and reference types. Classes define reference types. On the other hand, your basic types like Integer are value types.
The basic difference is in assignment:
Dim a as Integer
Dim b as Integer
a = 2
b = a
a = 1
The result is that a is 1 and b is 2. That's because assignment in value types makes a copy. That's because each variable has space allocated for the value on the stack (in the case of VB6, an Integer takes up 2 bytes on the stack).
For classes, it works differently:
Dim a as MyClass
Dim b as MyClass
Set a = New MyClass
a.Value1 = 2
Set b = a
a.Value1 = 1
The result is that both a.Value1 and b.Value1 are 1. That's because the state of the object is stored in the heap, not on the stack. Only the reference to the object is stored on the stack, so Set b = a overwrites the reference. Interestingly, VB6 is explicit about this by forcing you to use the Set keyword. Most other modern languages don't require this.
Now, you can create your own value types (in VB6 they're called User Defined Types, but in most other languages they're called structs or structures). Here's a tutorial.
The differences between a class and a user defined type (aside from a class being a reference type and a UDT being a value type) is that a class can contain behaviors (methods and properties) where a UDT cannot. If you're just looking for a record-type class, then a UDT may be your solution.
You can use a mix of these techniques. Let's say you need a Class because you have certain behaviors and calculations that you want to include along with the data. You can use the memento pattern to hold the state of an object inside of a UDT:
Type MyMemento
Value1 As Integer
Value2 As String
End Type
In your class, make sure that all your internal state is stored inside a private member of type MyMemento. Write your properties and methods so they only use data in that one private member variable.
Now making a copy of your object is simple. Just write a new method on your class called Copy() that returns a new instance of your class and initialize it with a copy of its own memento:
Private Memento As MyMemento
Friend Sub SetMemento(NewMemento As MyMemento)
Memento = NewMemento
End Sub
Public Function Copy() as MyClass
Dim Result as MyClass
Set Result = new MyClass
Call Result.SetMemento(Memento)
Set Copy = Result
End Function
The Friend only hides it from stuff outside your project, so it doesn't do much to hide the SetMemento sub, but it's all you can do with VB6.
HTH
#Scott Whitlock, I was not able to make your code work but if it works it would be great.
I've created a regular module where I put the memento type
Type MyMemento
Value1 As Integer
Value2 As String
End Type
Then I create a class module called MyClass with the code
Private Memento As MyMemento
Friend Sub SetMemento(NewMemento As MyMemento)
Memento = NewMemento
End Sub
Public Function Copy() as MyClass
Dim Result as MyClass
Set Result = new MyClass
Result.SetMemento(Memento)
Set Copy = Result
End Function
Finally I try to call the copy function in another regular module like this
Sub Pruebas()
Dim Primero As MyClass, segundo As MyClass
Set Primero = New MyClass
Set segundo = New MyClass
Set segundo = Primero.Copy
End Sub
I get the message (below the picture): Error de compilacion: El tipo de agumento de ByRef no coincide
Here is an image (short of 10 points so here is the link): http://i.stack.imgur.com/KPdBR.gif
I was not able to get the message in English, I live in Spain.
Would you be so kind to provide with an example in VBA Excel?, I have been really trying to make this work.
Thanks for your work
===============================================
EDIT: Problem Solved:
The problem was on line "Result.SetMemento(Memento)", in VBA it needed to be called with "Call"
Public Function Copy() As MyClass
Dim Result As MyClass
Set Result = New MyClass
Call Result.SetMemento(Memento)
Set Copy = Result
End Function
It works great, thanks Scott Whitlock, you are a genius
or do I have to copy the object one member at a time...
Unfortunately yes.
It is possible (but technically very very difficult) to write a COM server in C++ that - using the IDispatch interface - will copy the value of each property, but really this is High Temple programming, if I had to do it, I don't I know if I could do it, but I'd be looking at something like 10 days work ( and I know how COM is implemented in C++, I'd also need to investigate to see if ATL framework has anything to help etc).
I worked with Vb3, 4,5 & 6 for something like 10 years (hands on, 5 days a week) and never found a good way to do this, beyond manually implementing serialisation patterns like Mementos and Save & Store, which really just boiled down to fancy ways of copying each member, one at a time.

Dynamic property names in VBA

I have a custom class module in VBA (Access) that is supposed to handle a large amount of external data. Currently I have two functions Read(name) and Write(name, value) that allows to read and set dynamic properties.
Is there a way to define a more syntactic way to read and write those data? I know that some objects in VBA have a special way of accessing data, for example the RecordSet, which allows to read and set data using myRS!property_name. Is there a way to do exactly the same for custom class modules?
The exclamation mark syntax is used to access members of a Scripting.Dictionary instance(you'll need to add a reference to Microsoft Scripting Runtime through Tools > References first). To use this syntaxyou'll need to be storing the information internally in a dictionary.
The quickest way to use it in a class is to give your class an object variable of type Scripting.Dictionary and set it up as follows:
Option Explicit
Dim d As Scripting.Dictionary
Private Sub Class_Initialize()
Set d = New Scripting.Dictionary
End Sub
Private Sub Class_Terminate()
Set d = Nothing
End Sub
Public Property Get IntData() As Scripting.Dictionary
Set IntData = d
End Property
Now you can access properties using myinstance.IntData!MyProperty = 1... but to get to where you want to be you need to use Charlie Pearson's technique for making IntData the default member for your class.
Once that's done, you can use the following syntax:
Dim m As MyClass
Set m = New MyClass
Debug.Print "Age = " & m!Age ' prints: Age =
m!Age = 27
Debug.Print "Age = " & m!Age ' prints: Age = 27
Set m = Nothing
Okay, thanks to Alain and KyleNZ I have now found a working way to do this, without having a collection or enumerable object below.
Basically, thanks to the name of the ! operator, I found out, that access via the bang/pling operator is equivalent to accessing the default member of an object. If the property Value is the default member of my class module, then there are three equivalent statements to access that property:
obj.Value("param")
obj("param")
obj!param
So to make a short syntax working for a custom class module, all one has to do is to define a default member. For example, I now used the following Value property:
Property Get Value(name As String) As String
Value = SomeLookupInMyXMLDocument(name)
End Property
Property Let Value(name As String, val As String) As String
SetSomeNodeValueInMyXMLDocument(name, val)
End Property
Normally, you could now access that like this:
obj.Value("foo") = "New value"
MsgBox obj.Value("foo")
Now to make that property the default member, you have to add a line to the Property definition:
Attribute Value.VB_UserMemId = 0
So, I end up with this:
Property Get Value(name As String) As String
Attribute Value.VB_UserMemId = 0
Value = SomeLookupInMyXMLDocument(name)
End Property
Property Let Value(name As String, val As String) As String
Attribute Value.VB_UserMemId = 0
SetSomeNodeValueInMyXMLDocument(name, val)
End Property
And after that, this works and equivalent to the code shown above:
obj("foo") = "New value"
MsgBox obj("foo")
' As well as
obj!foo = "New value"
MsgBox obj!foo
' Or for more complex `name` entries (i.e. with invalid identifier symbols)
obj![foo] = "New value"
MsgBox obj![foo]
Note that you have to add the Attribute Value.VB_UserMemId = 0 in some other editor than the VBA editor that ships with Microsoft Office, as that one hides Attribute directives for some reason.. You can easily export the module, open it in notepad, add the directives, and import it back in the VBA editor. As long as you don't change too much with the default member, the directive should not be removed (just make sure you check from time to time in an external editor).
See this other question: Bang Notation and Dot Notation in VBA and MS-Access
The bang operator (!) is shorthand for
accessing members of a Collection or
other enumerable object
If you make your class extend the Collection class in VBA then you should be able to take advantage of those operators. In the following question is an example of a user who extended the collection class:
Extend Collections Class VBA