Accessing data from collection VBA - oop

I'm trying to access data from collection by using .item(). What I am trying to do is to collect data in collection function fncPopCcyLst and access it by .item(1) in cbSortCcy to get the row number. This is a test to see if I can store several data in my collection and access them via .item(). However, I get a VBA runtime error '5'. Will someone guide me kindly what I am doing wrong? Thank you.
Below are my codes.
Class Module: clsSngGenUtl
Private prpSngStrVal As String
Private prpSngRowNum As Long
Private prpSngClmNum As Long
'++ Define properties
'== String row number
Public Property Get SngStrVal() As String
SngStrVal = prpSngStrVal
End Property
Public Property Let SngStrVal(ByRef varStrVal As String)
prpSngStrVal = varStrVal
End Property
'++ Define properties
'== Scalar row number
Public Property Get SngRowNum() As Long
SngRowNum = prpSngRowNum
End Property
Public Property Let SngRowNum(ByVal varRowNum As Long)
prpSngRowNum = varRowNum
End Property
'++ Define properties
'== Single column number
Public Property Get SngClmNum() As Long
SngClmNum = prpSngClmNum
End Property
Public Property Let SngClmNum(ByRef varClmNum As Long)
prpSngClmNum = varClmNum
End Property
'++ Define functions
'== function get row number
Public Function fncGetRowNum(ByRef varWbName As Workbook, ByVal varWsName As String, ByRef varSttClm As Long) As Long
On Error GoTo Exception
prpSngRowNum = 0
prpSngRowNum = varWbName.Sheets(Trim(varWsName)).Cells(Rows.Count, varSttClm).End(xlUp).Row
fncGetRowNum = prpSngRowNum
ExitHere:
Exit Function
Exception:
Resume ExitHere
End Function
'== function get column number
Public Function fncGetClmNum(ByRef varWbName As Workbook, ByVal varWsName As String, ByRef varSttRow As Long) As Long
On Error GoTo Exception
prpSngClmNum = 0
prpSngClmNum = varWbName.Sheets(Trim(varWsName)).Cells(varSttRow, Columns.Count).End(xlToLeft).Column
fncGetClmNum = prpSngClmNum
ExitHere:
Exit Function
Exception:
Resume ExitHere
End Function`
Below is my collection class: clsColCcySrt
'++ Declare variables
Private prpColCcySrt As Collection
'++ Define properties
Public Property Get ColCcySrt() As Collection
Set ColCcySrt = prpColCcySrt
End Property
Public Property Set ColCcySrt(varColCcy As Collection)
Set prpColCcySrt = varColCcy
End Property
Public Function fncGetCcyRow(ByRef varStrVal As String) As Long
On Error GoTo Exception
Dim clsSngGen As clsSngGenUtl
Dim varRowNum As Long
varRowNum = 0
For Each clsSngGen In Me.ColCcySrt
varRowNum = clsSngGen.SngRowNum()
Next clsSngGen
'== Return value
fncGetCcyRow = varRowNum
ExitHere:
Exit Function
Exception:
If fncGetCcyRow = 0 Then
MsgBox "Exception: Value is <" & fncGetCcyRow & ">."
End If
Resume ExitHere
End Function
`
"Regular" Module to populate array: fncPopFxLst
`
Public Function fncPopCcyLst(ByRef varWbName As String, ByRef varWsName As String, ByRef varCcyTyp As String) As Collection
Dim clnColCcy As Collection
Dim clsArrGen As clsArrGenUtl
Dim clsSngGen As clsSngGenUtl
Dim varWbName As Workbook
Set clnColCcy = New Collection
'== Start collecting items
Set clsSngGen = New clsSngGenUtl
Set varWbName = ThisWorkbook
clsSngGen.SngStrVal = "Reuters"
clsSngGen.SngRowNum = clsSngGen.fncGetRowNum(varWbName, varWsName, 1)
clnColCcy.Add clsSngGen
Set fncPopCcyLst = clnColCcy
End Function
`
Lastly, the subroutine `
Private Sub cbSortCcy()
Dim clsColCcy As clsColCcySrt
Dim varDirPth As String
Dim varCcySrc As String
Dim varWsStrg As String
Dim varWbStrg As String
varDirPth = tbDirectoryName & "\" & tbFileName
varCcySrc = "Currency"
varWsStrg = "List"
varWbStrg = varDirPth
Set clsColCcy = New clsColCcySrt
Set clsColCcy.ColCcySrt = fncPopCcyLst(varWbStrg, varWsStrg, varCcySrc)
'Debug.Princ clsColCcy.fncGetCcyRow("Reuters")
Debug.Print clsColCcy.ColCcySrt.Item(1)
End Sub
`

