9
\$\begingroup\$

The following class monitors the clipboard and raises an event whenever the contents change. The monitoring starts when the class is created and ends when Dispose is called.

To achieve this a message-only window (a term that comes from the Win32 API) is created using an instance of HwndSource class (since I am using WPF). The window's handle is then registered via AddClipboardFormatListener to receive a message whenever the clipboard changes. The window's messages are processed by WndProc which raises the event when the WM_CLIPBOARDUPDATE message comes.

In other solutions for monitoring the clipboard I saw this was done on the actual main window of the application, but I wanted to separate the logic of monitoring the clipboard form the View and keep a clean MVVM. In an application I would instantiate the ClipboardMonitor in the ViewModel.

I am interested if there may be any hidden pitfalls where this approach may go wrong.

public sealed class ClipboardMonitor : IDisposable
{
    private static class NativeMethods
    {
        /// <summary>
        /// Places the given window in the system-maintained clipboard format listener list.
        /// </summary>
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool AddClipboardFormatListener(IntPtr hwnd);

        /// <summary>
        /// Removes the given window from the system-maintained clipboard format listener list.
        /// </summary>
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool RemoveClipboardFormatListener(IntPtr hwnd);

        /// <summary>
        /// Sent when the contents of the clipboard have changed.
        /// </summary>
        public const int WM_CLIPBOARDUPDATE = 0x031D;

        /// <summary>
        /// To find message-only windows, specify HWND_MESSAGE in the hwndParent parameter of the FindWindowEx function.
        /// </summary>
        public static IntPtr HWND_MESSAGE = new IntPtr(-3);
    }

    private HwndSource hwndSource = new HwndSource(0, 0, 0, 0, 0, 0, 0, null, NativeMethods.HWND_MESSAGE);

    public ClipboardMonitor()
    {
        hwndSource.AddHook(WndProc);
        NativeMethods.AddClipboardFormatListener(hwndSource.Handle);
    }

    public void Dispose()
    {
        NativeMethods.RemoveClipboardFormatListener(hwndSource.Handle);
        hwndSource.RemoveHook(WndProc);
        hwndSource.Dispose();
    }

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (msg == NativeMethods.WM_CLIPBOARDUPDATE)
        {
            OnClipboardContentChanged?.Invoke(this, EventArgs.Empty);
        }

        return IntPtr.Zero;
    }

    /// <summary>
    /// Occurs when the clipboard content changes.
    /// </summary>
    public event EventHandler OnClipboardContentChanged;
}
\$\endgroup\$
  • \$\begingroup\$ You may want to take a look at the Dispose Pattern and Implementing a Dispose Method pages on MSDN. \$\endgroup\$ – Dan Lyons Dec 30 '15 at 18:25
  • 1
    \$\begingroup\$ Please do not change the code in your question after answers have been posted. See What should I do when someone answers my question? for more details. \$\endgroup\$ – Phrancis Dec 30 '15 at 22:21
  • 1
    \$\begingroup\$ @Mast I do not understand the edit term -> terminal (The meaning of 'term': a word or phrase that has an exact meaning). Enlighten me please. \$\endgroup\$ – Martin Dec 30 '15 at 22:46
  • \$\begingroup\$ As it's currently stated, it reads like the message-only window is a Win32 terminal. If it isn't, consider re-writing that part of your question to remove ambiguity. Make sure you don't invalidate any answers in the process. \$\endgroup\$ – Mast Dec 30 '15 at 22:52
6
\$\begingroup\$

Your code looks fine except for two points:

  • a Dispose() method should never throw an exception but yours will likely throw one if it is called twice. This can be avoided by having a private bool disposed variable which is checked by an if condition.

  • an event based method OnXXX is usually either private or protected and used to actually raise the event. A simple rename to ClipboardChanged will be sufficient to handle that issue.

| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

Looks good. My comments:

  1. The ? C# 6 syntax for null checking not protect you from case that between the null check and the Invoke, your handler can be null. Update: As Dan Lyons wrote, this is not true. Thanks for the comment Dan, it helped me to answer my own question.

  2. About the Dispose, if your monitor supposed to be a thread safe, you need yo add the private flag like Heslacher wrote and do the disposed check in a thread safe manner.

  3. Your IDisposable implementation is incomplete. what if the user of your monitor will not call Dispose?

  4. AddClipboardFormatListener and RemoveClipboardFormatListener can return false, i.e. the operation failed. You need to handle these cases. AddClipboardFormatListener

I like the usage of HwndSource instead of creating an hidden window and use its handle.

| improve this answer | |
\$\endgroup\$
  • 3
    \$\begingroup\$ Point 1 is untrue. Event handlers are a specific case called-out for use with the new ?. operator: New Features in C# 6. The underlying implementation creates a temp variable so the original object is only referenced one time. \$\endgroup\$ – Dan Lyons Dec 30 '15 at 18:23
  • \$\begingroup\$ @DanLyons thanks for this. I checked it in the cil code but not on event handlers so I missing the special behavior \$\endgroup\$ – Dudi Keleti Dec 30 '15 at 18:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.