Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations strongm on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

pass a method as an argument or somehow call a method of a form from a class 3

Status
Not open for further replies.

1DMF

Programmer
Jan 18, 2005
8,795
GB
Hi,

I want to be able to pass a method of a class the name of a form method (sub) to call.

I have..
Code:
Application.Run (sSub)

where sSub = "My_Form_Name.My_Method_Name"

But this errors with "cannot find procedure"

How do I get my class to accept an argument that is either the method on a form to run or a string name and eval it somehow and then execute the method?

Thanks,
1DMF

"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
that's for calling a method in a class isn't it?

I want my class to call a method on a form by an argument variable.

Currently I have it working by my class method accepting a byref argument of type 'Form' and then hardcoding the method to call, like so...

Code:
Public sub my_sub (... byref fForm as Form)

...

 Call fForm.My_Method

end sub

But this now ties my class object to knowing the method to call rather than it accepting it via an argument making it dynamic and not tied.

How would I do this?

"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
OK, upon further investigation I have found this usage...
Code:
CallByName Forms("MyForm"), "MyFunction", vbMethod

I guess as technically a form is a class object, you can simply pass the class method string values and use them like so...

Code:
Public Submy_sub(... Optional ByVal sForm As String = "", Optional ByVal sFunc As String = "")

...

    If sForm <> "" And sFunc <> "" Then
        Call CallByName(Forms(sForm), sFunc, VbMethod)
    End If

end sub

However, the form I am trying to talk to is on a Navigation Control subform, so am finding it difficult to reference!

any ideas?

"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
I changed it to use both my solutions which ended up as...

Code:
Public Sub my_sub (... Optional ByRef fForm As Form = Nothing, Optional ByVal sFunc As String = "")

...

    If Not sForm Is Nothing And sFunc <> "" Then
        Call CallByName(fForm, sFunc, VbMethod)
    End If

End Sub

So now I can pass the form object and the method name and dynamically execute it.

Is this the correct solution?

"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
Is this the correct solution?
For VBA this is the limitation. In vb.net you have several cleaner ways to do this with delegates and lambda expressions. However, what exactly are you trying to do that you would need this type of construct? Why the need to call a class method this way? There might be a cleaner means to do this.
 
>technically a form is a class object

Not just technically. A form IS a class.
 
MajP ->

I have a filewatcher running on one of the Navigation Control's subform , once an email is dropped and processed, I want to Navigate away from the 'drop' section and back to the Navigation control that shows all the emails that have been drag/dropped onto the member's record.

I have on the Nav subform a public sub (Exit_Form) which does some tidying up and then runs the updateView method on the main parent form.

The code that handles the drag/drop is in a class object, and I need that object to issue a 'close the drag/drop form and update the main view'.

The only way to do this is either hardcode the form/function in the class object , which I don't want to do as objects should be encapsulated and have no knowledge of things outside itself such as form names and function names.

So the only thing I can think of is to dynamically pass in the form object and method to execute, thus making the drag/drop object usable in other applications and not tied directly to this particular one.

Unless you have an alternative suggestion?



"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
Unless you have an alternative suggestion?

I assume that your drag and drop class is trapping the events of the form and the controls involved in the drag and drop. If not, you will have to explain it to me because that would not make any sense not to do so.
Anyways your class should only need pointers to those control/form objects when you instantiate the code and it should trap the events of those objects. Your form should then trap the custom events of the class. The form should then know when it is time to do something based on the class doing something and vice versa. None of this should be passed or hard coded. Standard event driven OOP design.

So your class should look something like this (trapping control events, and raising its own events)

Class: Widget
Code:
Option Compare Database

Private WithEvents mFrm As Access.Form
Private WithEvents mButton As Access.CommandButton
Public Event SomethingDone()

Public Sub MyInitialize(AccessForm As Access.Form, Button As Access.CommandButton)
   Set mFrm = AccessForm
   Set mButton = Button
   mButton.OnClick = "[Event Procedure]"
   mFrm.OnCurrent = "[Event Procedure]"
End Sub


Private Sub mButton_Click()
  MsgBox "Button Click Event from Form trapped"
  Call DoSomething
End Sub

Private Sub mFrm_Current()
  MsgBox "Current Event of the Form Trapped"
End Sub
Private Sub DoSomething()
  MsgBox "The trapped click event is causing a class procedure to fire"
  'Raise a custom event in the class and then trap it in the form
  RaiseEvent SomethingDone
End Sub

Then you instantiate and use on the form
Code:
Private WithEvents MyWidget As Widget
Private Sub Form_Load()
  Set MyWidget = New Widget
  MyWidget.MyInitialize Me, Me.cmdOK
End Sub

Private Sub MyWidget_SomethingDone()
  MsgBox "Something was Done by the Class"
End Sub

