8000 Adding new strongly typed methods to Clipboard, DataObject and IDataO… · dotnet/winforms@f091e7b · GitHub
[go: up one dir, main page]

Skip to content

Commit f091e7b

Browse files
Adding new strongly typed methods to Clipboard, DataObject and IDataObject. (#11545)
Related to ##12362 Fixes #11350 The TryGetData methods use NRBF deserializer by default and will fall back to use BinaryFormatter if the application opts into BinaryFormatter use in this context. The GetData methods have a compatibility mode when the appropriate AppContext switches are enabled but by default they can read only known and primitive types or POCOs with primitive fields. These methods in the DataObject class are obsoleted.
1 parent 85ac69e commit f091e7b

File tree

42 files changed

+4252
-259
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4252
-259
lines changed

docs/list-of-diagnostics.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The acceptance criteria for adding an obsoletion includes:
1313
* Add new constants to `src\Common\src\Obsoletions.cs`, following the existing conventions
1414
* A `...Message` const using the same description added to the table below
1515
* A `...DiagnosticId` const for the `WFDEV###` id
16+
* If adding <Obsolete> attribute to Microsoft.VisualBasic.Forms assembly, edit src\Microsoft.VisualBasic.Forms\src\Obsoletions.vb file
1617
* Annotate `src` files by referring to the constants defined from `Obsoletions.cs`
1718
* Specify the `UrlFormat = Obsoletions.SharedUrlFormat`
1819
* Example: `[Obsolete(Obsoletions.DomainUpDownAccessibleObjectMessage, DiagnosticId = Obsoletions.DomainUpDownAccessibleObjectDiagnosticId, UrlFormat = Obsoletions.SharedUrlFormat)]`
@@ -39,6 +40,9 @@ The acceptance criteria for adding an obsoletion includes:
3940
| __`WFDEV002`__ | `DomainUpDown.DomainUpDownAccessibleObject` is no longer used to provide accessible support for `DomainUpDown` controls. Use `ControlAccessibleObject` instead. |
4041
| __`WFDEV003`__ | `DomainUpDown.DomainItemAccessibleObject` is no longer used to provide accessible support for `DomainUpDown` items. |
4142
| __`WFDEV004`__ | `Form.OnClosing`, `Form.OnClosed` and the corresponding events are obsolete. Use `Form.OnFormClosing`, `Form.OnFormClosed`, `Form.FormClosing` and `Form.FormClosed` instead. |
43+
| __`WFDEV005`__ | `Clipboard.GetData(string)` method is obsolete. Use `Clipboard.TryGetData<T>` methods instead. |
44+
| __`WFDEV005`__ | `DataObject.GetData` methods are obsolete. Use the corresponding `DataObject.TryGetData<T>` instead. |
45+
| __`WFDEV005`__ | `ClipboardProxy.GetData(As String)` method is obsolete. Use `ClipboardProxy.TryGetData(Of T)(As String, As T)` instead. |
4246

4347

4448
## Analyzer Warnings

src/Common/src/Obsoletions.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ internal static class Obsoletions
99
{
1010
internal const string SharedUrlFormat = "https://aka.ms/winforms-warnings/{0}";
1111

12-
// Please see docs\project\list-of-diagnostics.md for instructions on the steps required
12+
// Please see docs\list-of-diagnostics.md for instructions on the steps required
1313
// to introduce a new obsoletion, apply it to downlevel builds, claim a diagnostic id,
1414
// and ensure the "aka.ms/dotnet-warnings/{0}" URL points to documentation for the obsoletion
1515
// The diagnostic ids reserved for obsoletions are WFDEV### (WFDEV001 - WFDEV999).
@@ -24,4 +24,11 @@ internal static class Obsoletions
2424

2525
internal const string FormOnClosingClosedMessage = "Form.OnClosing, Form.OnClosed and the corresponding events are obsolete. Use Form.OnFormClosing, Form.OnFormClosed, Form.FormClosing and Form.FormClosed instead.";
2626
internal const string FormOnClosingClosedDiagnosticId = "WFDEV004";
27+
28+
internal const string ClipboardGetDataMessage = "`Clipboard.GetData(string)` method is obsolete. Use `Clipboard.TryGetData<T>` methods instead.";
29+
internal const string ClipboardGetDataDiagnosticId = "WFDEV005";
30+
31+
internal const string DataObjectGetDataMessage = "`DataObject.GetData` methods are obsolete. Use the corresponding `DataObject.TryGetData<T>` instead.";
32+
33+
internal const string ClipboardProxyGetDataMessage = "`ClipboardProxy.GetData(As String)` method is obsolete. Use `ClipboardProxy.TryGetData(Of T)(As String, As T)` instead.";
2734
}

src/Common/tests/TestUtilities/AppContextSwitchNames.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,23 @@ public const string EnableUnsafeBinaryFormatterSerialization
1414
= "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization";
1515

1616
/// <summary>
17-
/// Switch that controls <see cref="AppContext"/> switch caching.
17+
/// Switch that controls <see cref="AppContext"/> switch caching. This switch is set to
18+
/// <see langword="true" /> in our test assemblies.
1819
/// </summary>
1920
public const string LocalAppContext_DisableCaching
2021
= "TestSwitch.LocalAppContext.DisableCaching";
22+
23+
/// <summary>
24+
/// The switch that controls whether or not the <see cref="BinaryFormatter"/> is enabled in the
25+
/// Clipboard or drag and drop scenarios
26+
/// </summary>
27+
public const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName
28+
= "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization";
29+
30+
/// <summary>
31+
/// The switch that controls whether or not the System.Windows.Forms.BinaryFormat.Deserializer
32+
/// is enabled in the Clipboard or drag and drop scenarios.
33+
/// </summary>
34+
public const string ClipboardDragDropEnableNrbfSerializationSwitchName
35+
= "Windows.ClipboardDragDrop.EnableNrbfSerialization";
2136
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System;
5+
6+
public readonly ref struct BinaryFormatterInClipboardDragDropScope
7+
{
8+
private readonly AppContextSwitchScope _switchScope;
9+
10+
public BinaryFormatterInClipboardDragDropScope(bool enable)
11+
{
12+
Monitor.Enter(typeof(BinaryFormatterInClipboardDragDropScope));
13+
_switchScope = new(AppContextSwitchNames.ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName, GetDefaultValue, enable);
14+
}
15+
16+
public void Dispose()
17+
{
18+
try
19+
{
20+
_switchScope.Dispose();
21+
}
22+
finally
23+
{
24+
Monitor.Exit(typeof(BinaryFormatterInClipboardDragDropScope));
25+
}
26+
}
27+
28+
internal static bool GetDefaultValue()
29+
{
30+
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
31+
foreach (var assembly in assemblies)
32+
{
33+
if (assembly.FullName?.StartsWith("System.Windows.Forms.Primitives,", StringComparison.InvariantCultureIgnoreCase) == true)
34+
{
35+
var type = assembly.GetType("System.Windows.Forms.Primitives.LocalAppContextSwitches")
36+
?? throw new InvalidOperationException("Could not find LocalAppContextSwitches type in System.Windows.Forms.Primitives assembly.");
37+
38+
bool value = type.TestAccessor().CreateDelegate<Func<string, bool>>("GetSwitchDefaultValue")
39+
(AppContextSwitchNames.ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName);
40+
return value;
41+
}
42+
}
43+
44+
throw new InvalidOperationException("Could not find System.Windows.Forms.Primitives assembly in the test process.");
45+
}
46+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System;
5+
6+
public readonly ref struct NrbfSerializerInClipboardDragDropScope
7+
{
8+
private readonly AppContextSwitchScope _switchScope;
9+
10+
public NrbfSerializerInClipboardDragDropScope(bool enable)
11+
{
12+
Monitor.Enter(typeof(NrbfSerializerInClipboardDragDropScope));
13+
_switchScope = new(AppContextSwitchNames.ClipboardDragDropEnableNrbfSerializationSwitchName, GetDefaultValue, enable);
14+
}
15+
16+
public void Dispose()
17+
{
18+
try
19+
{
20+
_switchScope.Dispose();
21+
}
22+
finally
23+
{
24+
Monitor.Exit(typeof(NrbfSerializerInClipboardDragDropScope));
25+
}
26+
}
27+
28+
internal static bool GetDefaultValue()
29+
{
30+
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
31+
foreach (var assembly in assemblies)
32+
{
33+
if (assembly.FullName?.StartsWith("System.Windows.Forms.Primitives,", StringComparison.InvariantCultureIgnoreCase) == true)
34+
{
35+
var type = assembly.GetType("System.Windows.Forms.Primitives.LocalAppContextSwitches")
36+
?? throw new InvalidOperationException("Could not find LocalAppContextSwitches type in System.Windows.Forms.Primitives assembly.");
37+
38+
bool value = type.TestAccessor().CreateDelegate<Func<string, bool>>("GetSwitchDefaultValue")
39+
(AppContextSwitchNames.ClipboardDragDropEnableNrbfSerializationSwitchName);
40+
return value;
41+
}
42+
}
43+
44+
throw new InvalidOperationException("Could not find System.Windows.Forms.Primitives assembly in the test process.");
45+
}
46+
}

src/Microsoft.VisualBasic.Forms/src/Microsoft/VisualBasic/MyServices/ClipboardProxy.vb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Imports System.ComponentModel
66
Imports System.Drawing
77
Imports System.IO
88
Imports System.Windows.Forms
9+
Imports System.Reflection.Metadata
10+
Imports System.Runtime.InteropServices
911

1012
Namespace Microsoft.VisualBasic.MyServices
1113

@@ -93,8 +95,15 @@ Namespace Microsoft.VisualBasic.MyServices
9395
''' </summary>
9496
''' <param name="format">The type of data being sought.</param>
9597
''' <returns>The data.</returns>
98+
<Obsolete(
99+
Obsoletions.ClipboardProxyGetDataMessage,
100+
False,
101+
DiagnosticId:=Obsoletions.ClipboardGetDataDiagnosticId,
102+
UrlFormat:=Obsoletions.SharedUrlFormat)>
96103
Public Function GetData(format As String) As Object
104+
#Disable Warning WFDEV005 ' Type or member is obsolete
97105
Return Clipboard.GetData(format)
106+
#Enable Warning WFDEV005
98107
End Function
99108

100109
''' <summary>
@@ -183,6 +192,16 @@ Namespace Microsoft.VisualBasic.MyServices
183192
Clipboard.SetFileDropList(filePaths)
184193
End Sub
185194

195+
''' <inheritdoc cref="Clipboard.TryGetData(Of T)(String, Func(Of TypeName, Type), ByRef T)" />
196+
Public Function TryGetData(Of T)(format As String, resolver As Func(Of TypeName, Type), <Out> ByRef data As T) As Boolean
197+
Return Clipboard.TryGetData(format, resolver, data)
198+
End Function
199+
200+
''' <inheritdoc cref="Clipboard.TryGetData(Of T)(String, ByRef T)" />
201+
Public Function TryGetData(Of T)(format As String, <Out> ByRef data As T) As Boolean
202+
Return Clipboard.TryGetData(format, data)
203+
End Function
204+
186205
''' <summary>
187206
''' Saves the passed in <see cref="Image"/> to the clipboard.
188207
''' </summary>
@@ -192,7 +211,7 @@ Namespace Microsoft.VisualBasic.MyServices
192211
End Sub
193212

194213
''' <summary>
195-
''' Saves the passed in String to the clipboard.
214+
''' Saves the passed in <see cref="String" /> to the clipboard.
196215
''' </summary>
197216
''' <param name="text">The <see cref="String"/> to save.</param>
198217
Public Sub SetText(text As String)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
' Licensed to the .NET Foundation under one or more agreements.
2+
' The .NET Foundation licenses this file to you under the MIT license.
3+
4+
Friend NotInheritable Class Obsoletions
5+
6+
Friend Const SharedUrlFormat As String = "https://aka.ms/winforms-warnings/{0}"
7+
8+
' Please see docs\list-Of-diagnostics.md for how to claim a diagnostic id.
9+
' The diagnostic ids reserved for obsoletions are WFDEV### (WFDEV001 - WFDEV999).
10+
11+
Friend Const ClipboardProxyGetDataMessage As String = "`ClipboardProxy.GetData(As String)` method is obsolete. Use `ClipboardProxy.TryGetData(Of T)(As String, As T)` instead."
12+
Friend Const ClipboardGetDataDiagnosticId As String = "WFDEV005"
13+
End Class
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Microsoft.VisualBasic.MyServices.ClipboardProxy.TryGetData(Of T)(format As String, ByRef data As T) -> Boolean
2+
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

src/Microsoft.VisualBasic/tests/UnitTests/Microsoft/VisualBasic/MyServices/ClipboardProxyTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#nullable enable
55

66
using System.Drawing;
7+
using System.Reflection.Metadata;
78
using Microsoft.VisualBasic.Devices;
89
using DataFormats = System.Windows.Forms.DataFormats;
910
using TextDataFormat = System.Windows.Forms.TextDataFormat;
@@ -16,6 +17,7 @@ namespace Microsoft.VisualBasic.MyServices.Tests;
1617
[UISettings(MaxAttempts = 3)] // Try up to 3 times before failing.
1718
public class ClipboardProxyTests
1819
{
20+
#pragma warning disable WFDEV005 // Type or member is obsolete
1921
private static string GetUniqueText() => Guid.NewGuid().ToString("D");
2022

2123
[WinFormsFact]
@@ -83,4 +85,54 @@ public void Text()
8385
System.Windows.Forms.Clipboard.GetText(TextDataFormat.UnicodeText).Should().Be(clipboard.GetText(TextDataFormat.UnicodeText));
8486
clipboard.GetText(TextDataFormat.UnicodeText).Should().Be(text);
8587
}
88+
89+
[WinFormsFact]
90+
public void DataOfT_StringArray()
91+
{
92+
var clipboard = new Computer().Clipboard;
93+
string format = nameof(DataOfT_StringArray);
94+
// Array of primitive types does not require the OOB assembly.
95+
string[] data = ["thing1", "thing2"];
96+
clipboard.SetData(format, data);
97+
// Both methods return true.
98+
clipboard.TryGetData(format, out string[]? actual).Should()
99+
.Be(System.Windows.Forms.Clipboard.TryGetData(format, out string[]? expected));
100+
actual.Should().BeEquivalentTo(expected);
101+
}
102+
103+
[WinFormsFact]
104+
public void DataOfT_BinaryFormatterRequired()
105+
{
106+
var clipboard = new Computer().Clipboard;
107+
string format = nameof(DataOfT_BinaryFormatterRequired);
108+
DataWithObjectField data = new("thing1", "thing2");
109+
using BinaryFormatterScope scope = new(enable: true);
110+
using BinaryFormatterInClipboardDragDropScope clipboardScope = new(enable: true);
111+
// This test assembly does not reference the OOB package, we will write the NotSupportedException to the clipboard.
112+
clipboard.SetData(format, data);
113+
// Both methods return false.
114+
Action tryGetData = () => clipboard.TryGetData(format, DataWithObjectField.Resolver, out DataWithObjectField? actual);
115+
string actual = tryGetData.Should().Throw<NotSupportedException>().Which.Message;
116+
Action tryGetData1 = () => System.Windows.Forms.Clipboard.TryGetData(format, DataWithObjectField.Resolver, out DataWithObjectField? expected);
117+
string expected = tryGetData1.Should().Throw<NotSupportedException>().Which.Message;
118+
actual.Should().BeEquivalentTo(expected);
119+
}
120+
121+
[Serializable]
122+
private class DataWithObjectField
123+
{
124+
public DataWithObjectField(string text1, object object2)
125+
{
126+
_text1 = text1;
127+
_object2 = object2;
128+
}
129+
130+
public string _text1;
131+
public object _object2;
132+
133+
public static Type Resolver(TypeName typeName) =>
134+
typeof(DataWithObjectField).FullName == typeName.FullName
135+
? typeof(DataWithObjectField)
136+
: throw new NotSupportedException($"Can't resolve {typeName.AssemblyQualifiedName}");
137+
}
86138
}

src/System.Private.Windows.Core/src/System/Private/Windows/Core/Nrbf/SerializationRecordExtensions.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.ComponentModel;
66
using System.Drawing;
77
using System.Formats.Nrbf;
8+
using System.Private.Windows.Core.BinaryFormat;
89
using System.Reflection;
910
using System.Runtime.ExceptionServices;
1011
using System.Runtime.Serialization;
@@ -41,6 +42,48 @@ internal static SerializationRecord Decode(this Stream stream)
4142
}
4243
}
4344

