diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataFormatNames.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataFormatNames.cs index 36af2e05645..5d8a4f3d5cf 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataFormatNames.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataFormatNames.cs @@ -115,6 +115,26 @@ internal static void AddMappedFormats(string format, T formats) break; case BinaryFormatMetafile: formats.Add(Emf); + break; + default: + // The formats aren't case sensitive, notably when they are looked up via the OLE IDataObject + // proxy. We're most likely to see "TEXT", so we'll handle text cases. + if (string.Equals(format, Text, StringComparison.OrdinalIgnoreCase)) + { + formats.Add(String); + formats.Add(UnicodeText); + } + else if (string.Equals(format, UnicodeText, StringComparison.OrdinalIgnoreCase)) + { + formats.Add(String); + formats.Add(Text); + } + else if (string.Equals(format, String, StringComparison.OrdinalIgnoreCase)) + { + formats.Add(Text); + formats.Add(UnicodeText); + } + break; } } diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.cs index adbbc158412..b317fc861aa 100644 --- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.cs +++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.cs @@ -130,7 +130,7 @@ public bool GetDataPresent(string format, bool autoConvert) for (int i = 0; i < formats.Length; i++) { - if (format.Equals(formats[i])) + if (format.Equals(formats[i], StringComparison.OrdinalIgnoreCase)) { return true; } diff --git a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/ClipboardCoreTests.cs b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/ClipboardCoreTests.cs index 6c112352511..9ebe6037bfe 100644 --- a/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/ClipboardCoreTests.cs +++ b/src/System.Private.Windows.Core/tests/System.Private.Windows.Core.Tests/System/Private/Windows/Ole/ClipboardCoreTests.cs @@ -130,7 +130,7 @@ public void RoundTrip_Text() } [Fact] - public void Clipboard_DerivedDataObject_DataPresent() + public void DerivedDataObject_DataPresent() { // https://github.com/dotnet/winforms/issues/12789 SomeDataObject data = new(); @@ -152,4 +152,31 @@ internal class SomeDataObject : DataObject public override bool GetDataPresent(string format, bool autoConvert) => format == Format || base.GetDataPresent(format, autoConvert); } + + [Fact] + public void SerializableObject_InProcess_DoesNotUseBinaryFormatter() + { + // This test ensures that the SerializableObject does not use BinaryFormatter when running in process. + using ClipboardScope scope = new(); + + DataObject dataObject = new(); + SerializablePerson person = new() { Name = "John Doe", Age = 30 }; + dataObject.SetData(person); + HRESULT result = ClipboardCore.SetData(dataObject, copy: false, retryTimes: 1, retryDelay: 0); + result.Should().Be(HRESULT.S_OK); + + result = ClipboardCore.GetDataObject(out var data, retryTimes: 1, retryDelay: 0); + result.Should().Be(HRESULT.S_OK); + data.Should().NotBeNull(); + data!.GetDataPresent(typeof(SerializablePerson).FullName!).Should().BeTrue(); + + data.GetData(typeof(SerializablePerson).FullName!).Should().BeSameAs(person); + } + + [Serializable] + internal class SerializablePerson + { + public string Name { get; set; } = "DefaultName"; + public int Age { get; set; } + } } diff --git a/src/System.Windows.Forms/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/PublicAPI.Unshipped.txt index e69de29bb2d..801a7d8c14e 100644 --- a/src/System.Windows.Forms/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +static System.Windows.Forms.Clipboard.TryGetData(out T data) -> bool \ No newline at end of file diff --git a/src/System.Windows.Forms/System/Windows/Forms/OLE/Clipboard.cs b/src/System.Windows.Forms/System/Windows/Forms/OLE/Clipboard.cs index 1185d6eb5c5..cd62811547d 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/OLE/Clipboard.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/OLE/Clipboard.cs @@ -352,6 +352,10 @@ public static bool ContainsText(TextDataFormat format) return dataObject.TryGetData(format, out data); } + /// + public static bool TryGetData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + [NotNullWhen(true), MaybeNullWhen(false)] out T data) => TryGetData(typeof(T).FullName!, out data); + /// /// Retrieves a collection of file names from the . /// diff --git a/src/System.Windows.Forms/System/Windows/Forms/OLE/WrappingDataObject.cs b/src/System.Windows.Forms/System/Windows/Forms/OLE/WrappingDataObject.cs index 64aa061480f..b8549283c65 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/OLE/WrappingDataObject.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/OLE/WrappingDataObject.cs @@ -14,7 +14,6 @@ public WrappingDataObject(object data) : base(data) { // Don't wrap existing DataObject instances. Debug.Assert(data is not DataObject); - _originalIsIDataObject = data is IDataObject; } @@ -26,7 +25,25 @@ internal override bool TryUnwrapUserDataObject([NotNullWhen(true)] out IDataObje return base.TryUnwrapUserDataObject(out dataObject); } - dataObject = null; - return false; + // When we hand back our wrapper we're emulating what would happen via the native IDataObject proxy. + // As the native interface has no concept of "autoConvert", we need to always consider it "true" + // as our native IDataObject implementation would. + dataObject = this; + return true; } + + public override string[] GetFormats(bool autoConvert) + // Always auto convert to emulate native IDataObject behavior. + => base.GetFormats(autoConvert: true); + + public override bool GetDataPresent(string format, bool autoConvert) + // Always auto convert to emulate native IDataObject behavior. + => base.GetDataPresent(format, autoConvert: true); + +#pragma warning disable WFDEV005 // Type or member is obsolete + [Obsolete] + public override object? GetData(string format, bool autoConvert) + // Always auto convert to emulate native IDataObject behavior. + => base.GetData(format, autoConvert: true); +#pragma warning restore WFDEV005 // Type or member is obsolete } diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ClipboardTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ClipboardTests.cs index 4fe754a5ced..83457c4e083 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ClipboardTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ClipboardTests.cs @@ -276,7 +276,7 @@ public void SetDataObject_InvokeObjectNotIComDataObject_GetReturnsExpected(objec { Clipboard.SetDataObject(data); - DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType().Subject; + DataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; dataObject.GetData(data.GetType()).Should().Be(data); Clipboard.ContainsData(data.GetType().FullName).Should().BeTrue(); } @@ -303,7 +303,7 @@ public void SetDataObject_InvokeObjectBoolNotIComDataObject_GetReturnsExpected(o { Clipboard.SetDataObject(data, copy); - DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType().Subject; + DataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; dataObject.GetData(data.GetType()).Should().Be(data); Clipboard.ContainsData(data.GetType().FullName).Should().BeTrue(); } @@ -332,7 +332,7 @@ public void SetDataObject_InvokeObjectBoolIntIntNotIComDataObject_GetReturnsExpe { Clipboard.SetDataObject(data, copy, retryTimes, retryDelay); - DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType().Subject; + DataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; dataObject.GetData(data.GetType()).Should().Be(data); Clipboard.ContainsData(data.GetType().FullName).Should().BeTrue(); } @@ -1193,7 +1193,7 @@ public void SetData_Text_Format_AllUpper() formats.Should().BeEquivalentTo(["System.String", "UnicodeText", "Text"]); formats = dataObject.GetFormats(autoConvert: false); - formats.Should().BeEquivalentTo(["Text"]); + formats.Should().BeEquivalentTo(["System.String", "UnicodeText", "Text"]); // CLIPBRD_E_BAD_DATA returned when trying to get clipboard data. Clipboard.GetText().Should().BeEmpty(); @@ -1256,10 +1256,8 @@ public void SetDataObject_Text() Clipboard.GetData("System.String").Should().Be(expected); - // Case sensitivity matters so we end up reading stream/object from HGLOBAL instead of string. - MemoryStream stream = Clipboard.GetData("TEXT").Should().BeOfType().Subject; - byte[] array = stream.ToArray(); - array.Should().BeEquivalentTo("Hello, World!\0"u8.ToArray()); + string result = Clipboard.GetData("TEXT").Should().BeOfType().Subject; + result.Should().Be(expected); } [WinFormsFact] @@ -1324,18 +1322,19 @@ public void RoundTrip_Object_SupportsTypedInterface(bool copy) string format = typeof(SerializableTestData).FullName!; // Opt-in into access to the binary formatted stream. - using BinaryFormatterInClipboardDragDropScope clipboardScope = new(enable: true); + using BinaryFormatterInClipboardDragDropScope clipboardScope = new(enable: copy); + // We need the BinaryFormatter to flush the data from the managed object to the HGLOBAL // and to write data to HGLOBAL as a binary formatted stream now if it hadn't been flushed. - using BinaryFormatterScope scope = new(enable: true); + using BinaryFormatterScope scope = new(enable: copy); Clipboard.SetDataObject(data, copy); - DataObject received = Clipboard.GetDataObject().Should().BeOfType().Subject; + DataObject received = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; received.TryGetData( format, - (TypeName name) => name.FullName == typeof(SerializableTestData).FullName ? typeof(SerializableTestData) : null, + name => name.FullName == typeof(SerializableTestData).FullName ? typeof(SerializableTestData) : null, autoConvert: false, out SerializableTestData? result).Should().BeTrue(); @@ -1450,4 +1449,27 @@ public unsafe void OleGetClipboard_ProxyBehavior() using ComScope wrapperUnknown = wrapper.Query(); ((nint)wrapperUnknown.Value).Should().Be((nint)originalUnknown.Value); } + + [WinFormsFact] + public void SetDataObject_SerializableObject_CopyFalse_DoesNotUseBinaryFormatter() + { + // This test ensures that the SerializableObject does not use BinaryFormatter when running in process. + // This only works when copy is not set to true as that is the only case where we are able to extract + // the original DataObject we created for the user data. + + SerializablePerson person = new() { Name = "John Doe", Age = 30 }; + Clipboard.SetDataObject(person); + + bool result = Clipboard.TryGetData(out SerializablePerson? data); + result.Should().BeTrue(); + data.Should().NotBeNull(); + data.Should().BeSameAs(person); + } + + [Serializable] + internal class SerializablePerson + { + public string Name { get; set; } = "DefaultName"; + public int Age { get; set; } + } }