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