Click here to Skip to main content
15,041,266 members
Articles / Desktop Programming / Windows Forms
Article
Posted 14 Jun 2017

Tagged as

Stats

21.5K views
394 downloads
34 bookmarked

Opening a Specific File Format with a Single Instance of an Application

Rate me:
Please Sign up or sign in to vote.
4.95/5 (20 votes)
22 Jun 2017CPOL5 min read
Associating and opening a specific file format with a single instance of a WinForms application
In this article, you will learn to associate an application with a file extension and when a file with this extension is double clicked on Windows Explorer, automatically run the application or use the already running instance and open the file in it.

Introduction

This article and the sample code demonstrates how to associate an application with a file extension (e.g., .mytxt) and when a file with this extension is double clicked on Windows Explorer, automatically run the application or use the already running instance and open the file in it.

Steps required to accomplish this are:

  1. Associating a file extension with your application and make windows trigger your application when a file with this format is double clicked.
  2. Detecting if any other instance of your application is already running.
    • If there is no other instance (this is the only instance running), then open the file in this instance of your application.
    • If there is one instance already running, then make that already running application open this file and exit the new one.

Associating Your Application With a File Extension

Windows reads file extension associations from "HKEY_CLASSES_ROOT" registry. You need to add a few keys to this registry in order to tell Windows that it should use your application by default, for opening this type of files. So when user double clicks a file with this extension, Windows automatically triggers your application and passes the file path as the first argument to your application. Below is the helper class that performs file association.

C#
static class FileAssociationHelper
{
    public static void AssociateFileExtension
    (string fileExtension, string name, string description, string appPath)
    {
        //Create a key with specified file extension
        RegistryKey _extensionKey = Registry.ClassesRoot.CreateSubKey(fileExtension);
        _extensionKey.SetValue("", name);

        //Create main key for the specified file format
        RegistryKey _formatNameKey = Registry.ClassesRoot.CreateSubKey(name);
        _formatNameKey.SetValue("", description);
        _formatNameKey.CreateSubKey("DefaultIcon").SetValue("", "\"" + appPath + "\",0");

        //Create the 'Open' action under 'Shell' key
        RegistryKey _shellActionsKey = _formatNameKey.CreateSubKey("Shell");
        _shellActionsKey.CreateSubKey("open").CreateSubKey("command").SetValue
                                     ("", "\"" + appPath + "\" \"%1\"");

        _extensionKey.Close();
        _formatNameKey.Close();
        _shellActionsKey.Close();

        // Update Windows Explorer windows for this new file association
        SHChangeNotify(0x08000000, 0x0000, IntPtr.Zero, IntPtr.Zero);
    }

    [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern void SHChangeNotify
            (uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2);
}
  • fileExtension is the extension you want to associate (e.g., ".mytxt")
  • name is the name of your format (e.g., "MyTextFile")
  • description is the description for your format
  • appPath is the full path of your application that will be triggered by Windows

Sample usage for this method is:

C#
FileAssociationHelper.AssociateFileExtension
(".mytxt", "MyTxtFile", "Simple text file", Application.ExecutablePath);

Detecting the Running Instances of Your Application

Detecting if any instance of your application is running can be done in many ways. The sample code provides two solutions for instance detection.

The first and simplest one is getting the list of all the processes that has the same name with yours. If the number of found processes is greater than 1, that means there is at least one more instance that is running. Sample code is as follows:

C#
public static bool CheckInstancesFromRunningProcesses()
{
    //Get current process info
    Process _currentProcess = Process.GetCurrentProcess();
    //Get all the processes with this name
    Process[] _allProcesses = Process.GetProcessesByName(_currentProcess.ProcessName);

    //If there is more than 1 process with this name
    if (_allProcesses.Length > 1)//There is another instance
        return true;

    return false;
}

The second way of detecting other instances is using a kernel mode synchronization object. In this example, I used named Mutex. Sample code tries to acquire the named Mutex whose name is "OpenWithSingleInstance". If this is the only instance, then the lock will be acquired, otherwise not.

C#
public static bool CheckInstancesUsingMutex()
{
    Mutex _appMutex = new Mutex(false, "OpenWithSingleInstance");
    if (!_appMutex.WaitOne(1000))
        return true;       //There is another instance

    return false;
}

Opening the File in the Already Running Instance

Because we want to run only a single instance of our application and open all the files in this instance, we need to communicate with the already running instance and make it open the file. Communication can be done in many ways, TCP connection, named pipe, sending a window message...

In this example, I used window messages for passing the file name to the running instance. Windows has a special window message code and data structure for passing custom data to another window via window messages; WM_COPYDATA and COPYDATASTRUCT.

Definition SendMessage Win32 API method, WM_COPYDATA message code and COPYDATASTRUCT structure is as follows:

C#
const int WM_COPYDATA = 0x004A;

[DllImport("user32", EntryPoint = "SendMessageA")]
private static extern int SendMessage(IntPtr Hwnd, int wMsg, IntPtr wParam, IntPtr lParam);

//.....

[StructLayout(LayoutKind.Sequential)]
struct COPYDATASTRUCT
{
    public IntPtr dwData;    // Any value the sender chooses. Perhaps its main window handle?
    public int cbData;       // The count of bytes in the message.
    public IntPtr lpData;    // The address of the message.
}

And the SendDataMessage helper method that is used for sending string messages to the main window of a process is:

C#
public static void SendDataMessage(Process targetProcess, string msg)
{
    //Copy the string message to a global memory area in unicode format
    IntPtr _stringMessageBuffer = Marshal.StringToHGlobalUni(msg);

    //Prepare copy data structure
    COPYDATASTRUCT _copyData = new COPYDATASTRUCT();
    _copyData.dwData = IntPtr.Zero;
    _copyData.lpData = _stringMessageBuffer;
    _copyData.cbData = msg.Length * 2;   //Number of bytes required for marshalling 
                                         //this string as a series of unicode characters
    IntPtr _copyDataBuff = IntPtrAlloc(_copyData);

    //Send message to the other process
    SendMessage(targetProcess.MainWindowHandle, WM_COPYDATA, IntPtr.Zero, _copyDataBuff);

    Marshal.FreeHGlobal(_copyDataBuff);
    Marshal.FreeHGlobal(_stringMessageBuffer);
}

The next step is sending the full path of the file to the running instance in the entry point of the application, before showing any user interface to the user, then exiting the new instance and let the running instance open the file.

C#
static class Program
{    
    [STAThread]
    static void Main(params string[] args)
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        if (SingleInstanceHelper.CheckInstancesUsingMutex() && args.Length > 0)
        {
            Process _otherInstance = SingleInstanceHelper.GetAlreadyRunningInstance();

            MessageHelper.SendDataMessage(_otherInstance, args[0]);
            return;//Exit this instance and let the existing one open the file
        }

        Application.Run(new Form1(args.Length > 0 ? args[0] : null));
    }
}

The final step is of course watching for the file message in the running instance and opening the requested file. You can do it by overriding the WndProc method of the main window whose type is System.Windows.Forms.Form.

C#
protected override void WndProc(ref Message m)
{
    if (m.Msg == MessageHelper.WM_COPYDATA)
    {
        //Reconstruct copy data structure
        COPYDATASTRUCT _dataStruct = Marshal.PtrToStructure<COPYDATASTRUCT>(m.LParam);

        //Get the messag (file name we sent from the other instance)
        string _strMsg = Marshal.PtrToStringUni(_dataStruct.lpData, _dataStruct.cbData / 2);

        openFileInTabControl(_strMsg);
    }

    base.WndProc(ref m);
}

The openFileInTabControl method creates a new TabPage for this new file, then reads all the text content of the file, puts it into a TextBox and places this TextBox to the newly created TabPage.

C#
private void openFileInTabControl(string filePath)
{
    //Read the file contents and load them into a new tab
    string _strFileData = File.ReadAllText(filePath);

    TabPage _tabPage = new TabPage(Path.GetFileNameWithoutExtension(filePath));
    TextBox _textBox = new TextBox();
    _textBox.Multiline = true;
    _textBox.Dock = DockStyle.Fill;
    _textBox.Text = _strFileData;
    _tabPage.Controls.Add(_textBox);

    tabControl1.TabPages.Add(_tabPage);
}

After loading the file, you may also want to restore and activate the application if it is minimized or behind the other windows.

Sample Application

Sample application consists of a single Form and three helper classes for file association, detecting running instances and sending a string message to the main window of a process.

Image 1

"Register File Extension" button associates ".mytxt" file extension with this sample application. After associating the file extension, close the sample application and double click the files "Test_file1.mytxt" and then "Test_file2.mytxt" which are in the sample codes folder. The two files should be opened in the single instance of the application and you should see a similar window as above.

"Unregister File Extension" button disassociates the file format by removing the registry keys for this format.

Alternative Solution for Single Instance Requirement

Microsoft.Net BCL has a helper class for ensuring only one instance of an application can be run at the same time. Thanks to Ralph Lechterbeck for informing me about the existing of this library.

Unfortunately, the helper class is in the library named "Microsoft.VisualBasic.dll". Although the name is misleading, this library can be safely referenced and used in C# projects. I could not find any reasonable answer why Microsoft guys put all these useful utilities into a library whose name contains "VisualBasic".

