Click here to Skip to main content
15,881,938 members
Articles / Programming Languages / C#

Double Clicking a File in Explorer and Adding It to Your App while it's Running

Rate me:
Please Sign up or sign in to vote.
4.76/5 (5 votes)
1 Oct 2019CPOL7 min read 14.8K   235   9   15
How to double click a file in explorer and add it to your app while it's running

Introduction

When you double click a related file in Explorer, your app opens - but what if you want to open another file in the same instance of the app? If you double click another file in explorer, Windows will open a second instance ...

So you need to talk to the first instance and tell it the new file and let it handle it.

That's not too complex - but does involve Sockets - so I created the DynamicArgument class to do all the heavy lifting.

Background

Just an aside: If you want to open a specific file extension with your app in Windows 10 and the app doesn't self register to open them, then you can do it from within Explorer. I find this handy during dev, since I don't install my apps (when the registration of file extensions is normally done) on my dev machine as I want to run the debug version most of the time.

To do it:

Open Windows Explorer, and navigate to an example of your input file: (blahblah.myfiletype for example).

Right click the file, and select Properties:

Click "Change" and "More apps":

Scroll to the bottom of the list, make sure "Always use this app to open ..." is checked, and select "Look for another app on this PC":

Browse to the EXE file for your app, and click "OK".

Now when you double click, Windows will open the right app for you.

Using the Code

When you double click a file, Windows opens the associated app, and passes the full file path as a command line argument. This works fine, and has done for many, many versions of Windows - right back to V1.0, I believe.

And it's easy to get that argument - there are two ways.

For a Console app, they are passed as a string array to the Main method.

For a GUI app, they are available as an array of strings via the Environment.GetCommandLineArgs method.

The problem is ... once your app is running, that collection can't be added to, or changed in any way - Windows can't add "new arguments" to the array, and has no way to "alert you" that a new one is available.

So ... what you have to do is:

  1. Check if an instance of your app is running already.
  2. If it isn't, then handle the argument yourself, and prepare yourself in case the user tries to open another.
  3. If it is, pass the argument to the other instance, and close yourself down.

And that means Sockets and TCP stuff. Yeuch.

So, I created a DynamicArgument class that handles it all for you, and provides an event to tell you there is an argument available. It processes the arguments and raises an event for each in turn. If it's the first instance, it creates a thread which creates a Listener, and waits for information from other instances. When it gets a new argument, it raises an event for it.

To use it is pretty simple.

Add to your main form Load event:

C#
private void FrmMain_Load(object sender, FormClosingEventArgs e)
    {
    DynamicArgument da = DynamicArgument.Create();
    da.ArgumentReceived += Application_ArgumentReceived;
    bool result = da.Start();
    if (!result) Close();
    }

DynamicArgument is a Singleton class, because it would get difficult and messy if there were two Listeners trying to process arguments - so you have to Create it rather than use the new constructor.

Add your event handler, and Start the system. If this was the only instance running it returns true - if not, then probably you want to close the form, which will close the app - I'd do it for you, but this way, you get the option to clean up after yourself if you need to, which Application.Exit doesn't let you do.

Then just handle your event and do what you need to with the arguments:

C#
private void Application_ArgumentReceived(object sender, DynamicArgument.DynamicArgmentEventArgs e)
    {
    tbData.Text += "\r\n" + e.Argument;
    }

How It All Works

There are three files in the system, two of them are "support files": ByteArrayBuilder.cs and IByteArrayBuildable.cs.

These two are part of the "packaging the data up" system and are described in a different Tip: ByteArrayBuilder - a StringBuilder for Bytes - yes, I could have used JSON or XML, but I prefer JSON and forcing you to add a reference and a large nuget package to your app didn't really appeal. Free free to change it if JSON or XML is a better fit for you.

The other file is the class that does the work: DynamicArguments.cs

When you construct the one-and-only instance of the class, it reads the arguments and stores them in a queue.

You then call Start when you are ready to process them, which does one of two things.

  1. If this is the only instance, it starts a Listener thread, and generates the Events to tell your app there is data.
  2. Otherwise, it opens a Client and sends the other instance all its arguments.

Listener Thread

OK, there are fine details going on here ... so either skip this section and assume it "works by magic" or read on.

