Since Service Pack 1 of Visual Studio 2008, setting file associations has never been easier. The publish options dialog has four pages; once of which is the “File Associations” page. After one or more entries has been added, and the app published, the ClickOnce app will now be launched whenever an associated file is opened from explorer.

This is fine, unless you only want one instance of your app to be running at any one time. Each time an associated file is opened, a new instance of the ClickOnce app will be launched. If your single-instance app is open and you try to open an associated file, the reference to the file will be lost when the new instance detects that the original instance is running and terminates. Grrr.

However, you can get round this. After detecting that there is another instance of your app running, simply pass the file reference to the original instance before closing.

Note the callback is made using an IpcChannel rather than a TcpChannel. If a TcpChannel is specified, Vista and Windows 7 will require permission from the user to allow communication through the firewall.

Here is the code:

            // Single instance checked
            bool IsFirstInstance;
            Mutex theMutex =
                new Mutex(false, "Local\\" + Application.ProductName, out IsFirstInstance);
            string[] theActivationData =
                AppDomain.CurrentDomain.SetupInformation.ActivationArguments.ActivationData;

            if (IsFirstInstance)
            {
                IpcChannel theChannel =
                    new IpcChannel(Application.ProductName);

                try
                {
                    ChannelServices.RegisterChannel(theChannel, false);
                    RemotingServices.Marshal(this._MainView, "MainView");

                    this.MainView_Show();

                    this.ConsumeLaunchParameters(theActivationData);

                    Application.Run(this._AppContext);
                }
                catch (SocketException) { }
                finally
                {
                    ChannelServices.UnregisterChannel(theChannel);
                }
            }
            else
            {
                if (null != theActivationData &&
                    theActivationData.Length > 0)
                {
                    try
                    {
                        MainView theOriginalMainView =
                            (MainView)RemotingServices.Connect(
                                typeof(MainView),
                                "ipc://" + Application.ProductName + "/MainView");

                        theOriginalMainView.ConsumeArguments(theActivationData);
                    }
                    catch (SocketException) { }
                }
            }

            theMutex.Close();
            Application.Exit();

Since my app uses a centralised controller system, I had to add the following code to the MainView form to pass the arguments back to the controller:

        public void ConsumeArguments(
            string[] theArguments)
        {
            // Note that it is not allowed for non-UI thread to access
            // controls on the form, instead, we should use the Invoke method of the form
            // to execute a delegate on the UI thread that own's the control's underlying
            // windows handle.
            if (this.InvokeRequired)
            {
                this.Invoke(
                    new MethodCallback(ConsumeArguments),
                    new object[]
                        { theArguments });

                return;
            }
            else
            {
                if (null != this.ArgumentsConsumed)
                {
                    this.ArgumentsConsumed(theArguments);
                }
            }
        }

        public event MethodCallback ArgumentsConsumed;

        public delegate void MethodCallback(string[] args);

Leave a Reply

(required)

(required)