The Problem
In a recent posting on the GotDotNet forums, a user asked "Is there a way to support a 'ddeexec'-enabled file association in a vb.net app, so when the app is already running, and a file is double-clicked from Windows Explorer, the app can handle it?". DDE has not been recommended for a long time, and .NET has no support for it.
A Solution
Personally, I could never be bothered to get DDE working. Instead, I used a trick from the old vbAccelerator site:
- Create a mutex with a name specific to your app;
- If the mutex doesn't exist, your app is not running:
- Start the app;
- Mark the main form with a property to make it easily identifiable (SetProp);
- Subclass the main form, and wait for the WM_COPYDATA message. The data will contain the command-line from a new instance;
- If the mutex already exists, your app is already running:
- Enumerate all windows to find the one with your property (GetProp);
- If you don't find it, proceed as step 2;
- If you do find it, send the WM_COPYDATA message to it, with the command-line from this instance;
This may seem very complicated, but the App.PrevInstance
property was not always reliable, and didn't provide any means to transfer data to the other instance.
.NET
When I first read the question, I thought there must be a better solution in
.NET - this kind of application is so common, I was sure the functionality would be built-in. Some of it - such as Mutex
- is. Some of the classes in System.Diagnostics
look very promising, but don't seem to work. For example, there doesn't seem to be a way to get a NativeWindow
object from a Process.MainWindowHandle
- the FromHandle
method returns Nothing
. There is a COPYDATASTRUCT
in System.Windows.Forms
, but although the Object Browser thinks it's Public
, the compiler thinks it's Private
, so that can't be used.
In the end, I have had to resort to a lot of the same Win32 API calls I was using in VB6. Subclassing is much easier in
VB.NET, and the Mutex
object helps, but I can't see a pure managed-code solution to this problem. On the plus-side, the Marshal
class provides a very easy way to transfer data between managed objects and un-managed APIs, although some of the calls may not be obvious to a VB6 programmer. Object serialization also makes it possible to pass objects between processes as well.
Using the code
To use this in your own code, you will need to add the mMain.vb
file to your project. Set the startup object to be Sub Main
within this module, and change the references to frmMain
to reflect your application's main form. You will need to make your main form implement mMain.SingleInstance.ISingleInstanceForm
, using the following code as an example:
//
// frmMain.vb
//
...
Public Event WndProc2(ByVal m As System.Windows.Forms.Message, _
ByRef Cancel As Boolean) _
Implements mMain.SingleInstance.ISingleInstanceForm.WndProc
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
Dim bCancel As Boolean = False
RaiseEvent WndProc2(m, bCancel)
If Not bCancel Then MyBase.WndProc(m)
End Sub
Public ReadOnly Property hWnd() As System.IntPtr _
Implements mMain.SingleInstance.ISingleInstanceForm.Handle
Get
Return Handle
End Get
End Property
Public Sub HandleCommand(ByVal strCmd As String) _
Implements mMain.SingleInstance.ISingleInstanceForm.HandleCommand
Dim arrCmd() As String
Try
arrCmd = SerialHelper.DeserializeFromBase64String(strCmd)
HandleCommand(arrCmd)
Catch ex As Exception
MsgBox(ex.ToString)
HandleCommand(New String() {strCmd})
Finally
Erase arrCmd
End Try
End Sub
Public Sub HandleCommand(ByVal strArgs() As String)
Dim strCmd As String
For Each strCmd In strArgs
lstEvents.Items.Add("CMD: " & strCmd)
Next
End Sub
...
///
VB.NET and Pointers
One interesting thing to come out of this code - I have discovered how to deal with un-managed pointers. For example, the COPYDATASTRUC
structure contains the lpData
member, which is a pointer to a Byte Array
. If the member is defined as an IntPtr
, the structure cannot be passed to an API, so it has to be declared as an Integer
.
There are two operations required - set the lpData
member to point to a String
, and retrieve a String
from it. In VB6, you would use VarPtr
and the RtlMoveMemory
API (a.k.a. CopyMemory
) to do this. In .Net, VarPtr
no longer exists, but the Marshal
class provides much better alternatives.
To set the pointer to the value of a String
:
Dim B() As Byte = Encoding.Default.GetBytes(StringVarToSend)
Try
Dim lpB As IntPtr = Marshal.AllocHGlobal(B.Length)
Marshal.Copy(B, 0, lpB, B.Length)
Dim CD As COPYDATASTRUCT
With CD
.dwData = 0
.cbData = B.Length
.lpData = lpB.ToInt32
End With
Finally
Erase B
Marshal.FreeHGlobal(lpB)
End Try
To retrieve a String
from the pointer:
Dim B() As Byte
Try
Dim CD As COPYDATASTRUCT = m.GetLParam(GetType(COPYDATASTRUCT))
ReDim B(CD.cbData)
Dim lpData As IntPtr = New IntPtr(CD.lpData)
Marshal.Copy(lpData, B, 0, CD.cbData)
Dim strData As String = Encoding.Default.GetString(B)
Finally
Erase B
End Try
Done!
If anyone finds a better way to accomplish this, or a way to get a NativeWindow
object from a Process.MainWindowHandle
, please let me know!
Credits
The SerialHelper
class was "borrowed" from Dr GUI.Net #3.
I started writing code when I was 8, with my trusty ZX Spectrum and a subscription to "Input" magazine. Spent many a happy hour in the school's computer labs with the BBC Micros and our two DOS PCs.
After a brief detour into the world of Maths, I found my way back into programming during my degree via free copies of Delphi and Visual C++ given away with computing magazines.
I went straight from my degree into my first programming job, at Trinet Ltd. Eleven years later, the company merged to become ArcomIT. Three years after that, our project manager left to set up Nevalee Business Solutions, and took me with him. Since then, we've taken on four more members of staff, and more work than you can shake a stick at.
Between writing custom code to integrate with Visma Business, developing web portals to streamline operations for a large multi-national customer, and maintaining RedAtlas, our general aviation airport management system, there's certainly never a dull day in the office!
Outside of work, I enjoy real ale and decent books, and when I get the chance I "tinkle the ivories" on my Technics organ.