Can someone help me with events in vb6 from c# dll - com

I have found nice application for NFC card reading which in winforms works very nice.
Code found here:
NfcReader: A very simple NFC library for C# that supports insert and discard events
Git: https://github.com/h4kbas/NfcReader
But i have a problem. I need now to make this work in com for vb6.
I must "replicate" event hooking, like in the following code.
I exposed methods and events to vb6 successfully.
NFC = new NFCReader();
NFC.CardInserted += new NFCReader.CardEventHandlerDelgate(Card_Inserted);
NFC.CardEjected += new NFCReader.CardEventHandlerDelgate(Card_Ejected);
NFC.DeviceDisconnected += new NFCReader.CardEventHandlerDelgate(Device_disconected);
NFC.StartCardMonitoring();

With the information #kunif provided, you first have to make the .NET library COM Visible in order to use the NFC Reader in VB6. If you have the source code, you can do this fairly easily. Otherwise, if you just have a DLL, you can write a wrapper DLL and make that COM Visible. Sounds like you might've done this already when you say "i exposed events and methods to vb6 successfully".
The next step is to add a reference to the library in VB6: Project > References...
Then, you can create an instance of the NFCReader:
Public WithEvents objNFC As NFCReader
Private Sub Form_Load()
' Create NFCReader object
Set objNFC = New NFCReader
objNFC.StartCardMonitoring
End Sub
' Card Inserted event handler
Private Sub objNFC_CardInserted()
' Handle Card Inserted event
End Sub
' Card Ejected event handler
Private Sub objNFC_CardEjected()
' Handle Card Ejected event
End Sub
' Device Disconnected event handler
Private Sub objNFC_DeviceDisconnected()
' Handle Device Disconnected event
End Sub
Once you've added a reference to the DLL, you should see the events and their parameters show up in VB6. Make sure you declare the object as WithEvents and the object will appear in the left dropdown in the code window in Visual Studio. The right dropdown will display the available events.

Related

Passing data to different UI threads

As an old mainframe systems programmer, I'm not specialised at all in OO thinking, nor in Visual Basic. A former colleague and I wrote a VB program (named FotoDB) to manage and present our family&holyday photos. We have kind of a database with descriptions and GPS coordinates for all of them.
FotoDB can also work in "presentation mode" and then I'd like to also show the OpenStreetMap of the area where the active photo was taken. I already found out how to use Leaflet.js to display the map in a WebBrowser object.
A first problem I encountered when I tried to use a WebBrowser object was that I got:
cannot be instantiated because the current thread is not in a
single-threaded apartment.
Our FotoDB program must remain defined multi-threading (I think): it uses a BackgroundWorker for some things, and is also listening for 2 control files that might be changed by Notepad (or alike).
So, I abandoned the idea of adding the map as a small item on the main screen of FotoDB, but use another form for the WebBrowser object, and I start that in a new STA-mode thread:
mapThread = New Thread(AddressOf form4MapShow)
mapThread.SetApartmentState(ApartmentState.STA)
mapThread.Start()
End Sub
Private Sub form4MapShow()
Form4map.Show()
End Sub
That works fine, I can display a section of the OpenStreetMap in my "Form4map".
I still have 2 problems (probably reduceable to 1 problem)
I need to pass the GPS coordinates from the main program to the thread in which From4map runs
When the end-user selects a new photo to view, the main program must pass the new coordinates to Form4map
I've been reading a bit about Delegate, but without understanding it completely. I found examples about updating a ListView from another thread and tried to mimic it (when a new photo is displayed in the main frame, send its GPS coordinates to a text field on Form4map), So I coded (as a basic test):
NieuweFoto(Form4map.txtNwFoto, "Nieuw " & Now.ToLocalTime)
End Sub
Public Delegate Sub newPhotoInvoker(ByVal txt As TextBox, ByVal itemtext As String)
Public Sub NieuweFoto(ByVal txt As TextBox, ByVal gpsInfo As String)
If Form4map.txtNwFoto.InvokeRequired Then
Form4map.txtNwFoto.Invoke(New newPhotoInvoker(AddressOf NieuweFoto), txt, gpsInfo)
Else
txt.Text = gpsInfo
End If
End Sub
But, now I get a similar error as at the start:
An error occurred creating the form. See Exception.InnerException for details.
The error is: ActiveX control '8856f961-340a-11d0-a96b-00c04fd705a2' cannot be instantiated because the current thread is not in a single-threaded apartment.
So I'm stuck. Is there an easy solution?

