2008-09-30

Single instance application - revisited

Not so long ago I posted a solution to having a single-instance application. Rather than just preventing secondary instances from running the requirement was to have the 2nd instance pass its runtime parameters onto the 1st instance so that it can process them. My solution used remoting on the local machine. This appeared to work very well until recently when I needed an OpenFileDialog. Attempting to show the dialog resulted in an error about COM thread apartments. So, it wasn't THE solution.

After a bit of research I decided to use named pipes instead. This meant I had to upgrade my app from .NET 2 to 3.5, but I think it is worth it. To implement the feature in an app you need to do 2 things. First you need to realize the interface ISingleInstanceApplicationMainForm on your app's main form in order to accept command line arguments from any subsequently started instances. Next you need to change your Program.Main method like so:

[STAThread]
static void Main(string[] args)
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  new SingleInstanceApplication<MainForm>("CompanyName.ApplicationName", args);
}



The source code for SingeInstanceApplication is an adaptation of some code I read here; and here it is:


public interface ISingleInstanceApplicationMainForm
{
  void AcceptCommandLineArguments(string[] args);
}

public class SingleInstanceApplication<TForm> : IDisposable
  where TForm : ISingleInstanceApplicationMainForm
{
  Mutex Mutex;
  bool IsFirstInstance;
  string ApplicationUniqueID;
  protected TForm MainForm { get; private set; }

  public SingleInstanceApplication(string applicationUniqueID, string[] args)
  {
    ApplicationUniqueID = applicationUniqueID;
    Mutex = new Mutex(true, ApplicationUniqueID, out IsFirstInstance);
    if (IsFirstInstance)
    {
      MainForm = (TForm)Activator.CreateInstance(typeof(TForm));
      (MainForm as ISingleInstanceApplicationMainForm).AcceptCommandLineArguments(args);
      ThreadPool.QueueUserWorkItem(new WaitCallback(ListenForArguments));
      Application.Run((Form)(object)MainForm);
    }
    else
      PassArgumentsToFirstInstance(args);
  }

  private void ListenForArguments(Object state)
  {
    try
    {
      using (var server = new NamedPipeServerStream(ApplicationUniqueID))
      {
        using (var reader = new StreamReader(server))
        {
          server.WaitForConnection();

          string[] args = reader.ReadLine().Split(new char[] {'\t'}, StringSplitOptions.RemoveEmptyEntries);
          ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveArgumentsFromNewInstance), args);
        }//using reader
      }//using server
    }
    catch (IOException) { }
    finally
    {
      ListenForArguments(null);
    }
  }

  private void ReceiveArgumentsFromNewInstance(Object state)
  {
    string[] args = (string[])state;
    (MainForm as Form).Invoke(
      new ThreadStart(
        delegate()
        {
          MainForm.AcceptCommandLineArguments(args);
        }
        )
      );
  }

  private void PassArgumentsToFirstInstance(string[] args)
  {
    var builder = new StringBuilder();
    foreach (var arg in args)
      builder.AppendFormat("{0}\t", arg);

    try
    {
      using (var client = new NamedPipeClientStream(ApplicationUniqueID.ToString()))
      {
        using (var writer = new StreamWriter(client))
        {
          client.Connect(500); // 0.5 seconds timeout
          writer.WriteLine(builder.ToString());
        }//using writer
      }//using client
    }
    catch (TimeoutException) { }
    catch (IOException) { }
  }




  void IDisposable.Dispose()
  {
    if (IsFirstInstance && Mutex != null)
      Mutex.ReleaseMutex();
  }

}

No comments: