From cdb926b618484d8e7b07ae66afc8d4ca038cd4c7 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:00:21 -0700 Subject: [PATCH 01/34] Add JSON serialization support for OLE --- .../src/PublicAPI.Unshipped.txt | 2 + .../src/Resources/SR.resx | 2 + .../src/Resources/xlf/SR.cs.xlf | 5 ++ .../src/Resources/xlf/SR.de.xlf | 5 ++ .../src/Resources/xlf/SR.es.xlf | 5 ++ .../src/Resources/xlf/SR.fr.xlf | 5 ++ .../src/Resources/xlf/SR.it.xlf | 5 ++ .../src/Resources/xlf/SR.ja.xlf | 5 ++ .../src/Resources/xlf/SR.ko.xlf | 5 ++ .../src/Resources/xlf/SR.pl.xlf | 5 ++ .../src/Resources/xlf/SR.pt-BR.xlf | 5 ++ .../src/Resources/xlf/SR.ru.xlf | 5 ++ .../src/Resources/xlf/SR.tr.xlf | 5 ++ .../src/Resources/xlf/SR.zh-Hans.xlf | 5 ++ .../src/Resources/xlf/SR.zh-Hant.xlf | 5 ++ .../WinFormsBinaryFormatWriter.cs | 20 ++++- .../src/System/Windows/Forms/Control.cs | 6 +- .../src/System/Windows/Forms/OLE/Clipboard.cs | 48 +++++++++++ .../System/Windows/Forms/OLE/DataObject.cs | 41 ++++++++- .../src/System/Windows/Forms/OLE/JsonData.cs | 35 ++++++++ .../ComDisabledTests/ClipboardComTests.cs | 29 +++++++ .../ComDisabledTests/DataObjectComTests.cs | 21 +++++ .../WinFormsBinaryFormattedObjectTests.cs | 49 +++++++++++ .../System/Windows/Forms/ClipboardTests.cs | 71 ++++++++++++++++ .../System/Windows/Forms/DataObjectTests.cs | 83 +++++++++++++++++-- 25 files changed, 459 insertions(+), 13 deletions(-) create mode 100644 src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index d778a18227d..caab3d4bdbb 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -37,3 +37,5 @@ 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! +static System.Windows.Forms.Clipboard.SetDataAsJson(string! format, T data) -> void +System.Windows.Forms.DataObject.SetDataAsJson(string! format, T data) -> void diff --git a/src/System.Windows.Forms/src/Resources/SR.resx b/src/System.Windows.Forms/src/Resources/SR.resx index ce9389b6ab6..c2b633bd193 100644 --- a/src/System.Windows.Forms/src/Resources/SR.resx +++ b/src/System.Windows.Forms/src/Resources/SR.resx @@ -7035,5 +7035,7 @@ Stack trace where the illegal operation occurred was: The specified type '{0}' is not compatible with the specified format '{1}'. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf index b4ef4f50050..9b8a019c931 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf @@ -6059,6 +6059,11 @@ Trasování zásobníku, kde došlo k neplatné operaci: Informace o velikosti objektu MonthCalendar nelze načíst. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. Název třídy okna je neplatný. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf index e90fbea9383..2e5e6cf7bc5 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf @@ -6059,6 +6059,11 @@ Stapelüberwachung, in der der unzulässige Vorgang auftrat: Die MonthCalendar-Größeninformationen können nicht abgerufen werden. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. Ungültiger Fensterklassenname. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf index 355b00404ec..4d355d4f988 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf @@ -6059,6 +6059,11 @@ El seguimiento de la pila donde tuvo lugar la operación no válida fue: No se puede recuperar la información sobre el tamaño del elemento MonthCalendar. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. Nombre de clase de ventana no válido. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf index 752526ed75d..78caf5f415c 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf @@ -6059,6 +6059,11 @@ Cette opération non conforme s'est produite sur la trace de la pile : Les informations de taille de MonthCalendar ne peuvent pas être extraites. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. Nom de classe de fenêtre non valide. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf index 0602e028466..60eba923852 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf @@ -6059,6 +6059,11 @@ Traccia dello stack da cui si è verificata l'operazione non valida: Impossibile recuperare le informazioni sulla dimensione di MonthCalendar. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. Nome classe finestra non valido. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf index 4671afce77f..e1366384600 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf @@ -6059,6 +6059,11 @@ Stack trace where the illegal operation occurred was: MonthCalendar のサイズ情報を取得できません。 + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. ウィンドウ クラス名が有効ではありません。 diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf index bd2a5011a94..2662a4cdf0a 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf @@ -6059,6 +6059,11 @@ Stack trace where the illegal operation occurred was: MonthCalendar 크기 정보를 검색할 수 없습니다. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. 창 클래스 이름이 잘못되었습니다. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf index f0056df6350..780aee5b933 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf @@ -6059,6 +6059,11 @@ Stos śledzenia, w którym wystąpiła zabroniona operacja: Nie można pobrać informacji o rozmiarze elementu MonthCalendar. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. Nieprawidłowa nazwa klasy okna. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf index 076035afa09..13b60040647 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf @@ -6059,6 +6059,11 @@ Rastreamento de pilha em que a operação ilegal ocorreu: As informações de tamanho de MonthCalendar não podem ser recuperadas. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. O nome da classe de janela é inválido. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf index 8798818d7b2..d75a9ce5709 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf @@ -6060,6 +6060,11 @@ Stack trace where the illegal operation occurred was: Не удается извлечь информацию о размере MonthCalendar. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. Недопустимое имя класса Window. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf index 4beade99ede..bf78d70990f 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf @@ -6059,6 +6059,11 @@ Geçersiz işlemin gerçekleştiği yığın izi: MonthCalendar boyut bilgisi alınamıyor. + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. Pencere sınıf adı geçerli değil. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf index af8d7230e1c..22aad820b21 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf @@ -6059,6 +6059,11 @@ Stack trace where the illegal operation occurred was: 无法检索 MonthCalendar 的大小信息。 + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. 窗口类名无效。 diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf index 4db8d5ba43f..97a757bd3c9 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf @@ -6059,6 +6059,11 @@ Stack trace where the illegal operation occurred was: 無法擷取 MonthCalendar 的大小資訊。 + + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. + + Window class name is not valid. 無效的視窗類別名稱。 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..321f9b9c15d 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 @@ -16,7 +16,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 +51,19 @@ public static void WriteImageListStreamer(Stream stream, ImageListStreamer strea new ArraySinglePrimitive(3, data).Write(writer); } + public static void WriteJsonData(Stream stream, JsonData jsonData) + { + using BinaryFormatWriterScope writer = new(stream); + new BinaryLibrary(2, s_currentWinFormsFullName).Write(writer); + new ClassWithMembersAndTypes( + new ClassInfo(1, jsonData.TypeFullName, [$"<{nameof(jsonData.JsonBytes)}>k__BackingField"]), + libraryId: 2, + new MemberTypeInfo[] { new(BinaryType.PrimitiveArray, PrimitiveType.Byte) }, + new MemberReference(3)).Write(writer); + + new ArraySinglePrimitive(3, jsonData.JsonBytes).Write(writer); + } + /// /// Writes the given if supported. /// @@ -72,6 +85,11 @@ static bool Write(Stream stream, object value) WriteBitmap(stream, bitmap); return true; } + else if (value is JsonData 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..0cb45b51842 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs @@ -4788,10 +4788,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/OLE/Clipboard.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/Clipboard.cs index 25c0c437b2b..49a2c7f9d63 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,53 @@ 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 using JSON serialization. + /// + /// + /// If null, empty, or whitespace is passed as the format. + /// + /// + /// If is passed in as the data. cannot be JSON serialized meaningfully. + /// 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 . + /// + /// + /// + /// The default behavior of is used to serialize the data. + /// + /// + /// See + /// + /// and + /// for more details on default behavior. + /// + /// + /// If custom behavior is needed, manually JSON serialize the data and then use + /// to save the data onto the clipboard. + /// + /// + public static void SetDataAsJson(string format, T data) + { + if (string.IsNullOrWhiteSpace(format.OrThrowIfNull())) + { + throw new ArgumentException(SR.DataObjectWhitespaceEmptyFormatNotAllowed, nameof(format)); + } + + if (data is DataObject) + { + throw new InvalidOperationException(string.Format(SR.InvalidTypeForSetDataAsJson, nameof(SetDataObject))); + } + + JsonData jsonData = new() + { + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) + }; + + SetDataObject(new DataObject(format, jsonData), 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.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs index 5fcf8502256..af392bdaa28 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 @@ -6,6 +6,8 @@ using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; +using System.Runtime.Serialization; +using System.Text.Json; using Com = Windows.Win32.System.Com; using ComTypes = System.Runtime.InteropServices.ComTypes; @@ -100,13 +102,50 @@ internal IDataObject TryUnwrapInnerIDataObject() /// internal IDataObject? OriginalIDataObject => _innerData.OriginalIDataObject; + /// + /// Stores the specified data and its associated format in this instance as JSON. + /// + /// + /// If is passed in as the data. cannot be JSON serialized meaningfully. + /// If needs to be set, use + /// + /// + /// + /// The default behavior of is used to serialize the data. + /// + /// + /// See + /// + /// and + /// for more details on default behavior. + /// + /// + /// If custom behavior is needed, manually JSON serialize the data and then use + /// + /// + public void SetDataAsJson(string format, T data) + { + if (data is DataObject) + { + throw new InvalidOperationException(string.Format(SR.InvalidTypeForSetDataAsJson, nameof(SetData))); + } + + SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + } + #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? data = ((IDataObject)_innerData).GetData(format, autoConvert); +#pragma warning disable SYSLIB0050 // Type or member is obsolete + return data is JsonData jsonData && jsonData is IObjectReference reference ? reference.GetRealObject(default) : data; +#pragma warning restore SYSLIB0050 + } [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..ef0b2b9846c --- /dev/null +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -0,0 +1,35 @@ +// 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.Serialization; +using System.Text.Json; + +namespace System.Windows.Forms; + +/// +/// Wrapper which contains JSON serialized data along with the JSON data's original type information +/// to be deserialized later. +/// +[Serializable] +#pragma warning disable SYSLIB0050 // Type or member is obsolete +internal struct JsonData : IObjectReference, JsonData +#pragma warning restore SYSLIB0050 +{ + public byte[] JsonBytes { get; set; } + + public readonly string TypeFullName => $"{typeof(JsonData).FullName}`1[[{typeof(T).AssemblyQualifiedName}]]"; + + public readonly object GetRealObject(StreamingContext context) => + JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? 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 JsonData +{ + byte[] JsonBytes { get; set; } + + string TypeFullName { get; } +} diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs index 10de5202c2a..d85b5459040 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs @@ -19,4 +19,33 @@ public void Clipboard_SetText_InvokeString_GetReturnsExpected() Clipboard.GetText().Should().Be("text"); Clipboard.ContainsText().Should().BeTrue(); } + + [WinFormsFact] + public void Clipboard_SetDataAsJson_ReturnsExpected() + { + Point point = new() { X = 1, Y = 1 }; + + Clipboard.SetDataAsJson("point", point); + IDataObject? dataObject = Clipboard.GetDataObject(); + dataObject.Should().NotBeNull(); + dataObject!.GetDataPresent("point").Should().BeTrue(); + Point deserialized = dataObject.GetData("point").Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(point); + } + + [WinFormsTheory] + [BoolData] + public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) + { + Point point = new() { X = 1, Y = 1 }; + + DataObject dataObject = new(); + dataObject.SetDataAsJson("point", point); + + Clipboard.SetDataObject(dataObject, copy); + IDataObject? returnedDataObject = Clipboard.GetDataObject(); + returnedDataObject.Should().NotBeNull(); + Point deserialized = returnedDataObject!.GetData("point").Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(point); + } } diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs index 58923843eda..99567ab1def 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Drawing; using System.Runtime.InteropServices.ComTypes; using Com = Windows.Win32.System.Com; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; @@ -11,6 +12,26 @@ 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(); + + Point point = new() { X = 1, Y = 1 }; + DataObject data = new(); + 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); + outData.GetDataPresent("point").Should().BeTrue(); + outData.GetData("point").Should().BeOfType().Which.Should().BeEquivalentTo(point); + } + [WinFormsFact] public void DataObject_CustomIDataObject_MockRoundTrip() { 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..47bc5190d6d 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 @@ -9,6 +9,7 @@ using System.Runtime.Serialization.Formatters.Binary; using System.Windows.Forms.BinaryFormat; using System.Windows.Forms.Nrbf; +using System.Text.Json; namespace System.Private.Windows.Core.BinaryFormat.Tests; @@ -16,6 +17,54 @@ public class WinFormsBinaryFormattedObjectTests { private static readonly Attribute[] s_visible = [DesignerSerializationVisibilityAttribute.Visible]; + [Fact] + public void BinaryFormattedObject_JsonData_FromBinaryFormatter() + { + Point point = new() { X = 1, Y = 1 }; + + JsonData json = new() + { + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), + }; + + BinaryFormattedObject format = json.SerializeAndParse(); + ClassWithMembersAndTypes root = format.RootRecord.Should().BeOfType().Subject; + root.Name.Should().Be(typeof(JsonData).FullName); + root[$"<{nameof(json.JsonBytes)}>k__BackingField"].Should().BeOfType(); + format.TryGetObjectFromJson(out object? result).Should().BeTrue(); + result.Should().BeOfType(); + result.Should().BeEquivalentTo(point); + } + + [Fact] + public void BinaryFormattedObject_NonJsonData_RemainsSerialized() + { + Point point = new() { X = 1, Y = 1 }; + BinaryFormattedObject format = point.SerializeAndParse(); + format.TryGetObjectFromJson(out _).Should().BeFalse(); + } + + [Fact] + public void BinaryFormattedObject_JsonData_RoundTrip() + { + Point point = new() { X = 1, Y = 1 }; + + JsonData json = new() + { + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), + }; + + using MemoryStream stream = new(); + WinFormsBinaryFormatWriter.WriteJsonData(stream, json); + + stream.Position = 0; + BinaryFormattedObject binary = new(stream); + + binary.TryGetObjectFromJson(out object? result).Should().BeTrue(); + Point deserialized = result.Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(point); + } + [Fact] public void BinaryFormattedObject_Bitmap_FromBinaryFormatter() { 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..5c8aa61dbe9 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 @@ -495,6 +495,14 @@ public void Clipboard_SetImage_NullImage_ThrowsArgumentNullException() action.Should().Throw().WithParameterName("image"); } + [WinFormsFact] + public void Clipboard_SetText_InvokeString_GetReturnsExpected() + { + Clipboard.SetText("text"); + Assert.Equal("text", Clipboard.GetText()); + Assert.True(Clipboard.ContainsText()); + } + [WinFormsTheory] [EnumData] public void Clipboard_SetText_InvokeStringTextDataFormat_GetReturnsExpected(TextDataFormat format) @@ -875,4 +883,67 @@ 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() + { + Action action = () => Clipboard.SetDataAsJson("format", new DataObject()); + action.Should().Throw(); + } + + [WinFormsFact] + public void Clipboard_SetDataAsJson_ReturnsExpected() + { + Point point = new() { X = 1, Y = 1 }; + + Clipboard.SetDataAsJson("point", point); + IDataObject? dataObject = Clipboard.GetDataObject(); + dataObject.Should().NotBeNull(); + dataObject!.GetDataPresent("point").Should().BeTrue(); + Point deserialized = dataObject.GetData("point").Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(point); + } + + [WinFormsTheory] + [BoolData] + public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) + { + Point point = new() { X = 1, Y = 1 }; + + DataObject dataObject = new(); + dataObject.SetDataAsJson("point", point); + + Clipboard.SetDataObject(dataObject, copy); + IDataObject? returnedDataObject = Clipboard.GetDataObject(); + returnedDataObject.Should().NotBeNull(); + Point deserialized = returnedDataObject!.GetData("point").Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(point); + } + + [WinFormsTheory] + [BoolData] + public void Clipboard_SetDataObject_WithMultipleData_ReturnsExpected(bool copy) + { + Point point1 = new() { X = 1, Y = 1 }; + Point point2 = new() { Y = 2, X = 2 }; + DataObject data = new(); + data.SetDataAsJson("point1", point1); + data.SetDataAsJson("point2", point2); + data.SetData("Mystring", "test"); + Clipboard.SetDataObject(data, copy); + + Clipboard.GetData("point1").Should().BeOfType().Which.Should().BeEquivalentTo(point1); + Clipboard.GetData("point2").Should().BeOfType().Which.Should().BeEquivalentTo(point2); + Clipboard.GetData("Mystring").Should().Be("test"); + } } 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..da34c35ff1e 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 @@ -2700,14 +2700,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 +2715,31 @@ 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); + outData.GetDataPresent("point").Should().BeTrue(); + outData.GetData("point").Should().BeOfType().Which.Should().BeEquivalentTo(point); + } + [WinFormsFact] public unsafe void DataObject_StringData_MockRoundTrip_IsWrapped() { @@ -2722,7 +2747,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 +2763,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 +2779,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 +2826,48 @@ 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() + { + DataObject dataObject = new(); + Action action = () => dataObject.SetDataAsJson("format", new DataObject()); + action.Should().Throw(); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_ReturnsExpected() + { + Point point = new() { X = 1, Y = 1 }; + DataObject dataObject = new(); + dataObject.SetDataAsJson("point", point); + dataObject.GetDataPresent("point").Should().BeTrue(); + dataObject.GetData("point").Should().BeOfType().Which.Should().BeEquivalentTo(point); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_Wrapped_ReturnsExpected() + { + Point point = new() { X = 1, Y = 1 }; + DataObject dataObject = new(); + dataObject.SetDataAsJson("point", point); + DataObject wrapped = new(dataObject); + wrapped.GetDataPresent("point").Should().BeTrue(); + wrapped.GetData("point").Should().BeOfType().Which.Should().BeEquivalentTo(point); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_MultipleData_ReturnsExpected() + { + Point point1 = new() { X = 1, Y = 1 }; + Point point2 = new() { Y = 2, X = 2 }; + DataObject data = new(); + data.SetDataAsJson("point1", point1); + data.SetDataAsJson("point2", point2); + data.SetData("Mystring", "test"); + + data.GetData("point1").Should().BeOfType().Which.Should().BeEquivalentTo(point1); + data.GetData("point2").Should().BeOfType().Which.Should().BeEquivalentTo(point2); + data.GetData("Mystring").Should().Be("test"); + } } From 75c7fb7551b1620e53026b2a6222b0755b5dc30d Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:00:41 -0700 Subject: [PATCH 02/34] Add more details for custom json serialization --- .../src/System/Windows/Forms/OLE/Clipboard.cs | 7 +++++-- .../src/System/Windows/Forms/OLE/DataObject.cs | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) 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 49a2c7f9d63..a746ad4410f 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 @@ -537,8 +537,11 @@ public static void SetData(string format, object data) /// for more details on default behavior. /// /// - /// If custom behavior is needed, manually JSON serialize the data and then use - /// to save the data onto the clipboard. + /// 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. /// /// public static void SetDataAsJson(string format, T data) 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 af392bdaa28..8f53ad5ab5d 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 @@ -120,7 +120,11 @@ internal IDataObject TryUnwrapInnerIDataObject() /// for more details on default behavior. /// /// - /// If custom behavior is needed, manually JSON serialize the data and then use + /// 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. /// /// public void SetDataAsJson(string format, T data) From 65c12b8d5b3af86cf66007bae0415c20c4ddc6bc Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:01:25 -0700 Subject: [PATCH 03/34] Add custom assembly name to binary serialization to identify JsonData without needing to know generic type --- .../WinFormsBinaryFormatWriter.cs | 11 +++++---- .../src/System/Windows/Forms/OLE/Clipboard.cs | 1 + .../System/Windows/Forms/OLE/DataObject.cs | 3 ++- .../src/System/Windows/Forms/OLE/JsonData.cs | 8 +++---- .../UnitTests/SerializableAttributeTests.cs | 4 ++++ .../WinFormsBinaryFormattedObjectTests.cs | 23 +++---------------- 6 files changed, 20 insertions(+), 30 deletions(-) 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 321f9b9c15d..a98d036c2fb 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; @@ -51,17 +52,17 @@ public static void WriteImageListStreamer(Stream stream, ImageListStreamer strea new ArraySinglePrimitive(3, data).Write(writer); } - public static void WriteJsonData(Stream stream, JsonData jsonData) + public static void WriteJsonData(Stream stream, IJsonData jsonData) { using BinaryFormatWriterScope writer = new(stream); - new BinaryLibrary(2, s_currentWinFormsFullName).Write(writer); + new BinaryLibrary(libraryId: 2, "System.Private.Windows.VirtualJson").Write(writer); new ClassWithMembersAndTypes( new ClassInfo(1, jsonData.TypeFullName, [$"<{nameof(jsonData.JsonBytes)}>k__BackingField"]), libraryId: 2, new MemberTypeInfo[] { new(BinaryType.PrimitiveArray, PrimitiveType.Byte) }, - new MemberReference(3)).Write(writer); + new MemberReference(idRef: 3)).Write(writer); - new ArraySinglePrimitive(3, jsonData.JsonBytes).Write(writer); + new ArraySinglePrimitive(objectId: 3, jsonData.JsonBytes).Write(writer); } /// @@ -85,7 +86,7 @@ static bool Write(Stream stream, object value) WriteBitmap(stream, bitmap); return true; } - else if (value is JsonData jsonData) + else if (value is IJsonData jsonData) { WriteJsonData(stream, jsonData); return true; 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 a746ad4410f..a2fbe1d750f 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 @@ -5,6 +5,7 @@ using System.Drawing; using System.Formats.Nrbf; using System.Reflection.Metadata; +using System.Private.Windows; using System.Runtime.InteropServices; using System.Runtime.Serialization.Formatters.Binary; using System.Text.Json; 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 8f53ad5ab5d..ddae3563632 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,6 +4,7 @@ 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.Runtime.Serialization; @@ -147,7 +148,7 @@ public void SetDataAsJson(string format, T data) { object? data = ((IDataObject)_innerData).GetData(format, autoConvert); #pragma warning disable SYSLIB0050 // Type or member is obsolete - return data is JsonData jsonData && jsonData is IObjectReference reference ? reference.GetRealObject(default) : data; + return data is IJsonData jsonData && jsonData is IObjectReference reference ? reference.GetRealObject(default) : data; #pragma warning restore SYSLIB0050 } 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 index ef0b2b9846c..51391e860c5 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -4,7 +4,7 @@ using System.Runtime.Serialization; using System.Text.Json; -namespace System.Windows.Forms; +namespace System.Private.Windows; /// /// Wrapper which contains JSON serialized data along with the JSON data's original type information @@ -12,12 +12,12 @@ namespace System.Windows.Forms; /// [Serializable] #pragma warning disable SYSLIB0050 // Type or member is obsolete -internal struct JsonData : IObjectReference, JsonData +internal struct JsonData : IObjectReference, IJsonData #pragma warning restore SYSLIB0050 { public byte[] JsonBytes { get; set; } - public readonly string TypeFullName => $"{typeof(JsonData).FullName}`1[[{typeof(T).AssemblyQualifiedName}]]"; + public readonly string TypeFullName => $"{typeof(JsonData).FullName}"; public readonly object GetRealObject(StreamingContext context) => JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); @@ -27,7 +27,7 @@ public readonly object GetRealObject(StreamingContext context) => /// 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 JsonData +internal interface IJsonData { byte[] JsonBytes { 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 47bc5190d6d..8715fa53310 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 @@ -7,6 +7,7 @@ using System.Drawing; using System.Formats.Nrbf; using System.Runtime.Serialization.Formatters.Binary; +using System.Text.Json; using System.Windows.Forms.BinaryFormat; using System.Windows.Forms.Nrbf; using System.Text.Json; @@ -17,25 +18,6 @@ public class WinFormsBinaryFormattedObjectTests { private static readonly Attribute[] s_visible = [DesignerSerializationVisibilityAttribute.Visible]; - [Fact] - public void BinaryFormattedObject_JsonData_FromBinaryFormatter() - { - Point point = new() { X = 1, Y = 1 }; - - JsonData json = new() - { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), - }; - - BinaryFormattedObject format = json.SerializeAndParse(); - ClassWithMembersAndTypes root = format.RootRecord.Should().BeOfType().Subject; - root.Name.Should().Be(typeof(JsonData).FullName); - root[$"<{nameof(json.JsonBytes)}>k__BackingField"].Should().BeOfType(); - format.TryGetObjectFromJson(out object? result).Should().BeTrue(); - result.Should().BeOfType(); - result.Should().BeEquivalentTo(point); - } - [Fact] public void BinaryFormattedObject_NonJsonData_RemainsSerialized() { @@ -59,7 +41,8 @@ public void BinaryFormattedObject_JsonData_RoundTrip() stream.Position = 0; BinaryFormattedObject binary = new(stream); - + binary[2].Should().BeOfType().Which + .LibraryName.Should().Be("System.Private.Windows.VirtualJson"); binary.TryGetObjectFromJson(out object? result).Should().BeTrue(); Point deserialized = result.Should().BeOfType().Which; deserialized.Should().BeEquivalentTo(point); From fbfb08bbf59525e8f68722d4acc99014f2ab962c Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:02:05 -0700 Subject: [PATCH 04/34] Remove exception string from resources for now --- src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf | 5 ----- src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf | 5 ----- .../src/System/Windows/Forms/OLE/Clipboard.cs | 3 ++- .../src/System/Windows/Forms/OLE/DataObject.cs | 3 ++- 15 files changed, 4 insertions(+), 67 deletions(-) diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf index 9b8a019c931..b4ef4f50050 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.cs.xlf @@ -6059,11 +6059,6 @@ Trasování zásobníku, kde došlo k neplatné operaci: Informace o velikosti objektu MonthCalendar nelze načíst. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. Název třídy okna je neplatný. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf index 2e5e6cf7bc5..e90fbea9383 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.de.xlf @@ -6059,11 +6059,6 @@ Stapelüberwachung, in der der unzulässige Vorgang auftrat: Die MonthCalendar-Größeninformationen können nicht abgerufen werden. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. Ungültiger Fensterklassenname. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf index 4d355d4f988..355b00404ec 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.es.xlf @@ -6059,11 +6059,6 @@ El seguimiento de la pila donde tuvo lugar la operación no válida fue: No se puede recuperar la información sobre el tamaño del elemento MonthCalendar. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. Nombre de clase de ventana no válido. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf index 78caf5f415c..752526ed75d 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.fr.xlf @@ -6059,11 +6059,6 @@ Cette opération non conforme s'est produite sur la trace de la pile : Les informations de taille de MonthCalendar ne peuvent pas être extraites. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. Nom de classe de fenêtre non valide. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf index 60eba923852..0602e028466 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.it.xlf @@ -6059,11 +6059,6 @@ Traccia dello stack da cui si è verificata l'operazione non valida: Impossibile recuperare le informazioni sulla dimensione di MonthCalendar. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. Nome classe finestra non valido. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf index e1366384600..4671afce77f 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ja.xlf @@ -6059,11 +6059,6 @@ Stack trace where the illegal operation occurred was: MonthCalendar のサイズ情報を取得できません。 - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. ウィンドウ クラス名が有効ではありません。 diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf index 2662a4cdf0a..bd2a5011a94 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ko.xlf @@ -6059,11 +6059,6 @@ Stack trace where the illegal operation occurred was: MonthCalendar 크기 정보를 검색할 수 없습니다. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. 창 클래스 이름이 잘못되었습니다. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf index 780aee5b933..f0056df6350 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.pl.xlf @@ -6059,11 +6059,6 @@ Stos śledzenia, w którym wystąpiła zabroniona operacja: Nie można pobrać informacji o rozmiarze elementu MonthCalendar. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. Nieprawidłowa nazwa klasy okna. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf index 13b60040647..076035afa09 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.pt-BR.xlf @@ -6059,11 +6059,6 @@ Rastreamento de pilha em que a operação ilegal ocorreu: As informações de tamanho de MonthCalendar não podem ser recuperadas. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. O nome da classe de janela é inválido. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf index d75a9ce5709..8798818d7b2 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.ru.xlf @@ -6060,11 +6060,6 @@ Stack trace where the illegal operation occurred was: Не удается извлечь информацию о размере MonthCalendar. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. Недопустимое имя класса Window. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf index bf78d70990f..4beade99ede 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.tr.xlf @@ -6059,11 +6059,6 @@ Geçersiz işlemin gerçekleştiği yığın izi: MonthCalendar boyut bilgisi alınamıyor. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. Pencere sınıf adı geçerli değil. diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf index 22aad820b21..af8d7230e1c 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hans.xlf @@ -6059,11 +6059,6 @@ Stack trace where the illegal operation occurred was: 无法检索 MonthCalendar 的大小信息。 - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. 窗口类名无效。 diff --git a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf index 97a757bd3c9..4db8d5ba43f 100644 --- a/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf +++ b/src/System.Windows.Forms/src/Resources/xlf/SR.zh-Hant.xlf @@ -6059,11 +6059,6 @@ Stack trace where the illegal operation occurred was: 無法擷取 MonthCalendar 的大小資訊。 - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. - - Window class name is not valid. 無效的視窗類別名稱。 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 a2fbe1d750f..2379d548a75 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 @@ -554,7 +554,8 @@ public static void SetDataAsJson(string format, T data) if (data is DataObject) { - throw new InvalidOperationException(string.Format(SR.InvalidTypeForSetDataAsJson, nameof(SetDataObject))); + // TODO: Localize string + throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } JsonData jsonData = new() 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 ddae3563632..811fac0f706 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 @@ -132,7 +132,8 @@ public void SetDataAsJson(string format, T data) { if (data is DataObject) { - throw new InvalidOperationException(string.Format(SR.InvalidTypeForSetDataAsJson, nameof(SetData))); + // TODO: Localize string. + throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); From cdfdfbfe2891bf11091308f24bc44950e83eba84 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:02:34 -0700 Subject: [PATCH 05/34] Remove IObjectReference and add test to Json serialized data should JsonData not exist --- .../WinFormsBinaryFormatWriter.cs | 2 +- .../src/System/Windows/Forms/OLE/Clipboard.cs | 4 +- .../System/Windows/Forms/OLE/DataObject.cs | 7 +- .../src/System/Windows/Forms/OLE/JsonData.cs | 55 ++++++++++++++-- .../WinFormsBinaryFormattedObjectTests.cs | 64 +++++++++++++++++++ 5 files changed, 118 insertions(+), 14 deletions(-) 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 a98d036c2fb..ad61d5c7cd4 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 @@ -55,7 +55,7 @@ public static void WriteImageListStreamer(Stream stream, ImageListStreamer strea public static void WriteJsonData(Stream stream, IJsonData jsonData) { using BinaryFormatWriterScope writer = new(stream); - new BinaryLibrary(libraryId: 2, "System.Private.Windows.VirtualJson").Write(writer); + new BinaryLibrary(libraryId: 2, IJsonData.CustomAssemblyName).Write(writer); new ClassWithMembersAndTypes( new ClassInfo(1, jsonData.TypeFullName, [$"<{nameof(jsonData.JsonBytes)}>k__BackingField"]), libraryId: 2, 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 2379d548a75..b1d64d531d8 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 @@ -519,10 +519,10 @@ public static void SetData(string format, object data) /// Saves the data onto the clipboard in the specified format using JSON serialization. /// /// - /// If null, empty, or whitespace is passed as the format. + /// null, empty, or whitespace was passed as the format. /// /// - /// If is passed in as the data. cannot be JSON serialized meaningfully. + /// was passed in as the data. cannot be JSON serialized meaningfully. /// 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 . 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 811fac0f706..0ace57747fa 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 @@ -7,7 +7,6 @@ using System.Private.Windows; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; -using System.Runtime.Serialization; using System.Text.Json; using Com = Windows.Win32.System.Com; using ComTypes = System.Runtime.InteropServices.ComTypes; @@ -107,7 +106,7 @@ internal IDataObject TryUnwrapInnerIDataObject() /// Stores the specified data and its associated format in this instance as JSON. /// /// - /// If is passed in as the data. cannot be JSON serialized meaningfully. + /// was passed in as the data. cannot be JSON serialized meaningfully. /// If needs to be set, use /// /// @@ -148,9 +147,7 @@ public void SetDataAsJson(string format, T data) public virtual object? GetData(string format, bool autoConvert) { object? data = ((IDataObject)_innerData).GetData(format, autoConvert); -#pragma warning disable SYSLIB0050 // Type or member is obsolete - return data is IJsonData jsonData && jsonData is IObjectReference reference ? reference.GetRealObject(default) : data; -#pragma warning restore SYSLIB0050 + return data is IJsonData jsonData ? jsonData.Deserialize() : data; } [Obsolete( 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 index 51391e860c5..d2d596eb894 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -1,7 +1,6 @@ // 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.Serialization; using System.Text.Json; namespace System.Private.Windows; @@ -10,17 +9,56 @@ 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. +/// If this type needs to be deserialized from stream in these instances, a workaround would be to replicate this type and implement +/// . Then, use the deserializer to deserialize the JSON data with a binder that will +/// reroute JsonData's custom assembly name to the replicated type. +/// +/// +/// +/// : IObjectReference +/// { +/// public byte[] JsonBytes { get; set; } +/// +/// public readonly object GetRealObject(StreamingContext context) => +/// JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); +/// } +/// +/// class ReplicatedJsonDataBinder : SerializationBinder +/// { +/// public override Type? BindToType(string assemblyName, string typeName) +/// { +/// // The assembly name for JsonData should always be "System.Private.Windows.VirtualJson" +/// if (assemblyName == "System.Private.Windows.VirtualJson" && TypeName.TryParse(typeName, out TypeName? name)) +/// { +/// // TODO: Additional checking for the generic type to block unwanted types if needed. +/// return typeof(ReplicatedJsonData); +/// } +/// +/// // TODO: Rejection behavior +/// } +/// } +/// +/// void DeserializeJsonData(Stream stream) +/// { +/// BinaryFormattedObject binary = new(stream, new BinaryFormattedObject.Options { Binder = new ReplicatedJsonDataBinder() }); +/// // This should return the original data that was JSON serialized. +/// object? deserialized = binary.Deserialize(); +/// } +/// ]]> +/// [Serializable] -#pragma warning disable SYSLIB0050 // Type or member is obsolete -internal struct JsonData : IObjectReference, IJsonData -#pragma warning restore SYSLIB0050 +internal struct JsonData : IJsonData { public byte[] JsonBytes { get; set; } public readonly string TypeFullName => $"{typeof(JsonData).FullName}"; - public readonly object GetRealObject(StreamingContext context) => - JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); + public readonly object Deserialize() => JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); } /// @@ -29,7 +67,12 @@ public readonly object GetRealObject(StreamingContext context) => /// 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. + public const string CustomAssemblyName = "System.Private.Windows.VirtualJson"; + byte[] JsonBytes { get; set; } string TypeFullName { get; } + + object Deserialize(); } 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 8715fa53310..5f959c2bf68 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,6 +6,8 @@ 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; @@ -48,6 +50,68 @@ public void BinaryFormattedObject_JsonData_RoundTrip() deserialized.Should().BeEquivalentTo(point); } + [Fact] + public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream() + { + Point point = new() { X = 1, Y = 1 }; + JsonData data = new() + { + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) + }; + + using MemoryStream stream = new(); + WinFormsBinaryFormatWriter.WriteJsonData(stream, data); + stream.Position = 0; + + BinaryFormattedObject binary = new(stream, new BinaryFormattedObject.Options() { Binder = new JsonDataPointBinder() }); + Point deserialized = binary.Deserialize().Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(point); + } + + [Fact] + public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream_Throws() + { + Size point = new() { Height = 1, Width = 1 }; + JsonData data = new() + { + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) + }; + + using MemoryStream stream = new(); + WinFormsBinaryFormatWriter.WriteJsonData(stream, data); + stream.Position = 0; + + BinaryFormattedObject binary = new(stream, new BinaryFormattedObject.Options() { Binder = new JsonDataPointBinder() }); + Action action = () => binary.Deserialize(); + action.Should().Throw(); + } + + [Serializable] + private struct ReplicatedJsonData : IObjectReference + { + public byte[] JsonBytes { get; set; } + + public readonly object GetRealObject(StreamingContext context) => + JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); + } + + private class JsonDataPointBinder : SerializationBinder + { + public override Type? BindToType(string assemblyName, string typeName) + { + if (assemblyName == "System.Private.Windows.VirtualJson" && TypeName.TryParse(typeName, out TypeName? name)) + { + TypeName genericTypeName = name.GetGenericArguments()[0]; + if (genericTypeName.AssemblyQualifiedName == typeof(Point).AssemblyQualifiedName) + { + return typeof(ReplicatedJsonData); + } + } + + throw new InvalidOperationException(); + } + } + [Fact] public void BinaryFormattedObject_Bitmap_FromBinaryFormatter() { From fca51faf6892b3beb7af92cf47094b99fc4b2efe Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:04:01 -0700 Subject: [PATCH 06/34] Add Clipboard.SetDataAsJson for VB --- .../VisualBasic/MyServices/ClipboardProxy.vb | 8 ++++++++ .../src/PublicAPI.Unshipped.txt | 3 ++- .../MyServices/ClipboardProxyTests.cs | 19 ++++++++++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) 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..5925dce295d 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb @@ -208,6 +208,14 @@ Namespace Microsoft.VisualBasic.MyServices ''' The to be saved. Public Sub SetImage(image As Image) Clipboard.SetImage(image) + End Sub? + ''' + ''' Saves the passed in data to the clipboard in the passed in format using JSON serialization + ''' + ''' The format in which to save the data + ''' The data to be saved + Public Sub SetDataAsJson(Of T)(format As String, data As T) + Clipboard.SetDataAsJson(format, data) End Sub ''' diff --git a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt index 65993b67a39..1cd48b440e1 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 \ No newline at end of file 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..0f65dfd469f 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 FluentAssertions; using Microsoft.VisualBasic.Devices; using DataFormats = System.Windows.Forms.DataFormats; using TextDataFormat = System.Windows.Forms.TextDataFormat; @@ -87,9 +88,19 @@ public void Text() } [WinFormsFact] - public void DataOfT_StringArray() + public void SetDataAsJson() { var clipboard = new Computer().Clipboard; + Point point = new(1, 1); + clipboard.SetDataAsJson("point", point); + clipboard.ContainsData("point").Should().Be(System.Windows.Forms.Clipboard.ContainsData("point")); + Point retrieved = clipboard.GetData("point").Should().BeOfType().Which; + retrieved.Should().BeEquivalentTo(System.Windows.Forms.Clipboard.GetData("point")); + retrieved.Should().BeEquivalentTo(point); + } + + [WinFormsFact] + public void DataOfT_StringArray() string format = nameof(DataOfT_StringArray); // Array of primitive types does not require the OOB assembly. string[] data = ["thing1", "thing2"]; @@ -134,5 +145,11 @@ public static Type Resolver(TypeName typeName) => typeof(DataWithObjectField).FullName == typeName.FullName ? typeof(DataWithObjectField) : throw new NotSupportedException($"Can't resolve {typeName.AssemblyQualifiedName}"); + Point point = new(1, 1); + clipboard.SetDataAsJson("point", point); + clipboard.ContainsData("point").Should().Be(System.Windows.Forms.Clipboard.ContainsData("point")); + Point retrieved = clipboard.GetData("point").Should().BeOfType().Which; + retrieved.Should().BeEquivalentTo(System.Windows.Forms.Clipboard.GetData("point")); + retrieved.Should().BeEquivalentTo(point); } } From 184303348bd8221620f3f4f7c426d81e709dce52 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:06:43 -0700 Subject: [PATCH 07/34] add test deserializing replicated JsonData using BinaryFormatter --- .../src/System/Windows/Forms/OLE/JsonData.cs | 5 +++++ .../WinFormsBinaryFormattedObjectTests.cs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) 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 index d2d596eb894..097464bc53d 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -48,6 +48,11 @@ namespace System.Private.Windows; /// BinaryFormattedObject binary = new(stream, new BinaryFormattedObject.Options { Binder = new ReplicatedJsonDataBinder() }); /// // This should return the original data that was JSON serialized. /// object? deserialized = binary.Deserialize(); +/// +/// // OR +/// BinaryFormatter binaryFormatter = new() { Binder = new ReplicatedJsonDataBinder() }; +/// // This should return the original data that was JSON serialized. +/// binaryFormatter.Deserialize(stream); /// } /// ]]> /// 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 5f959c2bf68..2387d6f2007 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 @@ -68,6 +68,27 @@ public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream() deserialized.Should().BeEquivalentTo(point); } + [Fact] + public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream_WithBinaryFormatter() + { + Point point = new() { X = 1, Y = 1 }; + JsonData data = new() + { + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) + }; + + 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 JsonDataPointBinder() }; +#pragma warning restore SYSLIB0011 + Point deserialized = binaryFormatter.Deserialize(stream).Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(point); + } + [Fact] public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream_Throws() { From 747106445d1f073a1ccb16d815b917b73f3880e5 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:07:10 -0700 Subject: [PATCH 08/34] Update ClipboardProxy.SetDataAsJson to inherit docs from Clipboard.SetDataAsJson --- .../src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 5925dce295d..a8afa200229 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb @@ -209,11 +209,7 @@ Namespace Microsoft.VisualBasic.MyServices Public Sub SetImage(image As Image) Clipboard.SetImage(image) End Sub? - ''' - ''' Saves the passed in data to the clipboard in the passed in format using JSON serialization - ''' - ''' The format in which to save the data - ''' The data to be saved + ''' Public Sub SetDataAsJson(Of T)(format As String, data As T) Clipboard.SetDataAsJson(format, data) End Sub From 8faa96a0e2350a907260eb26baa3d809c6217c5e Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 11 Oct 2024 12:14:20 -0700 Subject: [PATCH 09/34] Fix rebase errors --- .../Microsoft/VisualBasic/MyServices/ClipboardProxy.vb | 3 ++- .../tests/ComDisabledTests/ClipboardComTests.cs | 2 ++ .../UnitTests/System/Windows/Forms/ClipboardTests.cs | 8 -------- 3 files changed, 4 insertions(+), 9 deletions(-) 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 a8afa200229..ee2e28cc510 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb @@ -208,7 +208,8 @@ Namespace Microsoft.VisualBasic.MyServices ''' The to be saved. Public Sub SetImage(image As Image) Clipboard.SetImage(image) - End Sub? + End Sub + ''' Public Sub SetDataAsJson(Of T)(format As String, data As T) Clipboard.SetDataAsJson(format, data) diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs index d85b5459040..f341e1f3d7a 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 System.Drawing; + namespace System.Windows.Forms.Tests; // Each registered Clipboard format is an OS singleton, 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 5c8aa61dbe9..ed1d22181ef 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 @@ -495,14 +495,6 @@ public void Clipboard_SetImage_NullImage_ThrowsArgumentNullException() action.Should().Throw().WithParameterName("image"); } - [WinFormsFact] - public void Clipboard_SetText_InvokeString_GetReturnsExpected() - { - Clipboard.SetText("text"); - Assert.Equal("text", Clipboard.GetText()); - Assert.True(Clipboard.ContainsText()); - } - [WinFormsTheory] [EnumData] public void Clipboard_SetText_InvokeStringTextDataFormat_GetReturnsExpected(TextDataFormat format) From ba7a79d0f75bd14a66ff3ab2520a79406b7a5eca Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Tue, 15 Oct 2024 09:54:37 -0700 Subject: [PATCH 10/34] Update some code based on nrbf changes --- .../WinFormsSerializationRecordExtensions.cs | 35 ++++++++++++++- .../src/System/Windows/Forms/OLE/JsonData.cs | 32 ++++++++++--- .../WinFormsBinaryFormattedObjectTests.cs | 45 ++----------------- 3 files changed, 64 insertions(+), 48 deletions(-) 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..79a78f7b8c5 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,9 @@ using System.Drawing; using System.Formats.Nrbf; +using System.Private.Windows; +using System.Reflection.Metadata; +using System.Text.Json; namespace System.Windows.Forms.Nrbf; @@ -53,6 +56,35 @@ public static bool TryGetBitmap(this SerializationRecord record, out object? bit return true; } + /// + /// Tries to deserialize this object if it was serialized as JSON. + /// + public static bool TryGetObjectFromJson(this SerializationRecord record, 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.HasMember("k__BackingField") + || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData + || !TypeName.TryParse(types.TypeName.FullName, out TypeName? result) + || Type.GetType(result.GetGenericArguments()[0].AssemblyQualifiedName) is not Type genericType) + { + // This is supposed to be JsonData, but somehow the binary formatted data is corrupt. + throw new InvalidOperationException(); + } + + // TODO: We should get the type from the Func that will be passed down instead of using Type.GetType() + @object = JsonSerializer.Deserialize(byteData.GetArray(), genericType); + + return true; + } + /// /// Try to get a supported object. This supports common types used in WinForms that do not have type converters. /// @@ -63,5 +95,6 @@ public static bool TryGetResXObject(this SerializationRecord record, [NotNullWhe public static bool TryGetCommonObject(this SerializationRecord record, [NotNullWhen(true)] out object? value) => record.TryGetResXObject(out value) - || record.TryGetDrawingPrimitivesObject(out value); + || record.TryGetDrawingPrimitivesObject(out value) + || record.TryGetObjectFromJson(out value); } 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 index 097464bc53d..1dbf798ce49 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.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.Formats.Nrbf; +using System.Reflection.Metadata; using System.Text.Json; namespace System.Private.Windows; @@ -12,9 +14,9 @@ namespace System.Private.Windows; /// /// /// There may be instances where this type is not available in different versions, e.g. .NET 8. -/// If this type needs to be deserialized from stream in these instances, a workaround would be to replicate this type and implement -/// . Then, use the deserializer to deserialize the JSON data with a binder that will -/// reroute JsonData's custom assembly name to the replicated type. +/// 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, use the to decode the stream and recreate the serialized type. Alternatively, but not recommended, +/// BinaryFormatter can also be used to deserialize the stream if this type is not available. /// /// /// @@ -24,10 +26,12 @@ namespace System.Private.Windows; /// { /// public byte[] JsonBytes { get; set; } /// +/// // For deserializing with BinaryFormatter only. This interface is not needed if using NrbfDecoder to help deserialize. /// public readonly object GetRealObject(StreamingContext context) => /// JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); /// } /// +/// // For deserializing with BinaryFormatter only. /// class ReplicatedJsonDataBinder : SerializationBinder /// { /// public override Type? BindToType(string assemblyName, string typeName) @@ -45,9 +49,25 @@ namespace System.Private.Windows; /// /// void DeserializeJsonData(Stream stream) /// { -/// BinaryFormattedObject binary = new(stream, new BinaryFormattedObject.Options { Binder = new ReplicatedJsonDataBinder() }); -/// // This should return the original data that was JSON serialized. -/// object? deserialized = binary.Deserialize(); +/// SerializationRecord record = NrbfDecoder.Decode(stream); +/// if (record.TypeName.AssemblyName.FullName != "System.Private.Windows.VirtualJson") +/// { +/// // The data was not serialized as JSON. +/// return false; +/// } +/// +/// if (record is not ClassRecord types +/// || !types.HasMember("k__BackingField") +/// || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData +/// || !TypeName.TryParse(types.TypeName.FullName, out TypeName? result) +/// || Type.GetType(result.GetGenericArguments()[0].AssemblyQualifiedName) is not Type genericType) +/// { +/// // This is supposed to be JsonData, but somehow the data is corrupt. +/// throw new InvalidOperationException(); +/// } +/// +/// // This should return the original data that was JSON serialized. +/// JsonSerializer.Deserialize(byteData.GetArray(), genericType); /// /// // OR /// BinaryFormatter binaryFormatter = new() { Binder = new ReplicatedJsonDataBinder() }; 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 2387d6f2007..d164b9840d3 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 @@ -12,7 +12,6 @@ using System.Text.Json; using System.Windows.Forms.BinaryFormat; using System.Windows.Forms.Nrbf; -using System.Text.Json; namespace System.Private.Windows.Core.BinaryFormat.Tests; @@ -24,7 +23,7 @@ public class WinFormsBinaryFormattedObjectTests public void BinaryFormattedObject_NonJsonData_RemainsSerialized() { Point point = new() { X = 1, Y = 1 }; - BinaryFormattedObject format = point.SerializeAndParse(); + SerializationRecord format = point.SerializeAndDecode(); format.TryGetObjectFromJson(out _).Should().BeFalse(); } @@ -42,34 +41,16 @@ public void BinaryFormattedObject_JsonData_RoundTrip() WinFormsBinaryFormatWriter.WriteJsonData(stream, json); stream.Position = 0; - BinaryFormattedObject binary = new(stream); - binary[2].Should().BeOfType().Which - .LibraryName.Should().Be("System.Private.Windows.VirtualJson"); + SerializationRecord binary = NrbfDecoder.Decode(stream); + binary.TypeName.AssemblyName!.FullName.Should().Be(IJsonData.CustomAssemblyName); binary.TryGetObjectFromJson(out object? result).Should().BeTrue(); Point deserialized = result.Should().BeOfType().Which; deserialized.Should().BeEquivalentTo(point); } - [Fact] - public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream() - { - Point point = new() { X = 1, Y = 1 }; - JsonData data = new() - { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) - }; - - using MemoryStream stream = new(); - WinFormsBinaryFormatWriter.WriteJsonData(stream, data); - stream.Position = 0; - - BinaryFormattedObject binary = new(stream, new BinaryFormattedObject.Options() { Binder = new JsonDataPointBinder() }); - Point deserialized = binary.Deserialize().Should().BeOfType().Which; - deserialized.Should().BeEquivalentTo(point); - } [Fact] - public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream_WithBinaryFormatter() + public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() { Point point = new() { X = 1, Y = 1 }; JsonData data = new() @@ -89,24 +70,6 @@ public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream_WithBi deserialized.Should().BeEquivalentTo(point); } - [Fact] - public void BinaryFormattedObject_JsonDataNonExist_Deserialize_FromStream_Throws() - { - Size point = new() { Height = 1, Width = 1 }; - JsonData data = new() - { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) - }; - - using MemoryStream stream = new(); - WinFormsBinaryFormatWriter.WriteJsonData(stream, data); - stream.Position = 0; - - BinaryFormattedObject binary = new(stream, new BinaryFormattedObject.Options() { Binder = new JsonDataPointBinder() }); - Action action = () => binary.Deserialize(); - action.Should().Throw(); - } - [Serializable] private struct ReplicatedJsonData : IObjectReference { From 03f320c162b0a1c2906194b850476ad0335c4aeb Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Tue, 15 Oct 2024 10:16:59 -0700 Subject: [PATCH 11/34] Add DataObject.SetDataAsJson overloads and fix build --- .../src/PublicAPI.Unshipped.txt | 2 ++ .../src/System/Windows/Forms/OLE/DataObject.cs | 14 ++++++++++++++ .../src/System/Windows/Forms/OLE/JsonData.cs | 2 -- .../WinFormsBinaryFormattedObjectTests.cs | 1 - 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index caab3d4bdbb..3e54a8a351c 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -38,4 +38,6 @@ virtual System.Windows.Forms.DataObject.TryGetDataCore(string! format, System [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! static System.Windows.Forms.Clipboard.SetDataAsJson(string! format, T data) -> void +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/OLE/DataObject.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs index 0ace57747fa..41f82af7a3a 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 @@ -138,6 +138,20 @@ public void SetDataAsJson(string format, T data) SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); } + /// + public void SetDataAsJson(T data) => SetData(typeof(T), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + + public void SetDataAsJson(string format, bool autoConvert, T data) + { + if (data is DataObject) + { + // TODO: Localize string. + throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); + } + + SetData(format, autoConvert, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + } + #region IDataObject [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 index 1dbf798ce49..d5ab074d501 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Formats.Nrbf; -using System.Reflection.Metadata; using System.Text.Json; namespace System.Private.Windows; 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 d164b9840d3..be33b17686f 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 @@ -48,7 +48,6 @@ public void BinaryFormattedObject_JsonData_RoundTrip() deserialized.Should().BeEquivalentTo(point); } - [Fact] public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() { From 18a0cae6bc43182c761e01a51718047ee7bb093f Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Tue, 15 Oct 2024 17:41:11 -0700 Subject: [PATCH 12/34] Add test for deserializing json data from stream manually --- .../src/System/Windows/Forms/OLE/JsonData.cs | 64 +++++++++++++------ .../System/Windows/Forms/ClipboardTests.cs | 51 +++++++++++++++ 2 files changed, 96 insertions(+), 19 deletions(-) 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 index d5ab074d501..97b00d9da13 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -13,8 +13,8 @@ namespace System.Private.Windows; /// /// There may be instances where this type is not available in different versions, e.g. .NET 8. /// 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, use the to decode the stream and recreate the serialized type. Alternatively, but not recommended, -/// BinaryFormatter can also be used to deserialize the stream if this type is not available. +/// 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. /// /// /// @@ -30,33 +30,59 @@ namespace System.Private.Windows; /// } /// /// // For deserializing with BinaryFormatter only. -/// class ReplicatedJsonDataBinder : SerializationBinder +/// public Type ResolveType(TypeName typeName) /// { -/// public override Type? BindToType(string assemblyName, string typeName) +/// // The assembly name for JsonData should always be "System.Private.Windows.VirtualJson" +/// if (name.AssemblyName == "System.Private.Windows.VirtualJson") /// { -/// // The assembly name for JsonData should always be "System.Private.Windows.VirtualJson" -/// if (assemblyName == "System.Private.Windows.VirtualJson" && TypeName.TryParse(typeName, out TypeName? name)) -/// { -/// // TODO: Additional checking for the generic type to block unwanted types if needed. -/// return typeof(ReplicatedJsonData); -/// } -/// -/// // TODO: Rejection behavior +/// // TODO: Additional checking for the generic type to block unwanted types if needed. +/// return typeof(ReplicatedJsonData); /// } +/// +/// // TODO: Rejection behavior /// } /// -/// void DeserializeJsonData(Stream stream) +/// +/// void DeserializeJsonData(DataObject dataObject) /// { -/// SerializationRecord record = NrbfDecoder.Decode(stream); +/// // Manually retrieve serialized stream. +/// System.Runtime.InteropServices.ComTypes.FORMATETC formatetc = new() +/// { +/// cfFormat = (short) DataFormats.GetFormat("testFormat").Id, +/// dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT, +/// lindex = -1, +/// tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL +/// }; +/// +/// dataObject.GetData(ref formatetc, out System.Runtime.InteropServices.ComTypes.STGMEDIUM medium); +/// HGLOBAL hglobal = (HGLOBAL)medium.unionmember; +/// Stream stream; +/// try +/// { +/// void* buffer = GlobalLock(hglobal); +/// int size = 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 +/// { +/// GlobalUnlock(hglobal); +/// } +/// +/// // Use Nrbf to decode stream and rehydrate data. +/// System.Formats.Nrbf.SerializationRecord record = NrbfDecoder.Decode(stream); /// if (record.TypeName.AssemblyName.FullName != "System.Private.Windows.VirtualJson") /// { /// // The data was not serialized as JSON. /// return false; /// } /// -/// if (record is not ClassRecord types +/// if (record is not System.Formats.Nrbf.ClassRecord types /// || !types.HasMember("k__BackingField") -/// || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData +/// || types.GetRawValue("k__BackingField") is not System.Formats.Nrbf.SZArrayRecord byteData /// || !TypeName.TryParse(types.TypeName.FullName, out TypeName? result) /// || Type.GetType(result.GetGenericArguments()[0].AssemblyQualifiedName) is not Type genericType) /// { @@ -65,12 +91,12 @@ namespace System.Private.Windows; /// } /// /// // This should return the original data that was JSON serialized. -/// JsonSerializer.Deserialize(byteData.GetArray(), genericType); +/// System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), genericType); /// /// // OR -/// BinaryFormatter binaryFormatter = new() { Binder = new ReplicatedJsonDataBinder() }; +/// /// // This should return the original data that was JSON serialized. -/// binaryFormatter.Deserialize(stream); +/// dataObject.TryGetData(format, ResolveType, out T data); /// } /// ]]> /// 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 ed1d22181ef..d35415ffa00 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,9 +7,11 @@ using System.ComponentModel; using System.Drawing; using System.Drawing.Imaging; +using System.Formats.Nrbf; using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Windows.Forms.Primitives; +using System.Text.Json; using Windows.Win32.System.Ole; using static System.Windows.Forms.Tests.BinaryFormatUtilitiesTests; using Com = Windows.Win32.System.Com; @@ -938,4 +940,53 @@ public void Clipboard_SetDataObject_WithMultipleData_ReturnsExpected(bool copy) Clipboard.GetData("point2").Should().BeOfType().Which.Should().BeEquivalentTo(point2); Clipboard.GetData("Mystring").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. + Point point = new(1, 1); + Clipboard.SetDataAsJson("testFormat", point); + + // 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, leaveOpen: true); + ClassRecord types = record.Should().BeAssignableTo().Which; + types.HasMember("k__BackingField").Should().BeTrue(); + SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Which; + TypeName.TryParse(types.TypeName.FullName, out TypeName? result).Should().BeTrue(); + TypeName checkedResult = result.Should().BeOfType().Which; + Type.GetType(checkedResult.GetGenericArguments()[0].AssemblyQualifiedName).Should().Be(typeof(Point)); + + JsonSerializer.Deserialize(byteData.GetArray(), typeof(Point)).Should().BeEquivalentTo(point); + } } From 19d40d5dccd792d1705c6e8f8ed209eb34de11f8 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 18 Oct 2024 10:26:03 -0700 Subject: [PATCH 13/34] Change SetDataAsJson to take type object --- .../VisualBasic/MyServices/ClipboardProxy.vb | 4 +- .../src/PublicAPI.Unshipped.txt | 2 +- .../src/PublicAPI.Unshipped.txt | 8 +-- .../WinFormsBinaryFormatWriter.cs | 7 ++- .../WinFormsSerializationRecordExtensions.cs | 12 ++-- .../src/System/Windows/Forms/OLE/Clipboard.cs | 9 +-- .../System/Windows/Forms/OLE/DataObject.cs | 22 +++++-- .../src/System/Windows/Forms/OLE/JsonData.cs | 57 +++++++++++-------- .../UnitTests/SerializableAttributeTests.cs | 2 +- .../WinFormsBinaryFormattedObjectTests.cs | 33 +++++++---- .../System/Windows/Forms/ClipboardTests.cs | 6 +- 11 files changed, 98 insertions(+), 64 deletions(-) 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 ee2e28cc510..c0291072eb6 100644 --- a/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb +++ b/src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb @@ -210,8 +210,8 @@ Namespace Microsoft.VisualBasic.MyServices Clipboard.SetImage(image) End Sub - ''' - Public Sub SetDataAsJson(Of T)(format As String, data As T) + ''' + Public Sub SetDataAsJson(format As String, data As Object) Clipboard.SetDataAsJson(format, data) End Sub diff --git a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt index 1cd48b440e1..d37162cae1b 100644 --- a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt @@ -1,3 +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.SetDataAsJson(Of T)(format As String, data As T) -> Void \ No newline at end of file +Microsoft.VisualBasic.MyServices.ClipboardProxy.SetDataAsJson(format As String, data As Object) -> Void diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index 3e54a8a351c..01323948b3b 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -37,7 +37,7 @@ 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! -static System.Windows.Forms.Clipboard.SetDataAsJson(string! format, T data) -> void -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 +static System.Windows.Forms.Clipboard.SetDataAsJson(string! format, object! data) -> void +System.Windows.Forms.DataObject.SetDataAsJson(object! data) -> void +System.Windows.Forms.DataObject.SetDataAsJson(string! format, bool autoConvert, object! data) -> void +System.Windows.Forms.DataObject.SetDataAsJson(string! format, object! 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 ad61d5c7cd4..69cfb7470dc 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 @@ -57,10 +57,11 @@ 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, jsonData.TypeFullName, [$"<{nameof(jsonData.JsonBytes)}>k__BackingField"]), + new ClassInfo(1, typeof(JsonData).FullName!, [$"<{nameof(jsonData.JsonBytes)}>k__BackingField", $"<{nameof(jsonData.OriginalAssemblyQualifiedTypeName)}>k__BackingField"]), libraryId: 2, - new MemberTypeInfo[] { new(BinaryType.PrimitiveArray, PrimitiveType.Byte) }, - new MemberReference(idRef: 3)).Write(writer); + new MemberTypeInfo[] { new(BinaryType.PrimitiveArray, PrimitiveType.Byte), new(BinaryType.String, null) }, + new MemberReference(idRef: 3), + new BinaryObjectString(objectId: 4, jsonData.OriginalAssemblyQualifiedTypeName)).Write(writer); new ArraySinglePrimitive(objectId: 3, jsonData.JsonBytes).Write(writer); } 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 79a78f7b8c5..6dbfe502fe2 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 @@ -63,24 +63,26 @@ public static bool TryGetObjectFromJson(this SerializationRecord record, out obj { @object = null; - if (record.TypeName.AssemblyName!.FullName != IJsonData.CustomAssemblyName) + if (record.TypeName.AssemblyName?.FullName != IJsonData.CustomAssemblyName) { // The data was not serialized as JSON. return false; } if (record is not ClassRecord types - || !types.HasMember("k__BackingField") || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData - || !TypeName.TryParse(types.TypeName.FullName, out TypeName? result) - || Type.GetType(result.GetGenericArguments()[0].AssemblyQualifiedName) is not Type genericType) + || types.GetRawValue("k__BackingField") is not string typeData + || !TypeName.TryParse(typeData, out TypeName? result) + || Type.GetType(result.AssemblyQualifiedName) is not Type originalType) { // This is supposed to be JsonData, but somehow the binary formatted data is corrupt. throw new InvalidOperationException(); } + // TODO: If the full name of the type user is asking for doesn't match the type that is saved, return false. + // TODO: We should get the type from the Func that will be passed down instead of using Type.GetType() - @object = JsonSerializer.Deserialize(byteData.GetArray(), genericType); + @object = JsonSerializer.Deserialize(byteData.GetArray(), originalType); return true; } 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 b1d64d531d8..3124f324afc 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 @@ -523,7 +523,7 @@ public static void SetData(string format, object data) /// /// /// was passed in as the data. cannot be JSON serialized meaningfully. - /// If needs to be placed on the clipboard, use + /// 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 . /// @@ -545,7 +545,7 @@ public static void SetData(string format, object data) /// on custom converters for JSON serialization. /// /// - public static void SetDataAsJson(string format, T data) + public static void SetDataAsJson(string format, object data) { if (string.IsNullOrWhiteSpace(format.OrThrowIfNull())) { @@ -558,9 +558,10 @@ public static void SetDataAsJson(string format, T data) throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } - JsonData jsonData = new() + JsonData jsonData = new() { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), + OriginalAssemblyQualifiedTypeName = data.GetType().AssemblyQualifiedName! }; SetDataObject(new DataObject(format, jsonData), copy: 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 41f82af7a3a..6b806f05b0f 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 @@ -127,7 +127,7 @@ internal IDataObject TryUnwrapInnerIDataObject() /// on custom converters for JSON serialization. /// /// - public void SetDataAsJson(string format, T data) + public void SetDataAsJson(string format, object data) { if (data is DataObject) { @@ -135,13 +135,23 @@ public void SetDataAsJson(string format, T data) throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } - SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), OriginalAssemblyQualifiedTypeName = data.GetType().AssemblyQualifiedName! }); } - /// - public void SetDataAsJson(T data) => SetData(typeof(T), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + /// + public void SetDataAsJson(object data) + { + if (data is DataObject) + { + // TODO: Localize string. + throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); + } + + Type type = data.GetType(); + SetData(type, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), OriginalAssemblyQualifiedTypeName = type.AssemblyQualifiedName! }); + } - public void SetDataAsJson(string format, bool autoConvert, T data) + public void SetDataAsJson(string format, bool autoConvert, object data) { if (data is DataObject) { @@ -149,7 +159,7 @@ public void SetDataAsJson(string format, bool autoConvert, T data) throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } - SetData(format, autoConvert, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + SetData(format, autoConvert, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), OriginalAssemblyQualifiedTypeName = data.GetType().AssemblyQualifiedName! }); } #region IDataObject 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 index 97b00d9da13..0700af63b2c 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -20,28 +20,35 @@ namespace System.Private.Windows; /// /// : IObjectReference +/// struct ReplicatedJsonData : IObjectReference /// { /// public byte[] JsonBytes { get; set; } /// +/// public string OriginalAssemblyQualifiedTypeName { get; set; } +/// /// // For deserializing with BinaryFormatter only. This interface is not needed if using NrbfDecoder to help deserialize. -/// public readonly object GetRealObject(StreamingContext context) => -/// JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); +/// public readonly object GetRealObject(StreamingContext context) +/// { +/// // TODO: Additional checking on OriginalAssemblyQualifiedTypeName to block unwanted types if needed. +/// return JsonSerializer.Deserialize(JsonBytes, Type.GetType(OriginalAssemblyQualifiedTypeName)) ?? throw new InvalidOperationException(); +/// } /// } /// /// // For deserializing with BinaryFormatter only. -/// public Type ResolveType(TypeName typeName) +/// class ReplicatedJsonDataBinder : SerializationBinder /// { -/// // The assembly name for JsonData should always be "System.Private.Windows.VirtualJson" -/// if (name.AssemblyName == "System.Private.Windows.VirtualJson") +/// public override Type? BindToType(string assemblyName, string typeName) /// { -/// // TODO: Additional checking for the generic type to block unwanted types if needed. -/// return typeof(ReplicatedJsonData); -/// } -/// -/// // TODO: Rejection behavior -/// } +/// // The assembly name for JsonData should always be "System.Private.Windows.VirtualJson" +/// if (assemblyName == "System.Private.Windows.VirtualJson" +/// && TypeName.TryParse(typeName, out TypeName? name) +/// && name.Name == "JsonData") +/// { +/// return typeof(ReplicatedJsonData); +/// } /// +/// // TODO: Rejection behavior +/// } /// /// void DeserializeJsonData(DataObject dataObject) /// { @@ -72,7 +79,7 @@ namespace System.Private.Windows; /// GlobalUnlock(hglobal); /// } /// -/// // Use Nrbf to decode stream and rehydrate data. +/// // Use Nrbf to decode stream and rehydrate data. (recommended) /// System.Formats.Nrbf.SerializationRecord record = NrbfDecoder.Decode(stream); /// if (record.TypeName.AssemblyName.FullName != "System.Private.Windows.VirtualJson") /// { @@ -81,38 +88,40 @@ namespace System.Private.Windows; /// } /// /// if (record is not System.Formats.Nrbf.ClassRecord types -/// || !types.HasMember("k__BackingField") -/// || types.GetRawValue("k__BackingField") is not System.Formats.Nrbf.SZArrayRecord byteData -/// || !TypeName.TryParse(types.TypeName.FullName, out TypeName? result) -/// || Type.GetType(result.GetGenericArguments()[0].AssemblyQualifiedName) is not Type genericType) +/// || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData +/// || types.GetRawValue("k__BackingField") is not string typeData +/// || !TypeName.TryParse(typeData, out TypeName? result) +/// || Type.GetType(result.AssemblyQualifiedName) is not Type originalType) /// { /// // This is supposed to be JsonData, but somehow the data is corrupt. /// throw new InvalidOperationException(); /// } /// /// // This should return the original data that was JSON serialized. -/// System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), genericType); +/// System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), originalType); /// /// // OR +/// // Use BinaryFormatter to rehydrate the data. /// /// // This should return the original data that was JSON serialized. -/// dataObject.TryGetData(format, ResolveType, out T data); +/// BinaryFormatter binaryFormatter = new() { Binder = new ReplicatedJsonDataBinder() }; +/// binaryFormatter.Deserialize(stream); /// } /// ]]> /// [Serializable] -internal struct JsonData : IJsonData +internal struct JsonData : IJsonData { public byte[] JsonBytes { get; set; } - public readonly string TypeFullName => $"{typeof(JsonData).FullName}"; + public string OriginalAssemblyQualifiedTypeName { get; set; } - public readonly object Deserialize() => JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); + public readonly object Deserialize() => JsonSerializer.Deserialize(JsonBytes, Type.GetType(OriginalAssemblyQualifiedTypeName)!) ?? 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. +/// identify a without needing to have the generic type information. /// internal interface IJsonData { @@ -121,7 +130,7 @@ internal interface IJsonData byte[] JsonBytes { get; set; } - string TypeFullName { get; } + public string OriginalAssemblyQualifiedTypeName { get; set; } object Deserialize(); } diff --git a/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs b/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs index 111592d0c52..c9313bc30b2 100644 --- a/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs @@ -16,7 +16,7 @@ public void EnsureSerializableAttribute() new HashSet { // This is needed for OLE JSON serialization support - { typeof(JsonData<>).FullName }, + { 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 be33b17686f..f17489b0910 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 @@ -32,9 +32,10 @@ public void BinaryFormattedObject_JsonData_RoundTrip() { Point point = new() { X = 1, Y = 1 }; - JsonData json = new() + JsonData json = new() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), + OriginalAssemblyQualifiedTypeName = typeof(Point).AssemblyQualifiedName! }; using MemoryStream stream = new(); @@ -52,9 +53,10 @@ public void BinaryFormattedObject_JsonData_RoundTrip() public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() { Point point = new() { X = 1, Y = 1 }; - JsonData data = new() + JsonData data = new() { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), + OriginalAssemblyQualifiedTypeName = typeof(Point).AssemblyQualifiedName! }; using MemoryStream stream = new(); @@ -70,25 +72,32 @@ public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() } [Serializable] - private struct ReplicatedJsonData : IObjectReference + private struct ReplicatedJsonData : IObjectReference { public byte[] JsonBytes { get; set; } - public readonly object GetRealObject(StreamingContext context) => - JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); + public string OriginalAssemblyQualifiedTypeName { get; set; } + + public readonly object GetRealObject(StreamingContext context) + { + if (OriginalAssemblyQualifiedTypeName == typeof(Point).AssemblyQualifiedName) + { + return JsonSerializer.Deserialize(JsonBytes, typeof(Point)) ?? throw new InvalidOperationException(); + } + + throw new InvalidOperationException(); + } } private class JsonDataPointBinder : SerializationBinder { public override Type? BindToType(string assemblyName, string typeName) { - if (assemblyName == "System.Private.Windows.VirtualJson" && TypeName.TryParse(typeName, out TypeName? name)) + if (assemblyName == "System.Private.Windows.VirtualJson" + && TypeName.TryParse(typeName, out TypeName? name) + && name.Name == "JsonData") { - TypeName genericTypeName = name.GetGenericArguments()[0]; - if (genericTypeName.AssemblyQualifiedName == typeof(Point).AssemblyQualifiedName) - { - return typeof(ReplicatedJsonData); - } + return typeof(ReplicatedJsonData); } throw new InvalidOperationException(); 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 d35415ffa00..437466c02c3 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 @@ -983,9 +983,11 @@ public unsafe void Clipboard_Deserialize_FromStream_Manually() ClassRecord types = record.Should().BeAssignableTo().Which; types.HasMember("k__BackingField").Should().BeTrue(); SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Which; - TypeName.TryParse(types.TypeName.FullName, out TypeName? result).Should().BeTrue(); + types.HasMember("k__BackingField").Should().BeTrue(); + string typeData = types.GetRawValue("k__BackingField").Should().BeOfType().Which; + TypeName.TryParse(typeData, out TypeName? result).Should().BeTrue(); TypeName checkedResult = result.Should().BeOfType().Which; - Type.GetType(checkedResult.GetGenericArguments()[0].AssemblyQualifiedName).Should().Be(typeof(Point)); + Type.GetType(checkedResult.AssemblyQualifiedName).Should().Be(typeof(Point)); JsonSerializer.Deserialize(byteData.GetArray(), typeof(Point)).Should().BeEquivalentTo(point); } From 255dd300a57b2c6d84f40af91529eb4e28aeef56 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 18 Oct 2024 11:16:44 -0700 Subject: [PATCH 14/34] Add convenience methods for Json serializing for drag/drop scenarios --- .../src/PublicAPI.Unshipped.txt | 3 + .../src/System/Windows/Forms/Control.cs | 33 +++++++++++ .../UIIntegrationTests/DragDropTests.cs | 56 +++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index 01323948b3b..55d4feab1e0 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -38,6 +38,9 @@ virtual System.Windows.Forms.DataObject.TryGetDataCore(string! format, System [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! static System.Windows.Forms.Clipboard.SetDataAsJson(string! format, object! data) -> void +System.Windows.Forms.Control.DoDragDropAsJson(object! data, System.Windows.Forms.DragDropEffects allowedEffects) -> System.Windows.Forms.DragDropEffects +System.Windows.Forms.Control.DoDragDropAsJson(object! 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.SetDataAsJson(object! data) -> void System.Windows.Forms.DataObject.SetDataAsJson(string! format, bool autoConvert, object! data) -> void System.Windows.Forms.DataObject.SetDataAsJson(string! format, object! data) -> void 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 0cb45b51842..92d91e8540e 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,39 @@ internal virtual void DisposeAxControls() } } + /// + public DragDropEffects DoDragDropAsJson(object data, DragDropEffects allowedEffects) => + DoDragDropAsJson(data, allowedEffects, dragImage: null, cursorOffset: default, useDefaultDragImage: false); + + /// + /// Begins a drag operation, storing the data as Json. + /// + /// 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 type . + public DragDropEffects DoDragDropAsJson( + object data, + DragDropEffects allowedEffects, + Bitmap? dragImage, + Point cursorOffset, + bool useDefaultDragImage) + { + if (data is DataObject) + { + // What should happen if ComTypes.IDataObject is received? + // TODO: Localize string + throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Start drag/drop operation with the data 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 diff --git a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs index 52bd6337be3..1a14743a032 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Drawing; +using FluentAssertions; using Windows.Win32.System.Com; using Windows.Win32.UI.Accessibility; using Windows.Win32.UI.WindowsAndMessaging; @@ -489,6 +490,61 @@ await InputSimulator.SendAsync( }); } + [WinFormsFact] + public async Task DragDrop_JsonSerialized_ReturnsExpected_Async() + { + // Verifies that we can successfully drag and drop a JSON serialized object. + + Point point = new(10, 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(Point)) ?? false) + { + e.Effect = DragDropEffects.Copy; + } + }; + form.DragOver += (s, e) => + { + if (e.Data?.GetDataPresent(typeof(Point)) ?? false) + { + // Get the JSON serialized Point. + dropped = e.Data?.GetData(typeof(Point)); + e.Effect = DragDropEffects.Copy; + } + }; + form.MouseDown += (s, e) => + { + form.DoDragDropAsJson(point, 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(Point)); + dropped.Should().BeEquivalentTo(point); + } + private void CloseExplorer(string directory) { foreach (Process process in Process.GetProcesses()) From b1a3c119b157da0de98f30ea9294f07ba024f4fe Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 18 Oct 2024 15:12:57 -0700 Subject: [PATCH 15/34] Add test for custom JSON serialization behavior --- .../System/Windows/Forms/DataObjectTests.cs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) 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 da34c35ff1e..dcd80ad0a25 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 @@ -9,6 +9,8 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; using Moq; using Windows.Win32.System.Ole; using Com = Windows.Win32.System.Com; @@ -2870,4 +2872,83 @@ public void DataObject_SetDataAsJson_MultipleData_ReturnsExpected() data.GetData("point2").Should().BeOfType().Which.Should().BeEquivalentTo(point2); data.GetData("Mystring").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. + WeatherForecast forecast = new() + { + Date = DateTimeOffset.Now, + TemperatureCelsius = 25, + Summary = "Hot" + }; + + DataObject dataObject = new(); + dataObject.SetDataAsJson("custom", forecast); + WeatherForecast deserialized = dataObject.GetData("custom").Should().BeOfType().Subject; + 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!"); + } + + [JsonConverter(typeof(WeatherForecastJsonConverter))] + private class WeatherForecast + { + public DateTimeOffset Date { get; set; } + public int TemperatureCelsius { get; set; } + public string Summary { get; set; } + } + + private class WeatherForecastJsonConverter : JsonConverter + { + public override WeatherForecast Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + WeatherForecast result = new(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + 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; + 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.WriteEndObject(); + } + } } From e11ae4417eb6ffad3f167d8d25fe98d64f238d3d Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Mon, 21 Oct 2024 15:11:51 -0700 Subject: [PATCH 16/34] re-introduce T for trimming --- .../VisualBasic/MyServices/ClipboardProxy.vb | 4 +-- .../src/PublicAPI.Unshipped.txt | 2 +- .../src/PublicAPI.Unshipped.txt | 12 +++---- .../WinFormsBinaryFormatWriter.cs | 7 ++-- .../src/System/Windows/Forms/Control.cs | 8 ++--- .../WinFormsSerializationRecordExtensions.cs | 10 +++--- .../src/System/Windows/Forms/OLE/Clipboard.cs | 7 ++-- .../System/Windows/Forms/OLE/DataObject.cs | 15 ++++---- .../src/System/Windows/Forms/OLE/JsonData.cs | 34 ++++++++----------- .../UnitTests/SerializableAttributeTests.cs | 2 +- .../WinFormsBinaryFormattedObjectTests.cs | 23 ++++--------- .../System/Windows/Forms/ClipboardTests.cs | 7 ++-- 12 files changed, 56 insertions(+), 75 deletions(-) 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 c0291072eb6..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,8 +210,8 @@ Namespace Microsoft.VisualBasic.MyServices Clipboard.SetImage(image) End Sub - ''' - Public Sub SetDataAsJson(format As String, data As Object) + ''' + Public Sub SetDataAsJson(Of T)(format As String, data As T) Clipboard.SetDataAsJson(format, data) End Sub diff --git a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt index d37162cae1b..c491c216c7f 100644 --- a/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt +++ b/src/Microsoft.VisualBasic.Forms/src/PublicAPI.Unshipped.txt @@ -1,3 +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.SetDataAsJson(format As String, data As Object) -> Void +Microsoft.VisualBasic.MyServices.ClipboardProxy.SetDataAsJson(Of T)(format As String, data As T) -> Void diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index 55d4feab1e0..c8216e1fd8c 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,10 +40,7 @@ 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! -static System.Windows.Forms.Clipboard.SetDataAsJson(string! format, object! data) -> void -System.Windows.Forms.Control.DoDragDropAsJson(object! data, System.Windows.Forms.DragDropEffects allowedEffects) -> System.Windows.Forms.DragDropEffects -System.Windows.Forms.Control.DoDragDropAsJson(object! 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.SetDataAsJson(object! data) -> void -System.Windows.Forms.DataObject.SetDataAsJson(string! format, bool autoConvert, object! data) -> void -System.Windows.Forms.DataObject.SetDataAsJson(string! format, object! data) -> void +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 69cfb7470dc..ad61d5c7cd4 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 @@ -57,11 +57,10 @@ 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(JsonData).FullName!, [$"<{nameof(jsonData.JsonBytes)}>k__BackingField", $"<{nameof(jsonData.OriginalAssemblyQualifiedTypeName)}>k__BackingField"]), + new ClassInfo(1, jsonData.TypeFullName, [$"<{nameof(jsonData.JsonBytes)}>k__BackingField"]), libraryId: 2, - new MemberTypeInfo[] { new(BinaryType.PrimitiveArray, PrimitiveType.Byte), new(BinaryType.String, null) }, - new MemberReference(idRef: 3), - new BinaryObjectString(objectId: 4, jsonData.OriginalAssemblyQualifiedTypeName)).Write(writer); + new MemberTypeInfo[] { new(BinaryType.PrimitiveArray, PrimitiveType.Byte) }, + new MemberReference(idRef: 3)).Write(writer); new ArraySinglePrimitive(objectId: 3, jsonData.JsonBytes).Write(writer); } 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 92d91e8540e..0a8424d8229 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs @@ -4780,8 +4780,8 @@ internal virtual void DisposeAxControls() } } - /// - public DragDropEffects DoDragDropAsJson(object data, DragDropEffects allowedEffects) => + /// + public DragDropEffects DoDragDropAsJson(T data, DragDropEffects allowedEffects) => DoDragDropAsJson(data, allowedEffects, dragImage: null, cursorOffset: default, useDefaultDragImage: false); /// @@ -4794,8 +4794,8 @@ public DragDropEffects DoDragDropAsJson(object data, DragDropEffects allowedEffe /// 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 type . - public DragDropEffects DoDragDropAsJson( - object data, + public DragDropEffects DoDragDropAsJson( + T data, DragDropEffects allowedEffects, Bitmap? dragImage, Point cursorOffset, 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 6dbfe502fe2..1cb8647a690 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 @@ -71,18 +71,16 @@ public static bool TryGetObjectFromJson(this SerializationRecord record, out obj if (record is not ClassRecord types || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData - || types.GetRawValue("k__BackingField") is not string typeData - || !TypeName.TryParse(typeData, out TypeName? result) - || Type.GetType(result.AssemblyQualifiedName) is not Type originalType) + || !TypeName.TryParse(types.TypeName.FullName, out TypeName? result) + || result.GetGenericArguments().FirstOrDefault() is not { } genericTypeName + || Type.GetType(genericTypeName.AssemblyQualifiedName) is not Type genericType) { // This is supposed to be JsonData, but somehow the binary formatted data is corrupt. throw new InvalidOperationException(); } - // TODO: If the full name of the type user is asking for doesn't match the type that is saved, return false. - // TODO: We should get the type from the Func that will be passed down instead of using Type.GetType() - @object = JsonSerializer.Deserialize(byteData.GetArray(), originalType); + @object = JsonSerializer.Deserialize(byteData.GetArray(), genericType); return true; } 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 3124f324afc..785b2ffd5ce 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 @@ -523,7 +523,7 @@ public static void SetData(string format, object data) /// /// /// was passed in as the data. cannot be JSON serialized meaningfully. - /// If needs to be placed on the clipboard, use + /// 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 . /// @@ -545,7 +545,7 @@ public static void SetData(string format, object data) /// on custom converters for JSON serialization. /// /// - public static void SetDataAsJson(string format, object data) + public static void SetDataAsJson(string format, T data) { if (string.IsNullOrWhiteSpace(format.OrThrowIfNull())) { @@ -558,10 +558,9 @@ public static void SetDataAsJson(string format, object data) throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } - JsonData jsonData = new() + JsonData jsonData = new() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), - OriginalAssemblyQualifiedTypeName = data.GetType().AssemblyQualifiedName! }; SetDataObject(new DataObject(format, jsonData), copy: 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 6b806f05b0f..64667c5c886 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 @@ -127,7 +127,7 @@ internal IDataObject TryUnwrapInnerIDataObject() /// on custom converters for JSON serialization. /// /// - public void SetDataAsJson(string format, object data) + public void SetDataAsJson(string format, T data) { if (data is DataObject) { @@ -135,11 +135,11 @@ public void SetDataAsJson(string format, object data) throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } - SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), OriginalAssemblyQualifiedTypeName = data.GetType().AssemblyQualifiedName! }); + SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); } - /// - public void SetDataAsJson(object data) + /// + public void SetDataAsJson(T data) { if (data is DataObject) { @@ -147,11 +147,10 @@ public void SetDataAsJson(object data) throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } - Type type = data.GetType(); - SetData(type, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), OriginalAssemblyQualifiedTypeName = type.AssemblyQualifiedName! }); + SetData(typeof(T), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); } - public void SetDataAsJson(string format, bool autoConvert, object data) + public void SetDataAsJson(string format, bool autoConvert, T data) { if (data is DataObject) { @@ -159,7 +158,7 @@ public void SetDataAsJson(string format, bool autoConvert, object data) throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); } - SetData(format, autoConvert, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), OriginalAssemblyQualifiedTypeName = data.GetType().AssemblyQualifiedName! }); + SetData(format, autoConvert, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); } #region IDataObject 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 index 0700af63b2c..2ac1afe0637 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -20,18 +20,12 @@ namespace System.Private.Windows; /// /// : IObjectReference /// { /// public byte[] JsonBytes { get; set; } /// -/// public string OriginalAssemblyQualifiedTypeName { get; set; } -/// /// // For deserializing with BinaryFormatter only. This interface is not needed if using NrbfDecoder to help deserialize. -/// public readonly object GetRealObject(StreamingContext context) -/// { -/// // TODO: Additional checking on OriginalAssemblyQualifiedTypeName to block unwanted types if needed. -/// return JsonSerializer.Deserialize(JsonBytes, Type.GetType(OriginalAssemblyQualifiedTypeName)) ?? throw new InvalidOperationException(); -/// } +/// public readonly object GetRealObject(StreamingContext context) => JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); /// } /// /// // For deserializing with BinaryFormatter only. @@ -41,10 +35,11 @@ namespace System.Private.Windows; /// { /// // The assembly name for JsonData should always be "System.Private.Windows.VirtualJson" /// if (assemblyName == "System.Private.Windows.VirtualJson" -/// && TypeName.TryParse(typeName, out TypeName? name) -/// && name.Name == "JsonData") +/// && TypeName.TryParse(typeName, out TypeName? name)) /// { -/// return typeof(ReplicatedJsonData); +/// TypeName genericTypeName = name.GetGenericArguments().FirstOrDefault() +/// // TODO: Additional checking on generic type to block unwanted types if needed. +/// return typeof(ReplicatedJsonData); /// } /// /// // TODO: Rejection behavior @@ -89,16 +84,17 @@ namespace System.Private.Windows; /// /// if (record is not System.Formats.Nrbf.ClassRecord types /// || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData -/// || types.GetRawValue("k__BackingField") is not string typeData /// || !TypeName.TryParse(typeData, out TypeName? result) -/// || Type.GetType(result.AssemblyQualifiedName) is not Type originalType) +/// || result.GetGenericArguments().FirstOrDefault() is not { } genericTypeName) /// { /// // This is supposed to be JsonData, but somehow the data is corrupt. /// throw new InvalidOperationException(); /// } /// +/// // TODO: Additional checking on generic type to block unwanted types if needed. +/// /// // This should return the original data that was JSON serialized. -/// System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), originalType); +/// System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), genericType); /// /// // OR /// // Use BinaryFormatter to rehydrate the data. @@ -110,18 +106,18 @@ namespace System.Private.Windows; /// ]]> /// [Serializable] -internal struct JsonData : IJsonData +internal struct JsonData : IJsonData { public byte[] JsonBytes { get; set; } - public string OriginalAssemblyQualifiedTypeName { get; set; } + public readonly string TypeFullName => $"{typeof(JsonData).FullName}"; - public readonly object Deserialize() => JsonSerializer.Deserialize(JsonBytes, Type.GetType(OriginalAssemblyQualifiedTypeName)!) ?? throw new InvalidOperationException(); + public readonly object Deserialize() => JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? 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. +/// identify a without needing to have the generic type information. /// internal interface IJsonData { @@ -130,7 +126,7 @@ internal interface IJsonData byte[] JsonBytes { get; set; } - public string OriginalAssemblyQualifiedTypeName { get; set; } + string TypeFullName { get; } object Deserialize(); } diff --git a/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs b/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs index c9313bc30b2..111592d0c52 100644 --- a/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs +++ b/src/System.Windows.Forms/tests/UnitTests/SerializableAttributeTests.cs @@ -16,7 +16,7 @@ public void EnsureSerializableAttribute() new HashSet { // This is needed for OLE JSON serialization support - { typeof(JsonData).FullName }, + { 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 f17489b0910..e6b6272c1bc 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 @@ -32,10 +32,9 @@ public void BinaryFormattedObject_JsonData_RoundTrip() { Point point = new() { X = 1, Y = 1 }; - JsonData json = new() + JsonData json = new() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), - OriginalAssemblyQualifiedTypeName = typeof(Point).AssemblyQualifiedName! }; using MemoryStream stream = new(); @@ -53,10 +52,9 @@ public void BinaryFormattedObject_JsonData_RoundTrip() public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() { Point point = new() { X = 1, Y = 1 }; - JsonData data = new() + JsonData data = new() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), - OriginalAssemblyQualifiedTypeName = typeof(Point).AssemblyQualifiedName! }; using MemoryStream stream = new(); @@ -72,21 +70,14 @@ public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() } [Serializable] - private struct ReplicatedJsonData : IObjectReference + private struct ReplicatedJsonData : IObjectReference { public byte[] JsonBytes { get; set; } public string OriginalAssemblyQualifiedTypeName { get; set; } - public readonly object GetRealObject(StreamingContext context) - { - if (OriginalAssemblyQualifiedTypeName == typeof(Point).AssemblyQualifiedName) - { - return JsonSerializer.Deserialize(JsonBytes, typeof(Point)) ?? throw new InvalidOperationException(); - } - - throw new InvalidOperationException(); - } + public readonly object GetRealObject(StreamingContext context) => + JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); } private class JsonDataPointBinder : SerializationBinder @@ -95,9 +86,9 @@ private class JsonDataPointBinder : SerializationBinder { if (assemblyName == "System.Private.Windows.VirtualJson" && TypeName.TryParse(typeName, out TypeName? name) - && name.Name == "JsonData") + && name.GetGenericArguments().FirstOrDefault()?.AssemblyQualifiedName == typeof(Point).AssemblyQualifiedName) { - return typeof(ReplicatedJsonData); + return typeof(ReplicatedJsonData); } throw new InvalidOperationException(); 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 437466c02c3..8dee7a9f20c 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 @@ -983,11 +983,10 @@ public unsafe void Clipboard_Deserialize_FromStream_Manually() ClassRecord types = record.Should().BeAssignableTo().Which; types.HasMember("k__BackingField").Should().BeTrue(); SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Which; - types.HasMember("k__BackingField").Should().BeTrue(); - string typeData = types.GetRawValue("k__BackingField").Should().BeOfType().Which; - TypeName.TryParse(typeData, out TypeName? result).Should().BeTrue(); + TypeName.TryParse(types.TypeName.FullName, out TypeName? result).Should().BeTrue(); TypeName checkedResult = result.Should().BeOfType().Which; - Type.GetType(checkedResult.AssemblyQualifiedName).Should().Be(typeof(Point)); + TypeName genericTypeName = checkedResult.GetGenericArguments().FirstOrDefault().Should().BeOfType().Subject; + Type.GetType(genericTypeName.AssemblyQualifiedName).Should().Be(typeof(Point)); JsonSerializer.Deserialize(byteData.GetArray(), typeof(Point)).Should().BeEquivalentTo(point); } From 4c88d2a8e5f31a44334c0a52a1982360aee48ccf Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Tue, 22 Oct 2024 17:43:41 -0700 Subject: [PATCH 17/34] use Single() and add Font to test type --- .../WinFormsSerializationRecordExtensions.cs | 3 +-- .../src/System/Windows/Forms/OLE/JsonData.cs | 6 ++--- .../WinFormsBinaryFormattedObjectTests.cs | 2 +- .../System/Windows/Forms/DataObjectTests.cs | 22 ++++++++++++++++++- 4 files changed, 26 insertions(+), 7 deletions(-) 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 1cb8647a690..1edb0549a00 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 @@ -72,8 +72,7 @@ public static bool TryGetObjectFromJson(this SerializationRecord record, out obj if (record is not ClassRecord types || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData || !TypeName.TryParse(types.TypeName.FullName, out TypeName? result) - || result.GetGenericArguments().FirstOrDefault() is not { } genericTypeName - || Type.GetType(genericTypeName.AssemblyQualifiedName) is not Type genericType) + || Type.GetType(result.GetGenericArguments().Single().AssemblyQualifiedName) is not Type genericType) { // This is supposed to be JsonData, but somehow the binary formatted data is corrupt. throw new InvalidOperationException(); 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 index 2ac1afe0637..16093e38a63 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -37,7 +37,7 @@ namespace System.Private.Windows; /// if (assemblyName == "System.Private.Windows.VirtualJson" /// && TypeName.TryParse(typeName, out TypeName? name)) /// { -/// TypeName genericTypeName = name.GetGenericArguments().FirstOrDefault() +/// TypeName genericTypeName = name.GetGenericArguments().Single() /// // TODO: Additional checking on generic type to block unwanted types if needed. /// return typeof(ReplicatedJsonData); /// } @@ -84,13 +84,13 @@ namespace System.Private.Windows; /// /// if (record is not System.Formats.Nrbf.ClassRecord types /// || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData -/// || !TypeName.TryParse(typeData, out TypeName? result) -/// || result.GetGenericArguments().FirstOrDefault() is not { } genericTypeName) +/// || !TypeName.TryParse(typeData, out TypeName? result)) /// { /// // This is supposed to be JsonData, but somehow the data is corrupt. /// throw new InvalidOperationException(); /// } /// +/// TypeName genericTypeName = result.GetGenericArguments().Single(); /// // TODO: Additional checking on generic type to block unwanted types if needed. /// /// // This should return the original data that was JSON serialized. 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 e6b6272c1bc..406529f575f 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 @@ -86,7 +86,7 @@ private class JsonDataPointBinder : SerializationBinder { if (assemblyName == "System.Private.Windows.VirtualJson" && TypeName.TryParse(typeName, out TypeName? name) - && name.GetGenericArguments().FirstOrDefault()?.AssemblyQualifiedName == typeof(Point).AssemblyQualifiedName) + && name.GetGenericArguments().Single().AssemblyQualifiedName == typeof(Point).AssemblyQualifiedName) { return typeof(ReplicatedJsonData); } 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 dcd80ad0a25..73cf38b5ac2 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 @@ -2878,11 +2878,13 @@ 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" + Summary = "Hot", + Font = font }; DataObject dataObject = new(); @@ -2892,6 +2894,7 @@ public void DataObject_SetDataAsJson_CustomJsonConverter_ReturnsExpected() 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))] @@ -2900,6 +2903,7 @@ 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 @@ -2907,10 +2911,18 @@ 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; } @@ -2934,6 +2946,12 @@ public override WeatherForecast Read(ref Utf8JsonReader reader, Type typeToConve 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(); } @@ -2948,6 +2966,8 @@ public override void Write(Utf8JsonWriter writer, WeatherForecast value, JsonSer 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(); } } From d1ab9d538c032d5e6c5a327e0b5a55e8d03fcbcb Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Wed, 23 Oct 2024 14:48:46 -0700 Subject: [PATCH 18/34] Update exception condition --- .../src/System/Windows/Forms/Control.cs | 11 +++++++---- .../src/System/Windows/Forms/OLE/Clipboard.cs | 8 ++++---- .../src/System/Windows/Forms/OLE/DataObject.cs | 18 +++++++++--------- .../src/System/Windows/Forms/OLE/JsonData.cs | 2 +- .../System/Windows/Forms/ClipboardTests.cs | 2 +- .../System/Windows/Forms/DataObjectTests.cs | 18 ++++++++++++++++++ 6 files changed, 40 insertions(+), 19 deletions(-) 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 0a8424d8229..4ff230ef0f6 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs @@ -4793,7 +4793,11 @@ public DragDropEffects DoDragDropAsJson(T data, DragDropEffects allowedEffect /// 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 type . + /// + /// If is a non derived . This is for better error reporting as will serialize empty. If + /// needs to used to start a drag operation, use to JSON serialize the data being held within the , + /// then pass the to . + /// public DragDropEffects DoDragDropAsJson( T data, DragDropEffects allowedEffects, @@ -4801,11 +4805,10 @@ public DragDropEffects DoDragDropAsJson( Point cursorOffset, bool useDefaultDragImage) { - if (data is DataObject) + if (typeof(T) == typeof(DataObject)) { - // What should happen if ComTypes.IDataObject is received? // TODO: Localize string - throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Start drag/drop operation with the data by using {nameof(DoDragDrop)} instead"); + 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(); 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 785b2ffd5ce..4a9018174bc 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 @@ -522,9 +522,9 @@ public static void SetData(string format, object data) /// null, empty, or whitespace was passed as the format. /// /// - /// was passed in as the data. cannot be JSON serialized meaningfully. + /// 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 + /// to JSON serialize the data to be held in the , then set the /// onto the clipboard via . /// /// @@ -552,10 +552,10 @@ public static void SetDataAsJson(string format, T data) throw new ArgumentException(SR.DataObjectWhitespaceEmptyFormatNotAllowed, nameof(format)); } - if (data is DataObject) + if (typeof(T) == typeof(DataObject)) { // TODO: Localize string - throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetDataObject)} instead."); } JsonData jsonData = new() 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 64667c5c886..c063f3666af 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 @@ -106,8 +106,9 @@ internal IDataObject TryUnwrapInnerIDataObject() /// Stores the specified data and its associated format in this instance as JSON. /// /// - /// was passed in as the data. cannot be JSON serialized meaningfully. - /// If needs to be set, use + /// 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 . /// /// /// @@ -129,10 +130,9 @@ internal IDataObject TryUnwrapInnerIDataObject() /// public void SetDataAsJson(string format, T data) { - if (data is DataObject) + if (typeof(T) == typeof(DataObject)) { - // TODO: Localize string. - throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); } SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); @@ -141,10 +141,10 @@ public void SetDataAsJson(string format, T data) /// public void SetDataAsJson(T data) { - if (data is DataObject) + if (typeof(T) == typeof(DataObject)) { // TODO: Localize string. - throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); } SetData(typeof(T), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); @@ -152,10 +152,10 @@ public void SetDataAsJson(T data) public void SetDataAsJson(string format, bool autoConvert, T data) { - if (data is DataObject) + if (typeof(T) == typeof(DataObject)) { // TODO: Localize string. - throw new InvalidOperationException($"DataObject cannot be JSON serialized meaningfully. Set the data by using {nameof(SetData)} instead"); + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); } SetData(format, autoConvert, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); 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 index 16093e38a63..73f20dcc1b5 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -79,7 +79,7 @@ namespace System.Private.Windows; /// if (record.TypeName.AssemblyName.FullName != "System.Private.Windows.VirtualJson") /// { /// // The data was not serialized as JSON. -/// return false; +/// return; /// } /// /// if (record is not System.Formats.Nrbf.ClassRecord types 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 8dee7a9f20c..39b1ca3a54e 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 @@ -985,7 +985,7 @@ public unsafe void Clipboard_Deserialize_FromStream_Manually() SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Which; TypeName.TryParse(types.TypeName.FullName, out TypeName? result).Should().BeTrue(); TypeName checkedResult = result.Should().BeOfType().Which; - TypeName genericTypeName = checkedResult.GetGenericArguments().FirstOrDefault().Should().BeOfType().Subject; + TypeName genericTypeName = checkedResult.GetGenericArguments().SingleOrDefault().Should().BeOfType().Subject; Type.GetType(genericTypeName.AssemblyQualifiedName).Should().Be(typeof(Point)); JsonSerializer.Deserialize(byteData.GetArray(), typeof(Point)).Should().BeEquivalentTo(point); 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 73cf38b5ac2..4ef19ddb5d5 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 @@ -2971,4 +2971,22 @@ public override void Write(Utf8JsonWriter writer, WeatherForecast value, JsonSer writer.WriteEndObject(); } } + + [WinFormsFact] + public void DataObject_SetDataAsJson_Throws() + { + string format = "format"; + DataObject dataObject = new(); + DerivedDataObject derived = new(); + Action clipboardSet1 = () => Clipboard.SetDataAsJson(format, dataObject); + clipboardSet1.Should().Throw(); + Action clipboardSet2 = () => Clipboard.SetDataAsJson(format, derived); + clipboardSet2.Should().NotThrow(); + + DataObject test = new(); + Action dataObjectSet1 = () => test.SetDataAsJson(dataObject); + dataObjectSet1.Should().Throw(); + Action dataObjectSet2 = () => test.SetDataAsJson(derived); + dataObjectSet2.Should().NotThrow(); + } } From ceb3e0e6f645b3d289a843803ac3bc5776202110 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Wed, 23 Oct 2024 16:13:16 -0700 Subject: [PATCH 19/34] Surface any JSON deserialization errors as NotSupportedException --- .../Nrbf/WinFormsSerializationRecordExtensions.cs | 9 ++++++++- .../src/System/Windows/Forms/OLE/JsonData.cs | 15 ++++++++++++++- .../System/Windows/Forms/ClipboardTests.cs | 12 ++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) 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 1edb0549a00..6ab7c97243a 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 @@ -79,7 +79,14 @@ public static bool TryGetObjectFromJson(this SerializationRecord record, out obj } // TODO: We should get the type from the Func that will be passed down instead of using Type.GetType() - @object = JsonSerializer.Deserialize(byteData.GetArray(), genericType); + try + { + @object = JsonSerializer.Deserialize(byteData.GetArray(), genericType); + } + catch (Exception ex) + { + @object = new NotSupportedException(ex.Message); + } return true; } 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 index 73f20dcc1b5..183f95a34e6 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -112,7 +112,20 @@ internal struct JsonData : IJsonData public readonly string TypeFullName => $"{typeof(JsonData).FullName}"; - public readonly object Deserialize() => JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); + public readonly object Deserialize() + { + object? result = null; + try + { + result = JsonSerializer.Deserialize(JsonBytes, typeof(T)); + } + catch (Exception ex) + { + result = new NotSupportedException(ex.Message); + } + + return result ?? throw new InvalidOperationException(); + } } /// 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 39b1ca3a54e..74c09e7582c 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 @@ -990,4 +990,16 @@ public unsafe void Clipboard_Deserialize_FromStream_Manually() JsonSerializer.Deserialize(byteData.GetArray(), typeof(Point)).Should().BeEquivalentTo(point); } + + [WinFormsFact] + public void Clipboard_SurfaceJsonError() + { + using Font font = new("Microsoft Sans Serif", emSize: 10); + byte[] serialized = JsonSerializer.SerializeToUtf8Bytes(font); + string format = "font"; + Clipboard.SetDataAsJson(format, font); + Clipboard.GetData(format).Should().BeOfType(); + DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType().Subject; + dataObject.GetData(format).Should().BeOfType(); + } } From 6e88b603faea0d2e8aa5a64382b577b8bd742ea5 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 25 Oct 2024 15:15:20 -0700 Subject: [PATCH 20/34] Add test avoiding BinaryFormatter with custom DataObject --- .../System/Windows/Forms/ClipboardTests.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) 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 74c09e7582c..6a3c89c11f3 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 @@ -1002,4 +1002,89 @@ public void Clipboard_SurfaceJsonError() DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType().Subject; dataObject.GetData(format).Should().BeOfType(); } + + [WinFormsTheory] + [BoolData] + public void Clipboard_CustomDataObject_AvoidBinaryFormatter(bool copy) + { + string format = "custom"; + TestData 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(TestData)).Should().BeEquivalentTo(data); + } + else + { + JsonDataObject received = Clipboard.GetDataObject().Should().BeOfType().Subject; + received.Should().Be(jsonDataObject); + received.Deserialize(format).Should().BeEquivalentTo(data); + } + } + + [Serializable] + private struct TestData + { + public int X { get; set; } + public int Y { get; set; } + } + + // 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(); + } } From 7a9f6b95d431396a531f91a1a89f60541647057f Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Thu, 31 Oct 2024 14:21:30 -0700 Subject: [PATCH 21/34] close stream in test Clipboard_Deserialize_FromStream_Manually --- .../tests/UnitTests/System/Windows/Forms/ClipboardTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6a3c89c11f3..3e23c35a875 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 @@ -979,7 +979,7 @@ public unsafe void Clipboard_Deserialize_FromStream_Manually() stream.Should().NotBeNull(); // Use NrbfDecoder to decode the stream and rehydrate the type. - SerializationRecord record = NrbfDecoder.Decode(stream, leaveOpen: true); + SerializationRecord record = NrbfDecoder.Decode(stream); ClassRecord types = record.Should().BeAssignableTo().Which; types.HasMember("k__BackingField").Should().BeTrue(); SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Which; From 8aaccea34a49316274b80df418feaf5415619359 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Tue, 5 Nov 2024 11:35:11 -0800 Subject: [PATCH 22/34] Move clipboard test in ClipboardTests.cs --- .../System/Windows/Forms/ClipboardTests.cs | 14 ++++++++++++++ .../System/Windows/Forms/DataObjectTests.cs | 5 ----- 2 files changed, 14 insertions(+), 5 deletions(-) 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 3e23c35a875..e2a6c305bba 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 @@ -1087,4 +1087,18 @@ public void SetData(string format, object? data) 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 DataObject_SetDataAsJson_Throws() + { + string format = "format"; + DataObject dataObject = new(); + DerivedDataObject derived = new(); + Action clipboardSet1 = () => Clipboard.SetDataAsJson(format, dataObject); + clipboardSet1.Should().Throw(); + Action clipboardSet2 = () => Clipboard.SetDataAsJson(format, derived); + clipboardSet2.Should().NotThrow(); + } } 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 4ef19ddb5d5..e3a77f31a20 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 @@ -2975,13 +2975,8 @@ public override void Write(Utf8JsonWriter writer, WeatherForecast value, JsonSer [WinFormsFact] public void DataObject_SetDataAsJson_Throws() { - string format = "format"; DataObject dataObject = new(); DerivedDataObject derived = new(); - Action clipboardSet1 = () => Clipboard.SetDataAsJson(format, dataObject); - clipboardSet1.Should().Throw(); - Action clipboardSet2 = () => Clipboard.SetDataAsJson(format, derived); - clipboardSet2.Should().NotThrow(); DataObject test = new(); Action dataObjectSet1 = () => test.SetDataAsJson(dataObject); From 7fbe0dfdb619f283434f9435f792ca6d029077fe Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Tue, 5 Nov 2024 12:28:54 -0800 Subject: [PATCH 23/34] try different format name --- .../tests/UnitTests/System/Windows/Forms/ClipboardTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e2a6c305bba..785e0a74d90 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 @@ -1007,7 +1007,7 @@ public void Clipboard_SurfaceJsonError() [BoolData] public void Clipboard_CustomDataObject_AvoidBinaryFormatter(bool copy) { - string format = "custom"; + string format = "customFormat"; TestData data = new() { X = 1, Y = 1 }; Clipboard.SetData(format, data); // BinaryFormatter not enabled. From ced8e9dc15cd078b58e7fd5371363a548505768d Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Wed, 6 Nov 2024 11:53:39 -0800 Subject: [PATCH 24/34] update guidance example --- .../src/System/Windows/Forms/OLE/JsonData.cs | 79 +++++++++---------- 1 file changed, 37 insertions(+), 42 deletions(-) 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 index 183f95a34e6..1027630d1a7 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -11,7 +11,7 @@ namespace System.Private.Windows; /// /// /// -/// There may be instances where this type is not available in different versions, e.g. .NET 8. +/// 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. @@ -19,52 +19,28 @@ namespace System.Private.Windows; /// /// /// : IObjectReference -/// { -/// public byte[] JsonBytes { get; set; } -/// -/// // For deserializing with BinaryFormatter only. This interface is not needed if using NrbfDecoder to help deserialize. -/// public readonly object GetRealObject(StreamingContext context) => JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); -/// } -/// -/// // For deserializing with BinaryFormatter only. -/// class ReplicatedJsonDataBinder : SerializationBinder -/// { -/// public override Type? BindToType(string assemblyName, string typeName) -/// { -/// // The assembly name for JsonData should always be "System.Private.Windows.VirtualJson" -/// if (assemblyName == "System.Private.Windows.VirtualJson" -/// && TypeName.TryParse(typeName, out TypeName? name)) -/// { -/// TypeName genericTypeName = name.GetGenericArguments().Single() -/// // TODO: Additional checking on generic type to block unwanted types if needed. -/// return typeof(ReplicatedJsonData); -/// } -/// -/// // TODO: Rejection behavior -/// } -/// +/// // Recommended: deserialize using NrbfDecoder. /// void DeserializeJsonData(DataObject dataObject) /// { /// // Manually retrieve serialized stream. /// System.Runtime.InteropServices.ComTypes.FORMATETC formatetc = new() /// { -/// cfFormat = (short) DataFormats.GetFormat("testFormat").Id, +/// cfFormat = (short) DataFormats.GetFormat("yourDataFormat").Id, /// dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT, /// lindex = -1, /// tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL /// }; /// -/// dataObject.GetData(ref formatetc, out System.Runtime.InteropServices.ComTypes.STGMEDIUM medium); -/// HGLOBAL hglobal = (HGLOBAL)medium.unionmember; +/// System.Runtime.InteropServices.ComTypes.IDataObject castedDataObject = (System.Runtime.InteropServices.ComTypes.IDataObject)dataObject; +/// castedDataObject.GetData(ref formatetc, out System.Runtime.InteropServices.ComTypes.STGMEDIUM medium); +/// IntPtr hglobal = medium.unionmember; /// Stream stream; /// try /// { -/// void* buffer = GlobalLock(hglobal); +/// IntPtr buffer = GlobalLock(hglobal); /// int size = GlobalSize(hglobal); /// byte[] bytes = new byte[size]; -/// Marshal.Copy((nint)buffer, bytes, 0, size); +/// Marshal.Copy(buffer, bytes, 0, size); /// // this comes from DataObject.Composition.s_serializedObjectID /// int index = 16; /// stream = new MemoryStream(bytes, index, bytes.Length - index); @@ -74,8 +50,8 @@ namespace System.Private.Windows; /// GlobalUnlock(hglobal); /// } /// -/// // Use Nrbf to decode stream and rehydrate data. (recommended) -/// System.Formats.Nrbf.SerializationRecord record = NrbfDecoder.Decode(stream); +/// // Use Nrbf to decode stream and rehydrate data. +/// System.Formats.Nrbf.SerializationRecord record = System.Formats.Nrbf.NrbfDecoder.Decode(stream); /// if (record.TypeName.AssemblyName.FullName != "System.Private.Windows.VirtualJson") /// { /// // The data was not serialized as JSON. @@ -83,25 +59,44 @@ namespace System.Private.Windows; /// } /// /// if (record is not System.Formats.Nrbf.ClassRecord types -/// || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData -/// || !TypeName.TryParse(typeData, out TypeName? result)) +/// || types.GetRawValue("k__BackingField") is not System.Formats.Nrbf.SZArrayRecord byteData +/// || !System.Reflection.Metadata.TypeName.TryParse(types.TypeName.FullName.ToCharArray(), out System.Reflection.Metadata.TypeName? result)) /// { /// // This is supposed to be JsonData, but somehow the data is corrupt. /// throw new InvalidOperationException(); /// } /// -/// TypeName genericTypeName = result.GetGenericArguments().Single(); +/// System.Reflection.Metadata.TypeName genericTypeName = result.GetGenericArguments().Single(); /// // TODO: Additional checking on generic type to block unwanted types if needed. /// /// // This should return the original data that was JSON serialized. /// System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), genericType); +/// } +/// +/// [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. /// -/// // OR -/// // Use BinaryFormatter to rehydrate the data. +/// // This definition must live in an assembly named System.Private.Windows.VirtualJson in order to work as expected. +/// namespace System.Private.Windows; +/// [Serializable] +/// struct JsonData : IObjectReference +/// { +/// public byte[] JsonBytes { get; set; } /// -/// // This should return the original data that was JSON serialized. -/// BinaryFormatter binaryFormatter = new() { Binder = new ReplicatedJsonDataBinder() }; -/// binaryFormatter.Deserialize(stream); +/// public object GetRealObject(StreamingContext context) +/// { +/// // TODO: Additional checking on generic type to block unwanted types if needed. +/// return JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); +/// } /// } /// ]]> /// From 1be562212f39c33c18a7ff4d7da77e054321899c Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Thu, 7 Nov 2024 17:00:25 -0800 Subject: [PATCH 25/34] Block setting null data --- .../src/System/Windows/Forms/Control.cs | 1 + .../src/System/Windows/Forms/OLE/Clipboard.cs | 1 + .../src/System/Windows/Forms/OLE/DataObject.cs | 3 +++ .../System/Windows/Forms/ClipboardTests.cs | 16 +++++++--------- .../Windows/Forms/ControlTests.Methods.cs | 11 ++++++++++- .../System/Windows/Forms/DataObjectTests.cs | 17 ++++++++--------- 6 files changed, 30 insertions(+), 19 deletions(-) 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 4ff230ef0f6..74e8c948a7c 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs @@ -4805,6 +4805,7 @@ public DragDropEffects DoDragDropAsJson( Point cursorOffset, bool useDefaultDragImage) { + data.OrThrowIfNull(nameof(data)); if (typeof(T) == typeof(DataObject)) { // TODO: Localize string 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 4a9018174bc..eec233fd5db 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 @@ -547,6 +547,7 @@ public static void SetData(string format, object data) /// public static void SetDataAsJson(string format, T data) { + data.OrThrowIfNull(nameof(data)); if (string.IsNullOrWhiteSpace(format.OrThrowIfNull())) { throw new ArgumentException(SR.DataObjectWhitespaceEmptyFormatNotAllowed, nameof(format)); 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 c063f3666af..a60fe352f8b 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 @@ -130,6 +130,7 @@ internal IDataObject TryUnwrapInnerIDataObject() /// public void SetDataAsJson(string format, T data) { + data.OrThrowIfNull(nameof(data)); if (typeof(T) == typeof(DataObject)) { throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); @@ -141,6 +142,7 @@ public void SetDataAsJson(string format, T data) /// public void SetDataAsJson(T data) { + data.OrThrowIfNull(nameof(data)); if (typeof(T) == typeof(DataObject)) { // TODO: Localize string. @@ -152,6 +154,7 @@ public void SetDataAsJson(T data) public void SetDataAsJson(string format, bool autoConvert, T data) { + data.OrThrowIfNull(nameof(data)); if (typeof(T) == typeof(DataObject)) { // TODO: Localize string. 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 785e0a74d90..702e6e047ec 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 @@ -891,8 +891,11 @@ public void Clipboard_SetDataAsJson_EmptyFormat_Throws(string? format) [WinFormsFact] public void Clipboard_SetDataAsJson_DataObject_Throws() { - Action action = () => Clipboard.SetDataAsJson("format", new DataObject()); + string format = "format"; + Action action = () => Clipboard.SetDataAsJson(format, new DataObject()); action.Should().Throw(); + Action clipboardSet2 = () => Clipboard.SetDataAsJson(format, new DerivedDataObject()); + clipboardSet2.Should().NotThrow(); } [WinFormsFact] @@ -1091,14 +1094,9 @@ public void SetData(string format, object? data) private class DerivedDataObject : DataObject { } [WinFormsFact] - public void DataObject_SetDataAsJson_Throws() + public void Clipboard_SetDataAsJson_NullData_Throws() { - string format = "format"; - DataObject dataObject = new(); - DerivedDataObject derived = new(); - Action clipboardSet1 = () => Clipboard.SetDataAsJson(format, dataObject); - clipboardSet1.Should().Throw(); - Action clipboardSet2 = () => Clipboard.SetDataAsJson(format, derived); - clipboardSet2.Should().NotThrow(); + 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 e3a77f31a20..dc1346d16a4 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 @@ -2832,9 +2832,13 @@ public unsafe void DataObject_Native_GetData_SerializationFailure() [WinFormsFact] public void DataObject_SetDataAsJson_DataObject_Throws() { + string format = "format"; DataObject dataObject = new(); - Action action = () => dataObject.SetDataAsJson("format", new DataObject()); + Action action = () => dataObject.SetDataAsJson(format, new DataObject()); action.Should().Throw(); + + Action dataObjectSet2 = () => dataObject.SetDataAsJson(format, new DerivedDataObject()); + dataObjectSet2.Should().NotThrow(); } [WinFormsFact] @@ -2973,15 +2977,10 @@ public override void Write(Utf8JsonWriter writer, WeatherForecast value, JsonSer } [WinFormsFact] - public void DataObject_SetDataAsJson_Throws() + public void DataObject_SetDataAsJson_NullData_Throws() { DataObject dataObject = new(); - DerivedDataObject derived = new(); - - DataObject test = new(); - Action dataObjectSet1 = () => test.SetDataAsJson(dataObject); - dataObjectSet1.Should().Throw(); - Action dataObjectSet2 = () => test.SetDataAsJson(derived); - dataObjectSet2.Should().NotThrow(); + Action dataObjectSet = () => dataObject.SetDataAsJson(null); + dataObjectSet.Should().Throw(); } } From 03c71767a4dc76c1a27501713e86c922628029e5 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Thu, 14 Nov 2024 14:54:19 -0800 Subject: [PATCH 26/34] Add RequiresUnreferencedCode attribute --- .../src/System/Windows/Forms/OLE/Clipboard.cs | 1 + .../System/Windows/Forms/OLE/DataObject.cs | 55 +++++++++++-------- 2 files changed, 32 insertions(+), 24 deletions(-) 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 eec233fd5db..c19b043aa74 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 @@ -545,6 +545,7 @@ public static void SetData(string format, object data) /// 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)); 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 a60fe352f8b..80a95cece31 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 @@ -102,9 +102,39 @@ 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) + { + data.OrThrowIfNull(nameof(data)); + if (typeof(T) == typeof(DataObject)) + { + throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); + } + + SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + } + + /// + [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] + public void SetDataAsJson(T data) + { + 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 use {nameof(SetData)} instead."); + } + + SetData(typeof(T), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + } + /// /// Stores the specified data and its associated format in this instance 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 @@ -128,30 +158,7 @@ internal IDataObject TryUnwrapInnerIDataObject() /// on custom converters for JSON serialization. /// /// - public void SetDataAsJson(string format, T data) - { - data.OrThrowIfNull(nameof(data)); - if (typeof(T) == typeof(DataObject)) - { - throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); - } - - SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); - } - - /// - public void SetDataAsJson(T data) - { - 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 use {nameof(SetData)} instead."); - } - - SetData(typeof(T), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); - } - + [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] public void SetDataAsJson(string format, bool autoConvert, T data) { data.OrThrowIfNull(nameof(data)); From 4968656d718d869c6e1ee68f672746e002d57730 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 13 Dec 2024 09:28:35 -0800 Subject: [PATCH 27/34] - React to TryGetData additions - Simplify JsonData serialization - Fix rebase errors --- .../MyServices/ClipboardProxyTests.cs | 13 +-- .../src/PublicAPI.Unshipped.txt | 1 - .../WinFormsBinaryFormatWriter.cs | 7 +- .../WinFormsSerializationRecordExtensions.cs | 30 +++++-- .../src/System/Windows/Forms/OLE/Clipboard.cs | 5 +- ...bject.Composition.BinaryFormatUtilities.cs | 6 +- .../OLE/DataObject.Composition.Binder.cs | 2 +- .../Windows/Forms/OLE/DataObject.DataStore.cs | 29 +++++-- .../System/Windows/Forms/OLE/DataObject.cs | 18 ++-- .../src/System/Windows/Forms/OLE/JsonData.cs | 35 +++++--- .../ComDisabledTests/ClipboardComTests.cs | 12 ++- .../ComDisabledTests/DataObjectComTests.cs | 7 +- .../UIIntegrationTests/DragDropTests.cs | 4 +- .../WinFormsBinaryFormattedObjectTests.cs | 34 +++++--- .../System/Windows/Forms/ClipboardTests.cs | 87 ++++++++++++++----- .../System/Windows/Forms/DataObjectTests.cs | 33 +++++-- 16 files changed, 209 insertions(+), 114 deletions(-) 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 0f65dfd469f..e8666d14ba9 100644 --- a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs +++ b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs @@ -5,7 +5,6 @@ using System.Drawing; using System.Reflection.Metadata; -using FluentAssertions; using Microsoft.VisualBasic.Devices; using DataFormats = System.Windows.Forms.DataFormats; using TextDataFormat = System.Windows.Forms.TextDataFormat; @@ -94,13 +93,15 @@ public void SetDataAsJson() Point point = new(1, 1); clipboard.SetDataAsJson("point", point); clipboard.ContainsData("point").Should().Be(System.Windows.Forms.Clipboard.ContainsData("point")); - Point retrieved = clipboard.GetData("point").Should().BeOfType().Which; - retrieved.Should().BeEquivalentTo(System.Windows.Forms.Clipboard.GetData("point")); + clipboard.TryGetData("point", out Point retrieved).Should().Be(System.Windows.Forms.Clipboard.TryGetData("point", out Point retrieved2)); + retrieved.Should().BeEquivalentTo(retrieved2); retrieved.Should().BeEquivalentTo(point); } [WinFormsFact] public void DataOfT_StringArray() + { + var clipboard = new Computer().Clipboard; string format = nameof(DataOfT_StringArray); // Array of primitive types does not require the OOB assembly. string[] data = ["thing1", "thing2"]; @@ -145,11 +146,5 @@ public static Type Resolver(TypeName typeName) => typeof(DataWithObjectField).FullName == typeName.FullName ? typeof(DataWithObjectField) : throw new NotSupportedException($"Can't resolve {typeName.AssemblyQualifiedName}"); - Point point = new(1, 1); - clipboard.SetDataAsJson("point", point); - clipboard.ContainsData("point").Should().Be(System.Windows.Forms.Clipboard.ContainsData("point")); - Point retrieved = clipboard.GetData("point").Should().BeOfType().Which; - retrieved.Should().BeEquivalentTo(System.Windows.Forms.Clipboard.GetData("point")); - retrieved.Should().BeEquivalentTo(point); } } diff --git a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt index c8216e1fd8c..6f68c96826e 100644 --- a/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/src/PublicAPI.Unshipped.txt @@ -40,7 +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.DataGridViewCellStyle.Font.get -> System.Drawing.Font? 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 ad61d5c7cd4..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 @@ -57,10 +57,11 @@ 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, jsonData.TypeFullName, [$"<{nameof(jsonData.JsonBytes)}>k__BackingField"]), + 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 MemberReference(idRef: 3)).Write(writer); + 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); } 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 6ab7c97243a..e3ec771057b 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 @@ -4,7 +4,9 @@ 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; @@ -59,7 +61,12 @@ public static bool TryGetBitmap(this SerializationRecord record, out object? bit /// /// Tries to deserialize this object if it was serialized as JSON. /// - public static bool TryGetObjectFromJson(this SerializationRecord record, out object? @object) + /// + /// if the data was serialized as JSON and was successfully deserialized. 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; @@ -71,21 +78,27 @@ public static bool TryGetObjectFromJson(this SerializationRecord record, out obj if (record is not ClassRecord types || types.GetRawValue("k__BackingField") is not SZArrayRecord byteData - || !TypeName.TryParse(types.TypeName.FullName, out TypeName? result) - || Type.GetType(result.GetGenericArguments().Single().AssemblyQualifiedName) is not Type genericType) + || types.GetRawValue("k__BackingField") is not string innerTypeFullName + || !TypeName.TryParse(innerTypeFullName, out TypeName? genericTypeName)) { // This is supposed to be JsonData, but somehow the binary formatted data is corrupt. - throw new InvalidOperationException(); + throw new SerializationException(); + } + + Type genericType = resolver.GetType(genericTypeName); + if (!genericType.IsAssignableTo(typeof(T))) + { + // Not the type the caller asked for. + return false; } - // TODO: We should get the type from the Func that will be passed down instead of using Type.GetType() try { - @object = JsonSerializer.Deserialize(byteData.GetArray(), genericType); + @object = JsonSerializer.Deserialize(byteData.GetArray()); } catch (Exception ex) { - @object = new NotSupportedException(ex.Message); + throw new NotSupportedException(ex.Message); } return true; @@ -101,6 +114,5 @@ public static bool TryGetResXObject(this SerializationRecord record, [NotNullWhe public static bool TryGetCommonObject(this SerializationRecord record, [NotNullWhen(true)] out object? value) => record.TryGetResXObject(out value) - || record.TryGetDrawingPrimitivesObject(out value) - || record.TryGetObjectFromJson(out value); + || record.TryGetDrawingPrimitivesObject(out value); } 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 c19b043aa74..36c380dd535 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 @@ -557,13 +557,14 @@ public static void SetDataAsJson(string format, T 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 use {nameof(SetDataObject)} instead."); + throw new InvalidOperationException($"'DataObject' will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetDataObject)} API instead."); } JsonData jsonData = new() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), - }; + InnerTypeAssemblyQualifiedName = typeof(T).ToTypeName().AssemblyQualifiedName + }; SetDataObject(new DataObject(format, jsonData), copy: true); } 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 144da1f2541..3e83bf1710a 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 @@ -110,14 +110,10 @@ record = stream.Decode(out recordMap); // or type that can be assigned to the requested type. if (!legacyMode && !typeof(T).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, typeof(T), (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 fcb82cadc3c..a4c00fd7d97 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_types = [ 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 80a95cece31..be8b306532d 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 @@ -112,7 +112,7 @@ public void SetDataAsJson(string format, T data) throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); } - SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + SetData(format, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), InnerTypeAssemblyQualifiedName = typeof(T).ToTypeName().AssemblyQualifiedName }); } /// @@ -123,14 +123,14 @@ public void SetDataAsJson(T 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 use {nameof(SetData)} instead."); + throw new InvalidOperationException($"'DataObject' will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} API instead."); } - SetData(typeof(T), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + SetData(typeof(T), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), InnerTypeAssemblyQualifiedName = typeof(T).ToTypeName().AssemblyQualifiedName }); } /// - /// Stores the specified data and its associated format in this instance as JSON. + /// Stores the data as JSON in the specified format. /// /// The format associated with the data. See for predefined formats. /// to allow the data to be converted to another format; otherwise, . @@ -165,10 +165,10 @@ public void SetDataAsJson(string format, bool autoConvert, T 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 use {nameof(SetData)} instead."); + throw new InvalidOperationException($"'DataObject' will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} API instead."); } - SetData(format, autoConvert, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }); + SetData(format, autoConvert, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), InnerTypeAssemblyQualifiedName = typeof(T).ToTypeName().AssemblyQualifiedName }); } #region IDataObject @@ -177,11 +177,7 @@ public void SetDataAsJson(string format, bool autoConvert, T data) error: false, DiagnosticId = Obsoletions.ClipboardGetDataDiagnosticId, UrlFormat = Obsoletions.SharedUrlFormat)] - public virtual object? GetData(string format, bool autoConvert) - { - object? data = ((IDataObject)_innerData).GetData(format, autoConvert); - return data is IJsonData jsonData ? jsonData.Deserialize() : data; - } + public virtual object? GetData(string format, bool autoConvert) => _innerData.GetData(format, autoConvert); [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 index 1027630d1a7..fa824455132 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -1,6 +1,7 @@ // 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; namespace System.Private.Windows; @@ -25,7 +26,7 @@ namespace System.Private.Windows; /// // Manually retrieve serialized stream. /// System.Runtime.InteropServices.ComTypes.FORMATETC formatetc = new() /// { -/// cfFormat = (short) DataFormats.GetFormat("yourDataFormat").Id, +/// cfFormat = (short)DataFormats.GetFormat("yourDataFormat").Id, /// dwAspect = System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT, /// lindex = -1, /// tymed = System.Runtime.InteropServices.ComTypes.TYMED.TYMED_HGLOBAL @@ -60,14 +61,14 @@ namespace System.Private.Windows; /// /// if (record is not System.Formats.Nrbf.ClassRecord types /// || types.GetRawValue("k__BackingField") is not System.Formats.Nrbf.SZArrayRecord byteData -/// || !System.Reflection.Metadata.TypeName.TryParse(types.TypeName.FullName.ToCharArray(), out System.Reflection.Metadata.TypeName? result)) +/// || 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(); /// } /// -/// System.Reflection.Metadata.TypeName genericTypeName = result.GetGenericArguments().Single(); -/// // TODO: Additional checking on generic type to block unwanted types if needed. +/// // TODO: Additional checking on result TypeName to ensure it is expected type. /// /// // This should return the original data that was JSON serialized. /// System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), genericType); @@ -85,17 +86,19 @@ namespace System.Private.Windows; /// // OR /// // Not recommended: deserialize using BinaryFormatter. /// -/// // This definition must live in an assembly named System.Private.Windows.VirtualJson in order to work as expected. +/// // 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 +/// struct JsonData : IObjectReference /// { /// public byte[] JsonBytes { get; set; } /// +/// public string InnerTypeAssemblyQualifiedName { get; set; } +/// /// public object GetRealObject(StreamingContext context) /// { -/// // TODO: Additional checking on generic type to block unwanted types if needed. -/// return JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); +/// // TODO: Additional checking on InnerTypeAssemblyQualifiedName to ensure it is expected type. +/// return JsonSerializer.Deserialize(JsonBytes, typeof(ExpectedType)) ?? throw new InvalidOperationException(); /// } /// } /// ]]> @@ -105,14 +108,14 @@ internal struct JsonData : IJsonData { public byte[] JsonBytes { get; set; } - public readonly string TypeFullName => $"{typeof(JsonData).FullName}"; + public string InnerTypeAssemblyQualifiedName { get; set; } public readonly object Deserialize() { - object? result = null; + object? result; try { - result = JsonSerializer.Deserialize(JsonBytes, typeof(T)); + result = JsonSerializer.Deserialize(JsonBytes); } catch (Exception ex) { @@ -134,7 +137,15 @@ internal interface IJsonData byte[] JsonBytes { get; set; } - string TypeFullName { get; } + /// + /// The assembly qualified name of the T in . This name should + /// have any names taken into account. + /// + string InnerTypeAssemblyQualifiedName { get; set; } + /// + /// Deserializes the data stored in the JsonData. This is a convenience method + /// to deserialize teh 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 f341e1f3d7a..4c92a3bae9d 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs @@ -28,10 +28,9 @@ public void Clipboard_SetDataAsJson_ReturnsExpected() Point point = new() { X = 1, Y = 1 }; Clipboard.SetDataAsJson("point", point); - IDataObject? dataObject = Clipboard.GetDataObject(); - dataObject.Should().NotBeNull(); - dataObject!.GetDataPresent("point").Should().BeTrue(); - Point deserialized = dataObject.GetData("point").Should().BeOfType().Which; + ITypedDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + dataObject.GetDataPresent("point").Should().BeTrue(); + dataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); deserialized.Should().BeEquivalentTo(point); } @@ -45,9 +44,8 @@ public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) dataObject.SetDataAsJson("point", point); Clipboard.SetDataObject(dataObject, copy); - IDataObject? returnedDataObject = Clipboard.GetDataObject(); - returnedDataObject.Should().NotBeNull(); - Point deserialized = returnedDataObject!.GetData("point").Should().BeOfType().Which; + ITypedDataObject returnedDataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + returnedDataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); deserialized.Should().BeEquivalentTo(point); } } diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs index 99567ab1def..b0f2a5c943d 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs @@ -27,9 +27,10 @@ public void DataObject_WithJson_MockRoundTrip() using var inDataPtr = ComHelpers.GetComScope(inData); IDataObject outData = dropTargetAccessor.CreateDelegate()(inDataPtr); - outData.Should().BeSameAs(data); - outData.GetDataPresent("point").Should().BeTrue(); - outData.GetData("point").Should().BeOfType().Which.Should().BeEquivalentTo(point); + ITypedDataObject typedOutData = outData.Should().BeAssignableTo().Subject; + typedOutData.GetDataPresent("point").Should().BeTrue(); + typedOutData.TryGetData("point", out Point deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(point); } [WinFormsFact] diff --git a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs index 1a14743a032..1a19e5c0c79 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs @@ -510,10 +510,10 @@ public async Task DragDrop_JsonSerialized_ReturnsExpected_Async() }; form.DragOver += (s, e) => { - if (e.Data?.GetDataPresent(typeof(Point)) ?? false) + if (e.Data?.TryGetData(out Point data) ?? false) { // Get the JSON serialized Point. - dropped = e.Data?.GetData(typeof(Point)); + dropped = data; e.Effect = DragDropEffects.Copy; } }; 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 406529f575f..7dae655046e 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 @@ -24,7 +24,8 @@ public void BinaryFormattedObject_NonJsonData_RemainsSerialized() { Point point = new() { X = 1, Y = 1 }; SerializationRecord format = point.SerializeAndDecode(); - format.TryGetObjectFromJson(out _).Should().BeFalse(); + ITypeResolver resolver = new DataObject.Composition.Binder(typeof(Point), resolver: null, legacyMode: false); + format.TryGetObjectFromJson(resolver, out _).Should().BeFalse(); } [Fact] @@ -43,7 +44,8 @@ public void BinaryFormattedObject_JsonData_RoundTrip() stream.Position = 0; SerializationRecord binary = NrbfDecoder.Decode(stream); binary.TypeName.AssemblyName!.FullName.Should().Be(IJsonData.CustomAssemblyName); - binary.TryGetObjectFromJson(out object? result).Should().BeTrue(); + ITypeResolver resolver = new DataObject.Composition.Binder(typeof(Point), resolver: null, legacyMode: false); + binary.TryGetObjectFromJson(resolver, out object? result).Should().BeTrue(); Point deserialized = result.Should().BeOfType().Which; deserialized.Should().BeEquivalentTo(point); } @@ -55,7 +57,8 @@ public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() JsonData data = new() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), - }; + InnerTypeAssemblyQualifiedName = typeof(Point).ToTypeName().AssemblyQualifiedName + }; using MemoryStream stream = new(); WinFormsBinaryFormatWriter.WriteJsonData(stream, data); @@ -70,25 +73,32 @@ public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() } [Serializable] - private struct ReplicatedJsonData : IObjectReference + private struct ReplicatedJsonData : IObjectReference { public byte[] JsonBytes { get; set; } - public string OriginalAssemblyQualifiedTypeName { get; set; } + public string InnerTypeFullName { get; } - public readonly object GetRealObject(StreamingContext context) => - JsonSerializer.Deserialize(JsonBytes, typeof(T)) ?? throw new InvalidOperationException(); + public readonly object GetRealObject(StreamingContext context) + { + object? result = null; + if (TypeName.TryParse(InnerTypeFullName, out TypeName? genericTypeName) + && genericTypeName.Matches(typeof(Point).ToTypeName())) + { + result = JsonSerializer.Deserialize(JsonBytes); + } + + return result ?? throw new InvalidOperationException(); + } } private class JsonDataPointBinder : SerializationBinder { public override Type? BindToType(string assemblyName, string typeName) { - if (assemblyName == "System.Private.Windows.VirtualJson" - && TypeName.TryParse(typeName, out TypeName? name) - && name.GetGenericArguments().Single().AssemblyQualifiedName == typeof(Point).AssemblyQualifiedName) + if (assemblyName == "System.Private.Windows.VirtualJson") { - return typeof(ReplicatedJsonData); + return typeof(ReplicatedJsonData); } throw new InvalidOperationException(); @@ -155,7 +165,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 702e6e047ec..a389c49692a 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 @@ -10,8 +10,8 @@ using System.Formats.Nrbf; using System.Reflection.Metadata; using System.Runtime.InteropServices; -using System.Windows.Forms.Primitives; using System.Text.Json; +using System.Windows.Forms.Primitives; using Windows.Win32.System.Ole; using static System.Windows.Forms.Tests.BinaryFormatUtilitiesTests; using Com = Windows.Win32.System.Com; @@ -898,19 +898,48 @@ public void Clipboard_SetDataAsJson_DataObject_Throws() clipboardSet2.Should().NotThrow(); } + [WinFormsFact] + public void Clipboard_SetDataAsJson_WithGeneric_ReturnsExpected() + { + List generic1 = []; + Clipboard.SetDataAsJson("list", generic1); + Clipboard.TryGetData("list", out List? points).Should().BeTrue(); + points.Should().BeEquivalentTo(generic1); + + List generic2 = []; + Clipboard.SetDataAsJson("list", generic2); + Clipboard.TryGetData("list", out List? testData).Should().BeTrue(); + testData.Should().BeEquivalentTo(generic2); + } + [WinFormsFact] public void Clipboard_SetDataAsJson_ReturnsExpected() { Point point = new() { X = 1, Y = 1 }; Clipboard.SetDataAsJson("point", point); - IDataObject? dataObject = Clipboard.GetDataObject(); - dataObject.Should().NotBeNull(); - dataObject!.GetDataPresent("point").Should().BeTrue(); - Point deserialized = dataObject.GetData("point").Should().BeOfType().Which; + IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + dataObject.GetDataPresent("point").Should().BeTrue(); + dataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); deserialized.Should().BeEquivalentTo(point); } + [WinFormsFact] + public void Clipboard_SetDataAsJson_GetData() + { + Point point = new() { X = 1, Y = 1 }; + // Note that this simulates out of process scenario. + Clipboard.SetDataAsJson("point", point); + Action a = () => Clipboard.GetData("point"); + a.Should().Throw(); + + using BinaryFormatterInClipboardDragDropScope scope = new(enable: true); + a.Should().Throw(); + + using BinaryFormatterScope scope2 = new(enable: true); + Clipboard.GetData("point").Should().BeOfType(); + } + [WinFormsTheory] [BoolData] public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) @@ -921,9 +950,8 @@ public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) dataObject.SetDataAsJson("point", point); Clipboard.SetDataObject(dataObject, copy); - IDataObject? returnedDataObject = Clipboard.GetDataObject(); - returnedDataObject.Should().NotBeNull(); - Point deserialized = returnedDataObject!.GetData("point").Should().BeOfType().Which; + ITypedDataObject returnedDataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; + returnedDataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); deserialized.Should().BeEquivalentTo(point); } @@ -939,9 +967,12 @@ public void Clipboard_SetDataObject_WithMultipleData_ReturnsExpected(bool copy) data.SetData("Mystring", "test"); Clipboard.SetDataObject(data, copy); - Clipboard.GetData("point1").Should().BeOfType().Which.Should().BeEquivalentTo(point1); - Clipboard.GetData("point2").Should().BeOfType().Which.Should().BeEquivalentTo(point2); - Clipboard.GetData("Mystring").Should().Be("test"); + Clipboard.TryGetData("point1", out Point deserializedPoint1).Should().BeTrue(); + deserializedPoint1.Should().BeEquivalentTo(point1); + Clipboard.TryGetData("point2", out Point deserializedPoint2).Should().BeTrue(); + deserializedPoint2.Should().BeEquivalentTo(point2); + Clipboard.TryGetData("Mystring", out string? deserializedString).Should().BeTrue(); + deserializedString.Should().Be("test"); } [WinFormsFact] @@ -985,11 +1016,15 @@ public unsafe void Clipboard_Deserialize_FromStream_Manually() SerializationRecord record = NrbfDecoder.Decode(stream); ClassRecord types = record.Should().BeAssignableTo().Which; types.HasMember("k__BackingField").Should().BeTrue(); - SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Which; - TypeName.TryParse(types.TypeName.FullName, out TypeName? result).Should().BeTrue(); - TypeName checkedResult = result.Should().BeOfType().Which; - TypeName genericTypeName = checkedResult.GetGenericArguments().SingleOrDefault().Should().BeOfType().Subject; - Type.GetType(genericTypeName.AssemblyQualifiedName).Should().Be(typeof(Point)); + types.HasMember("k__BackingField").Should().BeTrue(); + SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Subject; + string innerTypeFullName = types.GetRawValue("k__BackingField").Should().BeOfType().Subject; + TypeName.TryParse(innerTypeFullName, out TypeName? result).Should().BeTrue(); + TypeName checkedResult = result.Should().BeOfType().Subject; + // These should not be the same since we take TypeForwardedFromAttribute name into account during serialization, + // which changes the assembly name. + typeof(Point).AssemblyQualifiedName.Should().NotBe(checkedResult.AssemblyQualifiedName); + typeof(Point).ToTypeName().Matches(checkedResult).Should().BeTrue(); JsonSerializer.Deserialize(byteData.GetArray(), typeof(Point)).Should().BeEquivalentTo(point); } @@ -999,11 +1034,17 @@ 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); - Clipboard.GetData(format).Should().BeOfType(); - DataObject dataObject = Clipboard.GetDataObject().Should().BeOfType().Subject; - dataObject.GetData(format).Should().BeOfType(); + 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] @@ -1011,7 +1052,7 @@ public void Clipboard_SurfaceJsonError() public void Clipboard_CustomDataObject_AvoidBinaryFormatter(bool copy) { string format = "customFormat"; - TestData data = new() { X = 1, Y = 1 }; + SimpleTestData data = new() { X = 1, Y = 1 }; Clipboard.SetData(format, data); // BinaryFormatter not enabled. Clipboard.GetData(format).Should().BeOfType(); @@ -1028,18 +1069,18 @@ public void Clipboard_CustomDataObject_AvoidBinaryFormatter(bool copy) IDataObject received = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; received.Should().NotBe(jsonDataObject); byte[] jsonBytes = Clipboard.GetData(format).Should().BeOfType().Subject; - JsonSerializer.Deserialize(jsonBytes, typeof(TestData)).Should().BeEquivalentTo(data); + 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); + received.Deserialize(format).Should().BeEquivalentTo(data); } } [Serializable] - private struct TestData + private struct SimpleTestData { public int X { get; set; } public int Y { get; set; } 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 dc1346d16a4..df950bd2fd9 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,6 +5,7 @@ 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; @@ -2738,8 +2739,10 @@ public unsafe void DataObject_WithJson_MockRoundTrip_OutData_IsSame(DataObject d using var inDataPtr = ComHelpers.GetComScope(inData); IDataObject outData = dropTargetAccessor.CreateDelegate()(inDataPtr); outData.Should().BeSameAs(data); - outData.GetDataPresent("point").Should().BeTrue(); - outData.GetData("point").Should().BeOfType().Which.Should().BeEquivalentTo(point); + ITypedDataObject typedOutData = outData.Should().BeAssignableTo().Subject; + typedOutData.GetDataPresent("point").Should().BeTrue(); + typedOutData.TryGetData("point", out Point deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(point); } [WinFormsFact] @@ -2848,7 +2851,8 @@ public void DataObject_SetDataAsJson_ReturnsExpected() DataObject dataObject = new(); dataObject.SetDataAsJson("point", point); dataObject.GetDataPresent("point").Should().BeTrue(); - dataObject.GetData("point").Should().BeOfType().Which.Should().BeEquivalentTo(point); + dataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(point); } [WinFormsFact] @@ -2859,7 +2863,8 @@ public void DataObject_SetDataAsJson_Wrapped_ReturnsExpected() dataObject.SetDataAsJson("point", point); DataObject wrapped = new(dataObject); wrapped.GetDataPresent("point").Should().BeTrue(); - wrapped.GetData("point").Should().BeOfType().Which.Should().BeEquivalentTo(point); + wrapped.TryGetData("point", out Point deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(point); } [WinFormsFact] @@ -2872,9 +2877,21 @@ public void DataObject_SetDataAsJson_MultipleData_ReturnsExpected() data.SetDataAsJson("point2", point2); data.SetData("Mystring", "test"); - data.GetData("point1").Should().BeOfType().Which.Should().BeEquivalentTo(point1); - data.GetData("point2").Should().BeOfType().Which.Should().BeEquivalentTo(point2); - data.GetData("Mystring").Should().Be("test"); + data.TryGetData("point1", out Point deserializedPoint1).Should().BeTrue(); + deserializedPoint1.Should().BeEquivalentTo(point1); + data.TryGetData("point2", out Point deserializedPoint2).Should().BeTrue(); + deserializedPoint2.Should().BeEquivalentTo(point2); + data.TryGetData("Mystring", out string deserializedString).Should().BeTrue(); + deserializedString.Should().Be("test"); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_GetData_ReturnsNonDeserialized() + { + Point point = new() { X = 1, Y = 1 }; + DataObject data = new(); + data.SetDataAsJson("point", point); + data.GetData("point").Should().BeOfType>(); } [WinFormsFact] @@ -2893,7 +2910,7 @@ public void DataObject_SetDataAsJson_CustomJsonConverter_ReturnsExpected() DataObject dataObject = new(); dataObject.SetDataAsJson("custom", forecast); - WeatherForecast deserialized = dataObject.GetData("custom").Should().BeOfType().Subject; + 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); From 75a318053ed2e523af753de9efe63c71f8e23660 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 13 Dec 2024 14:45:38 -0800 Subject: [PATCH 28/34] - Make JsonData.InnerTypeAssemblyQualifiedName readonly - Avoid JSON serializing restricted formats - other feedback --- .../WinFormsSerializationRecordExtensions.cs | 9 +- .../src/System/Windows/Forms/OLE/Clipboard.cs | 10 +- ...ect.Composition.NativeToWinFormsAdapter.cs | 10 -- .../Forms/OLE/DataObject.Composition.cs | 22 ---- .../System/Windows/Forms/OLE/DataObject.cs | 75 +++++++++-- .../src/System/Windows/Forms/OLE/JsonData.cs | 5 +- .../TestUtilities/DataObjectTestHelpers.cs | 52 ++++++++ .../WinFormsBinaryFormattedObjectTests.cs | 9 +- .../System/Windows/Forms/DataObjectTests.cs | 51 ++++++-- .../Forms/NativeToWinFormsAdapterTests.cs | 121 ++++++------------ 10 files changed, 214 insertions(+), 150 deletions(-) create mode 100644 src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs 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 e3ec771057b..f3283667b71 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 @@ -79,14 +79,14 @@ public static bool TryGetObjectFromJson(this SerializationRecord record, ITyp 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? genericTypeName)) + || !TypeName.TryParse(innerTypeFullName, out TypeName? serializedTypeName)) { // This is supposed to be JsonData, but somehow the binary formatted data is corrupt. throw new SerializationException(); } - Type genericType = resolver.GetType(genericTypeName); - if (!genericType.IsAssignableTo(typeof(T))) + Type serializedType = resolver.GetType(serializedTypeName); + if (!serializedType.IsAssignableTo(typeof(T))) { // Not the type the caller asked for. return false; @@ -98,7 +98,8 @@ public static bool TryGetObjectFromJson(this SerializationRecord record, ITyp } catch (Exception ex) { - throw new NotSupportedException(ex.Message); + // loni TODO: localize string + throw new NotSupportedException("Failed to deserialize JSON data.", ex); } return true; 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 36c380dd535..e801999d94b 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 @@ -560,13 +560,9 @@ public static void SetDataAsJson(string format, T data) throw new InvalidOperationException($"'DataObject' will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetDataObject)} API instead."); } - JsonData jsonData = new() - { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), - InnerTypeAssemblyQualifiedName = typeof(T).ToTypeName().AssemblyQualifiedName - }; - - SetDataObject(new DataObject(format, jsonData), copy: true); + DataObject dataObject = new(); + dataObject.SetDataAsJson(format, data); + SetDataObject(dataObject, copy: true); } /// 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 14ac1ec1c7f..d081ff612af 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 @@ -480,16 +480,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.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObject.cs index be8b306532d..cec71aecf59 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 @@ -106,27 +106,26 @@ internal IDataObject TryUnwrapInnerIDataObject() [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] public void SetDataAsJson(string format, T data) { - data.OrThrowIfNull(nameof(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, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), InnerTypeAssemblyQualifiedName = typeof(T).ToTypeName().AssemblyQualifiedName }); + SetData(format, JsonSerializeNonRestricted(format, data)); } /// [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] public void SetDataAsJson(T data) { - 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 use {nameof(SetData)} API instead."); + // 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), new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), InnerTypeAssemblyQualifiedName = typeof(T).ToTypeName().AssemblyQualifiedName }); + SetData(typeof(T), JsonSerializeNonRestricted(typeof(T).FullName!, data)); } /// @@ -161,16 +160,72 @@ public void SetDataAsJson(T data) [RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")] public void SetDataAsJson(string format, bool autoConvert, T data) { - 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 use {nameof(SetData)} API instead."); + // 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, new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data), InnerTypeAssemblyQualifiedName = typeof(T).ToTypeName().AssemblyQualifiedName }); + SetData(format, autoConvert, JsonSerializeNonRestricted(format, data)); + } + + /// + /// JSON serialize the data only if the format is not a restricted deserialization format. + /// + /// + /// The passed in as is if the format is restricted. Otherwise the JSON serialized . + /// + private static object JsonSerializeNonRestricted(string format, T data) + { + format.OrThrowIfNull(nameof(format)); + data.OrThrowIfNull(nameof(data)); + return IsRestrictedFormat(format) + ? 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. + /// + internal 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, 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 index fa824455132..11d0214cc96 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Text.Json; +using System.Windows.Forms; namespace System.Private.Windows; @@ -108,7 +109,7 @@ internal struct JsonData : IJsonData { public byte[] JsonBytes { get; set; } - public string InnerTypeAssemblyQualifiedName { get; set; } + public readonly string InnerTypeAssemblyQualifiedName => typeof(T).ToTypeName().AssemblyQualifiedName; public readonly object Deserialize() { @@ -141,7 +142,7 @@ internal interface IJsonData /// The assembly qualified name of the T in . This name should /// have any names taken into account. /// - string InnerTypeAssemblyQualifiedName { get; set; } + string InnerTypeAssemblyQualifiedName { get; } /// /// Deserializes the data stored in the JsonData. This is a convenience method 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..7ef8920e68f --- /dev/null +++ b/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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" + ]; +} 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 7dae655046e..3b6557b3327 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 @@ -35,7 +35,7 @@ public void BinaryFormattedObject_JsonData_RoundTrip() JsonData json = new() { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) }; using MemoryStream stream = new(); @@ -56,8 +56,7 @@ public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() Point point = new() { X = 1, Y = 1 }; JsonData data = new() { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point), - InnerTypeAssemblyQualifiedName = typeof(Point).ToTypeName().AssemblyQualifiedName + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) }; using MemoryStream stream = new(); @@ -77,12 +76,12 @@ private struct ReplicatedJsonData : IObjectReference { public byte[] JsonBytes { get; set; } - public string InnerTypeFullName { get; } + public string InnerTypeAssemblyQualifiedName { get; set; } public readonly object GetRealObject(StreamingContext context) { object? result = null; - if (TypeName.TryParse(InnerTypeFullName, out TypeName? genericTypeName) + if (TypeName.TryParse(InnerTypeAssemblyQualifiedName, out TypeName? genericTypeName) && genericTypeName.Matches(typeof(Point).ToTypeName())) { result = JsonSerializer.Deserialize(JsonBytes); 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 df950bd2fd9..b723ba12893 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 @@ -12,6 +12,7 @@ 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; @@ -2885,15 +2886,6 @@ public void DataObject_SetDataAsJson_MultipleData_ReturnsExpected() deserializedString.Should().Be("test"); } - [WinFormsFact] - public void DataObject_SetDataAsJson_GetData_ReturnsNonDeserialized() - { - Point point = new() { X = 1, Y = 1 }; - DataObject data = new(); - data.SetDataAsJson("point", point); - data.GetData("point").Should().BeOfType>(); - } - [WinFormsFact] public void DataObject_SetDataAsJson_CustomJsonConverter_ReturnsExpected() { @@ -3000,4 +2992,45 @@ public void DataObject_SetDataAsJson_NullData_Throws() 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); + // + var test = dataObject.GetData(format); + test.Should().NotBeOfType(); + } + + [WinFormsFact] + public void DataObject_SetDataAsJson_PredefinedTypes_NotJsonSerialized() + { + string format = "test"; + using Bitmap bitmap = new(10, 10); + DataObject dataObject = new(); + + dataObject.SetDataAsJson(format, bitmap); + object result = dataObject.GetData(format); + result.Should().NotBeOfType(); + result.Should().Be(bitmap); + + string testString = "testString"; + dataObject.SetDataAsJson(format, testString); + result = dataObject.GetData(format); + result.Should().NotBeOfType(); + result.Should().Be(testString); + } + + [WinFormsTheory] + [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] + public void DataObject_SetDataAsJson_NonRestrictedFormat_JsonSerialized(string format) + { + DataObject data = new(); + data.SetDataAsJson(format, 1); + data.GetData(format).Should().BeOfType>(); + } } 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..12373211051 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(); From db5be00598ab75911df8dc77940deee8691111da Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 13 Dec 2024 14:46:10 -0800 Subject: [PATCH 29/34] remove unnecessary using --- .../src/System/Windows/Forms/OLE/Clipboard.cs | 1 - 1 file changed, 1 deletion(-) 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 e801999d94b..fb612c099f1 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 @@ -5,7 +5,6 @@ using System.Drawing; using System.Formats.Nrbf; using System.Reflection.Metadata; -using System.Private.Windows; using System.Runtime.InteropServices; using System.Runtime.Serialization.Formatters.Binary; using System.Text.Json; From c3e7d6505f44a8b71001f9477116d3cbb3688929 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 13 Dec 2024 15:41:44 -0800 Subject: [PATCH 30/34] Add comments --- src/System.Windows.Forms/src/Resources/SR.resx | 2 -- .../src/System/Windows/Forms/OLE/Clipboard.cs | 8 +++++++- .../src/System/Windows/Forms/OLE/DataObject.cs | 8 +++++++- .../UnitTests/System/Windows/Forms/DataObjectTests.cs | 4 +--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/System.Windows.Forms/src/Resources/SR.resx b/src/System.Windows.Forms/src/Resources/SR.resx index c2b633bd193..ce9389b6ab6 100644 --- a/src/System.Windows.Forms/src/Resources/SR.resx +++ b/src/System.Windows.Forms/src/Resources/SR.resx @@ -7035,7 +7035,5 @@ Stack trace where the illegal operation occurred was: The specified type '{0}' is not compatible with the specified format '{1}'. - - DataObject cannot be JSON serialized meaningfully. Set the data by using {0} instead. 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 fb612c099f1..2548c3f5298 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 @@ -515,7 +515,8 @@ public static void SetData(string format, object data) } /// - /// Saves the data onto the clipboard in the specified format using JSON serialization. + /// Saves the data onto the clipboard in the specified format. + /// If the data is a managed object and format allows for serialization of managed objects, the object will be serialized using JSON. /// /// /// null, empty, or whitespace was passed as the format. @@ -528,6 +529,11 @@ public static void SetData(string format, object data) /// /// /// + /// 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. /// /// 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 cec71aecf59..9dec7abd9cb 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 @@ -129,7 +129,8 @@ public void SetDataAsJson(T data) } /// - /// Stores the data as JSON in the specified format. + /// 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, . @@ -141,6 +142,11 @@ public void SetDataAsJson(T data) /// /// /// + /// 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. /// /// 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 b723ba12893..fd1ce1976d2 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 @@ -3001,9 +3001,7 @@ public void DataObject_SetDataAsJson_RestrictedFormats_NotJsonSerialized(string { DataObject dataObject = new(); dataObject.SetDataAsJson(format, 1); - // - var test = dataObject.GetData(format); - test.Should().NotBeOfType(); + dataObject.GetData(format).Should().NotBeOfType(); } [WinFormsFact] From 494b7fc5788ba438e04a708d9bc5396c8b6e189c Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Fri, 13 Dec 2024 19:12:31 -0800 Subject: [PATCH 31/34] - Avoid exposing JsonData in legacy GetData - Add test for requiring resolver with SetDataAsJson --- .../System/Windows/Forms/OLE/DataObject.cs | 7 ++- .../System/Windows/Forms/DataObjectTests.cs | 29 +++------- .../Forms/NativeToWinFormsAdapterTests.cs | 53 +++++++++++++++++++ 3 files changed, 67 insertions(+), 22 deletions(-) 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 9dec7abd9cb..5901c2e434f 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 @@ -238,7 +238,12 @@ or DataFormats.PaletteConstant 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/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/DataObjectTests.cs index fd1ce1976d2..e005f5260e6 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 @@ -3001,26 +3001,9 @@ public void DataObject_SetDataAsJson_RestrictedFormats_NotJsonSerialized(string { DataObject dataObject = new(); dataObject.SetDataAsJson(format, 1); - dataObject.GetData(format).Should().NotBeOfType(); - } - - [WinFormsFact] - public void DataObject_SetDataAsJson_PredefinedTypes_NotJsonSerialized() - { - string format = "test"; - using Bitmap bitmap = new(10, 10); - DataObject dataObject = new(); - - dataObject.SetDataAsJson(format, bitmap); - object result = dataObject.GetData(format); - result.Should().NotBeOfType(); - result.Should().Be(bitmap); - - string testString = "testString"; - dataObject.SetDataAsJson(format, testString); - result = dataObject.GetData(format); - result.Should().NotBeOfType(); - result.Should().Be(testString); + object storedData = dataObject.TestAccessor().Dynamic._innerData.GetData(format); + storedData.Should().NotBeAssignableTo(); + dataObject.GetData(format).Should().Be(1); } [WinFormsTheory] @@ -3029,6 +3012,10 @@ public void DataObject_SetDataAsJson_NonRestrictedFormat_JsonSerialized(string f { DataObject data = new(); data.SetDataAsJson(format, 1); - data.GetData(format).Should().BeOfType>(); + 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(); } } 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 12373211051..8152f9652c4 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 @@ -428,6 +428,59 @@ 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(); + dataObject.TryGetData("test", SimpleTestData.Resolver, autoConvert: false, out SimpleTestDataBase? deserialized).Should().BeTrue(); + var deserializedChecked = deserialized.Should().BeOfType().Subject; + deserializedChecked.Text.Should().Be(value.Text); + } + + 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 { From 79b3365bb5555692917dd314c32270ea24d8fedc Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Mon, 16 Dec 2024 11:07:25 -0800 Subject: [PATCH 32/34] Block JSON serializing intrinsic types --- .../MyServices/ClipboardProxyTests.cs | 20 +++-- .../src/System/Windows/Forms/Control.cs | 8 +- .../src/System/Windows/Forms/OLE/Clipboard.cs | 4 +- .../System/Windows/Forms/OLE/DataObject.cs | 12 +-- .../ComDisabledTests/ClipboardComTests.cs | 20 ++--- .../ComDisabledTests/DataObjectComTests.cs | 12 +-- .../UIIntegrationTests/DragDropTests.cs | 13 +-- .../TestUtilities/DataObjectTestHelpers.cs | 18 ++++- .../WinFormsBinaryFormattedObjectTests.cs | 41 +++++----- .../System/Windows/Forms/ClipboardTests.cs | 80 ++++++++++--------- .../System/Windows/Forms/DataObjectTests.cs | 55 ++++++++----- .../Forms/NativeToWinFormsAdapterTests.cs | 19 +++++ 12 files changed, 183 insertions(+), 119 deletions(-) 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 e8666d14ba9..ffcc275a4ff 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; @@ -90,12 +91,21 @@ public void Text() public void SetDataAsJson() { var clipboard = new Computer().Clipboard; - Point point = new(1, 1); - clipboard.SetDataAsJson("point", point); - clipboard.ContainsData("point").Should().Be(System.Windows.Forms.Clipboard.ContainsData("point")); - clipboard.TryGetData("point", out Point retrieved).Should().Be(System.Windows.Forms.Clipboard.TryGetData("point", out Point retrieved2)); + 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(point); + retrieved.Should().BeEquivalentTo(testData); + } + + [Serializable] + [TypeForwardedFrom("System.ForwardAssembly")] + public struct SimpleTestData + { + public int X { get; set; } + public int Y { get; set; } } [WinFormsFact] 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 74e8c948a7c..52a09f7e16a 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs @@ -4785,7 +4785,7 @@ public DragDropEffects DoDragDropAsJson(T data, DragDropEffects allowedEffect DoDragDropAsJson(data, allowedEffects, dragImage: null, cursorOffset: default, useDefaultDragImage: false); /// - /// Begins a drag operation, storing the data as Json. + /// Begins a drag operation /// /// The data being dragged. /// determine which drag operations can occur. @@ -4798,6 +4798,12 @@ public DragDropEffects DoDragDropAsJson(T data, DragDropEffects allowedEffect /// needs to 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 + /// as the same as . + /// + /// public DragDropEffects DoDragDropAsJson( T data, DragDropEffects allowedEffects, 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 2548c3f5298..b0a5a5559e6 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 @@ -516,7 +516,7 @@ public static void SetData(string format, object data) /// /// Saves the data onto the clipboard in the specified format. - /// If the data is a managed object and format allows for serialization of managed objects, the object will be serialized using JSON. + /// If the data is a non intrinsic type, the object will be serialized using JSON. /// /// /// null, empty, or whitespace was passed as the format. @@ -530,7 +530,7 @@ public static void SetData(string format, object data) /// /// /// If your data is an intrinsically handled type such as primitives, string, or Bitmap - /// and you are using a custom format or + /// and you are using a custom format or , /// it is recommended to use the APIs to avoid unnecessary overhead. /// /// 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 5901c2e434f..8a6dd47ec18 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 @@ -112,7 +112,7 @@ public void SetDataAsJson(string format, T data) throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); } - SetData(format, JsonSerializeNonRestricted(format, data)); + SetData(format, TryJsonSerialize(format, data)); } /// @@ -125,7 +125,7 @@ public void SetDataAsJson(T data) throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); } - SetData(typeof(T), JsonSerializeNonRestricted(typeof(T).FullName!, data)); + SetData(typeof(T), TryJsonSerialize(typeof(T).FullName!, data)); } /// @@ -172,20 +172,20 @@ public void SetDataAsJson(string format, bool autoConvert, T data) throw new InvalidOperationException($"DataObject will serialize as empty. JSON serialize the data within {nameof(data)}, then use {nameof(SetData)} instead."); } - SetData(format, autoConvert, JsonSerializeNonRestricted(format, data)); + SetData(format, autoConvert, TryJsonSerialize(format, data)); } /// - /// JSON serialize the data only if the format is not a restricted deserialization format. + /// 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 JsonSerializeNonRestricted(string format, T data) + private static object TryJsonSerialize(string format, T data) { format.OrThrowIfNull(nameof(format)); data.OrThrowIfNull(nameof(data)); - return IsRestrictedFormat(format) + return IsRestrictedFormat(format) || Composition.Binder.IsKnownType() ? data : new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) }; } diff --git a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs index 4c92a3bae9d..a2840c10c69 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/ClipboardComTests.cs @@ -3,7 +3,7 @@ #nullable enable -using System.Drawing; +using static System.Windows.Forms.TestUtilities.DataObjectTestHelpers; namespace System.Windows.Forms.Tests; @@ -25,27 +25,27 @@ public void Clipboard_SetText_InvokeString_GetReturnsExpected() [WinFormsFact] public void Clipboard_SetDataAsJson_ReturnsExpected() { - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; - Clipboard.SetDataAsJson("point", point); + Clipboard.SetDataAsJson("testData", testData); ITypedDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; - dataObject.GetDataPresent("point").Should().BeTrue(); - dataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); - deserialized.Should().BeEquivalentTo(point); + 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) { - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; DataObject dataObject = new(); - dataObject.SetDataAsJson("point", point); + dataObject.SetDataAsJson("testData", testData); Clipboard.SetDataObject(dataObject, copy); ITypedDataObject returnedDataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; - returnedDataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); - deserialized.Should().BeEquivalentTo(point); + 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 b0f2a5c943d..5276ef61930 100644 --- a/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs +++ b/src/System.Windows.Forms/tests/ComDisabledTests/DataObjectComTests.cs @@ -1,8 +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.Drawing; 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; @@ -18,9 +18,9 @@ public void DataObject_WithJson_MockRoundTrip() dynamic controlAccessor = typeof(Control).TestAccessor().Dynamic; var dropTargetAccessor = typeof(DropTarget).TestAccessor(); - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; DataObject data = new(); - data.SetDataAsJson("point", point); + data.SetDataAsJson("testData", testData); DataObject inData = controlAccessor.CreateRuntimeDataObjectForDrag(data); inData.Should().BeSameAs(data); @@ -28,9 +28,9 @@ public void DataObject_WithJson_MockRoundTrip() using var inDataPtr = ComHelpers.GetComScope(inData); IDataObject outData = dropTargetAccessor.CreateDelegate()(inDataPtr); ITypedDataObject typedOutData = outData.Should().BeAssignableTo().Subject; - typedOutData.GetDataPresent("point").Should().BeTrue(); - typedOutData.TryGetData("point", out Point deserialized).Should().BeTrue(); - deserialized.Should().BeEquivalentTo(point); + typedOutData.GetDataPresent("testData").Should().BeTrue(); + typedOutData.TryGetData("testData", out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); } [WinFormsFact] diff --git a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs index 1a19e5c0c79..7fd08a3b569 100644 --- a/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs +++ b/src/System.Windows.Forms/tests/IntegrationTests/UIIntegrationTests/DragDropTests.cs @@ -8,6 +8,7 @@ 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; @@ -495,7 +496,7 @@ public async Task DragDrop_JsonSerialized_ReturnsExpected_Async() { // Verifies that we can successfully drag and drop a JSON serialized object. - Point point = new(10, 10); + SimpleTestData testData = new() { X = 10, Y = 10 }; object? dropped = null; await RunFormWithoutControlAsync(() => new Form(), async (form) => { @@ -503,14 +504,14 @@ public async Task DragDrop_JsonSerialized_ReturnsExpected_Async() form.ClientSize = new Size(100, 100); form.DragEnter += (s, e) => { - if (e.Data?.GetDataPresent(typeof(Point)) ?? false) + if (e.Data?.GetDataPresent(typeof(SimpleTestData)) ?? false) { e.Effect = DragDropEffects.Copy; } }; form.DragOver += (s, e) => { - if (e.Data?.TryGetData(out Point data) ?? false) + if (e.Data?.TryGetData(out SimpleTestData data) ?? false) { // Get the JSON serialized Point. dropped = data; @@ -519,7 +520,7 @@ public async Task DragDrop_JsonSerialized_ReturnsExpected_Async() }; form.MouseDown += (s, e) => { - form.DoDragDropAsJson(point, DragDropEffects.Copy); + form.DoDragDropAsJson(testData, DragDropEffects.Copy); }; var startRect = form.DisplayRectangle; @@ -541,8 +542,8 @@ await InputSimulator.SendAsync( .LeftButtonUp()); }); - dropped.Should().BeOfType(typeof(Point)); - dropped.Should().BeEquivalentTo(point); + dropped.Should().BeOfType(typeof(SimpleTestData)); + dropped.Should().BeEquivalentTo(testData); } private void CloseExplorer(string directory) diff --git a/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs b/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs index 7ef8920e68f..0e7ea2f18ca 100644 --- a/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs +++ b/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.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.Runtime.CompilerServices; + namespace System.Windows.Forms.TestUtilities; /// @@ -23,10 +25,10 @@ public static TheoryData StringFormat() => ]; public static TheoryData UnboundedFormat() => -[ - DataFormats.Serializable, - "something custom" -]; + [ + DataFormats.Serializable, + "something custom" + ]; // These formats contain only known types. public static TheoryData UndefinedRestrictedFormat() => @@ -49,4 +51,12 @@ 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/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormattedObjectTests.cs b/src/System.Windows.Forms/tests/UnitTests/System/Windows/Forms/BinaryFormat/WinFormsBinaryFormattedObjectTests.cs index 3b6557b3327..85791825eea 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 @@ -12,6 +12,7 @@ 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; @@ -22,20 +23,20 @@ public class WinFormsBinaryFormattedObjectTests [Fact] public void BinaryFormattedObject_NonJsonData_RemainsSerialized() { - Point point = new() { X = 1, Y = 1 }; - SerializationRecord format = point.SerializeAndDecode(); - ITypeResolver resolver = new DataObject.Composition.Binder(typeof(Point), resolver: null, legacyMode: false); - format.TryGetObjectFromJson(resolver, out _).Should().BeFalse(); + 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() { - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; - JsonData json = new() + JsonData json = new() { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(testData) }; using MemoryStream stream = new(); @@ -44,19 +45,19 @@ public void BinaryFormattedObject_JsonData_RoundTrip() stream.Position = 0; SerializationRecord binary = NrbfDecoder.Decode(stream); binary.TypeName.AssemblyName!.FullName.Should().Be(IJsonData.CustomAssemblyName); - ITypeResolver resolver = new DataObject.Composition.Binder(typeof(Point), resolver: null, legacyMode: false); - binary.TryGetObjectFromJson(resolver, out object? result).Should().BeTrue(); - Point deserialized = result.Should().BeOfType().Which; - deserialized.Should().BeEquivalentTo(point); + ITypeResolver resolver = new DataObject.Composition.Binder(typeof(SimpleTestData), resolver: null, legacyMode: false); + 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() { - Point point = new() { X = 1, Y = 1 }; - JsonData data = new() + SimpleTestData testData = new() { X = 1, Y = 1 }; + JsonData data = new() { - JsonBytes = JsonSerializer.SerializeToUtf8Bytes(point) + JsonBytes = JsonSerializer.SerializeToUtf8Bytes(testData) }; using MemoryStream stream = new(); @@ -65,10 +66,10 @@ public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() using BinaryFormatterScope scope = new(enable: true); #pragma warning disable SYSLIB0011 // Type or member is obsolete - BinaryFormatter binaryFormatter = new() { Binder = new JsonDataPointBinder() }; + BinaryFormatter binaryFormatter = new() { Binder = new JsonDataTestDataBinder() }; #pragma warning restore SYSLIB0011 - Point deserialized = binaryFormatter.Deserialize(stream).Should().BeOfType().Which; - deserialized.Should().BeEquivalentTo(point); + SimpleTestData deserialized = binaryFormatter.Deserialize(stream).Should().BeOfType().Which; + deserialized.Should().BeEquivalentTo(testData); } [Serializable] @@ -82,16 +83,16 @@ public readonly object GetRealObject(StreamingContext context) { object? result = null; if (TypeName.TryParse(InnerTypeAssemblyQualifiedName, out TypeName? genericTypeName) - && genericTypeName.Matches(typeof(Point).ToTypeName())) + && genericTypeName.Matches(typeof(SimpleTestData).ToTypeName())) { - result = JsonSerializer.Deserialize(JsonBytes); + result = JsonSerializer.Deserialize(JsonBytes); } return result ?? throw new InvalidOperationException(); } } - private class JsonDataPointBinder : SerializationBinder + private class JsonDataTestDataBinder : SerializationBinder { public override Type? BindToType(string assemblyName, string typeName) { 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 a389c49692a..21f2b07e1ee 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 @@ -14,6 +14,7 @@ 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; @@ -902,75 +903,83 @@ public void Clipboard_SetDataAsJson_DataObject_Throws() public void Clipboard_SetDataAsJson_WithGeneric_ReturnsExpected() { List generic1 = []; - Clipboard.SetDataAsJson("list", generic1); - Clipboard.TryGetData("list", out List? points).Should().BeTrue(); + 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("list", generic2); - Clipboard.TryGetData("list", out List? testData).Should().BeTrue(); - testData.Should().BeEquivalentTo(generic2); + 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() { - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; - Clipboard.SetDataAsJson("point", point); + Clipboard.SetDataAsJson("testData", testData); IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; - dataObject.GetDataPresent("point").Should().BeTrue(); - dataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); - deserialized.Should().BeEquivalentTo(point); + dataObject.GetDataPresent("testData").Should().BeTrue(); + dataObject.TryGetData("testData", out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); } [WinFormsFact] public void Clipboard_SetDataAsJson_GetData() { - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; // Note that this simulates out of process scenario. - Clipboard.SetDataAsJson("point", point); - Action a = () => Clipboard.GetData("point"); + 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("point").Should().BeOfType(); + Clipboard.GetData("test").Should().BeOfType(); } [WinFormsTheory] [BoolData] public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) { - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; DataObject dataObject = new(); - dataObject.SetDataAsJson("point", point); + dataObject.SetDataAsJson("testData", testData); Clipboard.SetDataObject(dataObject, copy); ITypedDataObject returnedDataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; - returnedDataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); - deserialized.Should().BeEquivalentTo(point); + returnedDataObject.TryGetData("testData", out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); } [WinFormsTheory] [BoolData] public void Clipboard_SetDataObject_WithMultipleData_ReturnsExpected(bool copy) { - Point point1 = new() { X = 1, Y = 1 }; - Point point2 = new() { Y = 2, X = 2 }; + SimpleTestData testData1 = new() { X = 1, Y = 1 }; + SimpleTestData testData2 = new() { Y = 2, X = 2 }; DataObject data = new(); - data.SetDataAsJson("point1", point1); - data.SetDataAsJson("point2", point2); + data.SetDataAsJson("testData1", testData1); + data.SetDataAsJson("testData2", testData2); data.SetData("Mystring", "test"); Clipboard.SetDataObject(data, copy); - Clipboard.TryGetData("point1", out Point deserializedPoint1).Should().BeTrue(); - deserializedPoint1.Should().BeEquivalentTo(point1); - Clipboard.TryGetData("point2", out Point deserializedPoint2).Should().BeTrue(); - deserializedPoint2.Should().BeEquivalentTo(point2); + 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"); } @@ -981,8 +990,8 @@ 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. - Point point = new(1, 1); - Clipboard.SetDataAsJson("testFormat", point); + SimpleTestData testData = new() { X = 1, Y = 1 }; + Clipboard.SetDataAsJson("testFormat", testData); // Manually retrieve the serialized stream. ComTypes.IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Which; @@ -1023,10 +1032,10 @@ public unsafe void Clipboard_Deserialize_FromStream_Manually() TypeName checkedResult = result.Should().BeOfType().Subject; // These should not be the same since we take TypeForwardedFromAttribute name into account during serialization, // which changes the assembly name. - typeof(Point).AssemblyQualifiedName.Should().NotBe(checkedResult.AssemblyQualifiedName); - typeof(Point).ToTypeName().Matches(checkedResult).Should().BeTrue(); + typeof(SimpleTestData).AssemblyQualifiedName.Should().NotBe(checkedResult.AssemblyQualifiedName); + typeof(SimpleTestData).ToTypeName().Matches(checkedResult).Should().BeTrue(); - JsonSerializer.Deserialize(byteData.GetArray(), typeof(Point)).Should().BeEquivalentTo(point); + JsonSerializer.Deserialize(byteData.GetArray(), typeof(SimpleTestData)).Should().BeEquivalentTo(testData); } [WinFormsFact] @@ -1079,13 +1088,6 @@ public void Clipboard_CustomDataObject_AvoidBinaryFormatter(bool copy) } } - [Serializable] - private struct SimpleTestData - { - public int X { get; set; } - public int Y { get; set; } - } - // 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 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 e005f5260e6..06c2101db9f 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 @@ -18,6 +18,7 @@ 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; @@ -2848,40 +2849,42 @@ public void DataObject_SetDataAsJson_DataObject_Throws() [WinFormsFact] public void DataObject_SetDataAsJson_ReturnsExpected() { - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; DataObject dataObject = new(); - dataObject.SetDataAsJson("point", point); - dataObject.GetDataPresent("point").Should().BeTrue(); - dataObject.TryGetData("point", out Point deserialized).Should().BeTrue(); - deserialized.Should().BeEquivalentTo(point); + 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() { - Point point = new() { X = 1, Y = 1 }; + SimpleTestData testData = new() { X = 1, Y = 1 }; DataObject dataObject = new(); - dataObject.SetDataAsJson("point", point); + string format = "testData"; + dataObject.SetDataAsJson(format, testData); DataObject wrapped = new(dataObject); - wrapped.GetDataPresent("point").Should().BeTrue(); - wrapped.TryGetData("point", out Point deserialized).Should().BeTrue(); - deserialized.Should().BeEquivalentTo(point); + wrapped.GetDataPresent(format).Should().BeTrue(); + wrapped.TryGetData(format, out SimpleTestData deserialized).Should().BeTrue(); + deserialized.Should().BeEquivalentTo(testData); } [WinFormsFact] public void DataObject_SetDataAsJson_MultipleData_ReturnsExpected() { - Point point1 = new() { X = 1, Y = 1 }; - Point point2 = new() { Y = 2, X = 2 }; + SimpleTestData testData1 = new() { X = 1, Y = 1 }; + SimpleTestData testData2 = new() { Y = 2, X = 2 }; DataObject data = new(); - data.SetDataAsJson("point1", point1); - data.SetDataAsJson("point2", point2); + data.SetDataAsJson("testData1", testData1); + data.SetDataAsJson("testData2", testData2); data.SetData("Mystring", "test"); - data.TryGetData("point1", out Point deserializedPoint1).Should().BeTrue(); - deserializedPoint1.Should().BeEquivalentTo(point1); - data.TryGetData("point2", out Point deserializedPoint2).Should().BeTrue(); - deserializedPoint2.Should().BeEquivalentTo(point2); + 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"); } @@ -3008,12 +3011,24 @@ public void DataObject_SetDataAsJson_RestrictedFormats_NotJsonSerialized(string [WinFormsTheory] [CommonMemberData(typeof(DataObjectTestHelpers), nameof(DataObjectTestHelpers.UnboundedFormat))] - public void DataObject_SetDataAsJson_NonRestrictedFormat_JsonSerialized(string format) + 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().BeOfType>(); + 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(); 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 8152f9652c4..06dace6681b 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 @@ -7,6 +7,7 @@ using System.Reflection.Metadata; using System.Text.RegularExpressions; using System.Windows.Forms.TestUtilities; +using static System.Windows.Forms.Tests.TestAccessorTests; using Com = Windows.Win32.System.Com; namespace System.Windows.Forms.Tests; @@ -439,11 +440,29 @@ public void SetDataAsJson_TryGetData_Requires_Resolver() 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; } From dbebbe70ebd75591f3fe88ff114c9cb7f8922cc2 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Mon, 16 Dec 2024 14:00:53 -0800 Subject: [PATCH 33/34] Address feedback --- .../VisualBasic/MyServices/ClipboardProxyTests.cs | 1 - .../src/System/Windows/Forms/Control.cs | 8 ++++---- .../Nrbf/WinFormsSerializationRecordExtensions.cs | 6 +++--- .../src/System/Windows/Forms/OLE/Clipboard.cs | 2 +- .../src/System/Windows/Forms/OLE/DataObject.cs | 8 ++++++-- .../src/System/Windows/Forms/OLE/JsonData.cs | 4 ++-- .../tests/TestUtilities/DataObjectTestHelpers.cs | 2 +- .../WinFormsBinaryFormattedObjectTests.cs | 10 ++++++---- .../System/Windows/Forms/ClipboardTests.cs | 6 +++--- .../System/Windows/Forms/DataObjectTests.cs | 13 +++++++++++++ .../Windows/Forms/NativeToWinFormsAdapterTests.cs | 1 - 11 files changed, 39 insertions(+), 22 deletions(-) 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 ffcc275a4ff..1591fb76834 100644 --- a/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs +++ b/src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs @@ -100,7 +100,6 @@ public void SetDataAsJson() retrieved.Should().BeEquivalentTo(testData); } - [Serializable] [TypeForwardedFrom("System.ForwardAssembly")] public struct SimpleTestData { 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 52a09f7e16a..70e2ee017b6 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs @@ -4785,7 +4785,7 @@ public DragDropEffects DoDragDropAsJson(T data, DragDropEffects allowedEffect DoDragDropAsJson(data, allowedEffects, dragImage: null, cursorOffset: default, useDefaultDragImage: false); /// - /// Begins a drag operation + /// Begins a drag operation. /// /// The data being dragged. /// determine which drag operations can occur. @@ -4795,13 +4795,13 @@ public DragDropEffects DoDragDropAsJson(T data, DragDropEffects allowedEffect /// 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 used to start a drag operation, use to JSON serialize the data being held within the , + /// 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 - /// as the same as . + /// 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( 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 f3283667b71..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 @@ -62,7 +62,7 @@ public static bool TryGetBitmap(this SerializationRecord record, out object? bit /// Tries to deserialize this object if it was serialized as JSON. /// /// - /// if the data was serialized as JSON and was successfully deserialized. Otherwise, . + /// 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. @@ -88,8 +88,8 @@ public static bool TryGetObjectFromJson(this SerializationRecord record, ITyp Type serializedType = resolver.GetType(serializedTypeName); if (!serializedType.IsAssignableTo(typeof(T))) { - // Not the type the caller asked for. - return false; + // Not the type the caller asked for so @object remains null. + return true; } try 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 b0a5a5559e6..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 @@ -519,7 +519,7 @@ public static void SetData(string format, object data) /// If the data is a non intrinsic type, the object will be serialized using JSON. /// /// - /// null, empty, or whitespace was passed as the format. + /// , 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. 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 8a6dd47ec18..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 @@ -183,7 +183,11 @@ public void SetDataAsJson(string format, bool autoConvert, T data) /// private static object TryJsonSerialize(string format, T data) { - format.OrThrowIfNull(nameof(format)); + if (string.IsNullOrWhiteSpace(format.OrThrowIfNull())) + { + throw new ArgumentException(SR.DataObjectWhitespaceEmptyFormatNotAllowed, nameof(format)); + } + data.OrThrowIfNull(nameof(data)); return IsRestrictedFormat(format) || Composition.Binder.IsKnownType() ? data @@ -194,7 +198,7 @@ private static object TryJsonSerialize(string format, T 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. /// - internal static bool IsRestrictedFormat(string format) => RestrictDeserializationToSafeTypes(format) + private static bool IsRestrictedFormat(string format) => RestrictDeserializationToSafeTypes(format) || format is DataFormats.TextConstant or DataFormats.UnicodeTextConstant or DataFormats.RtfConstant 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 index 11d0214cc96..f7bfab8b3e9 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -134,7 +134,7 @@ public readonly object Deserialize() 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. - public const string CustomAssemblyName = "System.Private.Windows.VirtualJson"; + const string CustomAssemblyName = "System.Private.Windows.VirtualJson"; byte[] JsonBytes { get; set; } @@ -146,7 +146,7 @@ internal interface IJsonData /// /// Deserializes the data stored in the JsonData. This is a convenience method - /// to deserialize teh data when we are not dealing with a binary formatted record. + /// to deserialize the data when we are not dealing with a binary formatted record. /// object Deserialize(); } diff --git a/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs b/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs index 0e7ea2f18ca..f84142e8c17 100644 --- a/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs +++ b/src/System.Windows.Forms/tests/TestUtilities/DataObjectTestHelpers.cs @@ -27,7 +27,7 @@ public static TheoryData StringFormat() => public static TheoryData UnboundedFormat() => [ DataFormats.Serializable, - "something custom" + "something custom" ]; // These formats contain only known types. 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 85791825eea..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 @@ -46,6 +46,7 @@ public void BinaryFormattedObject_JsonData_RoundTrip() 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); @@ -58,7 +59,7 @@ public void BinaryFormattedObject_Deserialize_FromStream_WithBinaryFormatter() JsonData data = new() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(testData) - }; + }; using MemoryStream stream = new(); WinFormsBinaryFormatWriter.WriteJsonData(stream, data); @@ -82,8 +83,8 @@ private struct ReplicatedJsonData : IObjectReference public readonly object GetRealObject(StreamingContext context) { object? result = null; - if (TypeName.TryParse(InnerTypeAssemblyQualifiedName, out TypeName? genericTypeName) - && genericTypeName.Matches(typeof(SimpleTestData).ToTypeName())) + if (TypeName.TryParse(InnerTypeAssemblyQualifiedName, out TypeName? innerTypeName) + && innerTypeName.Matches(typeof(SimpleTestData).ToTypeName())) { result = JsonSerializer.Deserialize(JsonBytes); } @@ -96,7 +97,8 @@ private class JsonDataTestDataBinder : SerializationBinder { public override Type? BindToType(string assemblyName, string typeName) { - if (assemblyName == "System.Private.Windows.VirtualJson") + if (assemblyName == "System.Private.Windows.VirtualJson" + && typeName == "System.Private.Windows.JsonData") { return typeof(ReplicatedJsonData); } 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 21f2b07e1ee..97d04d11815 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 @@ -1027,9 +1027,9 @@ public unsafe void Clipboard_Deserialize_FromStream_Manually() types.HasMember("k__BackingField").Should().BeTrue(); types.HasMember("k__BackingField").Should().BeTrue(); SZArrayRecord byteData = types.GetRawValue("k__BackingField").Should().BeAssignableTo>().Subject; - string innerTypeFullName = types.GetRawValue("k__BackingField").Should().BeOfType().Subject; - TypeName.TryParse(innerTypeFullName, out TypeName? result).Should().BeTrue(); - TypeName checkedResult = result.Should().BeOfType().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); 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 06c2101db9f..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 @@ -3032,5 +3032,18 @@ public void DataObject_SetDataAsJson_NonRestrictedFormat_JsonSerialized(string f // 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 06dace6681b..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 @@ -7,7 +7,6 @@ using System.Reflection.Metadata; using System.Text.RegularExpressions; using System.Windows.Forms.TestUtilities; -using static System.Windows.Forms.Tests.TestAccessorTests; using Com = Windows.Win32.System.Com; namespace System.Windows.Forms.Tests; From 3d32e843219b490f74b43c0c181bde7514646355 Mon Sep 17 00:00:00 2001 From: Loni Tra Date: Mon, 16 Dec 2024 14:53:12 -0800 Subject: [PATCH 34/34] clear clipboard for test and change format to improve reliability --- .../src/System/Windows/Forms/OLE/JsonData.cs | 3 ++- .../System/Windows/Forms/ClipboardTests.cs | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) 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 index f7bfab8b3e9..1a9516c2b5b 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/JsonData.cs @@ -72,7 +72,8 @@ namespace System.Private.Windows; /// // TODO: Additional checking on result TypeName to ensure it is expected type. /// /// // This should return the original data that was JSON serialized. -/// System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), genericType); +/// var result = System.Text.Json.JsonSerializer.Deserialize(byteData.GetArray(), genericType); +/// // TODO: Process the payload as needed. /// } /// /// [DllImport("kernel32.dll")] 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 97d04d11815..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 @@ -892,6 +892,7 @@ public void Clipboard_SetDataAsJson_EmptyFormat_Throws(string? format) [WinFormsFact] public void Clipboard_SetDataAsJson_DataObject_Throws() { + Clipboard.Clear(); string format = "format"; Action action = () => Clipboard.SetDataAsJson(format, new DataObject()); action.Should().Throw(); @@ -902,6 +903,7 @@ public void Clipboard_SetDataAsJson_DataObject_Throws() [WinFormsFact] public void Clipboard_SetDataAsJson_WithGeneric_ReturnsExpected() { + Clipboard.Clear(); List generic1 = []; string format = "list"; Clipboard.SetDataAsJson(format, generic1); @@ -924,18 +926,20 @@ public void Clipboard_SetDataAsJson_WithGeneric_ReturnsExpected() [WinFormsFact] public void Clipboard_SetDataAsJson_ReturnsExpected() { + Clipboard.Clear(); SimpleTestData testData = new() { X = 1, Y = 1 }; - Clipboard.SetDataAsJson("testData", testData); + Clipboard.SetDataAsJson("testDataFormat", testData); IDataObject dataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; - dataObject.GetDataPresent("testData").Should().BeTrue(); - dataObject.TryGetData("testData", out SimpleTestData deserialized).Should().BeTrue(); + 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); @@ -953,14 +957,15 @@ public void Clipboard_SetDataAsJson_GetData() [BoolData] public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) { + Clipboard.Clear(); SimpleTestData testData = new() { X = 1, Y = 1 }; DataObject dataObject = new(); - dataObject.SetDataAsJson("testData", testData); + dataObject.SetDataAsJson("testDataFormat", testData); Clipboard.SetDataObject(dataObject, copy); ITypedDataObject returnedDataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject; - returnedDataObject.TryGetData("testData", out SimpleTestData deserialized).Should().BeTrue(); + returnedDataObject.TryGetData("testDataFormat", out SimpleTestData deserialized).Should().BeTrue(); deserialized.Should().BeEquivalentTo(testData); } @@ -968,6 +973,7 @@ public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy) [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(); @@ -990,6 +996,7 @@ 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);