Class constructor confusion - wrong number of arguments or invalid property assignment - excel

I'm having a class module with some data:
Private sharedFolders() As String
Public Property Let SetSharedFolders(val As String)
Dim i As Integer
sharedFolders = Array("folder one", "folder two")
i = UBound(sharedFolders)
i = UBound(sharedFolders)
ReDim Preserve sharedFolders(i)
sharedFolders(i) = CStr(val)
End Property
Property Get GetSharedFolders()
GetSharedFolders = sharedFolders()
End Property
And I want to add something to this property from other module like this:
Sub PrepareData()
Dim e
Dim s
Dim a(2) As String
Set e = New Entry
a(0) = "add one"
a(1) = "add two"
For Each s In a
e.SetSharedFolders (s) 'Here comes exception
Next
For Each s In e.GetSharedFolders
Debug.Print s
Next
End Sub
But I receive an "wrong number of arguments or invalid property assignment vba" exception... Can anyone assist?
Addendum
Thanks to #AJD and #Freeflow to pointing out a mistake and idea to make it easier. Decided to make as like below.
Class Module:
Private sharedFolders As New Collection
Public Property Let SetSharedFolders(val As String)
If sharedFolders.Count = 0 Then ' if empty fill with some preset data and add new item
sharedFolders.Add "folder 1"
sharedFolders.Add "folder 2"
sharedFolders.Add CStr(val)
Else
sharedFolders.Add CStr(val)
End If
End Property
Property Get GetSharedFolders() As Collection
Set GetSharedFolders = sharedFolders
End Property
and regular module:
Sub AddData()
Dim e As New Entry ' creating an instance of a class
Dim s As Variant ' variable to loop through collection
Dim a(1) As String 'some array with data to insert
a(0) = "add one"
a(1) = "add two"
For Each s In a
e.SetSharedFolders = s
Next
For Each s In e.GetSharedFolders
Debug.Print s
Next
End Sub