Handling custom messages in windows compact framework 3.5 using windows mobile handheld 6.5 on a POCKETPC

I'm updating an existing application that scans barcodes and is written in VB.net running on windows compact framework 3.5. The scanner is a POCKETPC running windows mobile handheld 6.5. I have added code that uses Asynchronous TCP sockets in a class module. The sockets code is reading and sending data to and from a buffer pool. I now need to “inform” the GUI form that data has been received from the TCP socket and is ready for processing. Because the two processes are running on different threads I realise I cannot access the GUI controls directly. I therefore create a windows message (WM_CUSTOMMSG = &H400) and then use “SENDMESSAGE”. There is an existing WndProc sub (Protected Overrides Sub WndProc(ByRef msg As Microsoft.WindowsCE.Forms.Message)) that handles the WM_DECODEDATA for the scanner message. I added in code to now also process the WM_CUSTOMMSG message I am creating. The WM_CUSTOMMSG is arriving at the WndProc and I am able to display a MessageBox and write a log file, but any changes made to the GUI controls just disappear. I tried to start a forms timer but this also has no effect. Code for the WM_DECODEDATA message updates the GUI controls perfectly. What am I missing / done wrong?
Public Class frmHome
Public SockReceiveMsg As Microsoft.WindowsCE.Forms.Message
Public Sub New()
Private yy As Integer = 0
Private xx As Integer = 0
InitializeComponent()
Me.MsgWin = New MsgWindow(Me)
' Add any initialization after the InitializeComponent() call.
SockReceiveMsg = Microsoft.WindowsCE.Forms.Message.Create(MsgWin.Hwnd, MsgWindow.WM_CUSTOMMSG, New IntPtr(xx), New IntPtr(yy))
end class
Private Sub ReceiveCallback(ByVal ar As IAsyncResult)
'This is the async call back sub
MessageWindow.SendMessage(frmHome.SockReceiveMsg)
end sub
Protected Overrides Sub WndProc(ByRef msg As Microsoft.WindowsCE.Forms.Message)
Dim rc As Integer
Dim ar() As String
If msg.Msg = WM_CUSTOMMSG Then
Try
MsgBox("restart timer") 'this displays
Reader.ReaderEngineAPI.Beeper(8, "") 'a quick ok beep. this works
frmHome.timer1.Enabled = False
frmHome.timer1.Interval = 100
frmHome.timer1.Enabled = True
Catch ex As Exception
MsgBox("wndproc Error1: " & ex.Message)
End Try
End If
'pass all messages onto the base processing. Here the windows ones get processed and our ones get cleared and the storage released
MyBase.WndProc(msg)
End Sub
I don't know if/what you're doing wrong, but you can probably do things alot easier. All Controls (including Forms) have an Invoke() method that you can call to let the framework deal with the windows messages.
This article describes it in a bit more detail, including the InvokeRequired property, which you can probably ignore if you know the data is sent from another thread.
If you do choose to handle these messages manually (since you already have the WndProc routine), be sure to catch all exceptions in the method that updates the GUI, and perhaps inspect the InvokeRequired to see if the control agrees that you can update it from that thread.
I would go with a delegate and an eventhandler on the thread code and use InvokeRequired with a custom delegate to update the GUI thread.
Handling custom message is not recommended. Or why does MS hide WndProc in WindowsCE.Forms? OTOH you may need to Refresh the controls that have been changed to let them know that they need to update. Do you use PostMessage or SendMessage. If later, that would block the thread code until the message is processed, which may block the GUI to update itself, if the WndProc itself uses SendMessage inside the code to handle the custom message.
Private Sub UpdateTextBox(ByVal sender As Object, ByVal e As EventArgs)
'---delegate to update the textbox control
txtMessagesArchive.Text += str
End Sub
That would be called from a background thread in the same class via:
Me.Invoke(New EventHandler(AddressOf UpdateTextBox))
Different ways to update GUI from background thread, but in C#
Thank you to all who provided answers. I had previously tried a delegate with invoke but this caused the system to crash. I spent over a week trying to find out what was wrong with the delegate code – I mean it is so simple, but was not successful. I then tried the WndProc route. Josef’s example in his answer showed me that the delegate must be in the same class as the backgound thread. I had placed the delegate in the form class. Once I put the delegate in the correct class it works perfectly. I have 50 years of experience in IT and programming and you can still learn.
I am not going to try to get to the bottom of the WndProc problem. This is now academic as the delegate route is a better solution and is now working. Thank you