45+
internal static SerializationRecord Decode(this Stream stream, out IReadOnlyDictionary<SerializationRecordId, SerializationRecord> recordMap)
46+
{
47+
try
48+
{
49+
return NrbfDecoder.Decode(stream, out recordMap, leaveOpen: true);
50+
}
51+
catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException)
52+
{
53+
// Make the exception easier to catch, but retain the original stack trace.
54+
throw ex.ConvertToSerializationException();
55+
}
56+
catch (TargetInvocationException ex)
57+
{
58+
throw ExceptionDispatchInfo.Capture(ex.InnerException!).SourceException.ConvertToSerializationException();
59+
}
60+
}
61+
62+
/// <summary>
63+
/// Deserializes the <see cref="SerializationRecord"/> to an object.
64+
/// </summary>
65+
[RequiresUnreferencedCode("Ultimately calls resolver for type names in the data.")]
66+
public static object? Deserialize(
67+
this SerializationRecord rootRecord,
68+
IReadOnlyDictionary<SerializationRecordId, SerializationRecord> recordMap,
69+
ITypeResolver typeResolver)
70+
{
71+
DeserializationOptions options = new()
72+
{
73+
TypeResolver = typeResolver
74+
};
75+
76+
try
77+
{
78+
return Deserializer.Deserialize(rootRecord.Id, recordMap, options);
79+
}
80+
catch (SerializationException ex)
81+
{
82+
throw ExceptionDispatchInfo.SetRemoteStackTrace(
83+
new NotSupportedException(ex.Message, ex), ex.StackTrace ?? string.Empty);
84+
}
85+
}
86+
4487
internal delegate bool TryGetDelegate(SerializationRecord record, [NotNullWhen(true)] out object? value);
4588

4689
internal static bool TryGet(TryGetDelegate get, SerializationRecord record, [NotNullWhen(true)] out object? value)

0 commit comments

Comments
 (0)
0