"Unknown function 'Workday2' in validation expression" - vba

I have a VBA module named 'Module1' under "Database (New Microsoft Access Database).
Here is the code (credit goes to another Stackoverflow user):
Option Compare Database
Option Explicit
Public Function Workday2(start_date, days, Optional Holiday As Variant) As Date
Static xlApp As Object
If xlApp Is Nothing Then Set xlApp = CreateObject("Excel.Application")
Workday2 = xlApp.WorksheetFunction.workday(start_date, days, Holiday)
End Function
I am using this function in my table design view under a field for "Default value." My intent is to set the default value 5 business days from the current date.
I would like to use it as
Default value: Workday2(Date(),5)
But I get an error that the function is unknown despite using a public function in a module.

Related

Calling a function of a classmodul

probably just a stupid syntax error but when I try to call a function i created in a class module I get the error message that my "objectvarable or withblock is not declarde".
Here the minimal code example from both modules:
'calling
Dim AllZyklen1 As New ArrayList
For Each Wartungsplan In ArrayWartungsplan
Set AllZyklen1 = Wartungsplan.GetAllZyklen 'added set
next Wartungsplan
'function itself
Public Function GetAllZyklen() As ArrayList
Dim AllZyklen2 As New ArrayList
'allZyklen2 gets calculated, no other functions are called just local varaibles of the class are used
If Not AllZyklen2.Contains(Zyklus) Then
AllZyklen2.Add Zyklus
end if
Set GetAllZyklen = AllZyklen2 'added set
End Function
(numbers are added to "allzyklen" just for easier reading, they are actually both called "allzyklen" without number)
Shouldnt that work? I just cant see the error.
EDIT: As for the Solution, what the answer states is absolutley correct and was necessary for my code to work. Unfortunatley I also had an spelling error for a attribute in the classmodule. In which case vba just highlights the call of this function, but no errors within the function... I ended up moving the function from the classmodule to the main module where the correct line with the spelling error got highlighted and the mistake was easier to spot.
You need to use Set for Objects (ArrayList is an object).
So it should be:
'calling
Dim AllZyklen1 As New ArrayList
For Each Wartungsplan In ArrayWartungsplan
Set AllZyklen1 = Wartungsplan.GetAllZyklen
Next Wartungsplan
and
'function itself
Public Function GetAllZyklen() As ArrayList
Dim AllZyklen2 As New ArrayList
'allZyklen2 gets calculated, no other unctions are called just local varaibles of the class are used
Set GetAllZyklen = AllZyklen2
End Function
Full example that works:
Class Module ClassWartungsplan:
Option Explicit
Public Function GetAllZyklen() As ArrayList
Dim AllZyklen2 As New ArrayList
'allZyklen2 gets calculated, no other unctions are called just local varaibles of the class are used
AllZyklen2.Add "abc"
Set GetAllZyklen = AllZyklen2
End Function
Standard Module:
Option Explicit
Sub Example()
Dim AllZyklen1 As New ArrayList
Dim Wartungsplan As New ClassWartungsplan
Set AllZyklen1 = Wartungsplan.GetAllZyklen
Debug.Print AllZyklen1(0) ' prints ABC in the immediate window
End Sub

Create class method with already existing name from a DLL

I come from this post : Create class method with already existing name, where I explain that I need to make code compatible with MS Access and PostgreSQL. My first thought was to create a new class for PostgreSQL support and rewrite the functions previously used. The solution brought could work, but the problem is that the MS Access database class is loaded from a DLL (Microsoft Shared\DAO\dao360.dll, DAO.DBEngine). How can I interface or override those DLL functions and get rid of this "ambiguous name detected" error ?
As said in the answer to your first post you can use the Implements feature to do this. Without further details and yout code it is just more than difficulut to tell where your issue is.
IDatabase could look like that
Option Explicit
Sub OpenDB(fileName As String)
End Sub
Function ReadData(lngNr As Long) As String
End Function
clsAccess like that
Option Explicit
Implements IDatabase
Dim m_Dbs As DAO.Database
Dim m_Rcd As DAO.Recordset
Dim m_Filename As String
Sub IDatabase_OpenDB(fileName As String)
m_Filename = fileName
Set m_Filename = OpenDatabase(m_Filename, , True)
End Sub
Function IDatabase_ReadData(lngNr As Long) As String
Set m_Rcd = m_dbs.OpenRecordset("qryTable", dbOpenSnapshot)
m_Rcd.Move lngNr - 1
IDatabase_ReadData = m_Rcd.Fields("fldName").Value
m_Rcd.Close
End Function
You need to add the references in Tools/References.