Initially I thought the problem lies in this code:
i = UBound(sharedFolders)
i = UBound(sharedFolders)
ReDim Preserve sharedFolders(i)
sharedFolders(i) = CStr(val)
i is set twice to the same value, and then the sharedFolders is reDimmed to the same value it was before! Also, there is some trickery happening with the use of ix within a 0-based array.
But the problem is most likely how you have declared your variables.
For Each s In a
e.SetSharedFolders (s) 'Here comes exception
Next
s is a Variant, and a is a Variant. At this point VBA is trying to guess how to handle a For Each loop with two Variants. And then the improper call is made. The correct syntax is:
e.SetSharedFolders s '<-- no parenthesis
There are plenty of posts on StackOverflow explaining how to call routines and what the impact of the evaluating parenthesis are!
However, at this point we are only assuming it is passing in a single element of the array - it could be passing the full array itself (albeit unlikely).
And the third factor -
Public Property Let SetSharedFolders(val As String)
The parameter val is being passed ByRef and should be passed ByVal. This also has unintended side effects as I found out (Type mismatch trying to set data in an object in a collection).
Public Property Let SetSharedFolders(ByVal val As String)
All in all you have the perfect storm of ambiguity driving to an unknown result.
The answer here is to strongly type your variables. This removes about two layers of ambiguity and areas where errors can happen. In addition, this will slightly improve code execution.
Another aspect is to understand when you should pass something ByVal and when to use the default (preferably explicitly) ByRef.
And a final gratuitous hint: Use a Collection instead of an Array. Your code you have implies a Collection will be more efficient and easier to manage.
Addendum
(thanks to #FreeFlow):
If the OP changes the definition of sharedfolders to Variant rather than String() then the array statement will work as expected.
The line e.SetSharedFolders (s) will work fine if it is changed to e.SetSharedFolders = s because the method SetSharedFolders is a Let Property not a Sub. There are other errors but these two changes will make the code run.

Related

Unable to find a way to store data into an array inside a class module

I've been wrestling with this problem for a while. My problem is that I have a bunch of JSON data and I want to represent it as objects.
Arrays are problematic.
I create a class module such as FancyCat with a public Name as String for its name.
Then I can set this with
Dim MyFancyCat as FancyCat
Set MyFancyCat = new FancyCat
FancyCat.Name = JSONData("Name")
I've typed that from memory but I think it's correct. Anyhoo, it works fine.
The problem is that a fancy cat has several pairs of socks. The number of socks is variable.
In vba you cannot for some reason have a public array. So this code is illegal:
public Socks() as FancySock 'Illegal
Looking on SO I found two solutions, one, to make it private and use a property to access it, and the other, to declare it as Variant and then stick an array into it later.
My approach to populating this array, is to examine the JSON array to get the Count, and then to ReDim the array to match and then populate it.
The problem is my ReDim statement refuses to work.
It seems I cannot redim a property, I get an error. And I also get an error trying to redim the public variant field. My ReDim works OK if I declare a local array and redim it, so potentially I could do that and then assign it to the property... but it just seems bizarre that I can't redim it directly.
Any idea why it's not working?
With the Variant approach above my code is:
ReDim MyFancyCat.Socks(socksLength) As FancySocks
And in the FancyCat class module:
public Socks As Variant
I get Method or Data Member Not Found.
The error for the other approach was different but I rejigged all my code to try the second approach so I am not sure what it was.
Edit: I'm gonna explain what I am trying to do a bit more clearly. I have some JSON data coming in, and I want to store it as an object hierarchy.
In C# I would do this (pseudo code without linq shortcuts):
var myData = ReadJsonData(); // Produces a kind of dictionary
var myFancyCat = new FancyCat();
myFancyCat.Name = myData["Name"];
myFancyCat.Age = myData["Age"];
myFancyCat.Socks = new List<FancySock>();
foreach (var sock in myData["Socks"])
{
myFancyCat.Socks.Add(sock);
}
In excel I want to do the same thing.
So I make a class module for FancyCat and FancySock and give FancyCat public members for Name, Age etc but then I also want an array of socks that my cat owns. I wanted to do this with strongly typed references, e.g. my c# code above I can do:
myFancyCat.Socks[0].Colour // Intellisense works, shows colour as a property
However it seems in excel you can't have publicly declared arrays. So you can get around this according to the comments by declaring it as variant and then sticking an array in anyway, but you would lose the intellisense. Or you can use a get/let property which kinda works but is more fiddly as it seems you can't actually expose an array using a get/let you have to have it take an index and expose elements individually.
So at this point I am thinking forget the strongly typed it's not happening, perhaps use a collection?
The FancySock class may have further nested arrays within it. I've read that there's no ByRef for arrays (at least, not completely - I think you can get an array ByRef but not set one?). I am not sure if that would create problems with trying to set it.
But ultimately, I just want to end up with my JSON data represented easily in an OO way, so that in my excel ultimately I can just do
myFancyCat.Name or myFancyCat.Socks.Count or myFancyCat.Socks(1).Colour etc
It seems much harder than it looks to simply deserialise JSON into 'objects' in vba.
Please, try the next way:
Insert a class module, name it FancyCat and copy the next code:
Option Explicit
Private arrL As Object
Public myName As String, myAge As Long
Public Sub Class_Initialize()
Set arrL = CreateObject("System.Collections.ArrayList")
End Sub
Public Property Let Name(strName As String)
myName = strName
End Property
Public Property Let Age(lngAge As String)
myAge = lngAge
End Property
Public Property Let SocksAdd(sMember)
arrL.Add sMember
End Property
Public Property Get Socks() As Variant
Socks = arrL.toarray()
End Property
Use it in the next testing Sub:
Sub testClassDictListArray()
Dim myFancyCat As New FancyCat, myData As Object
Dim arrSocks, sock
Set myData = CreateObject("Scripting.Dictionary") 'this should be the dictionary returned by ParseJSON
myData.Add "Name", "John Doe": myData.Add "Age", 35
myData.Add "Socks", Array("Blue", "White", "Red", "Green", "Yellow")
myFancyCat.Name = myData("Name")
myFancyCat.Age = myData("Age")
For Each sock In myData("Socks")
myFancyCat.SocksAdd = sock
Next sock
arrSocks = myFancyCat.Socks
Debug.Print Join(arrSocks, "|")
End Sub
I am not sure I perfectly understand the scenario you try putting in discussion...
If you want to benefit of instellisense suggestions, I will tell you what references to be added. Even, I will send two pieces of code to automatically add the necessary references (I mean, Scripting.Dictionary and ArrayList`).
Please, test it and send some feedback.
In your class:
Private m_Name As String
Private m_Socks() As String
Public Property Let Name(Name As String)
m_Name = Name
End Property
Public Property Get Name() As String
Name = m_Name
End Property
Public Sub SetSize(Quantity As Long)
ReDim m_Socks(1 To Quantity)
End Sub
Public Property Let Socks(Index As Long, Sock As String)
m_Socks(Index) = Sock
End Property
Public Property Get Socks(Index As Long) As String
Socks = m_Socks(Index)
End Property
In a regular module:
Sub UseFancyCat()
Dim MyFancyCat As FancyCat
Set MyFancyCat = New FancyCat
MyFancyCat.Name = "Fancy Name"
MyFancyCat.SetSize 2
MyFancyCat.Socks(1) = "Sock1"
MyFancyCat.Socks(2) = "Sock2"
Debug.Print MyFancyCat.Name
Debug.Print MyFancyCat.Socks(1)
Debug.Print MyFancyCat.Socks(2)
End Sub

How to run a sub in a `Create` function and how to make a mock/stub/fake for a chart `Series`?

Preface
About 10 years ago I started refactoring and improving the ChartSeries class of John Walkenbach. Unfortunately it seems that the original it is not available any more online.
Following the Rubberduck Blog for quite some time now I try to improve my VBA skills. But in the past I only have written -- I guess the experts would call it -- "script-like god-procedures" (because of not knowing better). So I am pretty new to classes and especially interfaces and factories.
Actual Questions
I try to refactor the whole class by dividing it into multiple classes also using interfaces and than also adding unit tests. For just reading the parts of a formula it would be sufficient to get the Series.Formula and then do all the processing. So it would be nice to call the Run sub in the Create function. But everything I tried so far to do so failed. Thus, I currently running Run in all Get properties etc. (and test, if the formula changed and exit Run than. Is this possible and when yes, how?
Second, to add unit tests -- of course using rubberduck for them -- I currently rely on real Charts/ChartObjects. How do I create a stub/mock/fake for a Series? (Sorry, I don't know the correct term.)
And here a simplified version of the code.
Many thanks in advance for any help.
normal module
'#Folder("ChartSeries")
Option Explicit
Public Sub ExampleUsage()
Dim wks As Worksheet
Set wks = ThisWorkbook.Worksheets(1)
Dim crt As ChartObject
Set crt = wks.ChartObjects(1)
Dim srs As Series
Set srs = crt.Chart.SeriesCollection(3)
Dim MySeries As IChartSeries
Set MySeries = ChartSeries.Create(srs)
With MySeries
Debug.Print .XValues.FormulaPart
End With
End Sub
IChartSeries.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Function IsSeriesAccessible() As Boolean
End Function
Public Property Get FullFormula() As String
End Property
Public Property Get XValues() As ISeriesPart
End Property
'more properties ...
ChartSeries.cls
'#PredeclaredId
'#Exposed
'#Folder("ChartSeries")
Option Explicit
Implements IChartSeries
Private Type TChartSeries
Series As Series
FullSeriesFormula As String
OldFullSeriesFormula As String
IsSeriesAccessible As Boolean
SeriesParts(eElement.[_First] To eElement.[_Last]) As ISeriesPart
End Type
Private This As TChartSeries
Public Function Create(ByVal Value As Series) As IChartSeries
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New ChartSeries
.Series = Value
Set Create = .Self
End With
End Function
Public Property Get Self() As IChartSeries
Set Self = Me
End Property
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
Private Function IChartSeries_IsSeriesAccessible() As Boolean
Call Run
IChartSeries_IsSeriesAccessible = This.IsSeriesAccessible
End Function
Private Property Get IChartSeries_FullFormula() As String
Call Run
IChartSeries_FullFormula = This.FullSeriesFormula
End Property
Private Property Get IChartSeries_XValues() As ISeriesPart
Call Run
Set IChartSeries_XValues = This.SeriesParts(eElement.eXValues)
End Property
'more properties ...
Private Sub Class_Initialize()
With This
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = New SeriesPart
Next
End With
End Sub
Private Sub Class_Terminate()
With This
Dim Element As LongPtr
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = Nothing
Next
End With
End Sub
Private Sub Run()
If Not GetFullSeriesFormula Then Exit Sub
If Not HasFormulaChanged Then Exit Sub
Call GetSeriesFormulaParts
End Sub
'(simplified version)
Private Function GetFullSeriesFormula() As Boolean
GetFullSeriesFormula = False
With This
'---
'dummy to make it work
.FullSeriesFormula = _
"=SERIES(Tabelle1!$B$2,Tabelle1!$A$3:$A$5,Tabelle1!$B$3:$B$5,1)"
'---
.OldFullSeriesFormula = .FullSeriesFormula
.FullSeriesFormula = .Series.Formula
End With
GetFullSeriesFormula = True
End Function
Private Function HasFormulaChanged() As Boolean
With This
HasFormulaChanged = (.OldFullSeriesFormula <> .FullSeriesFormula)
End With
End Function
Private Sub GetSeriesFormulaParts()
Dim MySeries As ISeriesFormulaParts
'(simplified version without check for Bubble Chart)
Set MySeries = SeriesFormulaParts.Create( _
This.FullSeriesFormula, _
False _
)
With MySeries
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last] - 1
This.SeriesParts(Element).FormulaPart = _
.PartSeriesFormula(Element)
Next
'---
'dummy which normally would be retrieved
'by 'MySeries.PartSeriesFormula(eElement.eXValues)'
This.SeriesParts(eElement.eXValues).FormulaPart = _
"Tabelle1!$A$3:$A$5"
'---
End With
Set MySeries = Nothing
End Sub
'more subs and functions ...
ISeriesPart.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Enum eEntryType
eNotSet = -1
[_First] = 0
eInaccessible = eEntryType.[_First]
eEmpty
eInteger
eString
eArray
eRange
[_Last] = eEntryType.eRange
End Enum
Public Property Get FormulaPart() As String
End Property
Public Property Let FormulaPart(ByVal Value As String)
End Property
Public Property Get EntryType() As eEntryType
End Property
Public Property Get Range() As Range
End Property
'more properties ...
SeriesPart.cls
'#PredeclaredId
'#Folder("ChartSeries")
'#ModuleDescription("A class to handle each part of the 'Series' string.")
Option Explicit
Implements ISeriesPart
Private Type TSeriesPart
FormulaPart As String
EntryType As eEntryType
Range As Range
RangeString As String
RangeSheet As String
RangeBook As String
RangePath As String
End Type
Private This As TSeriesPart
Private Property Get ISeriesPart_FormulaPart() As String
ISeriesPart_FormulaPart = This.FormulaPart
End Property
Private Property Let ISeriesPart_FormulaPart(ByVal Value As String)
This.FormulaPart = Value
Call Run
End Property
Private Property Get ISeriesPart_EntryType() As eEntryType
ISeriesPart_EntryType = This.EntryType
End Property
Private Property Get ISeriesPart_Range() As Range
With This
If .EntryType = eEntryType.eRange Then
Set ISeriesPart_Range = .Range
Else
' Call RaiseError
End If
End With
End Property
Private Property Set ISeriesPart_Range(ByVal Value As Range)
Set This.Range = Value
End Property
'more properties ...
Private Sub Class_Initialize()
This.EntryType = eEntryType.eNotSet
End Sub
Private Sub Run()
'- set 'EntryType'
'- If it is a range then find the range parts ...
End Sub
'a lot more subs and functions ...
ISeriesParts.cls
'#Folder("ChartSeries")
'#Interface
Option Explicit
Public Enum eElement
[_First] = 1
eName = eElement.[_First]
eXValues
eYValues
ePlotOrder
eBubbleSizes
[_Last] = eElement.eBubbleSizes
End Enum
'#Description("fill me")
Public Property Get PartSeriesFormula(ByVal Element As eElement) As String
End Property
SeriesFormulaParts.cls
'#PredeclaredId
'#Exposed
'#Folder("ChartSeries")
Option Explicit
Implements ISeriesFormulaParts
Private Type TSeriesFormulaParts
FullSeriesFormula As String
IsSeriesInBubbleChart As Boolean
WasRunCalled As Boolean
SeriesFormula As String
RemainingFormulaPart(eElement.[_First] To eElement.[_Last]) As String
PartSeriesFormula(eElement.[_First] To eElement.[_Last]) As String
End Type
Private This As TSeriesFormulaParts
Public Function Create( _
ByVal FullSeriesFormula As String, _
ByVal IsSeriesInBubbleChart As Boolean _
) As ISeriesFormulaParts
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New SeriesFormulaParts
.FullSeriesFormula = FullSeriesFormula
.IsSeriesInBubbleChart = IsSeriesInBubbleChart
Set Create = .Self
End With
End Function
Public Property Get Self() As ISeriesFormulaParts
Set Self = Me
End Property
'#Description("Set the full series formula ('ChartSeries')")
Public Property Let FullSeriesFormula(ByVal Value As String)
This.FullSeriesFormula = Value
End Property
Public Property Let IsSeriesInBubbleChart(ByVal Value As Boolean)
This.IsSeriesInBubbleChart = Value
End Property
Private Property Get ISeriesFormulaParts_PartSeriesFormula(ByVal Element As eElement) As String
'NOTE: Instead of running 'Run' here, it would be better to run it in 'Create'
Call Run
ISeriesFormulaParts_PartSeriesFormula = This.PartSeriesFormula(Element)
End Property
'(replaced with a dummy)
Private Sub Run()
If This.WasRunCalled Then Exit Sub
'extract stuff from
This.WasRunCalled = True
End Sub
'a lot more subs and functions ...
You can already!
Public Function Create(ByVal Value As Series) As IChartSeries
With New ChartSeries <~ With block variable has access to members of the ChartSeries class
.Series = Value
Set Create = .Self
End With
End Function
...only, like the .Series and .Self properties, it has to be a Public member of the ChartSeries interface/class (the line is blurry in VBA, since every class has a default interface / is also an interface).
Idiomatic Object Assignment
A note about this property:
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
Using a Property Let member to Set an object reference will work - but it isn't idiomatic VBA code anymore, as you can see in the .Create function:
.Series = Value
If we read this line without knowing about the nature of the property, this looks like any other value assignment. Only problem is, we're not assigning a value, but a reference - and reference assignments in VBA are normally made using a Set keyword. If we change the Let for a Set in the Series property definition, we would have to do this:
Set .Series = Value
And that would look much more readily like the reference assignment it is! Without it, there appears to be implicit let-coercion happening, and that makes it ambiguous code: VBA requires a Set keyword for reference assignments, because any given object can have a paraterless default property (e.g. how foo = Range("A1") implicitly assigns foo to the Value of the Range).
Caching & Responsibilities
Now, back to the Run method - if it's made Public on the ChartSeries class, but not exposed on the implemented IChartSeries interface, then it's a member that can only be invoked from 1) the ChartSeries default instance, or 2) any object variable that has a ChartSeries declared type. And since our "client code" is working off IChartSeries, we can guard against 1 and shrug off 2.
Note that the Call keyword is superfluous, and the Run method is really just pulling metadata from the encapsulated Series object, and caching it at instance level - I'd give it a name that sounds more like "refresh cached properties" than "run something".
Your hunch is a good one: Property Get should be a simple return function, without any side-effects. Invoking a method that scans an object and resets instance state in a Property Get accessor makes it side-effecting, which is a design smell - in theory.
If Run is invoked immediately after creation before the Create function returns the instance, then this Run method boils down to "parse the series and cache some metadata I'll reuse later", and there's nothing wrong with that: invoke it from Create, and remove it from the Property Get accessors.
The result is an object whose state is read-only and more robustly defined; the counterpart of that is that you now have an object whose state might be out of sync with the actual Excel Series object on the worksheet: if code (or the user) tweaks the Series object after the IChartSeries is initialized, the object and its state is stale.
One solution is to go out of your way to identify when a series is stale and make sure you keep the cache up-to-date.
Another solution would be to remove the problem altogether by no longer caching the state - that would mean one of two things:
Generating the object graph once on creation, effectively moving the caching responsibility to the caller: calling code gets a read-only "snapshot" to work with.
Generating a new object graph out of the series metadata, every time the calling code needs it: effectively, it moves the caching responsibility to the caller, which isn't a bad idea at all.
Making things read-only removes a lot of complexity! I'd go with the first option.
Overall, the code appears nice & clean (although it's unclear how much was scrubbed for this post) and you appear to have understood the factory method pattern leveraging the default instance and exposing a façade interface - kudos! The naming is overall pretty good (although "Run" sticks out IMO), and the objects look like they each have a clear, defined purpose. Good job!
Unit Testing
I currently rely on real Charts/ChartObjects. How do I create a stub/mock/fake for a Series? (Sorry, I don't know the correct term.)
Currently, you can't. When if/when this PR gets merged, you'll be able to mock Excel's interfaces (and much, much more) and write tests against your classes that inject a mock Excel.Series object that you can configure for your tests' purposes... but until then, this is where the wall is.
In the mean time, the best you can do is wrap it with your own interface, and stub it. In other words, wherever there's a seam between your code and Excel's object model, we slip an interface between the two: instead of taking in a Excel.Series object, you'd be taking in some ISeriesWrapper, and then the real code would be using an ExcelSeriesWrapper that works off an Excel.Series, and the test code might be using a StubSeriesWrapper whose properties return either hard-coded values, or values configured by tests: the code that works at the seam between the Excel library and your project, can't be tested - and we woulnd't want to anyway, because then we'd be testing Excel, not our own code.
You can see this in action in the example code for the next upcoming RD News article here; that article will discuss exactly this, using ADODB connections. The principle is the same: none of the 94 unit tests in that project ever open any actual connection, and yet with dependency injection and wrapper interfaces we're able to test every single bit of functionality, from opening a database connection to committing a transaction... without ever hitting an actual database.

VBA - Dictionary search - type mismatch

So I'm learning how to use Dictionaries but I have encountered a type problem. In the code below I get an error.
Type mismatch
Each time I try to call If Not target.SearchItem(sKey) Is Nothing Then. I want it to return a object then if it isn't Nothing I can convert it back to long.
For Each target In dList.getArray
For Each key In aList.getArray(1).getKeys
sKey = CStr(key)
'Error occurs here - as Type mismatch
If Not target.SearchItem(sKey) Is Nothing Then
cData.setData(sKey) = CLng(target.SearchItem(sKey))
target.removeData = sKey
Else
'if value doesn't exists it will just be 0
cData.setData(sKey) = 0
End If
Next key
Next target
data is my dictionary that is in a separate class:
Property Get SearchItem(name As String) As Variant
If data.exists(name) Then
'becomes a Variant/Integer - data(name) is a Variant/long
Set SearchItem = CVar(data(name))
Else
'Should return Nothing if item doesnt exist
Set SearchItem = Nothing
End If
End Property
Update: To explain the problem I bit more. Even though I return it as a variant it will still be partly Integer and therefor If Not target.SearchItem(sKey) Is Nothing Then will return mismatch as it needs an object and VBA doesn't read is as a variant or something. Is there anything like Nothing, null or equally that works for a long? That would solve the problem.
This code below returns a long as I want it to, but I can't use -99 as it would corrupt the data analyse. It needs to be something that isn't a value
Property Get SearchItem(name As String)
If data.exists(name) Then
SearchItem = data(name)
Else
'SearchItem = Nothing
SearchItem = -99
End If
End Property
If Not target.SearchItem(sKey) Is Nothing Then
This implies that the stored item is an Object
cData.setData(sKey) = CLng(target.SearchItem(sKey))
The only way that this line will work on an Object is that the Object has a default value that can be converted to long. If the Object's default value is returning a value that can be converted to long.
Does Clng(Object) work on the actual Object that is being stored in the Dictionary?
If you are storing mixed data type then SearchItem check if the return data is an Object.
Property Get SearchItem(name As String) As Variant
If isObject(data(name)) Then
Set SearchItem = data(name)
Else
SearchItem = data(name)
End If
End Property
I would not convert the datatype in this method. I would instead create one or more separate methods or convert the data type at the point of use.
Function getTypedItem(Key as Variant, DataType as VbVarType) as Variant
IsNumeric(Obj)
Since you are using actual Keys of the Dictionary we know that it will return something and there is not a need for .Exists in this context. What you will need to do is test if it is returning an Object before you test whether the Object Is Nothing.
If IsObject(Target.SearchItem(sKey)) Then
If IsNumeric(Target.SearchItem(sKey)) Then
cData.setData(sKey) = CLng(Target.SearchItem(sKey))
End If
ElseIf IsNumeric(Target.SearchItem(sKey)) Then
cData.setData(sKey) = CLng(Target.SearchItem(sKey))
End If
I don't think you can use Set to assign an integer, so the property will fail when the dictionary item is an Integer. Perhaps use:
Property Get SearchItem(name As String) As Variant
If data.exists(name) Then
'becomes a Variant/Integer - data(name) is a Variant/long
SearchItem = data(name)
...
Unfortunately, VBA does not have nullable primitive types such as a nullable Long. A usual workaround is to use a Variant, which can be assigned the value of Empty and which can also hold a Long natively.
I would rewrite your property as follows:
Property Get SearchItem(name As String) As Variant
If Data.exists(name) Then
SearchItem = Data(name)
Else
SearchItem = Empty
End If
End Property
And the test for empty would be:
If Not IsEmpty(target.SearchItem(sKey)) Then ...

Evaluate statement (#DbLookUp) doesn't work with Lotusscript

The last week I asked how to solve an error in an evaluate statement (Error in Evaluate statement macro).
Once fix it, I have other error with the same evaluate statement, it doesn't give me any value.
I will describe what I have and what I try.
#DbLookup in Calculate Text
I have this code into in an calculate Text and it works fine.
suc := #Trim(#Left(LlcPoliza;2));
_lkp := _lkp := #DbLookup("":"NoCache";"C1256EAD:00478951";"People2"; "D"+suc; "FullName");
#If( #IsError( _lkp ) ; " " ; _lkp );
#Name([CN];_lkp)
LlcPoliza is a document field (doc.LlcPoliza) and in a document it has for example the value C2H2H2.
The formula give first the value C2 and then look up into People2 who is D+C2 and give me a person.
It works fine.
Evaluate Statement (#DbLookup) in a Class
I have a class DirectorSucursal.
Class DirectorSucursal
Private m_branch As String
'Constructor class
Public Sub New (branch)
Dim subString As String
subString = Left(branch, 2)
me.m_branch = subString
End Sub
'Deleter Class
Public Sub Delete
End Sub
'Sub show the code about Suc
Public Sub GetCodSuc
MsgBox m_branch
End Sub
'Function get the name director
Public Function getNameDirector As String
Dim varResult As Variant
varResult = Evaluate({#DbLookup("":"NoCache";"C1256EAD:00478951";"People2"; "D} & m_branch & {"; "FullName)"})
getNameDirector = CStr( varResult(0) )
End Function
End Class
Then, in a button I instantiate the new object DirectorSucursal with the parameter of the field doc.LlcPoliza(0) like this.
Sub Click(Source As Button)
Dim director As New DirectorSucursal(doc.LlcPoliza(0))
director.GetCodSuc
director.getNameDirector
end Sub
The field doc.LlcPoliza(0) has the value C2H2H2. GetCodSuc show the value C2, but the function getNameDirector doesn't work.
It shows the error:
Operation failed
Evaluate Statement (#DbLookup) in click button
I have tried the same but into a click sub.
Sub Click(Source As Button)
Dim subString As String
subString = Left(doc.LlcPoliza(0), 2)
Dim eval As String
eval = Evaluate({#DbLookup("":"NoCache";"C1256EAD:00478951";"People2"; "D} & subString & {"; "FullName)"})
Msgbox eval
End Sub
The field doc.LlcPoliza(0) has the value C2H2H2. But it doesn't work
It shows the error:
Operation failed
My question is: what am i doing wrong? Why the code works fine in a calculate text with #Formula but with Lotusscript not?
Thanks.
EDIT 1:
I have added and Error Goto, modified the class code, modified #dblookup in calculate text and I have this error:
Error in EVALUATE macro
Please read documentation and use help! evaluate always returns an ARRAY, as stated in the help:
Return value
variant
The result of the evaluation. A scalar result is returned.
To make your code return a STRING you need to change it like this:
Public Function getNameDirector As String
Dim varResult as Variant
varResult = Evaluate({#DbLookup("":"NoCache";"C1256EAD:00478951";"People2"; "D} & m_branch & {"; "FullName")})
getNameDirector = Cstr( varResult(0) )
End Function
The CStr is just there for the case where the #DBLookup returns an error or a number (both possible)
Just a few things in general:
NEVER write even one line of LotusScript- code without error handler. It will cause you trouble FOR SURE. If you had error handling in place, then it would have told you in which line the error occured...
NEVER use the result of #DBLookup without checking for #IsError... It will cause lot of troubles when the lookup fails.
IF you use #Iserror, then don't do the Lookup twice, assign the lookup to a variable and check that one for #Iserror, like this. Otherwise performance will go down in big forms:
Example:
_lkp := #DbLookup("":"NoCache";"C1256EAD:00478951";"People2"; "D"+suc; "FullName");
#If( #IsError( _lkp ) ; " " ; _lkp )
EDIT: As Knut correctly stated in his answer the real cause for the error was a typo in the formula ( Fullname)" instead of Fullname") that I fixed in my example as well.
1) My suggestion is to never (or at least very seldom) use Evaluate() in Lotusscript. You have proper Lotusscript functionality to do almost everything.
One of the major reasons is that the code is very hard to debug (which is what you are now experiencing).
2) Don't use extended notation when you work with fields. The best practice is to use the GetItemValue and ReplaceItemValue methods of the NotesDocument class for performance reasons as well as compatibility reasons.
3) In the examples with buttons you have a reference to doc, but it is never declared or initialized in the code. If you would use Option Declare at the top of your code you would catch these kinds of errors.
4) I also reccomend against using replica ID to reference databases, that makes it very hard to maintain in the future. Unless you have a very good and convincing reason, reference them by server and filename instead.
I would suggest you refactor your code to something like this:
'Function get the name director
Public Function getNameDirector() As String
Dim db as NotesDatabase
Dim view as NotesView
Dim doc as NotesDocument
Dim key as String
Dim fullname As String
Dim varResult As Variant
Set db = New NotesDatabase("Server/Domain","path/database.nsf")
If db Is Nothing Then
MsgBox "Unable to open 'path/database.nsf'"
Exit Function
End if
Set view = db.GetView("People2")
If view Is Nothing Then
MsgBox "Unable to access the view 'People2'"
Exit Function
End if
key = "D" & m_branch
Set doc = view.GetDocumentByKey(key)
If doc Is Nothing Then
MsgBox "Could not locate document '" & key & "'"
Exit Function
End if
fullname = doc.GetItemValue("FullName")(0)
End Function
Ando of course update the button actions in the same way.
Yes, it is a few lines longer, but it is much more readable and easier to maintain and debug. And you have error handling as well.
Change your last part in #DbLoookup code line to:
"FullName")})

Option Explicit - Not sure how to dim this

I have an existing macro that I use to format columns. I've been using this without problems. Now, I'm looking to learn how to use Option Explicit and I am running into a problem with defining my variable.
What should I be dim'ing Level as? I tried Dim Level As String but that didn't work. I'm trying to get a better understanding so any feedback would be appreciated.
Option Explicit
Sub adviseformat()
Dim Form As Worksheet
Set Form = Sheets("Formatting")
With Form
Level = WorksheetFunction.Match("Level", .Rows("1:1"), 0)
.Columns(Level).Delete
.Columns("D:E").Delete
.Range("U:U").Value = Range("E:E").Value
.Columns("E").EntireColumn.Delete
.Columns("F:I").Delete
.Columns("I").Delete
.Columns("L").Delete
.Columns("M").Delete
Form.Range("A:B").EntireColumn.Insert
Form.Range("A1").Value = "Owner"
Form.Range("B1").Value = "Comment"
Form.Range("A1").Interior.Color = 65535
Form.Range("B1").Interior.Color = 65535
Form.Range("O1").Interior.Color = 65535
End With
End Sub
As you type the WorksheetFunction.Match part, the VBA editor should pop up and give you a clue to the return type. It should say something like:
Match(Arg1, Arg2, [Arg3]) as Double
The "As Double" part tells you the return type of the Match function. This is the type you should use to declare your Level variable.
Looking on MSDN, I found this :
MATCH returns the position of the matched value within lookup_array, not the value itself.
For example, MATCH("b",{"a","b","c"},0) returns 2, the relative position of "b" within the array {"a","b","c"}.
So my guess is that you should use Dim Level As Variant

Resources