Introduction
This article will attempt to show how to setup a VB.NET Windows application to become a Single Instance type application within the .NET Framework. Techniques used include Mutexes, .NET Remoting and Thread communications between Forms. Please note that I did not separate the functionality into separate classes to keep the code size down. Also note that I did not utilize any Imports
statements because when working with classes I am unfamiliar with, I like to keep the full references in the code to get used to their placement in the .NET Framework.
Background
Recent work on a project presented a problem where we needed the main application to allow only one instance of the application to run at a time. It was also required that secondary instances that were started should pass the command line arguments to the running application to be processed.
Using the code
To use this code in your VB.NET application, simply add the SingleInstanceHandler.vb file to your project and add the following code to your startup code:
Module ModMain
Private m_Handler As New SingleInstanceHandler()
Public Sub Main(ByVal args() As String)
AddHandler m_Handler.StartUpEvent, AddressOf StartUp
m_Handler.Run(args)
End Sub
Public Sub StartUp(ByVal sender As Object,
ByVal event_args As StartUpEventArgs)
End Sub
End Module
The StartUp
function is called by the SingleInstanceHandler
class whenever an instance of the application is started. The StartUpEventArgs
has two interesting members: NewInstance
is a boolean that indicates that this is the first instance of the application and Args
is a string collection of the arguments passed in on the command line.
For my example project, I have a MDI form SingleInstanceForm
that parents child forms that are created from the arguments that are passed in on the command line. The code is shown here:
Private m_MainForm As SingleInstanceForm
Public Sub StartUp(ByVal sender As Object, ByVal event_args As StartUpEventArgs)
If event_args.NewInstance Then
m_MainForm = New SingleInstanceForm()
m_MainForm.AddChildForms(event_args.Args)
Application.Run(m_MainForm)
Else
Dim parameters() As Object = {event_args.Args}
m_MainForm.Invoke(CType(AddressOf m_MainForm.AddChildForms,
SingleInstanceForm.AddChildFormsDelegate), parameters)
End If
End Sub
The SingleInstanceHandler
class does the majority of its work through the Run
method. This method is passed in the command line arguments as string collection. Here is the code for the Run
method:
Private m_Mutex As System.Threading.Mutex
Private m_UniqueIdentifier As String
Private m_TCPChannel As System.Runtime.Remoting.Channels.Tcp.TcpChannel
Public Sub Run(ByVal args() As String)
m_UniqueIdentifier = Application.ExecutablePath.Replace("\", "_")
' Create the Mutex class
m_Mutex = New System.Threading.Mutex(False, m_UniqueIdentifier)
' Attempt to lock the Mutex
' We locked it! We are the first instance!!!
If m_Mutex.WaitOne(1, True) Then
CreateInstanceChannel()
' Raise the StartUpEvent as a NewInstance
Dim event_args As New StartUpEventArgs(True, args)
RaiseStartUpEvent(event_args)
Else
' Not the first instance!!!
' Raise the StartUpEvent through the
' SingletonCommunicator not as a NewInstance
Dim event_args As New StartUpEventArgs(False, args)
UseInstanceChannel(event_args)
End If
End Sub
Please note that the m_UniqueIdentifier
that I used here is the executable path of the application. We have a special constraint that requires the application be uniquely identified by installation location rather than version number and/or assembly name. It is easy to modify this identifier to be any other unique representation for this application.
The CreateInstanceChannel
and UseInstanceChannel
are used respectively to communicate between instances of the application. If the application was able to lock the m_Mutex
object, it is obviously the first instance of the application running on that system, so it will call CreateInstanceChannel
and then call RaiseStartUpEvent
with a StartUpEventArgs
object with NewInstance
set to True
. If unable to obtain the lock on the m_Mutex
object, this is not the first instance of the application on the system, we will pass a StartUpEventArgs
object to UseInstanceChannel
with NewInstance
set to False
.
The CreateInstanceChannel
method will marshal the SingleInstanceHandler
class and uses .NET Remoting to create a TCP Channel that will be used by subsequent instances to communicate back to this instance. The URL for this TCP Channel is stored in the registry beneath the Application Data Registry section by the m_UniqueIdentifier
.
Private m_TCPChannel As System.Runtime.Remoting.Channels.Tcp.TcpChannel
Private Sub CreateInstanceChannel()
System.Runtime.Remoting.RemotingServices.Marshal(Me, m_UniqueIdentifier)
Dim tcp_properties As IDictionary = New Hashtable(2)
tcp_properties("bindTo") = "127.0.0.1"
tcp_properties("port") = 0
m_TCPChannel = New System.Runtime.Remoting.Channels.Tcp.TcpChannel( _
tcp_properties, Nothing, Nothing)
System.Runtime.Remoting.Channels.ChannelServices.RegisterChannel( _
m_TCPChannel)
Dim key As Microsoft.Win32.RegistryKey = Application.UserAppDataRegistry()
key.SetValue(m_UniqueIdentifier,
m_TCPChannel.GetUrlsForUri(m_UniqueIdentifier)(0))
End Sub
The UseInstanceChannel
will retrieve the URL for the TCP Channel from the Application Data Registry section by the m_UniqueIdentifier
, and will use RemotingServices to connect to the SingleInstanceHandler
class on the first instance, and call the RaiseStartUpEvent
method with the passed in StartUpEventArgs
.
Private Sub UseInstanceChannel(ByVal event_args As StartUpEventArgs)
Dim key As Microsoft.Win32.RegistryKey = Application.UserAppDataRegistry()
Dim remote_component As SingleInstanceHandler = CType( _
System.Runtime.Remoting.RemotingServices.Connect(_
GetType(SingleInstanceHandler), _
key.GetValue(m_UniqueIdentifier)), SingleInstanceHandler)
remote_component.RaiseStartUpEvent(event_args)
End Sub
The RaiseStartUpEvent
method simply raises the StartUpEvent
with the passed in StartUpEventArgs
.
Public Sub RaiseStartUpEvent(ByVal event_args As StartUpEventArgs)
RaiseEvent StartUpEvent(Me, event_args)
End Sub
Public Event StartUpEvent As StartUpEventArgsHandler
The StartUpEventArgs
class is shown here. The class is marked as Serializable
so that it can be sent through the TCP Channel. This class is simply designed to hold the NewInstance
boolean and Args
string collection. I did not make the member variable properties to keep the code size down.
<Serializable()> _
Public Class StartUpEventArgs
Inherits EventArgs
Public Sub New(ByVal new_instance As Boolean, ByVal the_args() As String)
NewInstance = new_instance
Args = the_args
End Sub
Public NewInstance As Boolean
Public Args() As String
End Class
Points of Interest
One of the interesting errors that I ran across when developing this solution was an exception that occurred when the application tried to create additional child forms from a second instance; the following exception would occur:
Using the Invoke
method ensures that proper thread is used to call methods within the forms classes so that the previous exception won't happen. The following code shows how the Invoke
method is used to call the AddChildForms
in the proper thread.
Dim parameters() As Object = {event_args.Args}
m_MainForm.Invoke(CType(AddressOf m_MainForm.AddChildForms, _
SingleInstanceForm.AddChildFormsDelegate), parameters)
History
March 12, 2003 - Initial release of the article.