Wednesday, September 10, 2008

Using a SingleInstance-Application with C# and ClickOnce

As we all know, since VS2008 SP1 there is a very nice way to register File-Extensions to your ClickOnce-Application. In former Versions you had to change the ApplicationManifest and rebuild the Package: now its just a Entry in the DeploymentProperties of the Application.

Now for the funny Part: imagine you have a ClickOnce-Application and you want to use the SingleInstance-Property (provided by VB.NET).

First you have to extend the WindowsFormsApplicationBase from VB and create your own. In this ApplicationBase you can now handle the StartupEvents and handle the SingleInstance.

private class App : WindowsFormsApplicationBase
{
    #region Delegates

    public delegate void ProcessParametersDelegate(object sender, string[] args);

    #endregion

    public bool IsSingle
    {
        set { IsSingleInstance = value; }
    }

    public App()
    {
        IsSingleInstance = true;
        EnableVisualStyles = true;

        ShutdownStyle = ShutdownMode.AfterMainFormCloses;

        StartupNextInstance += SIApp_StartupNextInstance;
    }

    /// 
    /// We are responsible for creating the application's main form in this override.
    /// 
    protected override void OnCreateMainForm()
    {
        // Create an instance of the main form and set it in the application; 
        // but don't try to run it.
        MainForm = new FrmMain();

        // We want to pass along the command-line arguments to this first instance

        // Allocate room in our string array
        int length = 0;
        ActivationArguments arguments = AppDomain.CurrentDomain.SetupInformation.ActivationArguments;
        if (arguments != null && arguments.ActivationData != null)
            length = arguments.ActivationData.Length;
        ((FrmMain)MainForm).Args = new string[length];

        // And copy the arguments over to our form
        if (arguments != null && arguments.ActivationData != null)
        {
            arguments.ActivationData.CopyTo(((FrmMain)MainForm).Args, 0);
        }
        else ((FrmMain)MainForm).Args = new string[0];
    }

    /// 
    /// This is called for additional instances. The application model will call this 
    /// function, and terminate the additional instance when this returns.
    /// 

    protected void SIApp_StartupNextInstance(object sender,
    StartupNextInstanceEventArgs eventArgs)
    {
        // Copy the arguments to a string array
        string[] args = new string[eventArgs.CommandLine.Count];
        eventArgs.CommandLine.CopyTo(args, 0);

        // Create an argument array for the Invoke method
        object[] parameters = new object[2];
        parameters[0] = MainForm;
        parameters[1] = args;

        // Need to use invoke to b/c this is being called from another thread.
        MainForm.Invoke(new ProcessParametersDelegate(
        ((FrmMain)MainForm).ProcessParameters),
        parameters);
    }
}

The next Step to do is handle the MainApplicationEntryPoint. Here you use the just created Class and pass the Parameters from the ApplicationStartup (for ClickOnce-Apps, this Parameters are hold in the AppDomain.CurrentDomain.SetupInformation.ActivationArguments.ActivationData)

Now we need to handle this Parameter in an (possible) existing Instance of the Application.

The Leak of this Solution is that it's not [easy/possible] to debug this stuff from the ClickOnceFramework, but you can write some Tests with common CommandlineParameters to see if this works.

1 comment:

  1. Actually it should be much more easier in VB.NET... You won't need the workaround I've made there for C#.

    ReplyDelete