Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cdb926b
Add JSON serialization support for OLE
lonitra Oct 11, 2024
75c7fb7
Add more details for custom json serialization
lonitra Oct 11, 2024
65c12b8
Add custom assembly name to binary serialization to identify JsonData…
lonitra Oct 11, 2024
fbfb08b
Remove exception string from resources for now
lonitra Oct 11, 2024
cdfdfbf
Remove IObjectReference and add test to Json serialized data should J…
lonitra Oct 11, 2024
fca51fa
Add Clipboard.SetDataAsJson for VB
lonitra Oct 11, 2024
1843033
add test deserializing replicated JsonData using BinaryFormatter
lonitra Oct 11, 2024
7471064
Update ClipboardProxy.SetDataAsJson to inherit docs from Clipboard.Se…
lonitra Oct 11, 2024
8faa96a
Fix rebase errors
lonitra Oct 11, 2024
ba7a79d
Update some code based on nrbf changes
lonitra Oct 15, 2024
03f320c
Add DataObject.SetDataAsJson overloads and fix build
lonitra Oct 15, 2024
18a0cae
Add test for deserializing json data from stream manually
lonitra Oct 16, 2024
19d40d5
Change SetDataAsJson to take type object
lonitra Oct 18, 2024
255dd30
Add convenience methods for Json serializing for drag/drop scenarios
lonitra Oct 18, 2024
b1a3c11
Add test for custom JSON serialization behavior
lonitra Oct 18, 2024
e11ae44
re-introduce T for trimming
lonitra Oct 21, 2024
4c88d2a
use Single() and add Font to test type
lonitra Oct 23, 2024
d1ab9d5
Update exception condition
lonitra Oct 23, 2024
ceb3e0e
Surface any JSON deserialization errors as NotSupportedException
lonitra Oct 23, 2024
6e88b60
Add test avoiding BinaryFormatter with custom DataObject
lonitra Oct 25, 2024
7a9f6b9
close stream in test Clipboard_Deserialize_FromStream_Manually
lonitra Oct 31, 2024
8aaccea
Move clipboard test in ClipboardTests.cs
lonitra Nov 5, 2024
7fbe0df
try different format name
lonitra Nov 5, 2024
ced8e9d
update guidance example
lonitra Nov 6, 2024
1be5622
Block setting null data
lonitra Nov 8, 2024
03c7176
Add RequiresUnreferencedCode attribute
lonitra Nov 14, 2024
4968656
- React to TryGetData additions
lonitra Dec 13, 2024
75a3180
- Make JsonData.InnerTypeAssemblyQualifiedName readonly
lonitra Dec 13, 2024
db5be00
remove unnecessary using
lonitra Dec 13, 2024
c3e7d65
Add comments
lonitra Dec 13, 2024
98d5eed
Merge remote-tracking branch 'upstream/main' into jsonclipboard
lonitra Dec 13, 2024
494b7fc
- Avoid exposing JsonData<T> in legacy GetData
lonitra Dec 14, 2024
59465ad
Merge remote-tracking branch 'upstream/main' into jsonclipboard
lonitra Dec 16, 2024
79b3365
Block JSON serializing intrinsic types
lonitra Dec 16, 2024
dbebbe7
Address feedback
lonitra Dec 16, 2024
3d32e84
clear clipboard for test and change format to improve reliability
lonitra Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ Namespace Microsoft.VisualBasic.MyServices
Clipboard.SetImage(image)
End Sub

