Broken custom IDataObject implementations
See original GitHub issue-
.NET Core Version: 5.0
-
Have you experienced this same bug with .NET Framework?: No
Problem description:
PR #3388 removed the ComVisible
attribute from System.Windows.Forms.IDataObject
. That largely breaks the marshaling custom implementation of IDataObject
through clipboard within the same application. The concern about marshaling of individual calls is not applicable in that case because the runtime could resolve the IDataObject
to the original .NET object implementation.
It likely broke this case in Clipboard.GetDataObject
and now all returned objects are wrapped in the default DataObject
which doesn’t support all the custom types:
https://github.com/dotnet/winforms/blob/2fd2a5840b345ef83e0d15a91747325af27441f1/src/System.Windows.Forms/src/System/Windows/Forms/Clipboard.cs#L170-L173
Expected behavior:
Minimal repro:
TBD
Issue Analytics
- State:
- Created 3 years ago
- Comments:32 (32 by maintainers)
FWIW, we’re working on making the clipboard API more resilient to address issues like this. I have a very rough draft (thanks to @AaronRobinsonMSFT) on my fork, and @JeremyKuhne is tinkering in this area as well.
@filipnavara I want to follow up on your original issue, as promised
First here’s what we are doing: just store a
MarshalByRefObject
(Desktop Framework only) or a serialized COM reference into a data object slot with a custom format. This could even be a back-reference to the custom data object itself if you want it to.example for putting a COM reference into a data object
Some notes about the code below:
Stream
is the only object that can be transferred safely between frameworks for custom formats, as all other object types are serialized viaBinaryFormatter
(even strings) which is not available in 3rd party frameworks that may want to talk to the clipboard and even is going away in future .NET Core. Though if you only care about in-process usage that may not matter to you and you can just stick in the moniker as a string.MemoryStream
since its transferred through aHGLOBAL
memory block, so I’m always working withMemoryStream
to simplify the codeComImport
, using a private GUID and without TLB registered)Having given that example of how we do it currently, there is a certain simplicity in your broken solution, especially performance-wise, so I have spent more time to research why it was working for you and if theres a way to make it work reliable.
more technical details of what happens
So lets break down what you were doing again:
OleGetClipboard
, which returned a COM object instead of your managed objectIDataObject
(or later to your custom copy of the interface)Note that the second step still returns a COM object and not a managed object, regardless of which framework I tried it on, which indicates that you do not have a reference to your original object, but to “something else”.
So I researched what
OleGetClipboard
is actually doing, here are the results:OleGetClipboard
constructs an intermediate implementation of the nativeIDataObject
to put between the producer and consumer. The clipboard is a global resource and implementations can have bugs or be incomplete, this intermediate objects tries to fix certain issues, mostly performingTYMED
conversion the provider didn’t bother to implement.QueryInterface
calls it doesn’t understand to be forwarded to the original data object. This is not allowed by COM because you can’t get back to theIUnknown
of the intermediate data object (and QueryInterface for IUnknown must always return the same instance, so this is where it violates COM rules)IDataObject
is also broken, the .NET runtime can’t figure out the cast (still doesn’t unwrap the object), and you get weird behavior. The behavior you observe only works for the first interface implemented on the data object, if you change interface order you suddenly get errors that you can’t cast, even if it is ComVisibleSo the result of that research is that
OleGetClipboard
returns a wrapper which will forwardQueryInterface
calls (violating COM rules) and the .NET runtime is too confused to resolve the reference.The good news is that you can rely on the behavior of
OleGetClipboard
(its always been there and won’t go away for compatibility reasons, applications rely on the ability to QI into the underlying object). The only thing you need to solve is help the .NET runtime to not be confused anymore. This is simple, just do the QI manually to move away from the wrapper object, once you are on the inner data object you have a “normal” COM object that plays by the rules and can be understood by the .NET runtime.IDataObject
being ComVisible - it just needs an arbitrary interface implemented on your data object but not implemented by the OLE data object.IDataObject
regardless of it being ComVisible or nothelper code