VBA Implements / Inheritance - excel

I'm having a hard time understanding and working with Implements and I'm failing to see why this is of any use if Inheritance isn't supported with VBA.
I'm testing the code below and I keep getting the error:
Compile Error:
Object module needs to implement '~' for interface '~'
Interface: ITransmission
Option Explicit
Public pVENDOR As String
Public Property Get VENDOR() As String
End Property
Public Property Let VENDOR(ByVal value As String)
End Property
Base Class: cASN
Option Explicit
Implements ITransmission
Private Property Let ITransmission_pVENDOR(ByVal value As String)
pVENDOR = value
End Property
Private Property Get ITransmission_pVENDOR() As String
ITransmission_pVENDOR = pVENDOR
End Property
Unit Test Method: mUnitTesting
Private Sub Test_cASN()
Dim foo As cASN
Set foo = New cASN
foo.VENDOR = "Test"
End Sub
Still very new to Implements and it is something I want to learn, and I've done a fair amount of research into it.
Question 1:
Why am I getting an error message when I try to unit test this?
Question 2:
What is the real benefit here, if inheritance isn't supported?

You implement pVENDOR but not the two VENDOR properties.
I'm assuming you want the interface to be a get/let of the VENDOR property.
Your Public pVENDOR As String looks like a backing field for this property, as an Interface cannot include an implementation then its not needed.
The Interface should look like:
Public Property Get VENDOR() As String
End Property
Public Property Let VENDOR(ByVal value As String)
End Property
Then when you implement it:
Implements ITransmission
Private pVENDOR As String '// local implementation detail
Public Property Let ITransmission_VENDOR(ByVal value As String)
pVENDOR = value
End Property
Public Property Get ITransmission_VENDOR() As String
ITransmission_VENDOR = pVENDOR
End Property
And to test:
Private Sub Test_cASN()
Dim foo As cASN
Set foo = New cASN
foo.ITransmission_VENDOR = "Test"
End Sub
What is the real benefit here
How will I know when to create an interface?
The point of an Interface

Related

When implementing an Interface in VBA, do the implemented functions need to be private or public?

I am reading up on creating class factories here: https://rubberduckvba.wordpress.com/2018/04/24/factories-parameterized-object-initialization/ and I am confused why they are making the implemented functions private, wouldn't we want them to be public so we can access them?
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Something"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Private Type TSomething
Bar As Long
Ducky As String
End Type
Private this As TSomething
Implements ISomething
Public Function Create(ByVal initialBar As Long, ByVal initialDucky As String) As ISomething
With New Something
.Bar = initialBar
.Ducky = initialDucky
Set Create = .Self
End With
End Function
Public Property Get Self() As ISomething
Set Self = Me
End Property
Public Property Get Bar() As Long
Bar = this.Bar
End Property
Friend Property Let Bar(ByVal value As Long)
this.Bar = value
End Property
Public Property Get Ducky() As String
Ducky = this.Ducky
End Property
Friend Property Let Ducky(ByVal value As String)
this.Ducky = value
End Property
Private Property Get ISomething_Bar() As Long
ISomething_Bar = Bar
End Property
Private Property Get ISomething_Ducky() As String
ISomething_Ducky = Ducky
End Property
Also, why do you need to provide get and let properties for public variables in an interface?
They should be Private.
The reason is because how interfaces work in VBA: the Public members of a class module define its default interface. That means the public members of Class1 define what members Class2 must implement if it Implements Class1.
So if you make Class1_DoSomething public, then you're exposing that member on the default interface of Class2, and that's... not pretty at all.
What interface you access an object with, is determined by how you declare it.
Dim thing As Class1
Set thing = New Class1
If thing is or implements Class1, then the code after this declaration can invoke all the members exposed by the default interface of Class1 (i.e. its public members).
If Class1 implements ISomething and we declare it like this:
Dim thing As ISomething
Set thing = New Class1
Now the members we get to work with are the members defined by the public members of the ISomething class/interface.
When you implement an interface or handle events, you should never manually type the signatures; instead, pick the interface (or event provider) from the upper-left dropdown in the code pane, then pick a member from the upper-right dropdown: the VBE automatically creates the correct procedure with the correct signature, and it's always going to be a Private member - rule of thumb, anything that has an underscore in its name in VBA has no business being Public
As for why you must supply Get and Let accessors for what you defined as a public field (/variable) on an interface class... Fields are implementation details, they should never be Public in the first place. Objects expose properties, not fields - keep fields for the private internal state of the implementing class.
The reason is technical: VBA code gets compiled into a COM type library, and that library sees your public variable and says "that's going to have to be a PUT and a GET method", and the VBA code implementing that interface thus needs to implement a property for every public field, because public fields compile down to properties.
This does have interesting implications with regards to the practice of exposing a public field on a class module (breaks encapsulation vs compiles down to a property anyway!), but that is a whole other discussion.