So now this can be used anywhere. If a form needs to do something after something happens in a class then it is the forms responsibility. You should never have a class call a method in another class in the way you describe, that would be poor OOP.

You can demo this simply by building a form and put a single command button on it.
 
I assume that your drag and drop class is trapping the events of the form and the controls involved in the drag and drop

Nope, it's done via strongm's filewatcher code using the infamous wombat and the windows API.

The subform of the Nav control is simply a browser object with a particular folder path set using the file protocol to instantiate it as a windows explorer folder object.

Separately I have a class object that fires up the windows API folder watching process.

No event from the Nav Control or its subforms has any triggered event in relation to the drag/drop process.

When the filewatcher class runs the code to move the dropped file to the desired location (which is triggered by the FILE_NOTIFICATION wombat event from the API), I want it to run a method on the subform, which clears object references and navigates to a particular form on the Nav control, in effect closing the drag/drop subform and loading the subform which shows the list of attached emails.







"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
When the filewatcher class runs the code to move the dropped file to the desired location (which is triggered by the FILE_NOTIFICATION wombat event from the API),
I assume the above is encapsulated in a class. And you would raise an event when that happens.
I want it to run a method on the subform, which clears object references and navigates to a particular form on the Nav control, in effect closing the drag/drop subform and loading the subform which shows the list of attached emails
So I would think you do not want your class to control this. You trap the raised event and the form clears the object references and moves to the control.
 
You trap the raised event and the form clears the object references and moves to the control.
The windows API is passed a reference to a function to run when the windows folder is altered, ok an event is triggered but at the OS level not the application level I believe.

I can see there is an event in Overlapped, but all this is happening in the class object, not sure how the form would know when to do something? and just because an event is triggered, I'm only interested in 'FILE_ACTION_ADDED'