VBA Run-time error '5' is:
"Invalid procedure call or argument"
In your class clsColCcySrt, you have a line:
varRowNum = clsSngGen.SngRowNum()
which would be correct if SngRowNum were a function and not a property. Remove the parentheses () to call the property.

Related

VBA Class Module - When to Stop and Where to declare public constant?

I have been coding vba for quite sometime now and it is only recently that I have begun diving into doing up some class module.
some questions on my mind is when should I stop including functions and properties in a class ?
i.e. I am creating a class module where by it record table properties (i.e. row of header column) I went on further to create a retrieve data function where it gets data from other table with the same class.
Question 1: am I over doing it? I tend to want to put all the function in one class.
Question 2: if I want to set all classes to have the same default initialization do I declare a public const in the class module itself or the normal modules?
Here is my class module :
Private pHeaderNames As Object
Private pHeaderRow As Long
Private pSubHeaderNames As Object
Private pSubHeaderRow As Long
Private pDataRowStart As Long
Private pInputColStart As Long
Private pTableColStart As Long
Private pHeaderLastCol As Long
Private pTblWorksheet As Worksheet
Private pFileURL As String
Private pFileName As String
'---------------------------------------------- Initialization
Private Sub Class_Initialize()
pHeaderRow = 1
pDataRowStart = 2
pTableColStart = 1
pInputColStart = pTableColStart
pSubHeaderRow = pHeaderRow
pHeaderLastCol = Cells(pHeaderRow, Columns.Count).End(xlToLeft).Column
Set pHeaderNames = CreateObject("Scripting.Dictionary")
Set pSubHeaderNames = CreateObject("Scripting.Dictionary")
End Sub
Public Property Get FileURL() As String
FileURL = pFileURL
End Property
Public Property Let FileURL(Value As String)
pFileURL = Value
End Property
Public Property Get FileName() As String
FileName = pFileName
End Property
Public Property Let FileName(Value As String)
pFileName = Value
End Property
Function OpenWorkbook(URL As String, Name As String)
pFileURL = URL
pFileName = Name
Call Workbook_open(pFileURL, pFileName)
End Function
Public Property Let SetSheet(Value As String)
If pFileName - "" Then
Set pTblWorksheet = ActiveWorkbook.Worksheets(Value)
Else
Set pTblWorksheet = Workbook(pFileName).Worksheets(Value)
End If
End Property
Public Property Get TblWorksheet() As Worksheet
TblWorksheet = pTblWorksheet
End Property
Public Property Let TblWorksheet(Sheet As Worksheet)
pTblWorksheet = Sheet
End Property
'---------------------------------------------- HeaderLastCol
Public Property Get HeaderLastCol() As Long
HeaderLastCol = pHeaderLastCol
End Property
Public Property Let HeaderLastCol(Value As Long)
pHeaderLastCol = Value
End Property
'---------------------------------------------- HeaderRow
Public Property Let HeaderRow(Value As Long)
pHeaderLastCol = Cells(pHeaderRow, Columns.Count).End(xlToLeft).Column
pHeaderRow = Value
End Property
Public Property Get HeaderRow() As Long
HeaderRow = pHeaderRow
End Property
'---------------------------------------------- SubHeaderRow
Public Property Let SubHeaderRow(Value As Long)
pSubHeaderRow = Value
End Property
Public Property Get SubHeaderRow() As Long
SubHeaderRow = pSubHeaderRow
End Property
'---------------------------------------------- InputColStart
Sub SetInputColStart(KEY As Variant)
pInputColStart = pHeaderNames(KEY)
End Sub
Public Property Get InputColStart() As Long
InputColStart = pInputColStart
End Property
Public Property Let InputColStart(Value As Long)
pInputColStart = Value
End Property
'---------------------------------------------- DataRowStart
Public Property Get DataRowStart() As Long
DataRowStart = pDataRowStart
End Property
Public Property Let DataRowStart(Value As Long)
pDataRowStart = Value
End Property
'---------------------------------------------- TableColStart
Public Property Get TableColStart() As Long
TableColStart = pTableColStart
End Property
Public Property Let TableColStart(Value As Long)
pTableColStart = Value
End Property
'---------------------------------------------- HeaderName
Sub GetHeaderNames()
With pHeaderNames
For i = pTableColStart To pHeaderLastCol
If Not .Exists(UCase(Cells(pHeaderRow, i).Value)) Then
.Add UCase(Cells(pHeaderRow, i).Value), i
End If
Next i
End With
End Sub
Function HeaderName(KEY As String)
If pHeaderNames.Exists(KEY) Then
HeaderName = pHeaderNames(KEY)
Else
HeaderName = ""
End If
End Function
Function CountHeaderNames()
CountHeaderNames = pHeaderNames.Count
End Function
Function PrintHeaderObject()
For Each KEY In pHeaderNames.keys
Debug.Print KEY, pHeaderNames(KEY)
Next
End Function
'---------------------------------------------- SubHeaderName
Sub GetSubHeaderNames()
With pSubHeaderNames
For i = pTableColStart To pHeaderLastCol
If Not .Exists(UCase(Cells(pSubHeaderRow, i).Value)) Then
.Add UCase(Cells(pSubHeaderRow, i).Value), i
End If
Next i
End With
End Sub
Function SubHeaderName(KEY As String)
If pSubHeaderNames.Exists(KEY) Then
SubHeaderName = pSubHeaderNames(KEY)
Else
SubHeaderName = "" 'or raise an error...
End If
End Function
Function CountSubHeaderNames()
CountSubHeaderNames = pSubHeaderNames.Count
End Function
Function PrintSubHeaderObject()
For Each KEY In pSubHeaderNames.keys
Debug.Print KEY, pSubHeaderNames(KEY)
Next
End Function
Function RetrieveData(FromSht As Worksheet, ByVal FromTable As cTable)
Dim KEY As String
'CurrentSht = ActiveSheet
For i = pTableColStart To pHeaderLastCol
KEY = Cells(pHeaderRow, i).Value
If FromTable.HeaderName(KEY) = "" Then
GoTo Nexti
Else
With FromSht
.Activate
rD_LastRow = 10
Set Source = .Range(.Cells(FromTable.DataRowStart, FromTable.HeaderName(KEY)), _
.Cells(rD_LastRow, FromTable.HeaderName(KEY)))
End With
With CurrentSht
.Activate
.Range(.Cells(DataRowStart, i), _
.Cells(DataRowStart, i)) _
.Resize(Source.Rows.Count, Source.Columns.Count).Cells.Value = Source.Cells.Value
End With
End If
Nexti:
Next i
End Function
Here is the module, so you can see that I have always need to declare headerRow , subHeaderRow and Datarowstart, even thou I have a default initialization set in the class module, is there away to change the default initialization based on the main module? or I just have to do it in the class module? (I trying to make the class portable)
Sub test()
Dim sht As Worksheet
Set wb = ActiveWorkbook
Set sht = wb.Sheets("Skin(Units)")
With Worksheets("Skin(Units)")
.Activate
Set SkinUnits = New cTable
Debug.Print TypeName(SkinUnits)
SkinUnits.HeaderRow = 1
SkinUnits.SubHeaderRow = 3
SkinUnits.DataRowStart = 4
SkinUnits.GetHeaderNames
SkinUnits.GetSubHeaderNames
SkinUnits.PrintHeaderObject
SkinUnits.PrintSubHeaderObject
SkinUnits.SetInputColStart ("Start")
End With
With Worksheets("Pain(Units)")
.Activate
Set PainUnits = New cTable
PainUnits.HeaderRow = 1
PainUnits.SubHeaderRow = 3
PainUnits.DataRowStart = 4
PainUnits.GetHeaderNames
PainUnits.GetSubHeaderNames
PainUnits.PrintHeaderObject
PainUnits.PrintSubHeaderObject
PainUnits.SetInputColStart ("Start")
Debug.Print PainUnits.HeaderName("SKU")
Debug.Print TypeName(sht), TypeName(SkinUnits)
Call test22222(SkinUnits)
Call PainUnits.RetrieveData(sht, SkinUnits)
End With
End Sub
Function test22222(ByVal X As cTable)
Debug.Print X.HeaderRow
End Function