Create class with multiple indexed properties

I'm trying to create a class with multiple properties that are indexed by the same number I pass into the property. I've tried several configurations of code, my latest failure being this:
'dataTypeClass class module
Private ap() As String
Private dt() As String
Public Property Get apos(index As Long) As Variant
Set apos = ap(index)
End Property
Public Property Get dataType(index As Long) As Variant
Set dataType = dt(index)
End Property
Public Property Let apos(index As Long, apVal As String)
ap(index) = apVal
End Property
Public Property Let dataType(index As Long, dtVal As String)
dt(index) = dtVal
End Property
I get this error code mostly:
Expecting to use below to get "INT" and "" for item 1 in sub below.
Sub classTest()
Dim d As New dataTypeClass
d(1).dataType = "INT"
d(1).apos = ""
Debug.Print d(1).dataType & d(1).apos
End Sub
What several things am I doing wrong?
EDIT:
Taking suggestions below (such as editing my question rather than commenting), I'm setting the variants to strings, and using let instead of set. Since I did indeed get a subscript out of range, I need to initialize the class, but do I need to redim index? How might I do that if the index will have a different maximum each time I run the script?
The apos property1 gets a Variant at the specified index; the implementation suggests that the encapsulated ap(index) is an Object, so the return type should probably be Object instead of Variant:
Public Property Get apos(index As Long) As Variant
Set apos = ap(index)
End Property
The setter uses Let assignment, which is hackish, but allowed given the value is Variant - oh wait no, it's a String!
Public Property Let apos(index As Long, apVal As String)
ap(index) = apVal
End Property
You're getting this error because, as the error states, the property definition is inconsistent. If ap(index) is a String, then the getter should look like this:
Public Property Get apos(index As Long) As String
apos = ap(index)
End Property
If ap(index) is an Object, then the setter should look like this:
Public Property Set apos(index As Long, apVal As Object)
Set ap(index) = apVal
End Property
...and the getter like this:
Public Property Get apos(index As Long) As Object
Set apos = ap(index)
End Property
Or like this:
Public Property Get apos(index As Long) As Variant
Set apos = ap(index)
End Property
Public Property Set apos(index As Long, apVal As Variant)
Set ap(index) = apVal
End Property
In other words:
The RHS/value parameter of Property Let/Property Set needs to be the same type as the return type of the same-name Property Get member.
If the type is an object type, use Property Set for assignments.
If the type is a value type, use Property Let for assignments.
Avoid Variant if possible.
Do not use Set to assign anything other than an object reference.
1All of this is also true for the dataType property.
You have a variant for the return type of your Get but your Let additional param is using a string. Perhaps you need dtVal As Variant? As mentioned in comments and other answer there are additional things to consider; some will be implementation specific.
Quote:
The parameters for Property Get, Property Let, and Property
Setprocedures for the sameproperty must match exactly, except that the
Property Let has one extra parameter, whose type must match the return
type of the corresponding Property Get, and the Property Set has one
more parameter than the corresponding Property Get, whose type is
either Variant, Object, a class name, or an object library type
specified in an object library. This error has the following causes
and solutions:
The number of parameters for the Property Get procedure isn't one less
than the number of parameters for the matching Property Let or
Property Set procedure. Add a parameter to Property Let or Property
Set or remove a parameter from Property Get, as appropriate.
The parameter types of Property Get must exactly match the
corresponding parameters of Property Let or Property Set, except for
the extra Property Set parameter. Modify the parameter declarations in
the corresponding procedure definitions so they are appropriately
matched.
The parameter type of the extra parameter of the Property Let must
match the return type of the corresponding Property Get procedure.
Modify either the extra parameter declaration in the Property Let or
the return type of the corresponding Property Get so they are
appropriately matched.
The parameter type of the extra parameter of the Property Set can
differ from the return type of the corresponding Property Get, but it
must be either a Variant, Object, class name, or a validobject library
type.
Make sure the extra parameter of the Property Set procedure is either
a Variant, Object, class name, or object library type.
You defined a Property procedure with an Optional or a ParamArray
parameter. ParamArray and Optional parameters aren't permitted in
Property procedures. Redefine the procedures without using
thesekeywords.

Worksheets vs. Worksheets(1), can't I do this from .net interop?