''' <inheritdoc cref="Clipboard.SetDataAsJson(Of T)(String, T)"/>
Public Sub SetDataAsJson(Of T)(format As String, data As T)
Clipboard.SetDataAsJson(format, data)
End Sub

''' <summary>
''' Saves the passed in <see cref="String" /> to the clipboard.
''' </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down
6 changes: 6 additions & 0 deletions src/System.Windows.Forms/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ static System.Windows.Forms.DataObjectExtensions.TryGetData<T>(this System.Windo
static System.Windows.Forms.DataObjectExtensions.TryGetData<T>(this System.Windows.Forms.IDataObject! dataObject, string! format, bool autoConvert, out T data) -> bool
static System.Windows.Forms.DataObjectExtensions.TryGetData<T>(this System.Windows.Forms.IDataObject! dataObject, string! format, out T data) -> bool
static System.Windows.Forms.DataObjectExtensions.TryGetData<T>(this System.Windows.Forms.IDataObject! dataObject, string! format, System.Func<System.Reflection.Metadata.TypeName!, System.Type!>! resolver, bool autoConvert, out T data) -> bool
static System.Windows.Forms.Clipboard.SetDataAsJson<T>(string! format, T data) -> void
System.Windows.Forms.Control.DoDragDropAsJson<T>(T data, System.Windows.Forms.DragDropEffects allowedEffects) -> System.Windows.Forms.DragDropEffects
System.Windows.Forms.Control.DoDragDropAsJson<T>(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<T>(out T data) -> bool
System.Windows.Forms.DataObject.TryGetData<T>(string! format, bool autoConvert, out T data) -> bool
Expand Down Expand Up @@ -37,3 +40,6 @@ virtual System.Windows.Forms.DataObject.TryGetDataCore<T>(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<System.Windows.Forms.DialogResult>!
[WFO5002]System.Windows.Forms.Form.ShowDialogAsync(System.Windows.Forms.IWin32Window! owner) -> System.Threading.Tasks.Task<System.Windows.Forms.DialogResult>!
System.Windows.Forms.DataObject.SetDataAsJson<T>(string! format, bool autoConvert, T data) -> void
System.Windows.Forms.DataObject.SetDataAsJson<T>(string! format, T data) -> void
System.Windows.Forms.DataObject.SetDataAsJson<T>(T data) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down Expand Up @@ -51,6 +52,20 @@ public static void WriteImageListStreamer(Stream stream, ImageListStreamer strea
new ArraySinglePrimitive<byte>(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<byte>(objectId: 3, jsonData.JsonBytes).Write(writer);
}

/// <summary>
/// Writes the given <paramref name="value"/> if supported.
/// </summary>
Expand All @@ -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;
}
Expand Down
49 changes: 45 additions & 4 deletions src/System.Windows.Forms/src/System/Windows/Forms/Control.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4780,6 +4780,49 @@ internal virtual void DisposeAxControls()
}
}

/// <inheritdoc cref="DoDragDropAsJson{T}(T, DragDropEffects, Bitmap?, Point, bool)"/>
public DragDropEffects DoDragDropAsJson<T>(T data, DragDropEffects allowedEffects) =>
DoDragDropAsJson(data, allowedEffects, dragImage: null, cursorOffset: default, useDefaultDragImage: false);

/// <summary>
/// Begins a drag operation.
/// </summary>
/// <param name="data">The data being dragged.</param>
/// <param name="allowedEffects">determine which drag operations can occur.</param>
/// <param name="dragImage">The drag image bitmap.</param>
/// <param name="cursorOffset">The drag image cursor offset.</param>
/// <param name="useDefaultDragImage">Indicating whether a layered window drag image is used.</param>
/// <returns>A value from the <see cref="DragDropEffects"/> enumeration that represents the final effect that was performed during the drag-and-drop operation.</returns>
/// <exception cref="InvalidOperationException">
/// If <paramref name="data"/> is a non derived <see cref="DataObject"/>. This is for better error reporting as <see cref="DataObject"/> will serialize empty. If <see cref="DataObject"/>
/// needs to be used to start a drag operation, use <see cref="DataObject.SetDataAsJson{T}(T)"/> to JSON serialize the data being held within the <paramref name="data"/>,
/// then pass the <paramref name="data"/> to <see cref="DoDragDrop(object, DragDropEffects)"/>.
/// </exception>
/// <remarks>
/// <para>
/// The data will be stored as JSON if the data is not an intrinsically handled type. Otherwise, it will be stored
/// the same as <see cref="DoDragDrop(object, DragDropEffects)"/>.
/// </para>
/// </remarks>
public DragDropEffects DoDragDropAsJson<T>(
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);
}