Here is the alternative sample code:

C#
namespace OpenWithSingleInstance
{
    static class Program
    {
        [STAThread]
        static void Main(params string[] args)
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            
            SingleInstanceController controller = new SingleInstanceController();
            controller.Run(args);
        }
    }

    public class SingleInstanceController : WindowsFormsApplicationBase
    {
        public SingleInstanceController()
        {
            //Run only single instance of this app
            IsSingleInstance = true;

            //Register to be informed for the additional instances
            StartupNextInstance += this_StartupNextInstance;
        }

        //This is called for the other instances and let you take appropriate action
        void this_StartupNextInstance(object sender, StartupNextInstanceEventArgs e)
        {
            Form1 form = MainForm as Form1; //Our main window form class

            //Pass the first command line argument (if exists) which is actually the file path
            form.OpenFileInTabControl(e.CommandLine.Count > 0 ? e.CommandLine[0] : null);
        }

        /// <summary>
        /// This method is called for the first instance
        /// </summary>
        protected override void OnCreateMainForm()
        {            
            MainForm = new Form1(this.CommandLineArgs.Count > 0 ? 
                                 this.CommandLineArgs[0] : null);
        }
    }
}

Although the usage is simple, the code under this functionality is a bit complicated. WindowsFormsApplicationBase class uses "memory mapped files", "tcp communication" and "wait handles" for ensuring only a single instance can run and passing the command line arguments to the running instance.

History

  • 14th June, 2017
    • Initial version
  • 16th June, 2017
    • "Unregister File Extension" button is added
    • Sample source code updated
  • 22nd June, 2017
    • "Alternative solution for single instance requirement" part is added, special thanks to Ralph Lechterbeck
    • Manually calculated the size of the message string

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mustafa Kok
Software Developer (Senior) Freelance
Sweden Sweden
Experienced senior C# developer, sometimes codes in Java and C++ also. He designs and codes desktop applications (WinForms, WPF), windows services and Web APIs (WCF, ASP.Net MVC). He is currently improving his ASP.Net MVC and JavaScript capabilities. He acts as an architect, coder and trainer.
He is mostly experienced in Media Asset Management (MAM) applications, broadcasting sector and medical applications.

LinkedIn: www.linkedin.com/mustafa-kok/
GitHub: github.com/nthdeveloper
StackOverflow: stackoverflow.com/users/1844220/nthdeveloper

Comments and Discussions

 
GeneralMy vote of 5 Pin
InvisibleMedia7-Jul-17 2:51
professionalInvisibleMedia7-Jul-17 2:51 
SuggestionWindows 10 UIPI WM_COPYDATA Message Pin
svansickle23-Jun-17 10:42
Membersvansickle23-Jun-17 10:42 
On Windows 10 you may need to utilize the ChangeWindowMessageFilterEx function to enable the WM_COPYDATA message. I make extensive use of WM_COPYDATA for interprocess communications and the message was suddenly blocked due to a Win 10 security update. To allow the message I had to utilize that function. This call is necessary if the integrity level of the calling process is lower than the level of the receiving process; that may not be applicable in your scenario but it is something to be aware of when you are using WM_COPYDATA.
QuestionAlternative solution Pin
Ralph Lechterbeck18-Jun-17 23:54
MemberRalph Lechterbeck18-Jun-17 23:54 
AnswerRe: Alternative solution Pin
Mustafa Kok21-Jun-17 22:18
professionalMustafa Kok21-Jun-17 22:18 
GeneralRe: Alternative solution Pin
Ralph Lechterbeck23-Jun-17 4:46
MemberRalph Lechterbeck23-Jun-17 4:46 
QuestionCan't you accomplish the same thing when you use ClickOnce publishing? Pin
asiwel15-Jun-17 7:15
professionalasiwel15-Jun-17 7:15 
AnswerRe: Can't you accomplish the same thing when you use ClickOnce publishing? Pin
Mustafa Kok15-Jun-17 22:10
professionalMustafa Kok15-Jun-17 22:10 
GeneralRe: Can't you accomplish the same thing when you use ClickOnce publishing? Pin
asiwel16-Jun-17 6:41
professionalasiwel16-Jun-17 6:41 
AnswerRe: Can't you accomplish the same thing when you use ClickOnce publishing? Pin
  Forogar  16-Jun-17 3:02
professional  Forogar  16-Jun-17 3:02 
QuestionGood work, but I have a suggestion... Pin
  Forogar  14-Jun-17 6:53
professional  Forogar  14-Jun-17 6:53 
AnswerRe: Good work, but I have a suggestion... Pin
Mustafa Kok15-Jun-17 5:54
professionalMustafa Kok15-Jun-17 5:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.