Casting from class to interface in Excel VBA

In Excel 2013, I have two classes: LoadCase and LoadCombination, which implement interface ILoadCase.
The declaration for ILoadCase is:
Option Explicit
'' Public properties
Public Property Get Name() As String
End Property
Public Property Let Name(ByVal value As String)
End Property
Public Property Get ID() As Long
End Property
Public Property Let ID(ByVal valus As Long)
End Property
And the (partial) implementations for both LoadCase and LoadCombination are:
Option Explicit
Implements ILoadCase
'' Public properties
Public Property Get ILoadCase_Name() As String
ILoadCase_Name = pName
End Property
Private Property Let ILoadCase_Name(ByVal value As String)
pName = value
End Property
Public Property Get ILoadCase_ID() As Long
ILoadCase_ID = pID
End Property
Private Property Let ILoadCase_ID(ByVal value As Long)
pID = value
End Property
I've omitted code which is irrelevant to the implementation of the interface.
I then have a class BeamForces, which contains results for a particular ILoadCase object:
Option Explicit
Public Fx As Double
Public Fy As Double
Public Fz As Double
Public Mx As Double
Public My As Double
Public Mz As Double
Public ParentLoadCase As ILoadCase
I thought that with this I'd be able to do something like this:
Set currentBeamForces = New BeamForces
With currentBeamForces
.Fx = forces(0)
.Fy = forces(1)
.Fz = forces(2)
.Mx = forces(3)
.My = forces(4)
.Mz = forces(5)
Set .ParentLoadCase = TargetLoadCase
End With
Where TargetLoadCase is either a LoadCase or a LoadCombination, but this gives me an error every time.
I've coded this like I would in .NET and just expected that it would work, but does casting to an interface not work in VBA? Or am I going wrong here somewhere?
EDIT
More details. I first call the following method:
Public Function LoadBeamForcesAtNode(ByVal TargetBeam As Beam, ByVal TargetNode As Node, Optional ByVal TargetLoadCases As Collection = Nothing) As Boolean
Dim i As Integer
Dim currentLoadCase As Variant
Dim targetBeamForces As BeamForces
If TargetLoadCases Is Nothing Then
For Each currentLoadCase In Me.LoadCases.Items
Call TargetLoadCases.Add(currentLoadCase)
Next
For Each currentLoadCase In Me.LoadCombinations.Items
Call TargetLoadCases.Add(currentLoadCase)
Next
End If
'On Error GoTo ExitPoint
For Each currentLoadCase In TargetLoadCases
Set targetBeamForces = InstantiateBeamForces(TargetBeam, TargetNode, currentLoadCase)
If TargetNode Is TargetBeam.Node1 Then
Set TargetBeam.Forces1 = targetBeamForces
Else
Set TargetBeam.Forces2 = targetBeamForces
End If
Next
LoadBeamForcesAtNode = True
ExitPoint:
End Function
Where TargetLoadCases is a collection which can contain both LoadCase and LoadCombination objects.
The problem occurs in InstantiateBeamForces, the code for which is
Private Function InstantiateBeamForces(ByVal TargetBeam As Beam, ByVal TargetNode As Node, ByVal TargetLoadCase As Variant) As BeamForces
Dim forces(5) As Double
Dim currentBeamForces As BeamForces
Call Me.output.GetMemberEndForces(TargetBeam.ID, IIf(TargetNode Is TargetBeam.Node1, 0, 1), TargetLoadCase.ILoadCase_ID, forces, 0)
Set currentBeamForces = New BeamForces
With currentBeamForces
.Fx = forces(0)
.Fy = forces(1)
.Fz = forces(2)
.Mx = forces(3)
.My = forces(4)
.Mz = forces(5)
Set .ParentLoadCase = TargetLoadCase
End With
Set InstantiateBeamForces = currentBeamForces
End Function
Which creates a new BeamForces object and populates it with the values returned by the ...GetMemberEndForces(...) API COM call.
The problem is that the .ParentLoadCase property is nothing after the assignment, so I'm assuming an invalid cast...
** EDIT 2 **
Here is a screenshot of TargetLoadCase when I put a breakpoint in InstantiateBeamForces.
The ILoadCase member is Nothing, but I don't get why. Could this be the cause of the problem?