/// <summary>
/// Begins a drag operation. The allowedEffects determine which
/// drag operations can occur. If the drag operation needs to interop
Expand All @@ -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.
/// </summary>
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);

/// <summary>
/// Begins a drag operation. The <paramref name="allowedEffects"/> determine which drag operations can occur.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -53,6 +58,53 @@ public static bool TryGetBitmap(this SerializationRecord record, out object? bit
return true;
}

/// <summary>
/// Tries to deserialize this object if it was serialized as JSON.
/// </summary>
/// <returns>
/// <see langword="true"/> if the data was serialized as JSON. Otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="SerializationException">If the data was supposed to be our <see cref="JsonData{T}"/>, but was serialized incorrectly./></exception>
/// <exception cref="NotSupportedException">If an exception occurred while JSON deserializing.</exception>
public static bool TryGetObjectFromJson<T>(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("<JsonBytes>k__BackingField") is not SZArrayRecord<byte> byteData
|| types.GetRawValue("<InnerTypeAssemblyQualifiedName>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<T>(byteData.GetArray());
}
catch (Exception ex)
{
// loni TODO: localize string
throw new NotSupportedException("Failed to deserialize JSON data.", ex);
}

return true;
}

/// <summary>
/// Try to get a supported object. This supports common types used in WinForms that do not have type converters.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -513,6 +514,62 @@ public static void SetData(string format, object data)
SetDataObject(new DataObject(format, data), copy: true);
}

/// <summary>
/// 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.
/// </summary>
/// <exception cref="ArgumentException">
/// <see langword="null"/>, empty string, or whitespace was passed as the format.
/// </exception>
/// <exception cref="InvalidOperationException">
/// If <paramref name="data"/> is a non derived <see cref="DataObject"/>. This is for better error reporting as <see cref="DataObject"/> will serialize as empty.
/// If <see cref="DataObject"/> needs to be placed on the clipboard, use <see cref="DataObject.SetDataAsJson{T}(string, T)"/>
/// to JSON serialize the data to be held in the <paramref name="data"/>, then set the <paramref name="data"/>
/// onto the clipboard via <see cref="SetDataObject(object)"/>.
/// </exception>
/// <remarks>
/// <para>
/// If your data is an intrinsically handled type such as primitives, string, or Bitmap
/// and you are using a custom format or <see cref="DataFormats.Serializable"/>,
/// it is recommended to use the <see cref="SetData(string, object?)"/> APIs to avoid unnecessary overhead.
/// </para>
/// <para>
/// The default behavior of <see cref="JsonSerializer"/> is used to serialize the data.
/// </para>
/// <para>
/// See
/// <see href="https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/how-to#serialization-behavior"/>
/// and <see href="https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/reflection-vs-source-generation#metadata-collection"/>
/// for more details on default <see cref="JsonSerializer"/> behavior.
/// </para>
/// <para>
/// If custom JSON serialization behavior is needed, manually JSON serialize the data and then use <see cref="SetData"/>
/// to save the data onto the clipboard, or create a custom <see cref="Text.Json.Serialization.JsonConverter"/>, attach the
/// <see cref="Text.Json.Serialization.JsonConverterAttribute"/>, and then recall this method.
/// See <see href="https://learn.microsoft.com/dotnet/standard/serialization/system-text-json/converters-how-to"/> for more details
/// on custom converters for JSON serialization.
/// </para>
/// </remarks>
[RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")]
public static void SetDataAsJson<T>(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);
}

/// <summary>
/// Clears the Clipboard and then adds a collection of file names in the <see cref="DataFormats.FileDrop"/> format.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> struct, type matches or not
// run IsAssignable in the JSON method
if (record.TryGetObjectFromJson(binder.GetType, out object? data))
if (record.TryGetObjectFromJson<T>((ITypeResolver)binder, out object? data))
{
return data;
}
#endif

if (!TypeNameIsAssignableToType(record.TypeName, type, (ITypeResolver)binder))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(
Expand Down
Loading