A Listener Socket is a blocking object: it stops your thread doing anything else until a client makes contact. That's fine, and exactly what you would expect, except it's also a pain since you're on the UI thread and that stops your whole system doing anything.

So you have to put the Listener onto a different thread, which brings up three problems:

  1. You have to be sure to Invoke back onto the UI thread to raise our "argument available" Event or the user has to do that - which means most won't bother and will complain when they get a cross-thread exception.
  2. It's a blocking object - so the new thread will be blocked, which means it can't be stopped "nicely" and has to be terminated properly by killing it or the app won't end and the domain won't get unloaded.
  3. If the main app crashes, or the coder is not paying attention, the DynamicArgument object won't get closed properly, and that means the Listener thread will keep running.

The way to avoid these is to use a BackgroundWorker - it automatically does all the Invoke work for you when it raises the ProgressChanged event, and it automatically creates the new thread as a Background thread, rather than Foreground - and that's an important difference.

Background threads are "don't care" threads: the system will kill them automatically when the app closes, unlike Foreground threads which run until they themselves exit. Yes, we could use a Thread or Task object directly, and set it as a Background thread and also handle the Invoke - but there is a class that does both for me, so I'll simplify my code and use that!

So ... you call Start on the DynamicArgument object and it creates a background thread and then passes the arguments as Events:

C#
BackgroundWorker listener = new BackgroundWorker();
listener.DoWork += Listener_DoWork;
listener.WorkerReportsProgress = true;
listener.ProgressChanged += Listener_NewArguments;
listener.RunWorkerAsync();
PassUpAllArguments();
return true;

The BackgroundWorker sets up the Socket and waits for instructions:

C#
IPAddress ipAddress = GetMyIP();
while (true)
    {
    TcpListener listener = new TcpListener(ipAddress, ListenPort);
    listener.Start();
    while (true)
        {
        try
            {
            using (Socket s = listener.AcceptSocket())
                {
                ...
                }
            }
        catch (Exception ex)
            {
            Debug.WriteLine(ex.ToString());
            }
        }
    }
}

When it gets a connection, it reads it, converts it to separate arguments and enqueues them before using ReportProgress to pass them to the external app:

C#
int length = s.Available;
if (length > 0)
    {
    byte[] buffer = new byte[length];
    s.Receive(buffer);
    // Simple data format:
    //    Command Code (byte)
    //    Number of arguments (int32)
    //    Arguments (string)
    ByteArrayBuilder bab = new ByteArrayBuilder(buffer);
    byte cc = bab.GetByte();
    if (cc == CC_Argument)
        {
        int argCount = bab.GetInt();
        for (int i = 0; i < argCount; i++)
            {
            string arg = bab.GetString();
            Arguments.Enqueue(arg);
            }
        work.ReportProgress(0, 0);
        }
    }

Since the ReportProgress handled is already Invoked back to the UI thread, the arguments can just be enqueued, and passed up via a series of Events:

C#
private void Listener_NewArguments(object sender, ProgressChangedEventArgs e)
    {
    if (e.UserState is string arg)
        {
        Arguments.Enqueue(arg);
        }
    PassUpAllArguments();
    }
C#
/// <summary>
/// Event to indicate program argument available
/// </summary>
public event EventHandler<DynamicArgmentEventArgs> ArgumentReceived;
/// <summary>
/// Called to signal to subscribers that program argument available
/// </summary>
/// <param name="e"></param>
protected virtual void OnArgumentReceived(DynamicArgmentEventArgs e)
    {
    ArgumentReceived?.Invoke(this, e);
    }
C#
private void PassUpAllArguments()
    {
    while (Arguments.Count > 0)
        {
        OnArgumentReceived(new DynamicArgmentEventArgs() { Argument = Arguments.Dequeue() });
        }
    }

Not that complicated really, it just seems like it when you are trying to debug this stuff...

Send and Forget

When this isn't the only instance, we need to do the reverse: build up a block of data, and open a Client to send it. That's a lot simpler; everything stays in the Start method:

C#
// Other process exists - send arguments to it.