VBA map implementation

I need good map class implementation in VBA.
This is my implementation for integer key
Box class:
Private key As Long 'Key, only positive digit
Private value As String 'Value, only
'Value getter
Public Function GetValue() As String
GetValue = value
End Function
'Value setter
Public Function setValue(pValue As String)
value = pValue
End Function
'Ket setter
Public Function setKey(pKey As Long)
Key = pKey
End Function
'Key getter
Public Function GetKey() As Long
GetKey = Key
End Function
Private Sub Class_Initialize()
End Sub
Private Sub Class_Terminate()
End Sub
Map class:
Private boxCollection As Collection
'Init
Private Sub Class_Initialize()
Set boxCollection = New Collection
End Sub
'Destroy
Private Sub Class_Terminate()
Set boxCollection = Nothing
End Sub
'Add element(Box) to collection
Public Function Add(Key As Long, value As String)
If (Key > 0) And (containsKey(Key) Is Nothing) Then
Dim aBox As New Box
With aBox
.setKey (Key)
.setValue (value)
End With
boxCollection.Add aBox
Else
MsgBox ("В словаре уже содержится элемент с ключем " + CStr(Key))
End If
End Function
'Get key by value or -1
Public Function GetKey(value As String) As Long
Dim gkBox As Box
Set gkBox = containsValue(value)
If gkBox Is Nothing Then
GetKey = -1
Else
GetKey = gkBox.GetKey
End If
End Function
'Get value by key or message
Public Function GetValue(Key As Long) As String
Dim gvBox As Box
Set gvBox = containsKey(Key)
If gvBox Is Nothing Then
MsgBox ("Key " + CStr(Key) + " dont exist")
Else
GetValue = gvBox.GetValue
End If
End Function
'Remove element from collection
Public Function Remove(Key As Long)
Dim index As Long
index = getIndex(Key)
If index > 0 Then
boxCollection.Remove (index)
End If
End Function
'Get count of element in collection
Public Function GetCount() As Long
GetCount = boxCollection.Count
End Function
'Get object by key
Private Function containsKey(Key As Long) As Box
If boxCollection.Count > 0 Then
Dim i As Long
For i = 1 To boxCollection.Count
Dim fBox As Box
Set fBox = boxCollection.Item(i)
If fBox.GetKey = Key Then Set containsKey = fBox
Next i
End If
End Function
'Get object by value
Private Function containsValue(value As String) As Box
If boxCollection.Count > 0 Then
Dim i As Long
For i = 1 To boxCollection.Count
Dim fBox As Box
Set fBox = boxCollection.Item(i)
If fBox.GetValue = value Then Set containsValue = fBox
Next i
End If
End Function
'Get element index by key
Private Function getIndex(Key As Long) As Long
getIndex = -1
If boxCollection.Count > 0 Then
For i = 1 To boxCollection.Count
Dim fBox As Box
Set fBox = boxCollection.Item(i)
If fBox.GetKey = Key Then getIndex = i
Next i
End If
End Function
All ok if i insert 1000 pairs key-value. But if 50000, a program freezes.
How i can solve this problem? Or maybe there more better solution?
Main problem with your implementation is that the operation containsKey is quite expensive (O(n) complex) and it is called at every insert and it never breaks even when it "knows" what would be the result.
This might help a little:
...
If fBox.GetKey = Key Then
Set containsKey = fBox
Exit Function
End If
...
In order to reduce the containsKey complexity typical things to do would be
keep the keys sorted so that you can use binary search instead of linear search
keep the keys in tree or in a hashed buckets
The most straightforward thing to do would be using the Collection's built-in (hopefully optimized) ability to store/retrieve items by a key.
Store:
...
boxCollection.Add Item := aBox, Key := CStr(Key)
...
Retrieve (not tested, based on this answer):
Private Function containsKey(Key As Long) As Box
On Error GoTo err
Set containsKey = boxCollection.Item(CStr(Key))
Exit Function
err:
Set containsKey = Nothing
End Function
See also:
MSDN: How to: Add, Delete, and Retrieve Items of a Collection (Visual Basic)
Stack Overflow: Does VBA have Dictionary Structure?
Newton Excel Bach: Arrays vs Collections vs Dictionary Objects (and Dictionary help)

