1

In a C# console app (.net framework 4.7), I am able to copy text to the clipboad like so:

using System.Windows.Forms;

class Program {
    [STAThread]
    public static void Main(string[] args] {
        Clipboard.SetText("this works");
    }
}

But my logic requires Main to be async because I will be calling other async methods from main. Once I change it to async, I keep getting an exception everytime I try to access the clipboard.

using System.Windows.Forms;

class Program {
    
    [STAThread]
    public static async Task Main(string[] args] {
        string text = await GetTextAsync();

        // this throws System.Threading.ThreadStateException

        Clipboard.SetText(text);

        await Task.Run(()=>Clipboard.SetText("this throws too"));

        await Task.Run([STAThread]()=>Clipboard.SetText("this doesn't compile"));
    }

    [STAThread]  // this attribute doesn't change anything
    public static async Task<string> GetTextAsync() {
        // fetching data from database that takes too much time
        // for simplicity, assume that the string value is ready
        // after one second
        await Task.Delay(1000);
        return "value fetched from database"; 
    }
}

How can I put text in the clipboard from within an Async method?

EDIT

I already tried the suggested answer from comments: https://stackoverflow.com/a/56737049/14171304

However, the code won't compile because of [NotNull] attribute not being recognized. When I remove it, There is another problem preventing compilation:

The type arguments for method STATask.Run(Func) cannot be inferred from the usage. Try specifying the type arguments explicitly.

Ahmad
  • 12,336
  • 6
  • 48
  • 88
  • 1
    The first should work. Is this your real code or is there anything before the call to `Clipboard.SetText`? – Klaus Gütter Jul 03 '22 at 07:04
  • 1
    https://stackoverflow.com/a/56737049/14171304 – dr.null Jul 03 '22 at 07:13
  • 1
    Did you happen to have any `ConfigureAwait(false)` before the `Clipboard.SetText`? – Klaus Gütter Jul 03 '22 at 07:13
  • Please show us the definition of `GetTextAsync`. – Dai Jul 03 '22 at 07:33
  • 1
    @Ahmad You can remove those `[EtcNull]` attributes if you're targeting .NET Framework instead of .NET Core 3.1+ – Dai Jul 03 '22 at 07:42
  • @dr.null I did. See my edited question – Ahmad Jul 03 '22 at 07:49
  • 1
    Mr. Ahmed I always test what I suggest. Just tested it again under .NET Framework 4.7.1, and 4.8. It works here. In async method: `await STATask.Run(() => Clipboard.SetText("Ahmed")); Console.WriteLine(Clipboard.GetText());` – dr.null Jul 03 '22 at 08:14
  • 1
    @dr.null YES. I managed to make it work by adopting the second solution in that answer. I was only trying to adopt the first solution. Shall I close this question? Should I wait for you to write an answer? – Ahmad Jul 03 '22 at 08:36
  • Thank you. Please post your solution if you want to. I have nothing to add, :) Have a good one. – dr.null Jul 03 '22 at 08:39

2 Answers2

0

One solution is to avoid async and await in the Main, and wait synchronously all the asynchronous methods:

[STAThread]
public static void Main(string[] args)
{
    string text = GetTextAsync().GetAwaiter().GetResult();

    Clipboard.SetText(text);
}

If your code has a single execution flow, this should be sufficient. But if you launch multiple concurrent async operations, things might become tricky.

A more powerful solution is to install a suitable SynchronizationContext on the main thread of the Console app, so that all async continuations are scheduled on the main STA thread. There is no such mechanism available in the standard .NET libraries, and writing one from scratch is not trivial, but you could use Stephen Cleary's AsyncContext from the Nito.AsyncEx package:

[STAThread]
public static void Main(string[] args)
{
    AsyncContext.Run(async () =>
    {
        string text = await GetTextAsync();

        Clipboard.SetText(text);
    });
}

The AsyncContext.Run is a blocking call, so again the Main should be synchronous. The asynchronous code should be placed inside the action delegate. In order to run all the continuations on the main STA thread, there should be no .ConfigureAwait(false) in the await points.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
0

I got several suggestions from the comments to my question (by @dr.null and @Dai), and I finally managed to have a working solution to my problem.

The answer in set clipboard in async method contained 2 solutions. Only the second one was working for me. I was about to close or delete the question, but decided to post this answer to remove any confusion caused by the answer in that question.

public static class STATask {
    public static Task Run(Action action) {
        var tcs = new TaskCompletionSource<ojbect>();
        var thread = new Thread(() => {
            try {
                action();
                tcs.SetResult(null);
            } catch (Exception e) {
                tcs.SetException(e);
            }
            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
            
            return tcs.Task;

        });
    }
}

Now from my Main method, I simply utilize the STATask by:

public static async Task Main() {
        string text = await GetTextAsync();
        await STATask.Run(() => Clipboard.SetText(text));
}

The solution above doesn't even require to use the attribute [STAThread] anywhere.

Ahmad
  • 12,336
  • 6
  • 48
  • 88
  • Why was my comment deleted? I explained why the code in this answer won't work reliably... – Dai Jul 03 '22 at 13:08
  • I don't know what happened to your comment. I didn't see a comment to this answer before @Dai – Ahmad Jul 03 '22 at 14:59