Simplify Application Instancing
Take advantage of the new PipeStream classes and XML features in .NET 3.5 and VB9 to resolve the age-old problem of creating a single-instance application.
Technology Toolbox: VB.NET, XML
- By Bill McCarthy
Some applications are best presented as a single-instance application. For example, you might have data forms in your application working with a local database, and you don't want to have to deal with the concurrency issues multiple instances bring. Or perhaps you want to limit the network resources by avoiding duplication of network data transfer. If your application is file- or document-centric, such as a photo album, you might want that to be a single-instance app to allow users to organize files in catalogues more easily. You also want your application to be able to process command-line arguments, so it can know which associated files to open.
There are compelling reasons to use single-instance apps, but there are also significant pitfalls that you need to be careful to avoid. I'll show you how to work around the pitfalls by reaching into the VB9 (aka VB 2008) bag of tricks and grabbing the new IO pipe classes and -- my favorite new feature -- the new XML capabilities built directly into the language.
Creating a single-instance application with no communication from other instances is relatively simple. You just iterate the processes, locate the process that's a match, get its MainWindowHandle property, and then use Windows API calls such as SetForeGroundWindow. But when you want to process command lines, such as opening a new file in the existing application, you'll need to communicate with the original instance of the application.
You might think that Visual Basic 2005 made it easy to create single-instance apps by providing the required plumbing for you. All it required was that you check the "Make single-instance application" checkbox in the application properties. If you wanted to get the command-line arguments from any other instances, you could do so in the StartupNextInstance event of the MyApplication partial class. Unfortunately, there are significant problems with this plumbing, so I recommend strongly that you avoid using it.
You might recall that a recent column I wrote showed a workaround for a problem caused by using remoting and single-instance applications that rely on the VB framework (Programming Techniques, "Reflecting on Generics," June 2007). The VB single-instance framework uses remoting with a secured channel, so an unsecured channel with the same priority and port fails. In that case a simple solution was to secure the remoting channel. But recently I've been alerted to a more insidious problem with the VB single-instance framework. Matthew Kleinwaks discovered that some firewalls cause an application to crash on startup if you use the single-instance framework.
The cause of the problem: Some firewalls cause an exception to be thrown in the net socket when blocking a TCP/IP port. Not all firewalls do this, but some do, including Comodo. The problem is exacerbated by the VB framework, which tries to open the socket before adding application-level unhandled exception handlers. But even firewalls that don't cause an exception to be thrown can block the first socket listening call, thereby breaking the single-instance framework. For example, Microsoft's OneCare does this. It swallows the first call when prompting you to allow or deny. Even if a user clicks on the allow button, that call is lost.
These are the pitfalls of remoting. You need to write code that handles firewalls, which can block your calls and even cause exceptions to be thrown. In the case of a single-instance application, the port used is 127.0.0.1, the local port. As such, it shouldn't even concern the firewall. But the reality is that it does. Even if your application doesn't need to talk over the Internet, opening any port can give people the impression that your application might be calling home.
With all of this in mind, it should be clear that remoting is not the best answer for a single-instance app framework. That's why I recommend you do not to use the VB 2005 framework's single-instance option. So, how can apps communicate locally, be it through command-line arguments or other inter-process communications? One way is to use the new IO pipe classes in .NET 3.5.
Communicate Through Pipes
Pipes give you the ability to conduct inter-process communication (IPC) on Windows platforms. A pipe is a communication conduit between a pipe server and a pipe client that utilizes shared memory. The communication can be one way or duplex (in/out), which enables a variety of communication possibilities, from signaling to conversations between processes. Named pipes are available on Windows NT, 2000, XP, 2003, Vista, and 2008, but are not available on Windows 95, 98, or ME.
The .NET 3.5 Framework wraps the Windows API for pipes and exposes them as streams. You use a NamedPipeServerStream and a NamedPipeClentStream for the two pipe end points.
To use a NamedPipeServerStream, you call its constructor, then call WaitForConnection. Once a client establishes a connection, the thread continues, and you can read or write to the stream using a StreamReader or StreamWriter:
Using ps As New NamedPipeServerStream("test")
Using sr As New StreamReader(ps)
Dim message As String
message = sr.ReadLine
Loop While message IsNot Nothing
For the NamedPipeClientStream end of the pipe, you construct the pipe stream, connect to it with the Connect method, and then read or write to the stream.
Using pipeStream As New NamedPipeClientStream("test")
Using sw As New StreamWriter(pipeStream)
sw.AutoFlush = True
These two blocks of code let you create a communication pipe between different processes (or even the same one) easily.
You're now ready to make an application utilize single-instancing with named pipes. You wrap the call to the NamedPipeServerStream in a try/catch and catch the exception that occurs if the pipe already exists. If the pipe exists, it means the instance you called is the second instance in which case you set up the pipe client and send a message back to the pipe server including the command-line arguments.
Messages can be strings or bytes, depending on how you specify the PipeTransmissionMode in an overload of the pipes' constructors. By default, the transmission mode is PipeTransmissionMode.Message, which basically means "as String."
For the sake of simplicity, I decided to stick to string messaging rather than bytes, making XML the most appropriate choice for this task. You have a couple more options to choose from when creating the XML from an array of strings. You can use binary or XML serialization to pass the array of strings over the pipe. You can create an XML serializer that generates an assembly at runtime to handle the serialization. Or, you can use the new XML features in VB9, mixing these in with just a splash of LINQ. I chose the latter approach:
Dim innerItems = From item In args _
Select <item><%= item %></item>
Dim items = <items><%= innerItems.Skip(1) %></items>
The preceding code creates an XElement with this form:
<item>command line argument 1</item>
<item>command line argument 2</item>
From here, handling the messaging is as simple as calling ToString on the XElement items.
Skip the First Element
You might wonder what the Skip(1) call on the innerItems is for. It skips the first <item>...</item> element. I added this because the first command-line argument returned from the System.Environment class is the name of the application, which I don't want in the list. Skip is an extension method.
If you haven't seen the <%=...%> syntax before, it behaves similarly to old-style ASP code, where you place the code inline in an HTML document by surrounding the code parts with <%= and %>.
Deserializing the XML is also an easy task with VB9:
Dim el As XElement = XElement.Parse(xmlString)
Dim args = (From item In el.<item> Select item.Value) _
In fact, creating and parsing XML is so easy in VB9 that it would be a crime not to use it!
You should keep one fact in mind about the NamedPipeServerStream.WaitForConnection call. It's a blocking call until a client connects; typically, you need to run that call on another thread. In a Windows.Forms application, you'll need to deal with threading issues if you want to update the UI from the pipe-server thread. One way to deal with threading issues is to write a wrapper class for your pipe server. In your wrapper class' constructor you create and store a System.ComponentModel.AsyncOperation reference. Next, from your application's main thread call this constructor. You can call the constructor implicitly from the main thread by declaring it as a field in your MyApplicaton class using As New:
WithEvents singleInst As New SingleInstanceListener
The AsyncOperation class stores a context internally. The context ensures that any delegate you call using the Post method is on the same context as when the AsyncOperation was created. This makes it easy to call a delegate from the pipe stream server thread and have the event be raised on the application's main UI thread.
The sample application includes a class called SingleInstanceListener that wraps all the required bits to make this easy to use. The class exposes only a constructor and a StartUpNextInstance event. Begin by adding the class to your application. Next, declare an instance of the SingleInstanceListener class WithEvents in your MyApplication class, where you would normally handle the StartUpNextInstanceevent. Finally, handle its StartUpNextInstance event as the situation dictates.
This is but one example of what you can do with pipes. The managed wrappers make it relatively easy to setup and use these IPC classes, and VB's integrated XML features make working with the XML sheer joy.
Bill McCarthy is an independent consultant based in Australia and is one of the foremost .NET language experts specializing in Visual Basic. He has been a Microsoft MVP for VB for the last nine years and sat in on internal development reviews with the Visual Basic team for the last five years where he helped to steer the language’s future direction. These days he writes his thoughts about language direction on his blog at http://msmvps.com/bill.