Friday, February 26, 2010

Wpf Single Instance Application

Sometimes it makes sense to keep the default behaviour of a .Net application that allows you to start multiple instances of the app.  For example: multiple instances of Visual Studio to house multiple solutions.  But other times it doesn't make sense to allow the user to open a second instance of the same application. Like Microsoft Word for example. It that case you want it to open the document but not open another instance of winword.exe.

Interestingly this is a simple issue to fix in VB.Net Winforms apps, but not so straight forward in WPF using C#.  Essentially there are two main strategies:
1) Use an operating system mutex to prevent a second instance from opening. (But the problem is how do you pass commandline arguments to the already open first instance).
2) Use the VisualBasic library to take advantage of some nice functionality that takes care of pretty much everything.

Option 2, still uses a normal WPF Visual Studio Template, but starts the app using a Sub Main entry as opposed to App.xml.
Here's the start up class:

    public class Startup {
        [STAThread]
        public static void Main(string[] args)
        {            
            var wrapper = new SingleInstanceApplicationWrapper();
            wrapper.Run(args);           
        }
    }
Here's the wrapper that references and uses the Visual Basic Library to perform the single instance check:

    public class SingleInstanceApplicationWrapper : Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase {        
        public SingleInstanceApplicationWrapper() {
            // Enable single-instance mode.
            this.IsSingleInstance = true;
        }

        private WpfApp app;

        protected override bool OnStartup(Microsoft.VisualBasic.ApplicationServices.StartupEventArgs e) {            
            this.app = new WpfApp();
            this.app.Run();

            return false;
        }

        // Direct multiple instances
        protected override void OnStartupNextInstance(Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs e) {
            if (e.CommandLine.Count > 0) {                
                app.ShowDocument(e.CommandLine[0]);
            }
        }
    }

And finally here's the Custom Wpf App class that loads the Wpf main window and can handle the second instance command-lines:

    public class WpfApp : System.Windows.Application {
        protected override void OnStartup(System.Windows.StartupEventArgs e) {
            base.OnStartup(e);
            
            // Load the main window.
            this.Documents = new ObservableCollection<DocumentReference>();
            this.MainWindow = list;
            list.Show();

            // Load the document that was specified as an argument.
            if (e.Args.Length > 0) ShowDocument(e.Args[0]);
        }

        public ObservableCollection<DocumentReference> Documents { get; set; }        

        public void ShowDocument(string filename) {
            try {                
                Document doc = new Document();
                DocumentReference docRef = new DocumentReference(doc, filename);
                doc.LoadFile(docRef);                
                doc.Owner = this.MainWindow;
                doc.Show();
                doc.Activate();
                Documents.Add(docRef);
            } catch {
                MessageBox.Show("Could not load document.");
            }
        }
    }   

The semantics of loading documents is not important, what is important is the user's attempt to load a second instance of the app and with a command-line (in this case to load a document) was intercepted by the already loaded first instance and displayed in that. The same behaviour as Microsoft Word.



For option 1, this is possible using the Sytem.Threading.Mutex class.
Here is a basic example:

var createdNew = true;
using (var mutex = new Mutex(true, "ApplicationNameOrSomeText", out createdNew)) {
    if (createdNew) {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new MainForm());
    }
}

The only missing nicety would be to give focus to the already loaded app.  A good example can be found on Fredrik Kelseths blog here.

In my opinion using the Visual Basic library feature is the tidiest way to achieve a good level of functionality, at least until WPF supports this natively.  Alternatives could be hosting a WCF service in process to receive the second instance command-line, but requires a fair amount of work to get this to sing.

No comments:

Post a Comment