Our object model contains a class called Unit and a collection of these called Units (which is stored in a Dictionary). These objects have unique Names and Keys (they originally came from a SQL db that enforced this) so I have added:
Public Units(N as String) As Unit ...
Public Units(K as Integer) As Unit...
which return a Unit object from the Units collection.
In Excel VBA, one can refer to most objects using similar methods; Worksheets(1) returns the first sheet, while Worksheets("Bob") returns the named sheet. But they have one additional method, Worksheets, which returns the entire collection. It's as if they have this method...
Public Worksheets() As List(Of Worksheet)
But you can't use List in interop (right?) so it's more like...
Public Worksheets() As ArrayList
So how would I do the same basic API in .net with interop? That is, have three methods...
Public Units(N as String) As Unit ...
Public Units(K as Integer) As Unit...
Public Units() As ArrayList...
As I understand it only the first method of a given name is exported (is this correct?). So how does Excel do it, and can I fake that in .net?
VBA's Worksheets is not a method. It is a class, Worksheets, that has a default property Item that accepts a parameter of type Variant. There is no overloading (COM does not support it), it's just that Variant can hold both a number or a string.
If you want a similar structure in VB.NET, you can have a collection class that implements a default property as VB.NET understands it, and this time you can overload it.
Public Class UnitsCollection
Default Public ReadOnly Property Item(ByVal i As Integer) As Unit
Get
Return ...
End Get
End Property
Default Public ReadOnly Property Item(ByVal i As String) As Unit
Get
Return ...
End Get
End Property
End Class

Trouble with object array as object property in VBA Excel 2010

Private oiCustoms() As CCustomClass
Public Property Get Partners() As CCustomClass()
Set Partners() = oiCustoms()
End Property
Public Property Set Partners(values() As CCustomClass)
ReDim oiPartners(values.Count)
Set oiCustoms() = values()
End Property
When I try to run I get a Compile error:
Definitions of property procedures for the same property are inconsistent, or property procudure has n optional parameter, a ParamArray, or an invalid Set final parameter.
What's wrong here? I have done some looking, and it looks like I can't use an array as a property parameter. Is this correct? Any good workarounds. I will need to have this data for the object stored in an array for use elsewhere.
You are confusing arrays with objects.
Private oiCustoms() As CCustomClass
Public Property Get Partners() As CCustomClass()
Partners = oiCustoms
End Property
Public Property Let Partners(values() As CCustomClass)
ReDim oiPartners(LBound(values) To UBound(values))
oiCustoms = values
End Property

Automapper with a base class enumeration property

Is there any way to get this to work? Here's a simplified/contrived illustration of my issue (Pardon my wordy VB):
Domain Model Classes
Public Class Car
Public Property Id As Integer
Public Property Seats As IEnumerable(Of Seat)
End Class
Public MustInherit Class Seat
End Class
Public Class StandardSeat
Inherits Seat
Public Property Manufacturer As String
End Class
Public Class CustomSeat
Inherits Seat
Public Property Installer As String
End Class
View Model Classes
Public Class CarModel
Public Property Id As String
Public Property Seats As IEnumerable(Of SeatModel)
End Class
Public Class SeatModel
Public Property Manufacturer As String
Public Property Installer As String
End Class
Mapping and Test Code
<Test()> Public Sub Test()
Mapper.CreateMap(Of Car, CarModel)()
Mapper.CreateMap(Of Seat, SeatModel)() _
.ForMember("Manufacturer", Sub(cfg) cfg.Ignore()) _
.ForMember("Installer", Sub(cfg) cfg.Ignore())
Mapper.CreateMap(Of StandardSeat, SeatModel)() _
.ForMember("Installer", Sub(cfg) cfg.Ignore())
Mapper.CreateMap(Of CustomSeat, SeatModel)() _
.ForMember("Manufacturer", Sub(cfg) cfg.Ignore())
Mapper.AssertConfigurationIsValid()
Dim car As New Car With {.Id = 4}
car.Seats = New Seat() {
New StandardSeat With {.Manufacturer = "Honda"},
New CustomSeat With {.Installer = "Napa"}}
Dim model = Mapper.Map(Of Car, CarModel)(car)
model.Id.ShouldEqual("4")
model.Seats.Count().ShouldEqual(2)
' These next two assertions fail.
model.Seats.First().Manufacturer.ShouldEqual("Honda")
model.Seats.Last().Installer.ShouldEqual("Napa")
End Sub
Instead of doing this, I'd map to a parallel inheritance hierarchy on the ViewModel side. Create a SeatModel, StandardSeatModel and a CustomSeatModel. You would then use the Include() configuration option to link the Seat -> SeatModel mapping configuration to the mapping configurations to StandardSeat -> StandardSeatModel and the other.
This way, you don't need all the Ignore() s and whatnot. If you still want to flatten your original model, you'll still need to include the Include() configuration on the Seat -> SeatModel piece.

Resources