// Kick the bugger into life!
// Omit this, and the first message is lost ...
SendTime();
String strHostName = Dns.GetHostName();
IPHostEntry ipEntry = Dns.GetHostEntry(strHostName);
IPAddress ipAddress = ipEntry.AddressList[0];
using (TcpClient tc = new TcpClient(strHostName, ListenPort))
    {
    NetworkStream stream = tc.GetStream();
    ByteArrayBuilder bab = new ByteArrayBuilder();
    bab.Append(CC_Argument);
    int argCount = Arguments.Count;
    bab.Append(argCount);
    while (argCount-- > 0)
        {
        bab.Append(Arguments.Dequeue());
        }
    byte[] data = bab.ToArray();
    stream.Write(data, 0, data.Length);
    stream.Flush();
    }
return false;

SendTime is annoying: for some reason, the first message sent from any TcpClient I build is thrown away. I'm guessing it's a timing issue or Windows hates me - if you know why, let me know...

C#
private void SendTime()
    {
    String strHostName = Dns.GetHostName();
    using (TcpClient tc = new TcpClient(strHostName, ListenPort))
        {
        NetworkStream stream = tc.GetStream();
        ByteArrayBuilder bab = new ByteArrayBuilder();
        bab.Append(CC_TimeStamp);
        bab.Append(1);
        bab.Append(DateTime.Now.ToString("hh:mm:ss"));
        byte[] data = bab.ToArray();
        stream.Write(data, 0, data.Length);
        }
    }

Once we have a connection, we just bundle the data up (here, you could use JSON or XML if you wanted) and send it to the Listener app.

And that's it: all done!

History

  • 2019-10-02: Download changed to remove "magic numbers": The port number in use was fixed at "51191" instead of using the ListenPort property. The code samples in the text have been revised to match. Sorry about that... :blush:
  • 2019-10-02: Typos fixed
  • 2019-10-01: First version

License

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


Written By
CEO
Wales Wales
Born at an early age, he grew older. At the same time, his hair grew longer, and was tied up behind his head.
Has problems spelling the word "the".
Invented the portable cat-flap.
Currently, has not died yet. Or has he?

Comments and Discussions

 
QuestionProblems when multiple files are selected Pin
Member 1356638321-Oct-19 4:46
Member 1356638321-Oct-19 4:46 
GeneralMy vote of 5 Pin
John B Oliver20-Oct-19 11:46
John B Oliver20-Oct-19 11:46 
Questionoption gone Pin
Pete Lomax Member 106645058-Oct-19 19:44
professionalPete Lomax Member 106645058-Oct-19 19:44 
AnswerRe: option gone Pin
OriginalGriff9-Oct-19 9:16
mveOriginalGriff9-Oct-19 9:16 
GeneralRe: option gone Pin
Pete Lomax Member 1066450510-Oct-19 3:56
professionalPete Lomax Member 1066450510-Oct-19 3:56 
GeneralRe: option gone Pin
OriginalGriff10-Oct-19 4:06
mveOriginalGriff10-Oct-19 4:06 
GeneralRe: option gone Pin
Pete Lomax Member 1066450510-Oct-19 18:57
professionalPete Lomax Member 1066450510-Oct-19 18:57 
GeneralRe: option gone Pin
OriginalGriff10-Oct-19 20:02
mveOriginalGriff10-Oct-19 20:02 
GeneralRe: option gone Pin
Pete Lomax Member 1066450511-Oct-19 10:19
professionalPete Lomax Member 1066450511-Oct-19 10:19 
Suggestionuseless Pin
Eugenij2-Oct-19 11:00
Eugenij2-Oct-19 11:00 
GeneralRe: useless Pin
OriginalGriff4-Oct-19 4:45
mveOriginalGriff4-Oct-19 4:45 
QuestionInteresting! Pin
davercadman2-Oct-19 5:53
davercadman2-Oct-19 5:53 
AnswerRe: Interesting! Pin
OriginalGriff4-Oct-19 4:51
mveOriginalGriff4-Oct-19 4:51 
GeneralRe: Interesting! Pin
davercadman4-Oct-19 7:56
davercadman4-Oct-19 7:56 
GeneralRe: Interesting! Pin
OriginalGriff4-Oct-19 7:57
mveOriginalGriff4-Oct-19 7:57 

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.