VBA Class Method Chaining - class

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.

Related

Why does VB disallow type conversion from parent to subclass?

I have already asked a question here where I basically require an instance of a base class to be converted into a subclass (or a new instance of the subclass to be created using the instance of the base class' properties). The conclusion seems to be that the best way to do this is to manually assign every property I need to transfer in the constructor of the base class.
While this is feasible in some cases, it certainly is not when there are many properties to transfer, or when the base class is subject to change — every time you add a property to the base class, the constructor needs to be changed too, so this solutions is inelegant.
I have searched online, and can't see any reason for why this kind of type-casting isn't implemented. The arguments I have seen so far describe this operation to 'not make any sense' (making a minivan from a car was an analogy I saw), question what to do about the non-inherited variables in the subclass, or claim that there must be some better solution for what was trying to be achieved.
As far as I can see, the operation doesn't need to 'make sense' as long as it's useful, so that isn't much of a good reason. What's wrong with adding a few more properties (and perhaps methods/overriding them) to change an instance into a subclass? In the case of the non-inherited variables, that can simply be solved by allowing this kind of type-cast only a constructor is added to the subclass or by just simply setting them to their default values. After all, constructors usually call MyBase.New(...) anyway. What's the difference between using the constructor of the base (essentially creating a new instance of the base) and using an instance which is already initialised? Lastly, I don't think the third argument is well-justified — there are times when all of the other solutions are inelegant.
So finally, is there any other reason for why this kind of casting isn't allowed, and is there an elegant way to circumvent this?
Edit:
Since I don't know a lot about this topic, I think I meant to say 'convert' rather than 'cast'. I'll also add an example to show what I'm trying to succeed. The conversion would only be allowed at the initialisation of the Subclass:
Class BaseClass
Dim x as Integer
Dim y as Integer
End Class
Class Subclass1 : Inherits BaseClass
Dim z as Integer
Sub New(Byval value As Integer)
'Standard initialisation method
MyBase.New()
z = value
End Sub
Sub New(Byval value As Integer, Byval baseInstance As BaseClass)
'Type conversion from base class to subclass
baseInstance.passAllproperties()
'This assigns all properties of baseInstance belonging to BaseClass to Me.
'Properties not in BaseClass (eg. if baseInstance is Subclass2) are ignored.
z = value
End Sub
End Class
Class Subclass2 : Inherits BaseClass
Dim v As Integer
End Class
What you describe is not casting. Have you ever heard the expression"to cast something in a different light"? It means to look at the same thing in a different way or to make the same thing look different. That is the exact way that the term "cast" is used in programming. When you cast, you do NOT change the type of the object but only the type of the reference used to access the object. If you want to cast from a base type to a derived type then the object you're referring to has to actually be that derived type. If it's not then you're not performing a cast but rather a conversion.
So, why can't you convert an instance of a base type to an instance of a derived type. Well, why would you be able to? Yes, it's something that might save writing a bit of code on occasion but does it actually make sense? Let's say that you have a base type with one property and a derived type that adds another property. Let's also say that that derived type has constructors that require you to provide a value for that second property. You're suggesting that the language should provide you with a way to magically convert an instance of the base class into an instance of the derived class, which would mean it would have to slow you to circumvent that rule defined by the author via the constructors. Why would that be a good thing?
Use System.Reflection to iterate over properties and fields of the base class and apply them to the derived class. This example includes a single public property and single public field, but will also work with multiple private/protected properties and fields. You can paste the entire example into a new console application to test it.
Imports System.Reflection
Module Module1
Sub Main()
Dim p As New Parent
p.Property1 = "abc"
p.Field1 = "def"
Dim c = New Child(p)
Console.WriteLine("Property1 = ""{0}"", Field1 = ""{1}""", c.Property1, c.Field1)
Console.ReadLine()
End Sub
Class Parent
Public Property Property1 As String = "not set"
Public Property Field1 As String = "not set"
End Class
Class Child
Inherits Parent
Public Sub New(myParent As Parent)
Dim fieldInfo = GetType(Parent).GetFields(BindingFlags.NonPublic _
Or BindingFlags.Instance)
For Each field In fieldInfo
field.SetValue(Me, field.GetValue(myParent))
Next
Dim propertyInfo = GetType(Parent).GetProperties(BindingFlags.NonPublic _
Or BindingFlags.Instance)
For Each prop In propertyInfo
prop.SetValue(Me, prop.GetValue(myParent))
Next
End Sub
End Class
End Module
Output:
Property1 = "abc", Field1 = "def"
This solution is automated, so you won't need to change anything when adding or removing properties and fields in the base class.
In general, because of this:
Class TheBase
End Class
Class Derived1 : TheBase
Sub Foo()
End Sub
End Class
Class Derived2 : TheBase
Sub Bar()
End Sub
End Class
Sub Main()
Dim myDerived1 As New Derived1
' cast derived to base
Dim myTheBase = CType(myDerived1, TheBase)
' cast base to derived?
' but myTheBase is actually a Derived1
Dim myDerived2 As Derived2 = CType(myTheBase, Derived2)
' which function call would you like to succeed?
myDerived2.Foo()
myDerived2.Bar()
End Sub

Constant inside class

I've tried to create a vb scripts class with a constant and got 800A03EA error. It's it a VBS bug? Isn't it an OOP fundamental rule?
Class customer
' comment it const and its works
const MAX_LEN=70
Private Name
Private Sub Class_Initialize
Name = ""
End Sub
' name property.
Public Property Get getName
getName = Name
End Property
Public Property Let letName(p_name)
Name = p_name
End Property
end class
The documentation lists all statements that are allowed in the context of classes. Const isn't among them, so it's not supported. You can work around the issue by using private member variables that you initialize during instantiation (i.e. in Class_Initialize):
Class customer
Private MAX_LEN
Private Name
Private Sub Class_Initialize
MAX_LEN = 70
Name = ""
End Sub
...
End Class
If instances of the class should expose this value, you could implement it as a read-only property:
Class customer
Private MAX_LEN
Private Sub Class_Initialize
MAX_LEN = 70
End Sub
'read-only property, so no "Property Let/Set"
Public Property Get MaxLength
MaxLength = MAX_LEN
End Property
...
End Class
However, as Ekkehard.Horner pointed out correctly, the value could still be changed by object-internal code. If immutability is the primary requirment for this value you should implement it as a global constant.
I agree with Ansgar Wiechers's answer, but would like to propose another option.
If immutability is more important than performance, you could put the value directly in the Get and use the property to refer to the value instead of a class-level variable.
Class customer
'read-only property, so no "Property Let/Set"
Public Property Get MaxLength
MaxLength = 70
End Property
...
End Class
A Private variable (perhaps with a getter) gives you a value that is read-only from the outside of the class, but class internal code can still change that value.
So using a global Const (perhaps with a 'namespace' name part) may be a better workaround in cases where the constness is most important.

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!

Nested VB (VBA) Enumeration

Ok guys, well i'd like to achieve an effect of nested enumeration for easy grouping some constant strings. Something like the pseudo code bellow:
Enum gKS
Colby = "Hello"
Hays = "World"
end Enum
Enum gMA
Dodge = "Seven"
Muscatine = "Ports"
end Enum
Enum gCountry
north as gMA
south as gKS
end Enum
Public USA as gCountry
So the code bellow should output a "Seven" message:
sub dol()
msgbox USA.north.Dodge
end sub
I don't want use types or classes because no initialisation is needed since all values are know (constants as i said).
Any suggestions?
thx.
Classes are the way to go on this. Enums are simply long values, where limited selection is needed. This will allow for the greatest flexibility with your objects, in case you need these objects to have other function/subs.
Here is a simple layout:
gCountry Class:
Public North As gMA
Public South As gKS
Private Sub Class_Initialize()
Set North = New gMA
Set South = New gKS
End Sub
gKS Class:
Public Property Get Colby() As String
Colby = "Hello"
End Property
Public Property Get Hays() As String
Hays = "World"
End Property
gMA Class:
Public Property Get Dodge() As String
Dodge = "Seven"
End Property
Public Property Get Muscatine() As String
Muscatine = "Ports"
End Property
Testing It:
Public Sub TestIt()
Dim USA As New gCountry
MsgBox USA.North.Dodge
End Sub
I don't believe you're going to be able to do embedded enums the way you are hoping, because enums are considered primitives in the CLR (source). You may as well try to embed ints within ints.
I realize you said you didn't want to use classes, but this is the sort of situation static classes are meant to fill in the .NET world. It will be easily and arbitrarily accessible with no initialization and quick when compiled. This page has more information on statics if you're not familiar with them. You should be able to do whatever you need to do to get the information set up the way you want within that class, be it multiple static classes, a hash table, a multi-dimensional array, or whatever have you.
THX,
So. i've decided to solve that issue using types:
Public Type fCAOCC
itRGI As String
...
End Type
Public Type fCAOBF
itRGI As String
ibEnviar As String
...
End Type
Public Type gTELAS
CAOBF As fCAOBF
CAOCC As fCAOCC
...
End Type
Public CSI As gTELAS
Sub iniGLOBALS()
CSI.CAOBF.itRGI = "DIVNRRGILIG"
CSI.CAOBF.ibEnviar = "DUMMYNAME1"
CSI.CAOCC.itRGI = "Hello"
...
End Sub
And thats ready for later use on code...
cya

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.