COM App calling .NET form calling COM form, works when compiled but not in VB6 IDE

I have a legacy VB6 / COM application that we are migrating all of the code to .NET (VB.NET) via a .NET DLL. All new form development is being done in the .NET DLL.
We use function pointers to call back to COM functions within .NET form:
I create a delegate in .NET
Set the delegate to the AddressOf the COM function at runtime (GetDelegateForFunctionPointer)
Show the .NET Form
Invoke the delegate
COM App --> .NET Form --> COM function
This works fine for functions. Where this breaks down is when I call a COM function that shows a form.
COM App --> .NET Form --> COM function --> COM Form
The COM function will execute and the form will show, but when executing in the VB6 IDE the COM form's logic does not execute. The controls are shown, but any click or form_load events are not executed. This is not an issue when the VB6 application is compiled.
Another issue I've noticed is that the VB6 IDE "freezes" until the .NET form unloads. I can not stop the debugger, set breakpoints, or interact with IDE's menu. It is as if the VB6 IDE allows the .NET DLL to take complete control over the thread. I believe these two issues are related.
EDIT:
The .NET form is modal, which may be the reason for the IDE thread lock. I'm just wondering if there is a way around it. Also, I've looked at the InteropForms Toolkit. It does not solve my problem. Even setting their sample forms to modal causes the same IDE thread lock to occur.
EDIT 2:
My previous edit did allude to another workaround: Make the .NET form show as modeless when the host application is VB6.
If InStr(Process.GetCurrentProcess().ProcessName, "vb6", CompareMethod.Text) > 0 Then
frm.Show()
Else
frm.ShowDialog()
frm.Dispose()
End If
The simple COM App --> .NET Form --> COM function --> COM Form scenario can work using the InteropForms Toolkit 2.1. Here is some code that does it, without using delegates or AddressOf.
First, we have our COM library which I'll call ComLibrary. We have the following code:
SimpleForm.frm:
Option Explicit
Private Sub Form_Load()
MsgBox "In SimpleForm_Load"
End Sub
Private Sub OkButton_Click()
Unload Me
End Sub
Private Sub TalkButton_Click()
MsgBox "Hello"
End Sub
SimpleClass.cls:
Option Explicit
Public Function ShowTheDialog() As Long
Dim f As SimpleForm
Set f = New SimpleForm
f.Show vbModal
ShowTheDialog = 1
End Function
Compile this to a .dll so that we can reference it in our .NET library.
Next, we'll create our .NET forms library. I did this using the "VB6 InteropForms Library" project template using the default project name of InteropFormsLibrary1. I added a reference to the ComLibrary project. The form has a single button named "CallComFunction."
InteropForm1.vb:
Imports Microsoft.InteropFormTools
<InteropForm()> _
Public Class InteropForm1
Private Sub CallComFunction_Click(sender As System.Object, e As System.EventArgs) Handles CallComFunction.Click
Dim sc As ComLibrary.SimpleClass
sc = New ComLibrary.SimpleClass
Dim result As Integer
result = sc.ShowTheDialog()
End Sub
End Class
Before compiling this, we need to create the InteropForms wrapper classes per the toolkit documentation. Do this by accessing the Tools > Generator InteropForms Wrapper Classes menu.
Finally, we create the VB6 host app.
Form1.frm:
Option Explicit
Private Sub DialogButton_Click()
Dim f As InteropFormLibrary1.InteropForm1
Set f = New InteropForm1
f.Show vbModal
End Sub
When I run the host application in the VB6 IDE, I can click the button the VB6 form which then displays the VB.NET form. From there, I click another button which uses an instance of SimpleClass to display SimpleForm. The MsgBox statements in the form load event and the button click handlers work. And everything works when compiling the VB6 host app as well.
It looks like your VB6 IDE is loading your VB application as an "in-process" COM object, although that does not make much sense to me... However if that is the case,it would mean that you have a single message loop for your application and your debugger, which might explain at least some of these issues. I have not used a VB-classic IDE in too many years, so I do not know how it works. Maybe there is an option there somewhere to load your app in a separated process?
Update -
It seems my guess was correct. See this other question and its answers for more information and possible workarounds:
https://stackoverflow.com/a/518217/501196