Code:
        ' Create our own event, and stick it in the OVERLAPPED structure so that
        ' we can link it to our asynch ReadDirectoryChangesW
        hEvent = CreateEvent(0&, False, False, "vbReadAsyncEvent")
        OL.hEvent = hEvent
        
        ' Get handle to nominated folder
        hFolder = CreateFile(sFolder, FILE_LIST_DIRECTORY, FILE_SHARE_READ + FILE_SHARE_DELETE + FILE_SHARE_WRITE, 0&, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS + FILE_FLAG_OVERLAPPED, 0)
        
        ' Filter the type of file events we want to monitor
        nFilter = FILE_NOTIFY_CHANGE_FILE_NAME + FILE_NOTIFY_CHANGE_LAST_WRITE + FILE_NOTIFY_CHANGE_CREATION
        
        ' Keep looping until user cancels
        Do While Not (Cancelled) And Not (WaitResult = WAIT_IO_COMPLETION)
        
            ' set up the async call
            ReadAsync hFolder, cBuffer(0), 1024, False, nFilter, nReturned, OL, [highlight #FCE94F]AddressOf FileIOCompletionRoutine[/highlight] 
            
            Do While Not (Cancelled) And Not (WaitResult = WAIT_IO_COMPLETION)
            
                ' Wait for event or timeout to occur
                
                WaitResult = WaitForSingleObjectEx(hEvent, 100, True)
                DoEvents ' Yield to OS
                
            Loop
        Loop

I had to get the module sub (FileIOCompletionRoutine) to then use the object method (FileIOCompletion) because apparently you cannot pass an address reference of a class method.

Code:
Public Sub FileIOCompletionRoutine(ByVal dwErrorCode As Long, ByVal dwNumberofBytes As Long, ByRef lpOverlapped As OVERLAPPED)

    If Not oFileWatcher Is Nothing Then
        Call oFileWatcher.FileIOCompletion(dwErrorCode, dwNumberofBytes, lpOverlapped, [b]Form_Drop_Email, "Exit_Form"[/b])
    End If
    
End Sub

You will see the two end arguments are the form class and sub to run.

There is nothing happening on the drag/drop subform to trigger the sub to close the form, the windows API (ReadAsync) is triggering a call to the (FileIOCompletionRoutine) sub which in turn wraps up the arguments passed to it and then passes them to the class method.

However, I guess I could have the (FileIOCompletion) method return a Boolean value and have the module sub (FileIOCompletionRoutine) simply call the form sub to execute?

Code:
    If Not oFileWatcher Is Nothing Then
    If(oFileWatcher.FileIOCompletion(dwErrorCode, dwNumberofBytes, lpOverlapped)) Then
        Form_Drop_Email.Exit_Form
    End If
End If

Would having the module sub call the form sub be better than having the class method do it?

"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
MajP -> With regard to creating a WithEvents, it seems my global object variable oFileWatcher can't use WithEvents , it errors with
Only valid in object module

I can't have it as a private object on the form due to the nature of the 'AddressOf FileIOCompletionRoutine' which must not be a class method, and now it appears WithEvents must be in a class, I seem to be snookered!


"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
However ... you do have an event in your class at THIS point if WaitResult = WAIT_IO_COMPLETION ...

WaitResult = WaitForSingleObjectEx(hEvent, 100, True)
 
Cheers Mike, for the ever cryptic advice!

Yes, that's in a loop that I guess is waiting for the OS event to trigger, what is WAIT_IO_COMPLETION ?

What does that enum equate to, when what is completed?

This doesn't appear to be anything to do with the actual wombat functionality that's all done in the parent loop...
Code:
        ' Keep looping until user cancels
        Do While Not (Cancelled) And Not (WaitResult = WAIT_IO_COMPLETION)
        
            ' set up the async call
            [highlight #FCE94F]ReadAsync hFolder, cBuffer(0), 1024, False, nFilter, nReturned, OL, AddressOf FileIOCompletionRoutine[/highlight]  
            
            Do While Not (Cancelled) And Not (WaitResult = WAIT_IO_COMPLETION)
            
                ' Wait for event or timeout to occur
                
                WaitResult = WaitForSingleObjectEx(hEvent, 100, True)
                DoEvents ' Yield to OS
                
            Loop
        Loop

How does WaitResult have any bearing on whether I want to close the form?

FileIOCompletionRoutine is where the code is run to decide what FileIO was performed and if I'm interested in it...
Code:
Public Function FileIOCompletion(ByVal dwErrorCode As Long, ByVal dwNumberofBytes As Long, ByRef lpOverlapped As OVERLAPPED) As Boolean

    Dim wombat As FILE_NOTIFY_INFORMATION ' the infamous wombat!
    Dim strFilename As String

    FileIOCompletion = False ' don't close Nav Control subform

    If dwNumberofBytes Then  ' did we get anything?
    
        CopyMemory wombat, cBuffer(0), dwNumberofBytes
        strFilename = Left(CStr(wombat.FileName), wombat.FileNameLength / 2)

        Select Case wombat.Action
        
            [highlight #FCE94F]Case FILE_ACTION_ADDED[/highlight]
            
                ' stop watching
                oFileWatcher.CancelWatcher = True
                    
                ' move dropped email file
                If Not (oFileWatcher.MoveFile(strFilename, ".msg", "D")) Then
                    MsgBox "Error : " & oFileWatcher.GetError
                End If
                
                [highlight #FCE94F]FileIOCompletion = True 'set that I want the Nav Control subform to close[/highlight]

            Case FILE_ACTION_MODIFIED
            Case FILE_ACTION_REMOVED
            Case FILE_ACTION_RENAMED_NEW_NAME
            Case FILE_ACTION_RENAMED_OLD_NAME
            Case Else
            
        End Select
        
    End If
    
End Function

I'm only interested in FILE_ACTION_ADDED , and that isn't triggered by WaitResult = WaitForSingleObjectEx(hEvent, 100, True) is it?

You need to throw me a bone here Mike, I'm still in cargo-cult-land with some of your code!

"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
WaitForSingleObjectEx can be triggered by a number of things

The main ones are

o that the time period that we told it to wait for has expired without the event occurring (WAIT_TIMEOUT)
o that a call back has been triggered and completed because the event was detected (WAIT_IO_COMPLETION)

There are other reasons why the function might return, but for this code only WAIT_IO_COMPLETION really matters (since we need to know whether we need set up the event again if it has been triggered).

Now it also true that you can have no idea at that point which specific FILE_ACTION event casued the completion - but mainly because we set the event filter rather wide:

' Filter the type of file events we want to monitor
nFilter = FILE_NOTIFY_CHANGE_FILE_NAME + FILE_NOTIFY_CHANGE_LAST_WRITE + FILE_NOTIFY_CHANGE_CREATION

I'd suggest that changing the filter to

nFilter = FILE_NOTIFY_CHANGE_CREATION

means the event will only fire if a new file is added to the watched folder. So now, if our WaitResult is WAIT_IO_COMPLETION then we know that a file has been added (we don't know which file, of course, since that is handled by the FileIOCompletion callback).
Mind you, in this expanded version of your code I note that FileIOCompletion appears to have a reference to the oFileWatcher object - Soooo ... surely you can add a method and/or properties to the class to allow FileIOCompletion to pass that information to the class.




 
Mind you, in this expanded version of your code I note that FileIOCompletion appears to have a reference to the oFileWatcher object - Soooo ... surely you can add a method and/or properties to the class to allow FileIOCompletion to pass that information to the class.

err no not really, that was my bad, I made a faux pas and was referencing the global object from within the class, I noticed that after posting, it should be talking to itself!

I have refactored it correctly to be...
Code:
Select Case wombat.Action
        
            Case FILE_ACTION_ADDED
            
                ' stop watching
                [highlight #FCE94F]Me[/highlight].CancelWatcher = True
                    
                ' move dropped email file
                If Not ([highlight #FCE94F]Me[/highlight].MoveFile(strFilename, ".msg", "D")) Then
                    MsgBox "Error : " & [highlight #FCE94F]Me[/highlight].GetError
                End If
                
                FileIOCompletion = True

            Case FILE_ACTION_MODIFIED
            Case FILE_ACTION_REMOVED
            Case FILE_ACTION_RENAMED_NEW_NAME
            Case FILE_ACTION_RENAMED_OLD_NAME
            Case Else
            
        End Select

FileIOCompletion is a class method of the oFileWatcher object!

I could have the class fire an Event type, but still I can't have the oFileWatcher object declared on the form otherwise FileIOCompletionRoutine wouldn't be able to reference it, well unless I make it public on the form and have it referenced by FileIOCompletionRoutine with
Code:
If Not [highlight #FCE94F]Form_Drop_Email[/highlight].oFileWatcher Is Nothing Then
    [highlight #FCE94F]Form_Drop_Email.[/highlight]oFileWatcher.FileIOCompletion(dwErrorCode, dwNumberofBytes, lpOverlapped)
End If

Is it good practice to have public class variable on a form?

I'm not sure of the benefit of having a public class variable on the form just so the oFileWatcher class can trigger an event that the from can respond to over having a global oFileWatcher object and simply calling the public form_close method the way I have it now.

Public 'Form_Exit' sub on the form vs Public oFileWatcher object on the form?

None of this would be an issue if 'AddressOf FileIOCompletionRoutine' could be a class method!

An aside that's puzzling me:- Any chance you could explain this for me
Code:
strFilename = Left(CStr(wombat.FileName), wombat.FileNameLength / 2)

Why does a string stored in a byte array when converted to a string have a length of 50% of the length when held as bytes?


"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
I think MajP's suggestion is definately the way to go. In the form you seem to want to tell the class which of the form's own procedures to call when it receives a file and let the form call its procedure. Better to have the class tell the form when it has received a file. A class is supposed to be a kind of "black box" and not rely on any object outside the class. It is much cleaner to have the class raise an event and have the form listen out for it. The class raises event IveDoneSomething and the form just has to call the required procedure in the myClass_IveDoneSomething sub.

Code:
Private Sub myClass_IveDoneSomething(FileName As String)
    myProcedure FileName
End Sub

Also, the event raised by the class isn't the same thing as the event raised by your widget. The class could be coded to raise its own event only when you get a FILE_ACTION_ADDED.

Code:
If wombat.Action = FILE_ACTION_ADDED Then
   RaiseEvent IveDoneSomething(wombat.FileName)
End If

You can get the event to pass a parameter, in your case it could be the file identified by your file watcher, which you could pass into the procedure that you need to call. You can also have multiple forms listening out for the same event and have all of them do their own thing when a file is received.

Simples

PeteJ
(Contract Code-monkey)

It's amazing how many ways there are to skin a cat
(apologies to the veggies)
 
Woops, Biological Component Failure

Should have said "let the class call the form's procedure" instead of "let the form call its procedure"

PeteJ
(Contract Code-monkey)

It's amazing how many ways there are to skin a cat
(apologies to the veggies)
 
Yes, and that would be great only you cannot have a WithEvents object unless it is declared in a class and oFileWatcher is a global variable not a class variable!

Due to the restriction of the AddressOf, I have had to create a module called FileWatcher which is separate from the class clsFileWatcher and contains the following...

Code:
Option Compare Database
Option Explicit

' watcher object
Global oFileWatcher As clsFileWatcher

' UDT's
Public Type OVERLAPPED
        Internal As Long
        InternalHigh As Long
        Offset As Long
        OffsetHigh As Long
        hEvent As Long
End Type

Public Sub FileIOCompletionRoutine(ByVal dwErrorCode As Long, ByVal dwNumberofBytes As Long, ByRef lpOverlapped As OVERLAPPED)

    If Not oFileWatcher Is Nothing Then
        If (oFileWatcher.FileIOCompletion(dwErrorCode, dwNumberofBytes, lpOverlapped)) Then
            Call Form_Drop_Email.Exit_Form
        End If
    End If
    
End Sub

The actual oFileWatcher object is an application global, if it wasn't the form couldn't access it or the FileIOCompletionRoutine couldn't access it.

The only way round this is to have it as a public oFileWatcher object on the form so that the public application sub FileIOCompletionRoutine can access it via Form_Drop_Email.oFileWatcher and I don't think that is gaining anything here is it?

"In complete darkness we are all the same, it is only our knowledge and wisdom that separates us, don't let your eyes deceive you."

"If a shortcut was meant to be easy, it wouldn't be a shortcut, it would be the way!"

Free Dance Music Downloads
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top