Get item in VBA Collection by Name

Ahoy hoy,
I'm trying to do stuff to a custom object in a custom collection by referencing it's name property in VBA Excel. I swear it worked before (or at least didn't throw an error) and now its kaput. I'm getting an invalid call or argument error when I try to Get something by a string. Thanks in advance for even reading this too, any help is appreciated. <\edit>
Here's the collection:
Option Explicit
Private DRAFields As New Collection
Sub Add(Name As String, Optional colNbr As Long, Optional Exists As Boolean)
Dim fld As New DRAFld
fld.colNbr = colNbr
fld.Name = Name
fld.Exists = Exists
DRAFields.Add fld
End Sub
Property Get Item(NameOrNumber As Variant)
Set Item = DRAFields(NameOrNumber) '<------- Error here
End Property
The collections has items added by passing an array of names in to a function and the collection is returned without issue. I can iterate over by using the key. But the error happens if get as such: Debug.Print myFlds.Item("Customer").colNbr
And the object class just in case:
Option Explicit
Private clmNbrPvt As Long
Private namePvt As String
Private existsPvt As Boolean
Public Property Get colNbr() As Long
colNbr = clmNbrPvt
End Property
Public Property Let colNbr(lngParam As Long)
clmNbrPvt = lngParam
End Property
Public Property Get Name() As String
Name = namePvt
End Property
Public Property Let Name(strParam As String)
namePvt = strParam
End Property
Public Property Get Exists() As Boolean
Exists = existsPvt
End Property
Public Property Let Exists(booParam As Boolean)
existsPvt = booParam
End Property
And why not that function too:
Function validateAndBuildDRAFields(ByRef arrReqFields() As String, _
inputSheet As Worksheet, _
Optional VBAModule As String) As clsDRAFields
Dim lEndCol As Long: lEndCol = Standard.zGetLastColumn(inputSheet, 1)
Dim i As Long
Dim x As Long
Dim intExit As Long
Dim myDRAFields As New clsDRAFields
Set validateAndBuildDRAFields = myDRAFields
'Builds myDRAFields items from arrReqFields
For i = LBound(arrReqFields) To UBound(arrReqFields)
myDRAFields.Add arrReqFields(i)
Next i
'checks if required fields exist on input sheet
'if found then sets column number and exists = true
For i = 1 To myDRAFields.Count
For x = 1 To lEndCol
If inputSheet.Cells(1, x) = myDRAFields.Item(i).Name Then
myDRAFields.Item(i).colNbr = x
myDRAFields.Item(i).Exists = True
intExit = intExit + 1
Exit For
End If
Next x
If intExit = UBound(arrReqFields) + 1 Then Exit For
Next i
' tells user if there are any missing fields and ends if true
If (Not intExit = UBound(arrReqFields) + 1) Or _
intExit = 0 Then
For i = 1 To myDRAFields.Count
If myDRAFields.Item(i).Exists = False Then
Call Standard.TheEndWithError("I couldn't find the " & myDRAFields.Item(i).Name & _
" column in your file. Please add " & myDRAFields.Item(i).Name & _
" to your DRA Layout.", False, VBAModule)
End If
Next i
Set myDRAFields = Nothing
Standard.TheEnd
End If
End Function
To access a collection item by its key, you have to supply a key when you add the item to the collection. The key is optional. When you access a collection item with a string, the Item method assumes you want to match the key. When you use an integer, it assumes you want the positional index.
So, change the line in your Add method to
DRAFields.Add fld, fld.Name
and you'll be able to access items by their Name property.

Accessing custom property's value gives 'Out of Memory' error when value is null

I'm trying to create a custom property in an excel sheet, then retrieve its value. This is fine when I don't use an empty string, i.e. "". When I use the empty string, I get this error:
Run-time error '7':
Out of memory
Here's the code I'm using:
Sub proptest()
Dim cprop As CustomProperty
Dim sht As Worksheet
Set sht = ThisWorkbook.Sheets("control")
sht.CustomProperties.Add "path", ""
For Each cprop In ThisWorkbook.Sheets("control").CustomProperties
If cprop.Name = "path" Then
Debug.Print cprop.Value
End If
Next
End Sub
The code fails at Debug.Print cprop.value. Shouldn't I be able to set the property to "" initially?
With vbNullChar it works, sample:
Sub proptest()
Dim sht As Worksheet
Set sht = ThisWorkbook.Sheets("control")
' On Error Resume Next
sht.CustomProperties.Item(1).Delete
' On Error GoTo 0
Dim pathValue As Variant
pathValue = vbNullChar
Dim pathCustomProperty As CustomProperty
Set pathCustomProperty = sht.CustomProperties.Add("path", pathValue)
Dim cprop As CustomProperty
For Each cprop In ThisWorkbook.Sheets("control").CustomProperties
If cprop.Name = "path" Then
Debug.Print cprop.Value
End If
Next
End Sub
I think from the comments and the answer from Daniel Dusek it is clear that this cannot be done. The property should have at least 1 character to be valid, an empty string just isnt allowed and will give an error when the .Value is called.
So you Add this property with a length 1 or more string and you Delete the property again when no actual value is to be assigned to it.
As already mentioned it is not possible to set empty strings.
An easy workaround is to use a magic word or character, such as ~Empty (or whatever seems proof enough for you):
Dim MyProperty As Excel.CustomProperty = ...
Dim PropertyValue As String = If(MyProperty.Value = "~Empty", String.Empty, MyPropertyValue)
A slightly more expensive workaround but 100% safe is to start all the values of your custom properties with a character that you then always strip off. When accessing the value, systematically remove the first character:
Dim MyProperty As Excel.CustomProperty = ...
Dim PropertyValue As String = Strings.Mid(MyProperty.Value, 2)
You can write an extension to make your life easier:
<System.Runtime.CompilerServices.Extension>
Function ValueTrim(MyProperty as Excel.CustomProperty) As String
Return Strings.Mid(MyProperty.Value, 2)
End Function
Now you can use it like this: Dim MyValue As String = MyProperty.ValueTrim
Use a reversed principle when you add a custom property:
<System.Runtime.CompilerServices.Extension>
Function AddTrim(MyProperties As Excel.CustomProperties, Name As String, Value As String) as Excel.CustomProperty
Dim ModifiedValue As String = String.Concat("~", Value) 'Use ~ or whatever character you lie / Note Strig.Concat is the least expensive way to join two strings together.
Dim NewProperty As Excel.CustomProperty = MyProperties.Add(Name, ModifiedValue)
Return NewProperty
End Function
To use like this: MyProperties.AddTrim(Name, Value)
Hope this helps other people who come across the issue..
Based on the other answers and some trial and error, I wrote a class to wrap a Worksheet.CustomProperty.
WorksheetProperty:Class
Sets and Gets the value of a Worksheet.CustomProperty and tests if a Worksheet has the CustomProperty
VERSION 1.0 CLASS
Attribute VB_Name = "WorksheetProperty"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
'#Folder("Classes")
'#PredeclaredId
Option Explicit
Private Type TMembers
Name As String
Worksheet As Worksheet
End Type
Private this As TMembers
Public Property Get Create(pWorksheet As Worksheet, pName As String) As WorksheetProperty
With New WorksheetProperty
Set .Worksheet = pWorksheet
.Name = pName
Set Create = .Self
End With
End Property
Public Property Get Self() As WorksheetProperty
Set Self = Me
End Property
Public Property Get Worksheet() As Worksheet
Set Worksheet = this.Worksheet
End Property
Public Property Set Worksheet(ByVal pValue As Worksheet)
Set this.Worksheet = pValue
End Property
Public Property Get Name() As String
Name = this.Name
End Property
Public Property Let Name(ByVal pValue As String)
this.Name = pValue
End Property
Public Property Get Value() As String
Dim P As CustomProperty
For Each P In Worksheet.CustomProperties
If P.Name = Name Then
Value = P.Value
Exit Property
End If
Next
End Property
Public Property Let Value(ByVal pValue As String)
Delete
Worksheet.CustomProperties.Add Name:=Name, Value:=pValue
End Property
Public Property Get hasCustomProperty(pWorksheet As Worksheet, pName As String) As Boolean
Dim P As CustomProperty
For Each P In pWorksheet.CustomProperties
If P.Name = pName Then
hasCustomProperty = True
Exit Property
End If
Next
End Property
Public Sub Delete()
Dim P As CustomProperty
For Each P In Worksheet.CustomProperties
If P.Name = Name Then
P.Delete
Exit For
End If
Next
End Sub
Usage
I have several properties of my custom Unit class return a WorksheetProperty. It makes it really easy to sync my database with my worksheets.
Public Function hasMeta(Ws As Worksheet) As Boolean
hasMeta = WorksheetProperty.hasCustomProperty(Ws, MetaName)
End Function
Public Property Get Id() As WorksheetProperty
Set Id = WorksheetProperty.Create(this.Worksheet, "id")
End Property
Public Property Get CourseID() As WorksheetProperty
Set CourseID = WorksheetProperty.Create(this.Worksheet, "course_id")
End Property
Public Property Get Name() As WorksheetProperty
Set Name = WorksheetProperty.Create(this.Worksheet, "unit_name")
End Property
Simple Usage
'ActiveSheet has a CustomProperty
Debug.Print WorksheetProperty.hasCustomProperty(ActiveSheet, "LastDateSynced")
'Set a CustomProperty
WorksheetProperty.Create(ActiveSheet, "LastDateSynced").Value = Now
'Retrieve a CustomProperty
Debug.Print WorksheetProperty.Create(ActiveSheet, "LastDateSynced").Value