Getting a collection property of a class take a property of another class of another type?

I wanted to first thank you all for the help you've given me implicitly over the last few months! I've gone from not knowing how to access the VBA IDE in Excel to writing fully integrated analysis programs for work. I couldn't have done it without the community here.
I'm currently trying to overhaul the first iteration of a data analysis program I wrote while learning how to code in VBA. While purpose driven and only really legible to myself, the code worked; but was a mess. From folks on this site I picked up Martin's Clean Code and gave it a read on how to try and be a better programmer.
From Martin's Clean Code, it was impressed on me to prioritize abstraction and decoupling of my code to allow for higher degrees of maintenance and modularization. I found this out the hard way since very minor changes requested above my pay grade would require massive and confusing rewrites! I'm trying to eliminate that problem going forward.
I am attempting to rewrite my code in terms of single responsibility classes (at least, where it is possible) and I am a bit confused. I apologize if my question isn't clear or if I'm using the wrong terminology. I want to be able to generate a collection of specific strings (the names of our detectors to be specific) with no duplicates from raw instrument data files from my lab. The purpose of this function is to assemble a bunch of metadata in a class and use it to standardize our file system and prevent clerical errors from newbies and old hands when they use the analysis program.
The testing initialization sub is below. It pops open a userform asking for the user to select the filepaths of the three files in the rawdatafiles class; then it kills the userform to free memory. The metadata object is currently for testing and will be rewritten properly when I get the output I want:
Sub setup()
GrabFiles.Show
Set rawdatafiles = New cRawDataFiles
rawdatafiles.labjobFile = GrabFiles.tboxLabJobFile.value
rawdatafiles.rawdatafirstcount = GrabFiles.tboxOriginal.value
rawdatafiles.rawdatasecondcount = GrabFiles.tboxRecount.value
Set GrabFiles = Nothing
Dim temp As cMetaData
Set temp = New cMetaData
temp.labjobName = rawdatafiles.labjobFile
'this works fine!
temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
' This throws run time error 424: Object Required
End Sub
The cMetadata class I have currently is as follows:
Private pLabjobName As String
Private pDetectorsOriginal As Collection
Private pDetectorsRecheck As Collection
Private Sub class_initialize()
Set pDetectorsOriginal = New Collection
Set pDetectorsRecheck = New Collection
End Sub
Public Property Get labjobName() As String
labjobName = pLabjobName
End Property
Public Property Let labjobName(fileName As String)
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
End Property
Public Property Get detectorsOriginal() As Collection
detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
When I step through the code it starts reading the "public property get rawdatafirstcount() as string" and throws the error after "End Property" and points back to the "temp.detectorsOriginal = rawdatafiles.rawdatafirstcount" line in the initialization sub.
I think I'm at least close because the temp.labjobName = rawdatafiles.labjobFile code executes properly. I've tried playing around with the data types since this is a collection being assigned by a string but I unsurprisingly get data type errors and can't seem to figure out how to proceed.
If everything worked the way I want it to, the following function would take the filepath string from the rawdatafiles.rawdatafirstcount property and return for me a collection containing detector names as strings with no duplicates (I don't know if this function works exactly the way I want since I haven't been able to get the filepath I want to parse properly in the initial sub; but I can deal that later!):
Function getDetectors(filePath As String) As Collection
Dim i As Integer
Dim detectorsCollection As Collection
Dim OriginalRawData As Workbook
Set OriginalRawData = Workbooks.Open(fileName:=filePath, ReadOnly:=True)
Set detectorsCollection = New Collection
For i = 1 To OriginalRawData.Worksheets(1).Range("D" & Rows.Count).End(xlUp).Row
detectorsCollection.Add OriginalRawData.Worksheets(1).Cells(i, 4).value, CStr(OriginalRawData.Worksheets(1).Cells(i, 4).value)
On Error GoTo 0
Next i
getDetectors = detectorsCollection
Set detectorsCollection = Nothing
Set OriginalRawData = Nothing
End Function
Thanks again for reading and any help you can offer!
temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
' This throws run time error 424: Object Required
It throws an error because, as others have already stated, the Set keyword is missing.
Now with that out of the way, a Set keyword is NOT what you want here. In fact, sticking a Set keyword in front of that assignment will only buy you another error.
Let's look at this property you're invoking:
Public Property Get detectorsOriginal() As Collection
detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
You're trying to assign detectorsOriginal with what appears to be some String value that lives in some TextBox control on that form you're showing - but the property's type is Collection, which is an object type - and that's not a String!
Now look at the property that does work:
Public Property Get labjobName() As String
labjobName = pLabjobName
End Property
Public Property Let labjobName(fileName As String)
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
End Property
This one is a String property, with a Property Let mutator that uses the fileName parameter it's given.
The broken one:
Public Property Set detectorsOriginal(originalFilepath As Collection)
pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
Is a Set mutator, takes a Collection parameter, and doesn't use the originalFilepath parameter it's given at all!
And this is where I'm confused about your intention: you're passing what has all the looks of a String except for its type (Collection) - the calling code wants to give it a String.
In other words the calling code is expecting this:
Public Property Let detectorsOriginal(ByVal originalFilepath As String)
See, I don't know what you meant to be doing here; it appears you're missing some pOriginalFilepath As String private field, and then detectorsOriginal would be some get-only property that returns some collection:
Private pOriginalFilePath As String
Public Property Get OriginalFilePath() As String
OriginalFilePath = pOriginalFilePath
End Property
Public Property Let OriginalFilePath(ByVal value As String)
pOriginalFilePath = value
End Property
I don't know what you're trying to achieve, but I can tell you this:
Don't make a Property Set member that ignores its parameter, it's terribly confusing code.
Don't make a Property (Get/Let/Set) member that does anything non-trivial. If it's not trivially simple and has a greater-than-zero chance of throwing an error, it probably shouldn't be a property. Make it a method (Sub, or Function if it needs to return a value) instead.
A word about this:
Dim FSO As New FileSystemObject
pLabjobName = FSO.GetBaseName(fileName)
Set FSO = Nothing
Whenever you Dim something As New, VBA will automatically instantiate the object whenever it's referred to. In other words, this wouldn't throw any errors:
Dim FSO As New FileSystemObject
Set FSO = Nothing
pLabjobName = FSO.GetBaseName(fileName)
Avoid As New if you can. In this case you don't even need a local variable - use a With block instead:
With New FileSystemObject
pLabjobName = .GetBaseName(fileName)
End With
May not be your issue but you're missing Set in your detectorsOriginal Set/Get methods:
Public Property Get detectorsOriginal() As Collection
Set detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
Set pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
So the error is one I've made a time or two (or more). Whenever you assign an object to another object, you have to use the Set reserved word to assign the reference to the Object.
In your code do the following:
In Sub setup()
Set temp.detectorsOriginal = rawdatafiles.rawdatafirstcount
And in the cMetadata class change the Public Property Set detectorsOriginal(originalFilepath As Collection) property to the following:
Public Property Get detectorsOriginal() As Collection
Set detectorsOriginal = pDetectorsOriginal
End Property
Public Property Set detectorsOriginal(originalFilepath As Collection)
Set pDetectorsOriginal = getDetectors(rawdatafiles.rawdatafirstcount)
End Property
Also in your function Function getDetectors(filePath as String) as Collection change the statement afterNext i` to
Set getDetectors = detectorsCollection
Also, I'm very glad to hear that you've learned how to use VBA.
When you're ready to create your own Custom Collections, check out this post. Your own custom Collections.
I also book marked Paul Kelly's Excel Macro Mastery VBA Class Modules – The Ultimate Guide as well as his Excel VBA Dictionary – A Complete Guide.
If you haven't been to Chip Pearson's site you should do so. He has a ton of useful code that will help your delivery your projects more quickly.
Happy Coding.

Generic Way to Determine if Invoking a Property Throws an Error

Say you have one slide with one chart on it, and you run this code(in a version of Office later than 2007):
Dim pptWorkbook As Object
Dim result As Object
Set pptWorkbook = ActivePresentation.slides(1).Shapes(1).Chart.ChartData.Workbook
Set result = pptWorkbook.ContentTypeProperties
You will generate an error:
Application-defined or object-defined error
I believe this is because "Smart tags are deprecated in Office 2010."(Source), Generally to avoiding this sort of issue from throwing an error and exiting your VBA you can take one of two different approaches:
//Method 1
If OfficeVersion <= 2007
Set result = pptWorkbook.ContentTypeProperties
//Method 2
On Error Resume Next // or GOTO error handler
Set result = pptWorkbook.ContentTypeProperties
Method one requires that you know the specific reason why the property would cause an error, which is easy in this case but may not be as easy with other properties. Method two requires that you use some form of error handling to deal with the error AFTER the fact, my understanding of most other Microsoft languages is that is typically discouraged(example, another example). Is this standard practice in VBA?
In VBA, is there any other way to determine whether a property of an object would throw an error if invoked, BEFORE invoking that property, and without knowing the specifics of that invoked property?
What I like to do for this situation is create a separate function that checks if the property exists and returns a Boolean. In this case it would look something like this:
Public Function CheckIfExists(targetObj As Object) As Boolean
Dim testObj As Object
On Error GoTo failedTest:
Set testObj = targetObj.ContentTypeProperties
CheckIfExists = True
Exit Function
failedTest:
CheckIfExists = False
End Function
Which would return false if that property causes an error and true if not-
Then modify your sub to be:
Public Sub FooSub()
Dim pptWorkbook As Object
Dim result As Object
Set pptWorkbook = ActivePresentation.slides(1).Shapes(1).Chart.ChartData.Workbook
If CheckIfExists(pptWorkbook) Then
Set result = pptWorkbook.ContentTypeProperties
End If
... rest of your code or appropriate error handling...
Hope this helps,
TheSilkCode

Excel VBA - QueryTable AfterRefresh function not being called after Refresh completes

I am developing an Excel (2010+) Application using VBA and have run into an issue where the AfterRefresh event function is not being invoked once the query finishes executing.
I have not been able to find many decent resources or documentation for how to have this event function triggered in a Class Module. I decided to use the Class Module design route instead of putting the event handlers in the worksheet after receiving a response to an earlier question about QueryTables (found here Excel VBA AfterRefresh).
Here is the code for my Class Module called CQtEvents
Option Explicit
Private WithEvents mQryTble As Excel.QueryTable
Private msOldSql As String
' Properties
Public Property Set QryTble(ByVal QryTable As QueryTable): Set mQryTble = QryTable:
End Property
Public Property Get QryTble() As QueryTable: Set QryTble = mQryTble:
End Property
Public Property Let OldSql(ByVal sOldSql As String): msOldSql = sOldSql:
End Property
Public Property Get OldSql() As String: OldSql = msOldSql:
End Property
Private Sub Class_Initialize()
MsgBox "CQtEvents init"
End Sub
' Resets the query sql to the original unmodified sql statement
' This method is invoked when the Refresh thread finishes executing
Private Sub mQryTble_AfterRefresh(ByVal Success As Boolean)
' Problem is here
' This function is never called :( Even if the query successfully runs
Me.QryTble.CommandText = Me.OldSql
End Sub
Here is a quick snapshot of the code the creates an instance of this class, finds a relevant QueryTable, then calls Refresh
Option Explicit
Sub RefreshDataQuery()
'Dependencies: Microsoft Scripting Runtime (Tools->References) for Dictionary (HashTable) object
'From MGLOBALS
cacheSheetName = "Cache"
Set cacheSheet = Worksheets(cacheSheetName)
Dim querySheet As Worksheet
Dim interface As Worksheet
Dim classQtEvents As CQtEvents
Set querySheet = Worksheets("QTable")
Set interface = Worksheets("Interface")
Set classQtEvents = New CQtEvents
Dim qt As QueryTable
Dim qtDict As New Scripting.Dictionary
Set qtDict = UtilFunctions.CollectAllQueryTablesToDict
Set qt = qtDict.Item("Query from fred2")
''' Building SQL Query String '''
Dim sqlQueryString As String
sqlQueryString = qt.CommandText
Set classQtEvents.QryTble = qt
classQtEvents.OldSql = sqlQueryString ' Cache the original query string
QueryBuilder.BuildSQLQueryStringFromInterface interface, sqlQueryString
' Test message
MsgBox sqlQueryString
qt.CommandText = sqlQueryString
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
''' CLEAN UP '''
' Free the dictionary
Set qtDict = Nothing
End Sub
Also here is a screenshot of the Module structure http://imgur.com/8fUcfLV
My first thought on what might be the issue was passing the QueryTable by value. I am not the most experienced VBA developer, but I reasoned this would create a copy and be calling the event on an unrelated table. However, this was not the case and passing by Reference did not fix the problem either.
Also the query is confirmed to run successfully as the data is correctly showing up and being refreshed.
EDIT
I added the BeforeRefresh event function to CQtEvents class Module and confirmed this function is called once Refresh is called
Private Sub mQryTble_BeforeRefresh(Cancel As Boolean)
MsgBox "Start of BeforeRefresh"
End Sub
How might I alter this code get my QueryTable from the QTableModule's RefreshDataQuery() Sub routine to have the AfterRefresh function invoked when the query is successfully ran?
How to catch the AfterRefresh event of QueryTable?
Explanation: in your situation, before event was fired you lost reference of your QueryTable by setting it to nothing when you made cleaning or procedure ended.
General solution: you must be sure that your code is still running and/or you need to keep any references to your QueryTable.
1st solution. When calling QT.Refresh method set the parameter to false in this way:
qt.Refresh false
which will stop further code execution until your qt is refreshed. But I don't consider this solution to be the best one.
2nd solution. Make your classQtEvents variable public and after RefreshDataQuery sub is finished check the status with some other code.
in you CQtEvents class module add the following public variable:
Public Refreshed As Boolean
in your BeforeRefresh event add this:
Refreshed = False
in your AfterRefresh event add this line of code:
Refreshed = True
Make your classQtEvents variable declaration public. Put this before Sub RefreshDataQuery()
Public classQtEvents as CQtEvents
but remove appropriate declaration from within your sub.
Now, even your sub is finished you will be able to check status of refreshment by checking .Refreshed property. You could do it in Immediate or within other Sub. This should work for Immediate:
Debug.Print classQtEvents.Refreshed
3rd solution. (a bit similar to 1st one) Follow steps 1 to 3 from 2nd solution. After you call qt.Refresh method you could add this loop which will stop further code execution until qt is refreshed:
'your code
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
'checking
Do Until classQtEvents.Refreshed
DoEvents
Loop
Final remark. I hope I didn't mixed up qt variable with classQtEvents variable. I didn't tried and tested any solution using your variables but wrote all above with referenced to code I use.
A github repo that demonstrates the minimum code needed to get this working can be found here.
As mentioned, if your event handler isn't in scope, or your QueryTable reference is lost, you won't catch the event. The key factors to ensuring you catch the event are:
Declare a global variable of your event-handling class module's type outside of any subroutines/methods, at the top of a file (I chose the ThisWorkbook file).
Add a Workbook_Open event handler and instantiate that variable there, so that it is available immediately and will remain in scope (since it's global).
At that point, or at any downstream point when you have a QueryTable you're interested in, pass that QueryTable to the global instance to wire up its events.
(It took me a couple tries to figure this out myself, when someone pointed me in this direction as an answer to this question.)