Single Instance App with Command-Line Parameters






3.62/5 (15 votes)
Demonstrates a single-instance application which can pass command-line parameters to a previous instance.
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
'//Subclassing the form - so much easier than VB6! :)
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
Dim bCancel As Boolean = False
RaiseEvent WndProc2(m, bCancel)
'If this message wasn't handled (i.e. WM_COPYDATA), pass it on
If Not bCancel Then MyBase.WndProc(m)
End Sub
'//The reference to the handle is required for SetProp
Public ReadOnly Property hWnd() As System.IntPtr _
Implements mMain.SingleInstance.ISingleInstanceForm.Handle
Get
Return Handle
End Get
End Property
'//This is where the string passed in the COPYDATASTRUCT is handled
'//In this case, we have a Base64 serialization of the CmdArgs() array
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
'//This is where we handle the command-line arguments
'//In this sample, I simply add them to the list-box
'//You would probably want to open the files, etc.
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
:
'//Get an array of bytes for the string
Dim B() As Byte = Encoding.Default.GetBytes(StringVarToSend)
Try
'//Allocate enough memory on the global heap
Dim lpB As IntPtr = Marshal.AllocHGlobal(B.Length)
'//Copy the managed array to the un-managed pointer
Marshal.Copy(B, 0, lpB, B.Length)
Dim CD As COPYDATASTRUCT
With CD
.dwData = 0
.cbData = B.Length
'//The ToInt32 function converts an IntPtr to an Integer pointer
.lpData = lpB.ToInt32
End With
'Do something with CD here...
Finally
'//Clean up
Erase B
Marshal.FreeHGlobal(lpB)
End Try
To retrieve a String
from the pointer:
Dim B() As Byte
Try
'//Get the COPYDATASTRUCT from the message
Dim CD As COPYDATASTRUCT = m.GetLParam(GetType(COPYDATASTRUCT))
'//Allocate enough space in B
ReDim B(CD.cbData)
'//Create an IntPtr from the Integer pointer
Dim lpData As IntPtr = New IntPtr(CD.lpData)
'//Copy the data into the array
Marshal.Copy(lpData, B, 0, CD.cbData)
'//Get the string from the Byte Array
Dim strData As String = Encoding.Default.GetString(B)
'//Do something with the String here...
Finally
'//Clean up
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.