Welcome to CSharp Labs

Type Safe Clipboard Access and Monitoring

Thursday, August 22, 2013

Microsoft's .Net Framework provides access to the Windows clipboard through the System.Windows.Forms.Clipboard static class. This class requires defining the expected data format and does not allow monitoring use. I have created the ClipboardAccessor static class to allow monitoring or accessing the clipboard in a type safe context.

How it Works

The ClipboardAccessor class uses a specially crafted static initializer to validate the TData generic argument and initialize static fields:

        static ClipboardAccessor()
        {
            //the ClipboardAccessor class can get or set various types
            //of clipboard data including strings, bitmaps, html, rtf
            //or any serializable object; the following determines
            //if a type fits into any of these parameters and sets static
            //fields appropriately
            DataType = typeof(TData);

            if (DataType == typeof(string))
                DataFormat = DataFormats.StringFormat;
            else if (DataType == typeof(Bitmap))
                DataFormat = DataFormats.Bitmap;
            else if (DataType == typeof(HtmlString))
                DataFormat = DataFormats.Html;
            else if (DataType == typeof(RtfString))
                DataFormat = DataFormats.Rtf;
            else if (DataType.IsSubclassOf(typeof(ISerializable)) || DataType.IsSerializable)
                DataFormat = DataFormats.Serializable;
            else
                throw new InvalidOperationException(DataType.Name + " is not a valid clipboard type.");
        } 

Declared static fields are only shared between instances with the same TData type specified. The following types can be used in the ClipboardAccessor class as TData argument:

  • .Net Primitives (System.Int16, System.UInt64, System.DateTime, etc)
  • System.String
  • System.Drawing.Bitmap
  • System.Windows.Forms.HtmlString (New)
  • System.Windows.Forms.RtfString (New)
  • Any class that implements System.Runtime.Serialization.System.ISerializable or is marked with the System.SerializableAttribute

The TryGetData method is responsible for returning clipboard data. The data on the clipboard is obtained and checked for the correct System.Windows.Forms.DataFormats that correlates with TData. Then, the System.Type of the object is checked and the data is converted to a TData type. The dynamic keyword is leveraged to allow the object conversion to TData at runtime:

        /// <summary>
        /// Attempts to get data from the clipboard in the specified 
        /// format <typeparam name="TData"/> and returns a value 
        /// indicating if the operation succeeded.
        /// </summary>
        /// <param name="data">When this method returns, contains the data 
        /// retrieved from the clipboard or the default value of 
        /// <typeparam name="TData"/> if this method failed.</param>
        /// <returns>true if data was retrieved from the clipboard; 
        /// otherwise, false.</returns>
        public static bool TryGetData(out TData data)
        {
            //get clipboard data
            IDataObject cache = Clipboard.GetDataObject();

            //determine if data exists in specified format
            if (cache.GetDataPresent(DataFormat))
            {
                //get data from clipboard object
                object obj = cache.GetData(DataFormat);

                //determine if data exists in return format OR
                //expected return is RTF/HTML and clipboard data is string
                if (obj is TData || (obj is string && (DataType == typeof(RtfString) || DataType == typeof(HtmlString))))
                {
                    //dynamic keyword is used to allow the type to convert from
                    //an object to a generic type using operator overloads
                    data = (TData)(dynamic)obj;
                    return true;
                }
            }

            //no data found, initialize default
            data = default(TData);
            return false;
        }

Clipboard change notifications are provided through an internal ClipboardMessageSink NativeWindow message sink instance which subscribes to the native SetClipboardViewer method. This instance is created and destroyed on demand when a caller subscribes or unsubscribes to the ClipboardAccessor.ClipboardChanged event:

        /// <summary>
        /// Event raised when a type <typeparam name="TData"/> is added to the clipboard.
        /// </summary>
        public static event EventHandler<ClipboardChangedEventArgs<TData>> ClipboardChanged
        {
            add
            {
                //add delegate to invocation list
                _ClipboardChanged += value;

                //create sink if it does not exist
                if (_MessageSink == null)
                {
                    _MessageSink = new ClipboardMessageSink();

                    //wire changed event to notify subscribers
                    _MessageSink.ClipboardChanged += (sender, e) =>
                    {
                        TData data;

                        //attempt to get data
                        if (TryGetData(out data))
                        {
                            //get the last generation and increment current
                            int lastSystemGeneration = ClipboardAccessor<object>._ApplicationGeneration++;

                            //if there are subscribers
                            if (_ClipboardChanged != null)
                            {
                                //check if object changed or different types have raised notifications
                                if (!object.Equals(data, _PreviousValue) || _PreviousInstanceGeneration != lastSystemGeneration)
                                {
                                    _PreviousValue = data;
                                    _ClipboardChanged(null, new ClipboardChangedEventArgs<TData>(data));
                                }
                            }

                            //set previous generation to current
                            _PreviousInstanceGeneration = ++lastSystemGeneration;
                        }
                    };
                }
            }
            remove
            {
                //remove delegate from invocation list
                _ClipboardChanged -= value;

                //if invocation list is empty, dispose the sink
                if (_ClipboardChanged == null && _MessageSink != null)
                {
                    _MessageSink.Dispose();
                    _MessageSink = null;
                }
            }
        }

To prevent duplicate event notification, caused by overzealous applications and frameworks, the previous clipboard value is compared to the new value and the generation is checked. Clipboard generation is tracked through a shared field to allow raising the event after identical but separated notifications.

Using

The ClipboardAccessor is a static class and cannot be instantiated. To access clipboard data, call the ClipboardAccessor<TData>.Value property or the ClipboardAccessor<TData>.TryGetValue method. Subscribe to the ClipboardAccessor<TData>.ClipboardChanged event to listen for clipboard change notifications.

Source Code

Download ClipboardAccessor and Supporting Classes

Comments