Click here to Skip to main content
Click here to Skip to main content

Single Instance App with Command-Line Parameters

, 13 May 2002
Rate this:
Please Sign up or sign in to vote.
Demonstrates a single-instance application which can pass command-line parameters to a previous instance.
<!-- Download Links --> <!-- Add the rest of your HTML here -->

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:

  1. Create a mutex with a name specific to your app;
  2. If the mutex doesn't exist, your app is not running:
    1. Start the app;
    2. Mark the main form with a property to make it easily identifiable (SetProp);
    3. Subclass the main form, and wait for the WM_COPYDATA message. The data will contain the command-line from a new instance;
  3. If the mutex already exists, your app is already running:
    1. Enumerate all windows to find the one with your property (GetProp);
    2. If you don't find it, proceed as step 2;
    3. 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Richard Deeming
Software Developer Nevalee Business Solutions
United Kingdom United Kingdom
No Biography provided
Follow on   Twitter   Google+

Comments and Discussions

 
QuestionVB BETA 2005 version PinmemberJamesthebod4-Oct-05 8:27 
AnswerRe: VB BETA 2005 version PinmemberRichard Deeming4-Oct-05 8:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 14 May 2002
Article Copyright 2002 by Richard Deeming
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid