Home > OS >  How can I put text in Clipboard from within an async method in C#?
How can I put text in Clipboard from within an async method in C#?

Time:07-03

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.

CodePudding user response:

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.

CodePudding user response:

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.

  • Related