4

Why doesn't System.Windows.Forms.Clipboard.GetDataObject() return when calling it from a different thread and before the main thread stops, after calling System.Windows.Forms.Clipboard.Clear() within the main thread?

I have written a sample program to explain my question:

public class ClipboardDemo
{
    [STAThread]
    public static void Main(string[] args)
    {
        Thread.CurrentThread.Name = "MAIN_THREAD";

        Thread clipboardViewerThread = new Thread(RunClipboardViewer);
        clipboardViewerThread.Name = "CLIPBOARD_VIEWER_THREAD";
        clipboardViewerThread.SetApartmentState(ApartmentState.STA);

        Thread clipboardClearerThread = new Thread(RunClipboardClearer);
        clipboardClearerThread.Name = "CLIPBOARD_CLEARER_THREAD";
        clipboardClearerThread.SetApartmentState(ApartmentState.STA);

        Console.WriteLine("Starting " + clipboardViewerThread.Name + ", expecting initial WM_DRAWCLIPBOARD message...");
        clipboardViewerThread.Start();
        Thread.Sleep(1000);

        Console.WriteLine("Clearing clipboard from " + clipboardClearerThread.Name + ", expecting WM_DRAWCLIPBOARD message...");
        clipboardClearerThread.Start();
        clipboardClearerThread.Join();

        Console.WriteLine("Clearing clipboard from " + Thread.CurrentThread.Name + ", expecting WM_DRAWCLIPBOARD message...");
        Clipboard.Clear();
        Thread.Sleep(1000);

        Application.Exit();
        Console.WriteLine("\t" + Thread.CurrentThread.Name + " stopped!");
    }

    private static void RunClipboardViewer()
    {
        ClipboardViewer viewer = new ClipboardViewer();
        viewer.ViewClipboard();
        viewer.Dispose();
    }

    private static void RunClipboardClearer()
    {
        Clipboard.Clear();
    }
}

internal class ClipboardViewer : NativeWindow, IDisposable
{
    private const int WM_CREATE = 0x0001;
    private const int WM_DRAWCLIPBOARD = 0x0308;
    private const int WM_CHANGECBCHAIN = 0x030D;

    private IntPtr nextViewer;

    public void ViewClipboard()
    {
        base.CreateHandle(new CreateParams());
        Application.Run();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        switch (m.Msg)
        {
            case WM_CREATE:
                nextViewer = User32Interop.SetClipboardViewer(base.Handle);
                break;
            case WM_DRAWCLIPBOARD:
                if (nextViewer != IntPtr.Zero)
                {
                    User32Interop.SendMessage(nextViewer, WM_DRAWCLIPBOARD, m.WParam, m.LParam);
                }
                Console.WriteLine("\tClipboard changed in " + Thread.CurrentThread.Name + ". Trying to receive data object...");
                Clipboard.GetDataObject();
                Console.WriteLine("\tData object received!");
                break;
            case WM_CHANGECBCHAIN:
                if (m.WParam == nextViewer)
                {
                    nextViewer = m.LParam;
                }
                else if (nextViewer != IntPtr.Zero)
                {
                    User32Interop.SendMessage(nextViewer, WM_CHANGECBCHAIN, m.WParam, m.LParam);
                }
                break;
        }
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            User32Interop.ChangeClipboardChain(base.Handle, nextViewer);
        }
        base.DestroyHandle();
    }

    ~ClipboardViewer()
    {
        Dispose(false);
    }
}

internal static class User32Interop
{
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr SetClipboardViewer(IntPtr hWndNewViewer);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern bool ChangeClipboardChain(IntPtr hWndRemove, IntPtr hWndNewNext);
}

The formatted output of this is:

Starting CLIPBOARD_VIEWER_THREAD, expecting initial WM_DRAWCLIPBOARD message...
  Clipboard changed in CLIPBOARD_VIEWER_THREAD. Trying to receive data object...
  Data object received!

Clearing clipboard from CLIPBOARD_CLEARER_THREAD, expecting WM_DRAWCLIPBOARD message...
  Clipboard changed in CLIPBOARD_VIEWER_THREAD. Trying to receive data object...
  Data object received!

Clearing clipboard from MAIN_THREAD, expecting WM_DRAWCLIPBOARD message...
  Clipboard changed in CLIPBOARD_VIEWER_THREAD. Trying to receive data object...
  MAIN_THREAD stopped!
  Data object received!

As you can see in the last three lines, System.Windows.Forms.Clipboard.GetDataObject() returns when the main thread stops, but not earlier. Is there a solution for this problem?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jonas W
  • 373
  • 1
  • 4
  • 15
  • Thanks for your quick reply! I´ve called Thread.SetApartmentState(ApartmentState.STA) for every Thread I start and added the STAThreadAttribute to the Main() method for the main thread, so every thread runs in STA mode. Isnt it possible to have multiple STA threads in one process? My approach is to write a class library for catching clipboard change events, so i havent any UI... – Jonas W Sep 09 '11 at 16:30
  • Excuse me Jon Skeet, I´m new to stackoverflow, it seems that I´ve deleted your post accidentally :S As I can remember you were regarding to the msdn where it says that i can access the clipboard only within STA threads and recommended me the Control.Invoke() approach – Jonas W Sep 09 '11 at 16:36

1 Answers1

3

You are doing it right, selecting STA for the worker threads and having them pump a message loop. Except in one: your main thread. It pumps only by accident. The Thread.Join() call makes the CLR pump. But Thread.Sleep() doesn't pump. You can arbitrarily replace it with this and fix your problem:

    var dummy = new AutoResetEvent(false);
    dummy.WaitOne(1000);

But that's a hack. I realize this is just a test app, think about what your real one is going to look like.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thanks for your hack, it works. But since I´m new to winforms i don´t really understand why. It would be great if you could provide me some more explanations or links to help me understanding my mistake! – Jonas W Sep 09 '11 at 17:03
  • Check this answer for more about the significance of STA: http://stackoverflow.com/questions/4154429/apartmentstate-for-dummies/4156000#4156000 – Hans Passant Sep 09 '11 at 17:33