0

I have a problem getting clipboard text via the async method. Because it always returns an empty value (while it's not empty). This is a simple demonstration of the problem:

    private async void button_Click(object sender, EventArgs e)
    {

        string result = await Task<string>.Run(() =>
        {
            System.Threading.Thread.Sleep(3000);

            return Clipboard.GetText(); //returns empty! (but clipboard is not empty)

        });

        MessageBox.Show(result);
    }

I'm sure that the clipboard is not empty. what's the solution?

  • 3
    Just a guess - I've never checked this out before, but... in general, just about everything with Windiws Forms must be dine from the main (UI) thread. Clipboard involves window messages and probably is included in that list. It's not because it's async, it's because you dispatched the work to another thread. Wrap the clipboard call in a method and `Invoke` it (or `BeginInvoke`) to fix the issue – Flydog57 Jul 13 '21 at 12:37
  • What's the point of making this async? What's the point of sleeping 3s? – Thomas Weller Jul 13 '21 at 12:44
  • @ThomasWeller This is just a simulation of the problem. Assume it is a long operation – user11448245 Jul 13 '21 at 12:46
  • Clipboard access requires a `STAThread`. By definition, Task.Run() cannot set `ApartmentState.STA`. Not clear why you'd need an async method for this. – Jimi Jul 13 '21 at 13:04
  • What about moving `string result = Clipboard.GetText();` outside the task, it will only get executed when the task is finished – Charlieface Jul 13 '21 at 13:05
  • @Jimi I have a long operation (which also requires the clipboard content) and I do not want to freeze the main UI – user11448245 Jul 13 '21 at 13:43
  • @Charlieface The long operation also requires the clipboard content – user11448245 Jul 13 '21 at 13:45
  • You should define *long operation*. You can start a Thread instead of running a Task, as the answer suggests. Or get the Clipboard text beforehand. It depends on what a *long operation* actually is and what it's doing, in practice. – Jimi Jul 13 '21 at 14:13

2 Answers2

5

It doesn't work because the clipboard only works when the COM threading model (apartment) is STA while your Task apartment is MTA. You can't change the apartment of the Task, BUT you can use Thread instead. Thread has a SetApartmentState method.

STA and MTA explained here

But I've found a solution to create an STA Task !

The trick is to run an STA thread using a Task:

public static Task<T> StartSTATask<T>(Func<T> func)
{
    var tcs = new TaskCompletionSource<T>();
    var thread = new Thread(() =>
    {
        try
        {
            var result = func();
            tcs.SetResult(result);
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }
    });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    return tcs.Task;
}

So now you can make it work like this:

private async void button1_Click(object sender, EventArgs e)
{
    var result = await StartSTATask(() =>
    {
        Thread.Sleep(3000);
        return Clipboard.GetText();
    });
    MessageBox.Show(result);
}
Mohammad-R
  • 307
  • 2
  • 11
  • works! very interesting.... I was not familiar with this method. Does it have any drawback or limitation (compared to the "Task.Run") ? – user11448245 Jul 13 '21 at 16:56
  • 2
    @user11448245 yes, the `Task.Run` offloads the supplied delegate to a `ThreadPool` thread, which is a small pool of reusable threads. So if you `await Task.Run(() => Something())` 1,000 times sequentially, only a handful of different threads (most probably one or two) will be used. On the contrary the `StartSTATask` method creates a new thread every time, which is destroyed when the delegate completes. This is quite wasteful, if all that you want to do is to execute a couple lines of code. Another difference: the `ThreadPool` threads are background threads (`IsBackground = true`). – Theodor Zoulias Jul 13 '21 at 22:29
0

This should work:

private static int counter;

private async void button1_Click(object sender, EventArgs e)
{
    counter++;
    button1.Text = counter.ToString();

    // Solution 1 (preferred)
    await LongOperation();
    Debug.WriteLine("Solution 1 completed for counter " + counter);

    // Solution 2 (use if LongOperation is CPU-bound)
    var t = Task.Run(LongOperation);
    await t;
    Debug.WriteLine("Solution 2 completed for counter " + counter);

    Debug.WriteLine(Clipboard.GetText());
}

private async Task LongOperation()
{
    await Task.Delay(10000);
}

Click 3 times in a row on button1. Result:

// Click 3 times in a row on the button. Result:

// After 10 seconds:
Solution 1 completed for counter 3
Solution 1 completed for counter 3
Solution 1 completed for counter 3

// After 10 more seconds:
Solution 2 completed for counter 3
<clipboard content>
Solution 2 completed for counter 3
<clipboard content>
Solution 2 completed for counter 3
<clipboard content>
evilmandarine
  • 4,241
  • 4
  • 17
  • 40
  • You might as well not do `async` in the first place then. You are going to lock up the UI thread like that – Charlieface Jul 13 '21 at 13:04
  • @Charlieface You're right. Updated my answer, but kept the async/await solution to match OP code. – evilmandarine Jul 13 '21 at 13:23
  • I would upvote the `// Solution 1` alone, but the `// Solution 2` demonstrates the bad practice of blocking a `ThreadPool` thread for no reason. – Theodor Zoulias Jul 13 '21 at 14:40
  • @TheodorZoulias If you're referring to the `Sleep()` code, this is just to simulate the long operation OP mentioned. The reason is not to block UI thread. How does this block a threadpool thread more than solution 1? – evilmandarine Jul 13 '21 at 16:25
  • @evilmandarine the `// Solution 1` does not block a thread. The whole point of [asynchronous programming](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model) is to avoid blocking threads. If you click multiple times the `button1`, you will not see one more thread to be added per click in the Windows Task Manager. – Theodor Zoulias Jul 13 '21 at 17:46
  • @TheodorZoulias Neither does solution 2. Indeed, Task.Run() will cause the code to be executed on a threadpool thread, but this does not mean a thread will be "blocked". As the OP did non specify if his long-running task is I/O- or CPU-bound nor the frequency of the clicks, my understanding is that both are suitable. Also for CPU-bound tasks you need a thread anyway. – evilmandarine Jul 13 '21 at 21:44
  • The `Thread.Sleep(3000)` will definitely block the calling thread for 3 seconds. That's the whole point of `Thread.Sleep`, to block/idle/suspend a thread for a given duration. If the `Thread.Sleep` is intended as a mock/placeholder of a synchronous/CPU-bound operation, and the `await Task.Delay(3000)` as a mock/placeholder of an asynchronous/I/O-bound operation, it should be explicitly expressed inside the answer IMHO. As it stands now, this answer could serve accidentally as a lesson of bad practices for novice developers. – Theodor Zoulias Jul 13 '21 at 22:15
  • 1
    @TheodorZoulias Of course it will, but this was provided as an example of long operation. There is the same Thread.Sleep() in both the question and accepted answer. However your point about novice developers is valid, so I updated the answer. Hopefully it's better now. – evilmandarine Jul 14 '21 at 08:01