diff --git a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb index 6187bc12fa5..ee2e28cc510 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb @@ -210,6 +210,11 @@ Namespace Microsoft.VisualBasic.MyServices Clipboard.SetImage(image) End Sub + ''' + Public Sub SetDataAsJson(Of T)(format As String, data As T) + Clipboard.SetDataAsJson(format, data) + End Sub + ''' ''' Saves the passed in to the clipboard. ''' diff --git a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt index 65993b67a39..c491c216c7f 100644 --- a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt @@ -1,2 +1,3 @@ Microsoft.VisualBasic.MyServices.ClipboardProxy.TryGetData(Of T)(format As String, ByRef data As T) -> Boolean -Microsoft.VisualBasic.MyServices.ClipboardProxy.TryGetData(Of T)(format As String, resolver As System.Func(Of System.Reflection.Metadata.TypeName, System.Type), ByRef data As T) -> Boolean \ No newline at end of file +Microsoft.VisualBasic.MyServices.ClipboardProxy.TryGetData(Of T)(format As String, resolver As System.Func(Of System.Reflection.Metadata.TypeName, System.Type), ByRef data As T) -> Boolean +Microsoft.VisualBasic.MyServices.ClipboardProxy.SetDataAsJson(Of T)(format As String, data As T) -> Void diff --git a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs index f95e21f215a..1591fb76834 100644 --- a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs +++ b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs @@ -5,6 +5,7 @@ using System.Drawing; using System.Reflection.Metadata; +using System.Runtime.CompilerServices; using Microsoft.VisualBasic.Devices; using DataFormats = System.Windows.Forms.DataFormats; using TextDataFormat = System.Windows.Forms.TextDataFormat; @@ -86,6 +87,26 @@ public void Text() clipboard.GetText(TextDataFormat.UnicodeText).Should().Be(text); } + [WinFormsFact] + public void SetDataAsJson() + { + var clipboard = new Computer().Clipboard; + SimpleTestData testData = new() { X = 1, Y = 1 }; + string format = "testData"; + clipboard.SetDataAsJson(format, testData); + clipboard.ContainsData(format).Should().Be(System.Windows.Forms.Clipboard.ContainsData(format)); + clipboard.TryGetData(format, out SimpleTestData retrieved).Should().Be(System.Windows.Forms.Clipboard.TryGetData(format, out SimpleTestData retrieved2)); + retrieved.Should().BeEquivalentTo(retrieved2); + retrieved.Should().BeEquivalentTo(testData); + } + + [TypeForwardedFrom("System.ForwardAssembly")] + public struct SimpleTestData + { + public int X { get; set; } + public int Y { get; set; } + } + [WinFormsFact] public void DataOfT_StringArray() { diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index d778a18227d..6f68c96826e 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -4,6 +4,9 @@ static System.Windows.Forms.DataObjectExtensions.TryGetData(this System.Windo static System.Windows.Forms.DataObjectExtensions.TryGetData(this System.Windows.Forms.IDataObject! dataObject, string! format, bool autoConvert, out T data) -> bool static System.Windows.Forms.DataObjectExtensions.TryGetData(this System.Windows.Forms.IDataObject! dataObject, string! format, out T data) -> bool static System.Windows.Forms.DataObjectExtensions.TryGetData(this System.Windows.Forms.IDataObject! dataObject, string! format, System.Func! resolver, bool autoConvert, out T data) -> bool +static System.Windows.Forms.Clipboard.SetDataAsJson(string! format, T data) -> void +System.Windows.Forms.Control.DoDragDropAsJson(T data, System.Windows.Forms.DragDropEffects allowedEffects) -> System.Windows.Forms.DragDropEffects +System.Windows.Forms.Control.DoDragDropAsJson(T data, System.Windows.Forms.DragDropEffects allowedEffects, System.Drawing.Bitmap? dragImage, System.Drawing.Point cursorOffset, bool useDefaultDragImage) -> System.Windows.Forms.DragDropEffects System.Windows.Forms.DataGridViewCellStyle.Font.get -> System.Drawing.Font? System.Windows.Forms.DataObject.TryGetData(out T data) -> bool System.Windows.Forms.DataObject.TryGetData(string! format, bool autoConvert, out T data) -> bool @@ -37,3 +40,6 @@ virtual System.Windows.Forms.DataObject.TryGetDataCore(string! format, System [WFO5002]System.Windows.Forms.Form.ShowAsync(System.Windows.Forms.IWin32Window? owner = null) -> System.Threading.Tasks.Task! [WFO5002]System.Windows.Forms.Form.ShowDialogAsync() -> System.Threading.Tasks.Task! [WFO5002]System.Windows.Forms.Form.ShowDialogAsync(System.Windows.Forms.IWin32Window! owner) -> System.Threading.Tasks.Task! +System.Windows.Forms.DataObject.SetDataAsJson(string! format, bool autoConvert, T data) -> void +System.Windows.Forms.DataObject.SetDataAsJson(string! format, T data) -> void +System.Windows.Forms.DataObject.SetDataAsJson(T data) -> void diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormatWriter.cs b/src/System.Windows.Forms/src/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormatWriter.cs index 0a7819f6bb2..ef6c79b14d2 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormatWriter.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormatWriter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Drawing; +using System.Private.Windows; using System.Private.Windows.Core.BinaryFormat; using System.Private.Windows.Core.BinaryFormat.Serializer; @@ -16,7 +17,7 @@ internal static class WinFormsBinaryFormatWriter private static readonly string s_currentWinFormsFullName = typeof(WinFormsBinaryFormatWriter).Assembly.FullName!; - public static unsafe void WriteBitmap(Stream stream, Bitmap bitmap) + public static void WriteBitmap(Stream stream, Bitmap bitmap) { using MemoryStream memoryStream = new(); bitmap.Save(memoryStream); @@ -51,6 +52,20 @@ public static void WriteImageListStreamer(Stream stream, ImageListStreamer strea new ArraySinglePrimitive(3, data).Write(writer); } + public static void WriteJsonData(Stream stream, IJsonData jsonData) + { + using BinaryFormatWriterScope writer = new(stream); + new BinaryLibrary(libraryId: 2, IJsonData.CustomAssemblyName).Write(writer); + new ClassWithMembersAndTypes( + new ClassInfo(1, $"{typeof(IJsonData).Namespace}.JsonData", [$"<{nameof(jsonData.JsonBytes)}>k__BackingField", $"<{nameof(jsonData.InnerTypeAssemblyQualifiedName)}>k__BackingField"]), + libraryId: 2, + new MemberTypeInfo[] { new(BinaryType.PrimitiveArray, PrimitiveType.Byte), new(BinaryType.String, null) }, + new MemberReference(idRef: 3), + new BinaryObjectString(objectId: 4, jsonData.InnerTypeAssemblyQualifiedName)).Write(writer); + + new ArraySinglePrimitive(objectId: 3, jsonData.JsonBytes).Write(writer); + } + /// /// Writes the given if supported. /// @@ -72,6 +87,11 @@ static bool Write(Stream stream, object value) WriteBitmap(stream, bitmap); return true; } + else if (value is IJsonData jsonData) + { + WriteJsonData(stream, jsonData); + return true; + } return false; } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs index 97fb2978718..70e2ee017b6 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs @@ -4780,6 +4780,49 @@ internal virtual void DisposeAxControls() } } + /// + public DragDropEffects DoDragDropAsJson(T data, DragDropEffects allowedEffects) => + DoDragDropAsJson(data, allowedEffects, dragImage: null, cursorOffset: default, useDefaultDragImage: false); + + /// + /// Begins a drag operation. + /// + /// The data being dragged. + /// determine which drag operations can occur. + /// The drag image bitmap. + /// The drag image cursor offset. + /// Indicating whether a layered window drag image is used. + /// A value from the enumeration that represents the final effect that was performed during the drag-and-drop operation. + /// + /// If is a non derived . This is for better error reporting as will serialize empty. If + /// needs to be used to start a drag operation, use to JSON serialize the data being held within the , + /// then pass the to . + /// + /// + /// + /// The data will be stored as JSON if the data is not an intrinsically handled type. Otherwise, it will be stored + /// the same as . + /// + /// + public DragDropEffects DoDragDropAsJson( + T data, + DragDropEffects allowedEffects, + Bitmap? dragImage, + Point cursorOffset, + bool useDefaultDragImage) + { + data.OrThrowIfNull(nameof(data)); + if (typeof(T) == typeof(DataObject)) + { + // TODO: Localize string + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)} then start drag/drop operation by using {nameof(DoDragDrop)} instead."); + } + + DataObject dataObject = new(); + dataObject.SetDataAsJson(data); + return DoDragDrop(dataObject, allowedEffects, dragImage, cursorOffset, useDefaultDragImage); + } + /// /// Begins a drag operation. The allowedEffects determine which /// drag operations can occur. If the drag operation needs to interop @@ -4788,10 +4831,8 @@ internal virtual void DisposeAxControls() /// that implements System.Runtime.Serialization.ISerializable. data can also be any Object that /// implements System.Windows.Forms.IDataObject. /// - public DragDropEffects DoDragDrop(object data, DragDropEffects allowedEffects) - { - return DoDragDrop(data, allowedEffects, dragImage: null, cursorOffset: default, useDefaultDragImage: false); - } + public DragDropEffects DoDragDrop(object data, DragDropEffects allowedEffects) => + DoDragDrop(data, allowedEffects, dragImage: null, cursorOffset: default, useDefaultDragImage: false); /// /// Begins a drag operation. The determine which drag operations can occur. diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/Nrbf/WinFormsSerializationRecordExtensions.cs b/src/System.Windows.Forms/src/System/Windows/Forms/Nrbf/WinFormsSerializationRecordExtensions.cs index 352bd28457f..940f28c0bc9 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Nrbf/WinFormsSerializationRecordExtensions.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Nrbf/WinFormsSerializationRecordExtensions.cs @@ -3,6 +3,11 @@ using System.Drawing; using System.Formats.Nrbf; +using System.Private.Windows; +using System.Private.Windows.Core.BinaryFormat; +using System.Reflection.Metadata; +using System.Runtime.Serialization; +using System.Text.Json; namespace System.Windows.Forms.Nrbf; @@ -53,6 +58,53 @@ public static bool TryGetBitmap(this SerializationRecord record, out object? bit return true; } + /// + /// Tries to deserialize this object if it was serialized as JSON. + /// + /// + /// if the data was serialized as JSON. Otherwise, . + /// + /// If the data was supposed to be our , but was serialized incorrectly./> + /// If an exception occurred while JSON deserializing. + public static bool TryGetObjectFromJson(this SerializationRecord record, ITypeResolver resolver, out object? @object) + { + @object = null; + + if (record.TypeName.AssemblyName?.FullName != IJsonData.CustomAssemblyName) + { + // The data was not serialized as JSON. + return false; + } + + if (record is not ClassRecord types + || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData + || types.GetRawValue("k__BackingField") is not string innerTypeFullName + || !TypeName.TryParse(innerTypeFullName, out TypeName? serializedTypeName)) + { + // This is supposed to be JsonData, but somehow the binary formatted data is corrupt. + throw new SerializationException(); + } + + Type serializedType = resolver.GetType(serializedTypeName); + if (!serializedType.IsAssignableTo(typeof(T))) + { + // Not the type the caller asked for so @object remains null. + return true; + } + + try + { + @object = JsonSerializer.Deserialize(byteData.GetArray()); + } + catch (Exception ex) + { + // loni TODO: localize string + throw new NotSupportedException("Failed to deserialize JSON data.", ex); + } + + return true; + } + /// /// Try to get a supported object. This supports common types used in WinForms that do not have type converters. /// diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs index 25c0c437b2b..67ad0d0fd66 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs @@ -7,6 +7,7 @@ using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Runtime.Serialization.Formatters.Binary; +using System.Text.Json; using Windows.Win32.System.Com; using Com = Windows.Win32.System.Com; @@ -513,6 +514,62 @@ public static void SetData(string format, object data) SetDataObject(new DataObject(format, data), copy: true); } + /// + /// Saves the data onto the clipboard in the specified format. + /// If the data is a non intrinsic type, the object will be serialized using JSON. + /// + /// + /// , empty string, or whitespace was passed as the format. + /// + /// + /// If is a non derived . This is for better error reporting as will serialize as empty. + /// If needs to be placed on the clipboard, use + /// to JSON serialize the data to be held in the , then set the + /// onto the clipboard via . + /// + /// + /// + /// If your data is an intrinsically handled type such as primitives, string, or Bitmap + /// and you are using a custom format or , + /// it is recommended to use the APIs to avoid unnecessary overhead. + /// + /// + /// The default behavior of is used to serialize the data. + /// + /// + /// See + /// + /// and + /// for more details on default behavior. + /// + /// + /// If custom JSON serialization behavior is needed, manually JSON serialize the data and then use + /// to save the data onto the clipboard, or create a custom , attach the + /// , and then recall this method. + /// See for more details + /// on custom converters for JSON serialization. + /// + /// + [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] + public static void SetDataAsJson(string format, T data) + { + data.OrThrowIfNull(nameof(data)); + if (string.IsNullOrWhiteSpace(format.OrThrowIfNull())) + { + throw new ArgumentException(SR.DataObjectWhitespaceEmptyFormatNotAllowed, nameof(format)); + } + + if (typeof(T) == typeof(DataObject)) + { + // TODO: Localize string + throw new InvalidOperationException($"'DataObject' will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetDataObject)} API instead."); + } + + DataObject dataObject = new(); + dataObject.SetDataAsJson(format, data); + SetDataObject(dataObject, copy: true); + } + /// /// Clears the Clipboard and then adds a collection of file names in the format. /// diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs index fe7081fb527..5ba3bc394c6 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.BinaryFormatUtilities.cs @@ -104,14 +104,10 @@ record = stream.Decode(out recordMap); Type type = typeof(T); if (!legacyMode && !type.MatchExceptAssemblyVersion(record.TypeName)) { -#if false // TODO (TanyaSo): - modify TryGetObjectFromJson to take a resolver and rename to HasJsonData??? - // Return true if the payload contains valid JsonData struct, type matches or not - // run IsAssignable in the JSON method - if (record.TryGetObjectFromJson(binder.GetType, out object? data)) + if (record.TryGetObjectFromJson((ITypeResolver)binder, out object? data)) { return data; } -#endif if (!TypeNameIsAssignableToType(record.TypeName, type, (ITypeResolver)binder)) { diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs index 080c21d0ca4..dbc5d64eb5d 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.Binder.cs @@ -32,7 +32,7 @@ internal sealed class Binder : SerializationBinder, ITypeResolver private readonly bool _legacyMode; // These types are read from and written to serialized stream manually, accessing record field by field. - // Thus they are re-hydrated with no formatters and are safe. The default resolver should recognize them + // Thus they are re-hydrated with no formatters and are safe. The default resolver should recognize them // to resolve primitive types or fields of the specified type T. private static readonly Type[] s_intrinsicTypes = [ diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs index d543e25287f..2bab6de38df 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.NativeToWinFormsAdapter.cs @@ -482,16 +482,6 @@ static bool IsUnboundedType() // Image is a special case because we are reading Bitmaps directly from the SerializationRecord. return type.IsInterface || (typeof(T) != typeof(Image) && type.IsAbstract); } - - static bool IsRestrictedFormat(string format) => RestrictDeserializationToSafeTypes(format) - || format is DataFormats.TextConstant - or DataFormats.UnicodeTextConstant - or DataFormats.RtfConstant - or DataFormats.HtmlConstant - or DataFormats.OemTextConstant - or DataFormats.FileDropConstant - or CF_DEPRECATED_FILENAME - or CF_DEPRECATED_FILENAMEW; } private bool TryGetDataInternal( diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs index 878bec806d4..e04fb728c06 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.Composition.cs @@ -82,28 +82,6 @@ public static Composition CreateFromRuntimeDataObject(ComTypes.IDataObject runti /// public IDataObject? OriginalIDataObject { get; private set; } - /// - /// We are restricting binary serialization and deserialization of formats that represent strings, bitmaps or OLE types. - /// - /// format name - /// - serialize only safe types, strings or bitmaps. - private static bool RestrictDeserializationToSafeTypes(string format) => - format is DataFormats.StringConstant - or BitmapFullName - or DataFormats.CsvConstant - or DataFormats.DibConstant - or DataFormats.DifConstant - or DataFormats.LocaleConstant - or DataFormats.PenDataConstant - or DataFormats.RiffConstant - or DataFormats.SymbolicLinkConstant - or DataFormats.TiffConstant - or DataFormats.WaveAudioConstant - or DataFormats.BitmapConstant - or DataFormats.EmfConstant - or DataFormats.PaletteConstant - or DataFormats.WmfConstant; - #region IDataObject public object? GetData(string format, bool autoConvert) => _winFormsDataObject.GetData(format, autoConvert); public object? GetData(string format) => _winFormsDataObject.GetData(format); diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs index 00a26e8d327..b651edfb082 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.DataStore.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.Drawing; +using System.Private.Windows; using System.Reflection.Metadata; using System.Runtime.Serialization; @@ -25,10 +26,18 @@ private bool TryGetDataInternal( return false; } - if (_mappedData.TryGetValue(format, out DataStoreEntry? dse) && dse.Data is T t) + if (_mappedData.TryGetValue(format, out DataStoreEntry? dse)) { - data = t; - return true; + if (dse.Data is T t) + { + data = t; + return true; + } + else if (dse.Data is JsonData jsonData) + { + data = (T)jsonData.Deserialize(); + return true; + } } if (!autoConvert @@ -45,10 +54,18 @@ private bool TryGetDataInternal( continue; } - if (_mappedData.TryGetValue(mappedFormats[i], out DataStoreEntry? found) && found.Data is T value) + if (_mappedData.TryGetValue(mappedFormats[i], out DataStoreEntry? found)) { - data = value; - return true; + if (found.Data is T value) + { + data = value; + return true; + } + else if (found.Data is JsonData jsonData) + { + data = (T)jsonData.Deserialize(); + return true; + } } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs index 5fcf8502256..6808ce1d017 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs @@ -4,8 +4,10 @@ using System.Collections.Specialized; using System.Drawing; using System.Reflection.Metadata; +using System.Private.Windows; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; +using System.Text.Json; using Com = Windows.Win32.System.Com; using ComTypes = System.Runtime.InteropServices.ComTypes; @@ -100,13 +102,152 @@ internal IDataObject TryUnwrapInnerIDataObject() /// internal IDataObject? OriginalIDataObject => _innerData.OriginalIDataObject; + /// + [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] + public void SetDataAsJson(string format, T data) + { + if (typeof(T) == typeof(DataObject)) + { + // loni TODO: localize string. + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); + } + + SetData(format, TryJsonSerialize(format, data)); + } + + /// + [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] + public void SetDataAsJson(T data) + { + if (typeof(T) == typeof(DataObject)) + { + // loni TODO: localize string. + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); + } + + SetData(typeof(T), TryJsonSerialize(typeof(T).FullName!, data)); + } + + /// + /// Stores the data in the specified format. + /// If the data is a managed object and format allows for serialization of managed objects, the object will be serialized as JSON. + /// + /// The format associated with the data. See for predefined formats. + /// to allow the data to be converted to another format; otherwise, . + /// The data to store. + /// + /// If is a non derived . This is for better error reporting as will serialize as empty. + /// If needs to be set, JSON serialize the data held in using this method, then use + /// passing in . + /// + /// + /// + /// If your data is an intrinsically handled type such as primitives, string, or Bitmap + /// and you are using a custom format or + /// it is recommended to use the APIs to avoid unnecessary overhead. + /// + /// + /// The default behavior of is used to serialize the data. + /// + /// + /// See + /// + /// and + /// for more details on default behavior. + /// + /// + /// If custom JSON serialization behavior is needed, manually JSON serialize the data and then use , + /// or create a custom , attach the + /// , and then recall this method. + /// See for more details + /// on custom converters for JSON serialization. + /// + /// + [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] + public void SetDataAsJson(string format, bool autoConvert, T data) + { + if (typeof(T) == typeof(DataObject)) + { + // loni TODO: localize string. + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); + } + + SetData(format, autoConvert, TryJsonSerialize(format, data)); + } + + /// + /// JSON serialize the data only if the format is not a restricted deserialization format and the data is not an intrinsic type. + /// + /// + /// The passed in as is if the format is restricted. Otherwise the JSON serialized . + /// + private static object TryJsonSerialize(string format, T data) + { + if (string.IsNullOrWhiteSpace(format.OrThrowIfNull())) + { + throw new ArgumentException(SR.DataObjectWhitespaceEmptyFormatNotAllowed, nameof(format)); + } + + data.OrThrowIfNull(nameof(data)); + return IsRestrictedFormat(format) || Composition.Binder.IsKnownType() + ? data + : new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }; + } + + /// + /// Check if the is one of the restricted formats, which formats that + /// correspond to primitives or are pre-defined in the OS such as strings, bitmaps, and OLE types. + /// + private static bool IsRestrictedFormat(string format) => RestrictDeserializationToSafeTypes(format) + || format is DataFormats.TextConstant + or DataFormats.UnicodeTextConstant + or DataFormats.RtfConstant + or DataFormats.HtmlConstant + or DataFormats.OemTextConstant + or DataFormats.FileDropConstant + or CF_DEPRECATED_FILENAME + or CF_DEPRECATED_FILENAMEW; + + /// + /// We are restricting binary serialization and deserialization of formats that represent strings, bitmaps or OLE types. + /// + /// format name + /// - serialize only safe types, strings or bitmaps. + /// + /// + /// These formats are also restricted in WPF + /// https://github.com/dotnet/wpf/blob/db1ae73aae0e043326e2303b0820d361de04e751/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/dataobject.cs#L2801 + /// + /// + private static bool RestrictDeserializationToSafeTypes(string format) => + format is DataFormats.StringConstant + or BitmapFullName + or DataFormats.CsvConstant + or DataFormats.DibConstant + or DataFormats.DifConstant + or DataFormats.LocaleConstant + or DataFormats.PenDataConstant + or DataFormats.RiffConstant + or DataFormats.SymbolicLinkConstant + or DataFormats.TiffConstant + or DataFormats.WaveAudioConstant + or DataFormats.BitmapConstant + or DataFormats.EmfConstant + or DataFormats.PaletteConstant + or DataFormats.WmfConstant; + #region IDataObject [Obsolete( Obsoletions.DataObjectGetDataMessage, error: false, DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, UrlFormat = Obsoletions.SharedUrlFormat)] - public virtual object? GetData(string format, bool autoConvert) => _innerData.GetData(format, autoConvert); + public virtual object? GetData(string format, bool autoConvert) + { + object? result = _innerData.GetData(format, autoConvert); + // Avoid exposing our internal JsonData + return result is IJsonData ? null : result; + } [Obsolete( Obsoletions.DataObjectGetDataMessage, diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs new file mode 100644 index 00000000000..1a9516c2b5b --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Windows.Forms; + +namespace System.Private.Windows; + +/// +/// Wrapper which contains JSON serialized data along with the JSON data's original type information +/// to be deserialized later. +/// +/// +/// +/// There may be instances where this type is not available in different versions, e.g. .NET 8, .NET Framework. +/// If this type needs to be deserialized from stream in these instances, a workaround would be to create an assembly with the name +/// and replicate this type. Then, manually retrieve the serialized stream and use the to decode the stream and rehydrate the serialized type. +/// Alternatively, but not recommended, BinaryFormatter can also be used to deserialize the stream if this type is not available. +/// +/// +/// +/// k__BackingField") is not System.Formats.Nrbf.SZArrayRecord byteData +/// || types.GetRawValue("k__BackingField") is not string innerTypeName +/// || !System.Reflection.Metadata.TypeName.TryParse(innerTypeName.ToCharArray(), out System.Reflection.Metadata.TypeName? result)) +/// { +/// // This is supposed to be JsonData, but somehow the data is corrupt. +/// throw new InvalidOperationException(); +/// } +/// +/// // TODO: Additional checking on result TypeName to ensure it is expected type. +/// +/// // This should return the original data that was JSON serialized. +/// var result = System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), genericType); +/// // TODO: Process the payload as needed. +/// } +/// +/// [DllImport("kernel32.dll")] +/// static extern int GlobalSize(IntPtr hMem); +/// +/// [DllImport("kernel32.dll")] +/// static extern IntPtr GlobalLock(IntPtr hMem); +/// +/// [DllImport("kernel32.dll")] +/// static extern int GlobalUnlock(IntPtr hMem); +/// +/// // OR +/// // Not recommended: deserialize using BinaryFormatter. +/// +/// // This definition must live in an assembly named System.Private.Windows.VirtualJson and referenced in order to work as expected. +/// namespace System.Private.Windows; +/// [Serializable] +/// struct JsonData : IObjectReference +/// { +/// public byte[] JsonBytes { get; set; } +/// +/// public string InnerTypeAssemblyQualifiedName { get; set; } +/// +/// public object GetRealObject(StreamingContext context) +/// { +/// // TODO: Additional checking on InnerTypeAssemblyQualifiedName to ensure it is expected type. +/// return JsonSerializer.Deserialize(JsonBytes, typeof(ExpectedType)) ?? throw new InvalidOperationException(); +/// } +/// } +/// ]]> +/// +[Serializable] +internal struct JsonData : IJsonData +{ + public byte[] JsonBytes { get; set; } + + public readonly string InnerTypeAssemblyQualifiedName => typeof(T).ToTypeName().AssemblyQualifiedName; + + public readonly object Deserialize() + { + object? result; + try + { + result = JsonSerializer.Deserialize(JsonBytes); + } + catch (Exception ex) + { + result = new NotSupportedException(ex.Message); + } + + return result ?? throw new InvalidOperationException(); + } +} + +/// +/// Represents an object that contains JSON serialized data. This interface is used to +/// identify a without needing to have the generic type information. +/// +internal interface IJsonData +{ + // We use a custom assembly name to allow versions where JsonData doesn't exist to still be able rehydrate JSON serialized data. + const string CustomAssemblyName = "System.Private.Windows.VirtualJson"; + + byte[] JsonBytes { get; set; } + + /// + /// The assembly qualified name of the T in . This name should + /// have any names taken into account. + /// + string InnerTypeAssemblyQualifiedName { get; } + + /// + /// Deserializes the data stored in the JsonData. This is a convenience method + /// to deserialize the data when we are not dealing with a binary formatted record. + /// + object Deserialize(); +} diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs index 10de5202c2a..a2840c10c69 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs @@ -3,6 +3,8 @@ #nullable enable +using static System.Windows.Forms.TestUtilities.DataObjectTestHelpers; + namespace System.Windows.Forms.Tests; // Each registered Clipboard format is an OS singleton, @@ -19,4 +21,31 @@ public void Clipboard_SetText_InvokeString_GetReturnsExpected() Clipboard.GetText().Should().Be("text"); Clipboard.ContainsText().Should().BeTrue(); } + + [WinFormsFact] + public void Clipboard_SetDataAsJson_ReturnsExpected() + { + SimpleTestData testData = new() { X = 1, Y = 1 }; + + Clipboard.SetDataAsJson("testData", testData); + ITypedDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + dataObject.GetDataPresent("testData").Should().BeTrue(); + dataObject.TryGetData("testData", out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); + } + + [WinFormsTheory] + [BoolData] + public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) + { + SimpleTestData testData = new() { X = 1, Y = 1 }; + + DataObject dataObject = new(); + dataObject.SetDataAsJson("testData", testData); + + Clipboard.SetDataObject(dataObject, copy); + ITypedDataObject returnedDataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + returnedDataObject.TryGetData("testData", out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); + } } diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs index 58923843eda..5276ef61930 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices.ComTypes; +using static System.Windows.Forms.TestUtilities.DataObjectTestHelpers; using Com = Windows.Win32.System.Com; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; @@ -11,6 +12,27 @@ public unsafe partial class DataObjectTests { private delegate IDataObject CreateWinFormsDataObjectForOutgoingDropData(Com.IDataObject* dataObject); + [WinFormsFact] + public void DataObject_WithJson_MockRoundTrip() + { + dynamic controlAccessor = typeof(Control).TestAccessor().Dynamic; + var dropTargetAccessor = typeof(DropTarget).TestAccessor(); + + SimpleTestData testData = new() { X = 1, Y = 1 }; + DataObject data = new(); + data.SetDataAsJson("testData", testData); + + DataObject inData = controlAccessor.CreateRuntimeDataObjectForDrag(data); + inData.Should().BeSameAs(data); + + using var inDataPtr = ComHelpers.GetComScope(inData); + IDataObject outData = dropTargetAccessor.CreateDelegate()(inDataPtr); + ITypedDataObject typedOutData = outData.Should().BeAssignableTo().Subject; + typedOutData.GetDataPresent("testData").Should().BeTrue(); + typedOutData.TryGetData("testData", out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); + } + [WinFormsFact] public void DataObject_CustomIDataObject_MockRoundTrip() { diff --git a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs index 52bd6337be3..7fd08a3b569 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs @@ -3,10 +3,12 @@ using System.ComponentModel; using System.Drawing; +using FluentAssertions; using Windows.Win32.System.Com; using Windows.Win32.UI.Accessibility; using Windows.Win32.UI.WindowsAndMessaging; using Xunit.Abstractions; +using static System.Windows.Forms.TestUtilities.DataObjectTestHelpers; namespace System.Windows.Forms.UITests; @@ -489,6 +491,61 @@ await InputSimulator.SendAsync( }); } + [WinFormsFact] + public async Task DragDrop_JsonSerialized_ReturnsExpected_Async() + { + // Verifies that we can successfully drag and drop a JSON serialized object. + + SimpleTestData testData = new() { X = 10, Y = 10 }; + object? dropped = null; + await RunFormWithoutControlAsync(() => new Form(), async (form) => + { + form.AllowDrop = true; + form.ClientSize = new Size(100, 100); + form.DragEnter += (s, e) => + { + if (e.Data?.GetDataPresent(typeof(SimpleTestData)) ?? false) + { + e.Effect = DragDropEffects.Copy; + } + }; + form.DragOver += (s, e) => + { + if (e.Data?.TryGetData(out SimpleTestData data) ?? false) + { + // Get the JSON serialized Point. + dropped = data; + e.Effect = DragDropEffects.Copy; + } + }; + form.MouseDown += (s, e) => + { + form.DoDragDropAsJson(testData, DragDropEffects.Copy); + }; + + var startRect = form.DisplayRectangle; + var startCoordinates = form.PointToScreen(GetCenter(startRect)); + Point endCoordinates = new(startCoordinates.X + 5, startCoordinates.Y + 5); + var virtualPointStart = ToVirtualPoint(startCoordinates); + var virtualPointEnd = ToVirtualPoint(endCoordinates); + + await InputSimulator.SendAsync( + form, + inputSimulator + => inputSimulator.Mouse + .MoveMouseTo(virtualPointStart.X + 6, virtualPointStart.Y + 6) + .LeftButtonDown() + .MoveMouseTo(virtualPointEnd.X, virtualPointEnd.Y) + .MoveMouseTo(virtualPointEnd.X, virtualPointEnd.Y) + .MoveMouseTo(virtualPointEnd.X + 2, virtualPointEnd.Y + 2) + .MoveMouseTo(virtualPointEnd.X + 4, virtualPointEnd.Y + 4) + .LeftButtonUp()); + }); + + dropped.Should().BeOfType(typeof(SimpleTestData)); + dropped.Should().BeEquivalentTo(testData); + } + private void CloseExplorer(string directory) { foreach (Process process in Process.GetProcesses()) diff --git a/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs b/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs new file mode 100644 index 00000000000..f84142e8c17 --- /dev/null +++ b/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System.Windows.Forms.TestUtilities; + +/// +/// Test utilities relating to . +/// +public static class DataObjectTestHelpers +{ + // These formats set and get strings by accessing HGLOBAL directly. + public static TheoryData StringFormat() => + [ + DataFormats.Text, + DataFormats.UnicodeText, + DataFormats.StringConstant, + DataFormats.Rtf, + DataFormats.Html, + DataFormats.OemText, + DataFormats.FileDrop, + "FileName", + "FileNameW" + ]; + + public static TheoryData UnboundedFormat() => + [ + DataFormats.Serializable, + "something custom" + ]; + + // These formats contain only known types. + public static TheoryData UndefinedRestrictedFormat() => + [ + DataFormats.CommaSeparatedValue, + DataFormats.Dib, + DataFormats.Dif, + DataFormats.PenData, + DataFormats.Riff, + DataFormats.Tiff, + DataFormats.WaveAudio, + DataFormats.SymbolicLink, + DataFormats.EnhancedMetafile, + DataFormats.MetafilePict, + DataFormats.Palette + ]; + + public static TheoryData BitmapFormat() => + [ + DataFormats.Bitmap, + "System.Drawing.Bitmap" + ]; + + [Serializable] + [TypeForwardedFrom("System.ForwardAssembly")] + public struct SimpleTestData + { + public int X { get; set; } + public int Y { get; set; } + } +} diff --git a/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs b/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs index e539ee8b30c..111592d0c52 100644 --- a/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Private.Windows; + namespace System.Windows.Forms.Tests.Serialization; // NB: doesn't require thread affinity @@ -13,6 +15,8 @@ public void EnsureSerializableAttribute() typeof(ListViewItem).Assembly, new HashSet { + // This is needed for OLE JSON serialization support + { typeof(JsonData<>).FullName }, // This state is serialized to communicate to the native control { typeof(AxHost.State).FullName }, // Following classes are participating in resx serialization scenarios. diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormattedObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormattedObjectTests.cs index e548248d552..18d459391e0 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormattedObjectTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormattedObjectTests.cs @@ -6,9 +6,13 @@ using System.ComponentModel; using System.Drawing; using System.Formats.Nrbf; +using System.Reflection.Metadata; +using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters.Binary; +using System.Text.Json; using System.Windows.Forms.BinaryFormat; using System.Windows.Forms.Nrbf; +using static System.Windows.Forms.TestUtilities.DataObjectTestHelpers; namespace System.Private.Windows.Core.BinaryFormat.Tests; @@ -16,6 +20,93 @@ public class WinFormsBinaryFormattedObjectTests { private static readonly Attribute[] s_visible = [DesignerSerializationVisibilityAttribute.Visible]; + [Fact] + public void BinaryFormattedObject_NonJsonData_RemainsSerialized() + { + SimpleTestData testData = new() { X = 1, Y = 1 }; + SerializationRecord format = testData.SerializeAndDecode(); + ITypeResolver resolver = new DataObject.Composition.Binder(typeof(SimpleTestData), resolver: null, legacyMode: false); + format.TryGetObjectFromJson(resolver, out _).Should().BeFalse(); + } + + [Fact] + public void BinaryFormattedObject_JsonData_RoundTrip() + { + SimpleTestData testData = new() { X = 1, Y = 1 }; + + JsonData json = new() + { + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(testData) + }; + + using MemoryStream stream = new(); + WinFormsBinaryFormatWriter.WriteJsonData(stream, json); + + stream.Position = 0; + SerializationRecord binary = NrbfDecoder.Decode(stream); + binary.TypeName.AssemblyName!.FullName.Should().Be(IJsonData.CustomAssemblyName); + ITypeResolver resolver = new DataObject.Composition.Binder(typeof(SimpleTestData), resolver: null, legacyMode: false); + binary.TryGetObjectFromJson(resolver, out _).Should().BeTrue(); + binary.TryGetObjectFromJson(resolver, out object? result).Should().BeTrue(); + SimpleTestData deserialized = result.Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(testData); + } + + [Fact] + public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() + { + SimpleTestData testData = new() { X = 1, Y = 1 }; + JsonData data = new() + { + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(testData) + }; + + using MemoryStream stream = new(); + WinFormsBinaryFormatWriter.WriteJsonData(stream, data); + stream.Position = 0; + + using BinaryFormatterScope scope = new(enable: true); +#pragma warning disable SYSLIB0011 // Type or member is obsolete + BinaryFormatter binaryFormatter = new() { Binder = new JsonDataTestDataBinder() }; +#pragma warning restore SYSLIB0011 + SimpleTestData deserialized = binaryFormatter.Deserialize(stream).Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(testData); + } + + [Serializable] + private struct ReplicatedJsonData : IObjectReference + { + public byte[] JsonBytes { get; set; } + + public string InnerTypeAssemblyQualifiedName { get; set; } + + public readonly object GetRealObject(StreamingContext context) + { + object? result = null; + if (TypeName.TryParse(InnerTypeAssemblyQualifiedName, out TypeName? innerTypeName) + && innerTypeName.Matches(typeof(SimpleTestData).ToTypeName())) + { + result = JsonSerializer.Deserialize(JsonBytes); + } + + return result ?? throw new InvalidOperationException(); + } + } + + private class JsonDataTestDataBinder : SerializationBinder + { + public override Type? BindToType(string assemblyName, string typeName) + { + if (assemblyName == "System.Private.Windows.VirtualJson" + && typeName == "System.Private.Windows.JsonData") + { + return typeof(ReplicatedJsonData); + } + + throw new InvalidOperationException(); + } + } + [Fact] public void BinaryFormattedObject_Bitmap_FromBinaryFormatter() { @@ -76,7 +167,7 @@ public void BinaryFormattedObject_ImageListStreamer_FromBinaryFormatter() using ImageListStreamer stream = sourceList.ImageStream!; SerializationRecord rootRecord = stream.SerializeAndDecode(); - Formats.Nrbf.ClassRecord root = rootRecord.Should().BeAssignableTo().Subject; + ClassRecord root = rootRecord.Should().BeAssignableTo().Subject; root.TypeName.FullName.Should().Be(typeof(ImageListStreamer).FullName); root.TypeName.AssemblyName!.FullName.Should().Be(typeof(WinFormsBinaryFormatWriter).Assembly.FullName); root.GetArrayRecord("Data")!.Should().BeAssignableTo>(); diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs index 09d4ff46d9b..7477f1d4b1f 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ClipboardTests.cs @@ -7,11 +7,14 @@ using System.ComponentModel; using System.Drawing; using System.Drawing.Imaging; +using System.Formats.Nrbf; using System.Reflection.Metadata; using System.Runtime.InteropServices; +using System.Text.Json; using System.Windows.Forms.Primitives; using Windows.Win32.System.Ole; using static System.Windows.Forms.Tests.BinaryFormatUtilitiesTests; +using static System.Windows.Forms.TestUtilities.DataObjectTestHelpers; using Com = Windows.Win32.System.Com; using ComTypes = System.Runtime.InteropServices.ComTypes; @@ -875,4 +878,275 @@ public void Clipboard_TryGetOffsetArray() // Can't decode the root record, thus can't validate the T. tryGetData.Should().Throw(); } + + [WinFormsTheory] + [InlineData("")] + [InlineData(null)] + [InlineData(" ")] + public void Clipboard_SetDataAsJson_EmptyFormat_Throws(string? format) + { + Action action = () => Clipboard.SetDataAsJson(format!, 1); + action.Should().Throw(); + } + + [WinFormsFact] + public void Clipboard_SetDataAsJson_DataObject_Throws() + { + Clipboard.Clear(); + string format = "format"; + Action action = () => Clipboard.SetDataAsJson(format, new DataObject()); + action.Should().Throw(); + Action clipboardSet2 = () => Clipboard.SetDataAsJson(format, new DerivedDataObject()); + clipboardSet2.Should().NotThrow(); + } + + [WinFormsFact] + public void Clipboard_SetDataAsJson_WithGeneric_ReturnsExpected() + { + Clipboard.Clear(); + List generic1 = []; + string format = "list"; + Clipboard.SetDataAsJson(format, generic1); + DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType().Subject; + Action a = () => dataObject.TestAccessor().Dynamic._innerData.GetData(format); + // We do not handle List + a.Should().Throw(); + Clipboard.TryGetData(format, out List? points).Should().BeTrue(); + points.Should().BeEquivalentTo(generic1); + + List generic2 = []; + Clipboard.SetDataAsJson(format, generic2); + dataObject = Clipboard.GetDataObject().Should().BeOfType().Subject; + a = () => dataObject.TestAccessor().Dynamic._innerData.GetData(format); + a.Should().NotThrow(); + Clipboard.TryGetData(format, out List? intList).Should().BeTrue(); + intList.Should().BeEquivalentTo(generic2); + } + + [WinFormsFact] + public void Clipboard_SetDataAsJson_ReturnsExpected() + { + Clipboard.Clear(); + SimpleTestData testData = new() { X = 1, Y = 1 }; + + Clipboard.SetDataAsJson("testDataFormat", testData); + IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + dataObject.GetDataPresent("testDataFormat").Should().BeTrue(); + dataObject.TryGetData("testDataFormat", out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); + } + + [WinFormsFact] + public void Clipboard_SetDataAsJson_GetData() + { + Clipboard.Clear(); + SimpleTestData testData = new() { X = 1, Y = 1 }; + // Note that this simulates out of process scenario. + Clipboard.SetDataAsJson("test", testData); + Action a = () => Clipboard.GetData("test"); + a.Should().Throw(); + + using BinaryFormatterInClipboardDragDropScope scope = new(enable: true); + a.Should().Throw(); + + using BinaryFormatterScope scope2 = new(enable: true); + Clipboard.GetData("test").Should().BeOfType(); + } + + [WinFormsTheory] + [BoolData] + public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) + { + Clipboard.Clear(); + SimpleTestData testData = new() { X = 1, Y = 1 }; + + DataObject dataObject = new(); + dataObject.SetDataAsJson("testDataFormat", testData); + + Clipboard.SetDataObject(dataObject, copy); + ITypedDataObject returnedDataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + returnedDataObject.TryGetData("testDataFormat", out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); + } + + [WinFormsTheory] + [BoolData] + public void Clipboard_SetDataObject_WithMultipleData_ReturnsExpected(bool copy) + { + Clipboard.Clear(); + SimpleTestData testData1 = new() { X = 1, Y = 1 }; + SimpleTestData testData2 = new() { Y = 2, X = 2 }; + DataObject data = new(); + data.SetDataAsJson("testData1", testData1); + data.SetDataAsJson("testData2", testData2); + data.SetData("Mystring", "test"); + Clipboard.SetDataObject(data, copy); + + Clipboard.TryGetData("testData1", out SimpleTestData deserializedTestData1).Should().BeTrue(); + deserializedTestData1.Should().BeEquivalentTo(testData1); + Clipboard.TryGetData("testData2", out SimpleTestData deserializedTestData2).Should().BeTrue(); + deserializedTestData2.Should().BeEquivalentTo(testData2); + Clipboard.TryGetData("Mystring", out string? deserializedString).Should().BeTrue(); + deserializedString.Should().Be("test"); + } + + [WinFormsFact] + public unsafe void Clipboard_Deserialize_FromStream_Manually() + { + // This test demonstrates how a user can manually deserialize JsonData that has been serialized onto + // the clipboard from stream. This may need to be done if type JsonData does not exist in the .NET version + // the user is utilizing. + Clipboard.Clear(); + SimpleTestData testData = new() { X = 1, Y = 1 }; + Clipboard.SetDataAsJson("testFormat", testData); + + // Manually retrieve the serialized stream. + ComTypes.IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Which; + ComTypes.FORMATETC formatetc = new() + { + cfFormat = (short)DataFormats.GetFormat("testFormat").Id, + dwAspect = ComTypes.DVASPECT.DVASPECT_CONTENT, + lindex = -1, + tymed = ComTypes.TYMED.TYMED_HGLOBAL + }; + dataObject.GetData(ref formatetc, out ComTypes.STGMEDIUM medium); + HGLOBAL hglobal = (HGLOBAL)medium.unionmember; + Stream stream; + try + { + void* buffer = PInvokeCore.GlobalLock(hglobal); + int size = (int)PInvokeCore.GlobalSize(hglobal); + byte[] bytes = new byte[size]; + Marshal.Copy((nint)buffer, bytes, 0, size); + // this comes from DataObject.Composition.s_serializedObjectID + int index = 16; + stream = new MemoryStream(bytes, index, bytes.Length - index); + } + finally + { + PInvokeCore.GlobalUnlock(hglobal); + } + + stream.Should().NotBeNull(); + // Use NrbfDecoder to decode the stream and rehydrate the type. + SerializationRecord record = NrbfDecoder.Decode(stream); + ClassRecord types = record.Should().BeAssignableTo().Which; + types.HasMember("k__BackingField").Should().BeTrue(); + types.HasMember("k__BackingField").Should().BeTrue(); + SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Subject; + string innerTypeAssemblyQualifiedName = types.GetRawValue("k__BackingField").Should().BeOfType().Subject; + TypeName.TryParse(innerTypeAssemblyQualifiedName, out TypeName? innerTypeName).Should().BeTrue(); + TypeName checkedResult = innerTypeName.Should().BeOfType().Subject; + // These should not be the same since we take TypeForwardedFromAttribute name into account during serialization, + // which changes the assembly name. + typeof(SimpleTestData).AssemblyQualifiedName.Should().NotBe(checkedResult.AssemblyQualifiedName); + typeof(SimpleTestData).ToTypeName().Matches(checkedResult).Should().BeTrue(); + + JsonSerializer.Deserialize(byteData.GetArray(), typeof(SimpleTestData)).Should().BeEquivalentTo(testData); + } + + [WinFormsFact] + public void Clipboard_SurfaceJsonError() + { + using Font font = new("Microsoft Sans Serif", emSize: 10); + byte[] serialized = JsonSerializer.SerializeToUtf8Bytes(font); + Action a1 = () => JsonSerializer.Deserialize(serialized); + a1.Should().Throw(); + + string format = "font"; + Clipboard.SetDataAsJson(format, font); + Action a2 = () => Clipboard.TryGetData(format, out Font? _); + a2.Should().Throw(); + + DataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + Action a3 = () => dataObject.TryGetData(format, out Font? _); + a3.Should().Throw(); + } + + [WinFormsTheory] + [BoolData] + public void Clipboard_CustomDataObject_AvoidBinaryFormatter(bool copy) + { + string format = "customFormat"; + SimpleTestData data = new() { X = 1, Y = 1 }; + Clipboard.SetData(format, data); + // BinaryFormatter not enabled. + Clipboard.GetData(format).Should().BeOfType(); + + Clipboard.Clear(); + JsonDataObject jsonDataObject = new(); + jsonDataObject.SetData(format, data); + + Clipboard.SetDataObject(jsonDataObject, copy); + + if (copy) + { + // Pasting in different process has been simulated. Manual Json deserialization will need to occur. + IDataObject received = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + received.Should().NotBe(jsonDataObject); + byte[] jsonBytes = Clipboard.GetData(format).Should().BeOfType().Subject; + JsonSerializer.Deserialize(jsonBytes, typeof(SimpleTestData)).Should().BeEquivalentTo(data); + } + else + { + JsonDataObject received = Clipboard.GetDataObject().Should().BeOfType().Subject; + received.Should().Be(jsonDataObject); + received.Deserialize(format).Should().BeEquivalentTo(data); + } + } + + // Test class to demonstrate one way to write IDataObject to totally control serialization/deserialization + // and have it avoid BinaryFormatter. + private class JsonDataObject : IDataObject, ComTypes.IDataObject + { + private readonly Dictionary _formatToJson = []; + private readonly Dictionary _formatToTypeName = []; + + public T? Deserialize(string format) + { + if (typeof(T).AssemblyQualifiedName != _formatToTypeName[format]) + { + return default; + } + + return JsonSerializer.Deserialize(_formatToJson[format]); + } + + public object GetData(string format, bool autoConvert) => GetData(format); + public object GetData(string format) => _formatToJson[format]; + public object GetData(Type format) => throw new NotImplementedException(); + public bool GetDataPresent(string format, bool autoConvert) => throw new NotImplementedException(); + public bool GetDataPresent(string format) => _formatToJson.ContainsKey(format); + public bool GetDataPresent(Type format) => throw new NotImplementedException(); + public string[] GetFormats(bool autoConvert) => throw new NotImplementedException(); + public string[] GetFormats() => _formatToJson.Keys.ToArray(); + public void SetData(string format, bool autoConvert, object? data) => throw new NotImplementedException(); + public void SetData(string format, object? data) + { + _formatToTypeName.Add(format, data!.GetType().AssemblyQualifiedName!); + _formatToJson.Add(format, JsonSerializer.SerializeToUtf8Bytes(data)); + } + + public void SetData(Type format, object? data) => throw new NotImplementedException(); + public void SetData(object? data) => throw new NotImplementedException(); + + public int DAdvise(ref ComTypes.FORMATETC pFormatetc, ComTypes.ADVF advf, ComTypes.IAdviseSink adviseSink, out int connection) => throw new NotImplementedException(); + public void DUnadvise(int connection) => throw new NotImplementedException(); + public int EnumDAdvise(out ComTypes.IEnumSTATDATA? enumAdvise) => throw new NotImplementedException(); + public ComTypes.IEnumFORMATETC EnumFormatEtc(ComTypes.DATADIR direction) => throw new NotImplementedException(); + public int GetCanonicalFormatEtc(ref ComTypes.FORMATETC formatIn, out ComTypes.FORMATETC formatOut) => throw new NotImplementedException(); + public void SetData(ref ComTypes.FORMATETC formatIn, ref ComTypes.STGMEDIUM medium, bool release) => throw new NotImplementedException(); + public void GetData(ref ComTypes.FORMATETC format, out ComTypes.STGMEDIUM medium) => throw new NotImplementedException(); + public void GetDataHere(ref ComTypes.FORMATETC format, ref ComTypes.STGMEDIUM medium) => throw new NotImplementedException(); + public int QueryGetData(ref ComTypes.FORMATETC format) => throw new NotImplementedException(); + } + + private class DerivedDataObject : DataObject { } + + [WinFormsFact] + public void Clipboard_SetDataAsJson_NullData_Throws() + { + Action clipboardSet = () => Clipboard.SetDataAsJson("format", null!); + clipboardSet.Should().Throw(); + } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ControlTests.Methods.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ControlTests.Methods.cs index 923287ddf1e..f116b7e62e1 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ControlTests.Methods.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/ControlTests.Methods.cs @@ -1856,7 +1856,16 @@ public void Control_DoDragDrop_InvokeWithHandle_ReturnsNone(object data, DragDro public void Control_DoDragDrop_NullData_ThrowsArgumentNullException() { using Control control = new(); - Assert.Throws("data", () => control.DoDragDrop(null, DragDropEffects.All)); + Action dragDrop = () => control.DoDragDrop(null, DragDropEffects.All); + dragDrop.Should().Throw("data"); + } + + [WinFormsFact] + public void Control_DoDragDropAsJson_NullData_ThrowsArgumentNullException() + { + using Control control = new(); + Action dragDrop = () => control.DoDragDropAsJson(null, DragDropEffects.Copy); + dragDrop.Should().Throw("data"); } public static IEnumerable DrawToBitmap_TestData() diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs index 91b20cba277..72155782228 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs @@ -5,15 +5,20 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Drawing; +using System.Private.Windows; using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Windows.Forms.TestUtilities; using Moq; using Windows.Win32.System.Ole; using Com = Windows.Win32.System.Com; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; using Point = System.Drawing.Point; +using SimpleTestData = System.Windows.Forms.TestUtilities.DataObjectTestHelpers.SimpleTestData; namespace System.Windows.Forms.Tests; @@ -2700,14 +2705,14 @@ public unsafe void DataObject_MockRoundTrip_OutData_IsSame(object data) dynamic controlAccessor = typeof(Control).TestAccessor().Dynamic; var dropTargetAccessor = typeof(DropTarget).TestAccessor(); - IComDataObject inData = controlAccessor.CreateRuntimeDataObjectForDrag(data); - if (data is DataObject) + DataObject inData = controlAccessor.CreateRuntimeDataObjectForDrag(data); + if (data is CustomDataObject) { - inData.Should().BeSameAs(data); + inData.Should().NotBeSameAs(data); } else { - inData.Should().NotBeSameAs(data); + inData.Should().BeSameAs(data); } using var inDataPtr = ComHelpers.GetComScope(inData); @@ -2715,6 +2720,33 @@ public unsafe void DataObject_MockRoundTrip_OutData_IsSame(object data) outData.Should().BeSameAs(data); } + public static IEnumerable DataObjectWithJsonMockRoundTripData() + { + yield return new object[] { new DataObject() }; + yield return new object[] { new DerivedDataObject() }; + } + + [WinFormsTheory] + [MemberData(nameof(DataObjectWithJsonMockRoundTripData))] + public unsafe void DataObject_WithJson_MockRoundTrip_OutData_IsSame(DataObject data) + { + dynamic controlAccessor = typeof(Control).TestAccessor().Dynamic; + var dropTargetAccessor = typeof(DropTarget).TestAccessor(); + + Point point = new() { X = 1, Y = 1 }; + data.SetDataAsJson("point", point); + DataObject inData = controlAccessor.CreateRuntimeDataObjectForDrag(data); + inData.Should().BeSameAs(data); + + using var inDataPtr = ComHelpers.GetComScope(inData); + IDataObject outData = dropTargetAccessor.CreateDelegate()(inDataPtr); + outData.Should().BeSameAs(data); + ITypedDataObject typedOutData = outData.Should().BeAssignableTo().Subject; + typedOutData.GetDataPresent("point").Should().BeTrue(); + typedOutData.TryGetData("point", out Point deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(point); + } + [WinFormsFact] public unsafe void DataObject_StringData_MockRoundTrip_IsWrapped() { @@ -2722,7 +2754,7 @@ public unsafe void DataObject_StringData_MockRoundTrip_IsWrapped() dynamic accessor = typeof(Control).TestAccessor().Dynamic; var dropTargetAccessor = typeof(DropTarget).TestAccessor(); - IComDataObject inData = accessor.CreateRuntimeDataObjectForDrag(testString); + DataObject inData = accessor.CreateRuntimeDataObjectForDrag(testString); inData.Should().BeAssignableTo(); using var inDataPtr = ComHelpers.GetComScope(inData); @@ -2738,7 +2770,7 @@ public unsafe void DataObject_IDataObject_MockRoundTrip_IsWrapped() dynamic accessor = typeof(Control).TestAccessor().Dynamic; var dropTargetAccessor = typeof(DropTarget).TestAccessor(); - IComDataObject inData = accessor.CreateRuntimeDataObjectForDrag(data); + DataObject inData = accessor.CreateRuntimeDataObjectForDrag(data); inData.Should().BeAssignableTo(); inData.Should().NotBeSameAs(data); @@ -2754,7 +2786,7 @@ public unsafe void DataObject_ComTypesIDataObject_MockRoundTrip_IsWrapped() dynamic accessor = typeof(Control).TestAccessor().Dynamic; var dropTargetAccessor = typeof(DropTarget).TestAccessor(); - IComDataObject inData = accessor.CreateRuntimeDataObjectForDrag(data); + DataObject inData = accessor.CreateRuntimeDataObjectForDrag(data); inData.Should().NotBeSameAs(data); inData.Should().BeAssignableTo(); @@ -2801,4 +2833,217 @@ public unsafe void DataObject_Native_GetData_SerializationFailure() // Validate that HGLOBAL had been freed when handling an error. medium.hGlobal.IsNull.Should().BeTrue(); } + + [WinFormsFact] + public void DataObject_SetDataAsJson_DataObject_Throws() + { + string format = "format"; + DataObject dataObject = new(); + Action action = () => dataObject.SetDataAsJson(format, new DataObject()); + action.Should().Throw(); + + Action dataObjectSet2 = () => dataObject.SetDataAsJson(format, new DerivedDataObject()); + dataObjectSet2.Should().NotThrow(); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_ReturnsExpected() + { + SimpleTestData testData = new() { X = 1, Y = 1 }; + DataObject dataObject = new(); + string format = "testData"; + dataObject.SetDataAsJson(format, testData); + dataObject.GetDataPresent(format).Should().BeTrue(); + dataObject.TryGetData(format, out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_Wrapped_ReturnsExpected() + { + SimpleTestData testData = new() { X = 1, Y = 1 }; + DataObject dataObject = new(); + string format = "testData"; + dataObject.SetDataAsJson(format, testData); + DataObject wrapped = new(dataObject); + wrapped.GetDataPresent(format).Should().BeTrue(); + wrapped.TryGetData(format, out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_MultipleData_ReturnsExpected() + { + SimpleTestData testData1 = new() { X = 1, Y = 1 }; + SimpleTestData testData2 = new() { Y = 2, X = 2 }; + DataObject data = new(); + data.SetDataAsJson("testData1", testData1); + data.SetDataAsJson("testData2", testData2); + data.SetData("Mystring", "test"); + + data.TryGetData("testData1", out SimpleTestData deserializedTestData1).Should().BeTrue(); + deserializedTestData1.Should().BeEquivalentTo(testData1); + data.TryGetData("testData2", out SimpleTestData deserializedTestData2).Should().BeTrue(); + deserializedTestData2.Should().BeEquivalentTo(testData2); + data.TryGetData("Mystring", out string deserializedString).Should().BeTrue(); + deserializedString.Should().Be("test"); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_CustomJsonConverter_ReturnsExpected() + { + // This test demonstrates one way users can achieve custom JSON serialization behavior if the + // default JSON serialization behavior that is used in SetDataAsJson APIs is not enough for their scenario. + Font font = new("Consolas", emSize: 10); + WeatherForecast forecast = new() + { + Date = DateTimeOffset.Now, + TemperatureCelsius = 25, + Summary = "Hot", + Font = font + }; + + DataObject dataObject = new(); + dataObject.SetDataAsJson("custom", forecast); + dataObject.TryGetData("custom", out WeatherForecast deserialized).Should().BeTrue(); + string offsetFormat = "MM/dd/yyyy"; + deserialized.Date.ToString(offsetFormat).Should().Be(forecast.Date.ToString(offsetFormat)); + deserialized.TemperatureCelsius.Should().Be(forecast.TemperatureCelsius); + deserialized.Summary.Should().Be($"{forecast.Summary} custom!"); + deserialized.Font.Should().Be(font); + } + + [JsonConverter(typeof(WeatherForecastJsonConverter))] + private class WeatherForecast + { + public DateTimeOffset Date { get; set; } + public int TemperatureCelsius { get; set; } + public string Summary { get; set; } + public Font Font { get; set; } + } + + private class WeatherForecastJsonConverter : JsonConverter + { + public override WeatherForecast Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + WeatherForecast result = new(); + string fontFamily = null; + int size = -1; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + if (fontFamily is null || size == -1) + { + throw new JsonException(); + } + + result.Font = new(fontFamily, size); + return result; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string propertyName = reader.GetString(); + + reader.Read(); + + switch (propertyName) + { + case nameof(WeatherForecast.Date): + result.Date = DateTimeOffset.ParseExact(reader.GetString(), "MM/dd/yyyy", null); + break; + case nameof(WeatherForecast.TemperatureCelsius): + result.TemperatureCelsius = reader.GetInt32(); + break; + case nameof(WeatherForecast.Summary): + result.Summary = reader.GetString(); + break; + case nameof(Font.FontFamily): + fontFamily = reader.GetString(); + break; + case nameof(Font.Size): + size = reader.GetInt32(); + break; + default: + throw new JsonException(); + } + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, WeatherForecast value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(nameof(WeatherForecast.Date), value.Date.ToString("MM/dd/yyyy")); + writer.WriteNumber(nameof(WeatherForecast.TemperatureCelsius), value.TemperatureCelsius); + writer.WriteString(nameof(WeatherForecast.Summary), $"{value.Summary} custom!"); + writer.WriteString(nameof(Font.FontFamily), value.Font.FontFamily.Name); + writer.WriteNumber(nameof(Font.Size), value.Font.Size); + writer.WriteEndObject(); + } + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_NullData_Throws() + { + DataObject dataObject = new(); + Action dataObjectSet = () => dataObject.SetDataAsJson(null); + dataObjectSet.Should().Throw(); + } + + [WinFormsTheory] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] + public void DataObject_SetDataAsJson_RestrictedFormats_NotJsonSerialized(string format) + { + DataObject dataObject = new(); + dataObject.SetDataAsJson(format, 1); + object storedData = dataObject.TestAccessor().Dynamic._innerData.GetData(format); + storedData.Should().NotBeAssignableTo(); + dataObject.GetData(format).Should().Be(1); + } + + [WinFormsTheory] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] + public void DataObject_SetDataAsJson_NonRestrictedFormat_NotJsonSerialized(string format) + { + DataObject data = new(); + data.SetDataAsJson(format, 1); + object storedData = data.TestAccessor().Dynamic._innerData.GetData(format); + storedData.Should().NotBeAssignableTo(); + data.GetData(format).Should().Be(1); + } + + [WinFormsTheory] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] + public void DataObject_SetDataAsJson_NonRestrictedFormat_JsonSerialized(string format) + { + DataObject data = new(); + SimpleTestData testData = new() { X = 1, Y = 1 }; + data.SetDataAsJson(format, testData); + object storedData = data.TestAccessor().Dynamic._innerData.GetData(format); + storedData.Should().BeOfType>(); + + // We don't expose JsonData in public legacy API + data.GetData(format).Should().BeNull(); + + // For the clipboard, we don't expose JsonData either for in proc scenarios. + Clipboard.SetDataObject(data, copy: false); + Clipboard.GetData(format).Should().BeNull(); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_WrongType_ReturnsNull() + { + DataObject dataObject = new(); + dataObject.SetDataAsJson("test", new SimpleTestData() { X = 1, Y = 1 }); + dataObject.TryGetData("test", out Bitmap data).Should().BeFalse(); + data.Should().BeNull(); + } } diff --git a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/NativeToWinFormsAdapterTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/NativeToWinFormsAdapterTests.cs index 90fc5ff19d7..746d1d373ca 100644 --- a/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/NativeToWinFormsAdapterTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/NativeToWinFormsAdapterTests.cs @@ -6,54 +6,13 @@ using System.Drawing; using System.Reflection.Metadata; using System.Text.RegularExpressions; +using System.Windows.Forms.TestUtilities; using Com = Windows.Win32.System.Com; namespace System.Windows.Forms.Tests; public unsafe partial class NativeToWinFormsAdapterTests { - public static TheoryData UnboundedFormat() => - [ - DataFormats.Serializable, - "something custom" - ]; - - // These formats contain only known types. - public static TheoryData UndefinedRestrictedFormat() => - [ - DataFormats.CommaSeparatedValue, - DataFormats.Dib, - DataFormats.Dif, - DataFormats.PenData, - DataFormats.Riff, - DataFormats.Tiff, - DataFormats.WaveAudio, - DataFormats.SymbolicLink, - DataFormats.EnhancedMetafile, - DataFormats.MetafilePict, - DataFormats.Palette - ]; - - public static TheoryData BitmapFormat() => - [ - DataFormats.Bitmap, - "System.Drawing.Bitmap" - ]; - - // These formats set and get strings by accessing HGLOBAL directly. - public static TheoryData StringFormat() => - [ - DataFormats.Text, - DataFormats.UnicodeText, - DataFormats.StringConstant, - DataFormats.Rtf, - DataFormats.Html, - DataFormats.OemText, - DataFormats.FileDrop, - "FileName", - "FileNameW" - ]; - [GeneratedRegex(@"{[0-9]}")] private static partial Regex PlaceholdersPattern(); @@ -66,7 +25,7 @@ public static TheoryData StringFormat() => "BinaryFormatter serialization and deserialization are disabled within this application. See https://aka.ms/binaryformatter for more information."; [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_AsObject_Primitive_Success(string format) { DataObject native = new(); @@ -82,7 +41,7 @@ public void TryGetData_AsObject_Primitive_Success(string format) } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_AsObject_Primitive_RequiresResolver(string format) { DataObject native = new(); @@ -100,8 +59,8 @@ public void TryGetData_AsObject_Primitive_RequiresResolver(string format) } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_AsObject_Primitive_InvalidTypeFormatCombination(string format) { DataObject native = new(); @@ -126,7 +85,7 @@ private static (DataObject dataObject, TestData value) SetDataObject(string form } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_AsObject_Custom_RequiresResolver(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -136,7 +95,7 @@ public void TryGetData_AsObject_Custom_RequiresResolver(string format) } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_AsObject_Custom_FormatterEnabled_RequiresResolver(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -149,8 +108,8 @@ public void TryGetData_AsObject_Custom_FormatterEnabled_RequiresResolver(string } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_AsObject_Custom_InvalidTypeFormatCombination(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -162,7 +121,7 @@ public void TryGetData_AsObject_Custom_InvalidTypeFormatCombination(string forma } [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_AsObject_Custom_ReturnsNotSupportedException(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -174,7 +133,7 @@ public void TryGetData_AsObject_Custom_ReturnsNotSupportedException(string forma } [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_AsObject_Custom_FormatterEnabled_ReturnsFalse(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -187,7 +146,7 @@ public void TryGetData_AsObject_Custom_FormatterEnabled_ReturnsFalse(string form } [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_AsInterface_ListOfPrimitives_Success(string format) { DataObject native = new(); @@ -200,7 +159,7 @@ public void TryGetData_AsInterface_ListOfPrimitives_Success(string format) } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_AsInterface_ListOfPrimitives_RequiresResolver(string format) { DataObject native = new(); @@ -215,8 +174,8 @@ public void TryGetData_AsInterface_ListOfPrimitives_RequiresResolver(string form } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_AsInterface_ListOfPrimitives_InvalidTypeFormatCombination(string format) { DataObject native = new(); @@ -230,8 +189,8 @@ public void TryGetData_AsInterface_ListOfPrimitives_InvalidTypeFormatCombination } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_AsConcreteType_ListOfPrimitives_Success(string format) { DataObject native = new(); @@ -244,8 +203,8 @@ public void TryGetData_AsConcreteType_ListOfPrimitives_Success(string format) } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_AsConcreteType_ListOfPrimitives_InvalidTypeFormatCombination(string format) { DataObject native = new(); @@ -259,7 +218,7 @@ public void TryGetData_AsConcreteType_ListOfPrimitives_InvalidTypeFormatCombinat } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_AsConcreteType_Custom_FormatterEnabled_RequiresResolver(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -272,7 +231,7 @@ public void TryGetData_AsConcreteType_Custom_FormatterEnabled_RequiresResolver(s } [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_AsConcreteType_Custom_FormatterEnabled_ReturnsFalse(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -286,7 +245,7 @@ public void TryGetData_AsConcreteType_Custom_FormatterEnabled_ReturnsFalse(strin } [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_AsConcreteType_Custom_FormattersDisabled_ReturnFalse(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -298,8 +257,8 @@ public void TryGetData_AsConcreteType_Custom_FormattersDisabled_ReturnFalse(stri } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_AsConcreteType_Custom_InvalidTypeFormatCombination(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -310,7 +269,7 @@ public void TryGetData_AsConcreteType_Custom_InvalidTypeFormatCombination(string } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_WithResolver_AsConcreteType_Custom_FormatterEnabled_Success(string format) { (DataObject dataObject, TestData value) = SetDataObject(format); @@ -323,8 +282,8 @@ public void TryGetData_WithResolver_AsConcreteType_Custom_FormatterEnabled_Succe } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_WithResolver_AsConcreteType_Custom_InvalidTypeFormatCombination(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -335,8 +294,8 @@ public void TryGetData_WithResolver_AsConcreteType_Custom_InvalidTypeFormatCombi } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_WithResolver_AsConcreteType_Custom_FormatterEnabled_InvalidTypeFormatCombination(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -350,7 +309,7 @@ public void TryGetData_WithResolver_AsConcreteType_Custom_FormatterEnabled_Inval } [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_WithResolver_AsConcreteType_Custom_FormatterDisabledException(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -361,7 +320,7 @@ public void TryGetData_WithResolver_AsConcreteType_Custom_FormatterDisabledExcep } [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_AsAbstract_Custom_FormatterEnabled_ReturnFalse(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -375,8 +334,8 @@ public void TryGetData_AsAbstract_Custom_FormatterEnabled_ReturnFalse(string for } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_AsAbstract_Custom_InvalidTypeFormatCombination(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -387,7 +346,7 @@ public void TryGetData_AsAbstract_Custom_InvalidTypeFormatCombination(string for } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_AsAbstract_Custom_RequiresResolver(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -400,7 +359,7 @@ public void TryGetData_AsAbstract_Custom_RequiresResolver(string format) } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_AsAbstract_Custom_FormatterEnabled_RequiresResolver(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -413,7 +372,7 @@ public void TryGetData_AsAbstract_Custom_FormatterEnabled_RequiresResolver(strin } [WinFormsTheory] - [MemberData(nameof(UndefinedRestrictedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UndefinedRestrictedFormat))] public void TryGetData_WithResolver_AsAbstract_Custom_FormatterEnabled_ReturnFalse(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -427,7 +386,7 @@ public void TryGetData_WithResolver_AsAbstract_Custom_FormatterEnabled_ReturnFal } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_WithResolver_AsAbstract_Custom_FormatterEnabled_Success(string format) { (DataObject dataObject, TestData value) = SetDataObject(format); @@ -440,8 +399,8 @@ public void TryGetData_WithResolver_AsAbstract_Custom_FormatterEnabled_Success(s } [WinFormsTheory] - [MemberData(nameof(StringFormat))] - [MemberData(nameof(BitmapFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] public void TryGetData_WithResolver_AsAbstract_Custom_InvalidTypeFormatCombination(string format) { (DataObject dataObject, TestData _) = SetDataObject(format); @@ -453,7 +412,7 @@ public void TryGetData_WithResolver_AsAbstract_Custom_InvalidTypeFormatCombinati } [WinFormsTheory] - [MemberData(nameof(UnboundedFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] public void TryGetData_AsConcrete_NotSerializable_FormatterEnabled_ReturnFalse(string format) { DataObject native = new(); @@ -469,6 +428,77 @@ public void TryGetData_AsConcrete_NotSerializable_FormatterEnabled_ReturnFalse(s data.Should().BeNull(); } + [WinFormsFact] + public void SetDataAsJson_TryGetData_Requires_Resolver() + { + SimpleTestData value = new("text", new(10, 10)); + + DataObject native = new(); + native.SetDataAsJson("test", value); + + DataObject dataObject = new(ComHelpers.GetComPointer(native)); + Action a = () => dataObject.TryGetData("test", out SimpleTestDataBase? _); + a.Should().Throw(); + // This requires a resolver because this simulates out of process scenario with a type that is not intrinsic and not supported. + dataObject.TryGetData("test", SimpleTestData.Resolver, autoConvert: false, out SimpleTestDataBase? deserialized).Should().BeTrue(); + var deserializedChecked = deserialized.Should().BeOfType().Subject; + deserializedChecked.Text.Should().Be(value.Text); + } + + [WinFormsTheory] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.StringFormat))] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.BitmapFormat))] + public void SetDataAsJson_InvalidTypeFormatCombination(string format) + { + SimpleTestData value = new("text", new(10, 10)); + + DataObject native = new(); + native.SetDataAsJson(format, value); + + DataObject dataObject = new(ComHelpers.GetComPointer(native)); + Action a = () => dataObject.TryGetData(format, out SimpleTestDataBase? _); + + a.Should().Throw() + .WithMessage(expectedWildcardPattern: InvalidTypeFormatCombinationMessage); + } + + private class SimpleTestDataBase + { + public string? Text { get; set; } + } + + private class SimpleTestData : SimpleTestDataBase + { + public SimpleTestData(string text, Point point) + { + Text = text; + Point = point; + } + + public Point Point { get; set; } + + public static Type Resolver(TypeName typeName) + { + (string name, Type type)[] allowedTypes = + [ + (typeof(SimpleTestData).FullName!, typeof(SimpleTestData)), + (typeof(SimpleTestDataBase).FullName!, typeof(SimpleTestDataBase)), + ]; + + string fullName = typeName.FullName; + foreach (var (name, type) in allowedTypes) + { + // Namespace-qualified type name. + if (name == fullName) + { + return type; + } + } + + throw new NotSupportedException($"Can't resolve {fullName}"); + } + } + // This class does not have [Serializable] attribute, serialization stream will be corrupt. private class NotSerializableData {