Search code examples
c#winformsdrag-and-dropcom

Windows drag drop between different applications


I have two WinForms applications and want to be able to drag objects from one to the other.

My data object code is very simple:

// the data object
[ComVisible(true)]
[Serializable]
public class MyData : ISerializable {
    public int Value1 { get; set; }
    public int Value2 { get; set; } 

    public MyData() { }

    public MyData(int value1, int value2) {
        Value1 = value1;
        Value2 = value2;
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context) {
        info.AddValue(nameof(Value1), Value1);
        info.AddValue(nameof(Value2), Value2);
    }
}

The object is part of a dll, which is referenced in both of my WinForms applications.

I'm initializing the drag drop using:

// inside some control
MyData toBeTransmitted = new MyData(0, 0);
IDataObject dataObject = new DataObject(DataFormats.Serializable, toBeTransmitted);
this.DoDragDrop(dataObject, DragDropEffects.All);

and handling it using:

// inside some drag over handler
IDataObject dataObject = dragEvent.DataObject;
if (dataObject.GetDataPresent(DataFormats.Serializable)) {
    object obj = e.DataObject.GetData(DataFormats.Serializable);
}

All this works fine, as long as I'm dragging and dropping data inside a single application. But as soon as I drag data over from one process to the other retrieving the dragged data returns an object of type System.__ComObject instead of MyData.

screnshot from visual studio

How can I retrieve the actual data contained inside the IDataObject?

(note: I also tried using a custom format instead of DataFormats.Serializable, no luck there.)


Solution

  • The problem:
    When DataFormats.Serializable is specified, the DataObject class uses a BinaryFormatter to serialize a class object that implement ISerializable (BTW, you should add a public MyData(SerializationInfo info, StreamingContext context) constructor).

    The BinaryFormatter object is created using the standard form with a default Binder, which implies that the serialized object contains strict Assembly information.
    As a consequence, you can Drag/Drop your object(s) to/from Processes that represent instances of the same Assembly without problem, but if the Assemblies are different or their version doesn't match, the BinaryFormatter fails deserialization and you get an unwrapped IComDataObject as result.

    You could marshal this COM object yourself, which means you have to build a compatible FORMATETC struct object (System.Runtime.InteropServices.ComTypes.FORMATETC), get the STGMEDIUM from IDataObject.GetData([FORMATETC], out [STGMEDIUM]), get the IStream object using Marshal.GetObjectForIUnknown(), passing the [STGMEDIUM].unionmember pointer.
    Then create a BinaryFormatter specifying a less restrictive / custom Binder and deserialize the Stream ignoring or replacing the Assembly name.

    Before you ask, you cannot set the [SerializationInfo].AssemblyName (even though it's not read-only) directly in your ISerializable class, won't work.

    A possible solution:
    A simple approach is to replace the BinaryFormatter with a different serializer and create the IDataObject setting a custom format (or a predefined DataFormat compatible with the generated data).


    An example using XmlSerializer as serializer and a MemoryStream:

    The class object can be simplified, removing the ISerializable implementation:

    [Serializable]
    public class MyData {
        public int Value1 { get; set; }
        public int Value2 { get; set; }
    
        public MyData() { }
    
        public MyData(int value1, int value2)
        {
            Value1 = value1;
            Value2 = value2;
        }
    
        public override string ToString()
        {
            return $"Value1: {Value1}, Value2: {Value2}";
        }
    }
    

    Two static methods used to generate an IDataObject on the Source side and to extract its content on the Target side. XmlSerializer is used to serialize / deserialize the class object(s).

    private static IDataObject SetObjectData<T>(object value, string format) where T : class
    {
        using (var ms = new MemoryStream())
        using (var sw = new StreamWriter(ms)) {
            var serializer = new XmlSerializer(typeof(T), "");
            serializer.Serialize(sw, value);
            sw.Flush();
            var data = new DataObject(format, ms.ToArray());
            // Failsafe custom data type - could be a GUID, anything else, or removed entirely
            data.SetData("MyApp_DataObjectType", format);
            return data;
        };
    }
    
    private static T GetObjectData<T>(IDataObject data, string format) where T : class
    {
        // Throws if the byte[] cast fails
        using (var ms = new MemoryStream(data.GetData(format) as byte[])) {
            var serializer = new XmlSerializer(typeof(T));
            var obj = serializer.Deserialize(ms);
            return (T)obj;
        }
    }
    

    In the example, I'm using a Dictionary<string, Action> to call methods nased on the Type contained in the IDataObject received from the DragDrop operation.
    This because I suppose you could transfer different Types. Of course you can use anything else.
    You could also use a common Interface and just use this as <T>. It would simplify a lot the whole implementation (and future expansion, if generic enough methods and properties can be defined).

    Dictionary<string, Action<IDataObject>> dataActions = new Dictionary<string, Action<IDataObject>>() {
        ["MyData"] = (data) => {
            // The Action delegate deserialzies the IDataObject...
            var myData = GetObjectData<MyData>(data, "MyData");
            // ...and calls a method passing the class object
            MessageBox.Show(myData.ToString());
        },
        ["MyOtherData"] = (data) => {
            var otherData = GetObjectData<MyOtherData>(data, "MyOtherData");
            MessageBox.Show(otherData.ToString());
        }
    };
    

    On the Source side (Drag/Drop initiator):

    Point mouseDownPos = Point.Empty;
    
    private void SomeSourceControl_MouseDown(object sender, MouseEventArgs e)
    {
        mouseDownPos = e.Location;
    }
    
    private void SomeSourceControl_MouseMove(object sender, MouseEventArgs e)
    {
        MyData toBeTransmitted = new MyData(100, 100);
    
        if (e.Button == MouseButtons.Left &&
            ((Math.Abs(e.X - mouseDownPos.X) > SystemInformation.DragSize.Width) ||
             (Math.Abs(e.Y - mouseDownPos.Y) > SystemInformation.DragSize.Height))) {
    
            var data = SetObjectData<MyData>(toBeTransmitted, "MyData");
            DoDragDrop(data, DragDropEffects.All);
        }
    }
    

    On the Target side (Drag/Drop target)

    private void SomeTargetControl_DragEnter(object sender, DragEventArgs e)
    {
        var formats = e.Data.GetFormats();
        // Verify that a Data Type is defined in the Dictionary
        if (formats.Any(f => dataActions.ContainsKey(f))) {
            e.Effect = DragDropEffects.All;
        }
    }
    
    private void SomeTargetControl_DragDrop(object sender, DragEventArgs e)
    {
        // Double check: the fail-safe Data Type is present
        string dataType = (string)e.Data.GetData("MyApp_DataObjectType");
        // If the Data Type is in the Dictionary, deserialize and call the Action
        if (dataActions.ContainsKey(dataType)) {
            dataActions[dataType](e.Data);
        }
    }