How to Close VB6 Parent Form From .NET Interop-ed UserControl?

I'm building a "plug-in" of sorts for an already-deployed VB6 executable. I'm using .NET along with COM-Interop. The VB6 creates a blank form and then loads my .NET UserControl into it (however by now the .dll has been compiled into a .ocx ActiveX UserControl that can be seen by VB6).
I've got it working well, but I would like to be able to close the VB6 parent form from inside of my .NET code. I am able to add VB6 code into my VB6-ifyed UserControl, but I cannot seem to find an event that fires when the UserControl is destroyed.
What I've tried so far:
Calling ParentForm.Close from the Disposing event of my .NET control. Receive error Object Reference not set to Instance of an Object.
Trying to close from the VB6 (I am able to get a handle on the parent form from there). Using ControlRemoved, Terminated, and a couple other hackish workarounds that truly make no sense in retrospect don't get triggered.
Calling Application.Exit (truly getting desperate at this point) closes the whole Application (who woulda thunk...)
I looked in the VB6 Interop code that I put in my .NET control and the following does look promising:
#Region "VB6 Events"
'This section shows some examples of exposing a UserControl's events to VB6. Typically, you just
'1) Declare the event as you want it to be shown in VB6
'2) Raise the event in the appropriate UserControl event.
Public Shadows Event Click() 'Event must be marked as Shadows since .NET UserControls have the same name.
Public Event DblClick()
Private Sub InteropUserControl_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Click
RaiseEvent Click()
End Sub
Private Sub InteropUserControl_DoubleClick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.DoubleClick
RaiseEvent DblClick()
End Sub
#End Region
Is it just a matter of adding an event in this section? I'm not terribly familiar with Interop, or VB6 for that matter.
Alright, I figured it out and will post what I did for future generations :P
I was right with the event handlers in the VB6 code, and MarkJ was correct as well.
I created an event in the .NET code,
Public Event KillApp()
and then when I wanted to close everything, raised it:
RaiseEvent KillApp()
In the VB6 UserControl code, I declared the event again,
Public Event KillApp()
and then added a handler for it:
Private Sub MenuCtl_KillApp()
Unload Me.ParentForm
End Sub
where MenuCtl is my instance of the .NET control, and Me.ParentForm is the VB6 container form that houses the control. It now correctly closes the form!
In retrospect it makes a lot of sense, but I was unaware that you could pass events back and forth between managed/unmanaged that easily.

How to add event handling to a control from a dynamically loaded OCX library in Excel VBA

In an Excel 2003 VBA project, I am using the TreeCtrl from MSCOMCTL.OCX. I add the control dynamically to a form in my application (see code below).
I would like to handle the NodeClick event of the TreeCtrl.
Since the control is added dynamically, just using TreeCtrl_NodeClick does not seem to work. Also I cannot use the WithEvents approch since it does not seem to work with types which are not known at compile time.
'Does not work. WithEvents probably only works on known data types that have events.
'private WithEvents treeCtrl As Object
'This code adds the control to the form
Private Sub UserForm_Initialize()
Dim treeCtrl As Object
Set treeCtrl = Me.Controls.Add("MSComctlLib.TreeCtrl.2", "MyTreeCtrl")
'Does not work: AddHandler is not available in VBA.
'AddHandler TreeCtrl.NodeClick, AddressOf UserForm1.MyTreeCtrl_NodeClick
End Sub
'This is supposed to be the event handler but it does not get called.
Private Sub MyTreeCtrl_NodeClick(ByVal Node As Object)
MsgBox "Node clicked"
End Sub
Note that I need to load MSCOMCTL.OCX dynamically. I cannot put it as a static reference into my VBA project (since my project has to run in different versions of Excel).
According to this response from MS "you cannot handle the events for a TreeView control added in runtime using Excel VBA", although there is a suggested workaround using VB6(!).
Is adding the control at design-time and then hiding it until required an option?