diff --git a/src/System.Private.Windows.Core/src/GlobalSuppressions.cs b/src/System.Private.Windows.Core/src/GlobalSuppressions.cs
index 8e12efe40d7..fe87cb4607f 100644
--- a/src/System.Private.Windows.Core/src/GlobalSuppressions.cs
+++ b/src/System.Private.Windows.Core/src/GlobalSuppressions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// Compatibility suppressions.
+
[assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Private.Windows.Ole.Composition`2.NativeToManagedAdapter.ReadByteStreamFromHGLOBAL(Windows.Win32.Foundation.HGLOBAL,System.Boolean@)~System.IO.MemoryStream")]
[assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Private.Windows.Ole.Composition`2.NativeToRuntimeAdapter.EnumFormatEtc(System.Runtime.InteropServices.ComTypes.DATADIR)~System.Runtime.InteropServices.ComTypes.IEnumFORMATETC")]
@@ -12,5 +13,6 @@
[assembly: SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Trimming not supported", Scope = "member", Target = "~M:System.Private.Windows.Ole.BinaryFormatUtilities`1.ReadObjectFromStream``1(System.IO.MemoryStream,System.Private.Windows.Ole.DataRequest@)~System.Object")]
[assembly: SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Trimming not supported", Scope = "member", Target = "~M:System.Private.Windows.Ole.BinaryFormatUtilities`1.TypeNameIsAssignableToType(System.Reflection.Metadata.TypeName,System.Type,System.Private.Windows.BinaryFormat.ITypeResolver)~System.Boolean")]
[assembly: SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Trimming not supported", Scope = "member", Target = "~M:System.Private.Windows.Ole.DataStore`1.TryGetDataInternal``1(System.String,System.Boolean,``0@)~System.Boolean")]
+[assembly: SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Trimming not supported", Scope = "member", Target = "~M:System.Private.Windows.Ole.DataObjectCore`4.TryJsonSerialize``1(System.String,``0)~System.Object")]
[assembly: SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "Trimming not supported", Scope = "member", Target = "~M:System.Private.Windows.Ole.TypeBinder`1.BindToType(System.Reflection.Metadata.TypeName)~System.Type")]
[assembly: SuppressMessage("Trimming", "IL2093:'DynamicallyAccessedMemberTypes' on the return value of method don't match overridden return value of method. All overridden members must have the same 'DynamicallyAccessedMembersAttribute' usage.", Justification = "Trimming not supported", Scope = "member", Target = "~M:System.Private.Windows.Ole.TypeBinder`1.BindToType(System.Reflection.Metadata.TypeName)~System.Type")]
diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardCore.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardCore.cs
new file mode 100644
index 00000000000..bd21716a93a
--- /dev/null
+++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/ClipboardCore.cs
@@ -0,0 +1,183 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Windows.Win32.System.Com;
+
+namespace System.Private.Windows.Ole;
+
+///
+/// Contains platform-agnostic clipboard operations.
+///
+internal static unsafe class ClipboardCore
+ where TOleServices : IOleServices
+{
+ ///
+ /// The number of times to retry OLE clipboard operations.
+ ///
+ private const int OleRetryCount = 10;
+
+ ///
+ /// The amount of time in milliseconds to sleep between retrying OLE clipboard operations.
+ ///
+ private const int OleRetryDelay = 100;
+
+ ///
+ /// Removes all data from the Clipboard.
+ ///
+ /// An indicating the success or failure of the operation.
+ internal static HRESULT Clear()
+ {
+ TOleServices.EnsureThreadState();
+
+ HRESULT result;
+ int retryCount = OleRetryCount;
+
+ while ((result = PInvokeCore.OleSetClipboard(null)).Failed)
+ {
+ if (--retryCount < 0)
+ {
+ break;
+ }
+
+ Thread.Sleep(millisecondsTimeout: OleRetryDelay);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Attempts to set the specified data on the Clipboard.
+ ///
+ /// The data object to set on the Clipboard.
+ /// Indicates whether to copy the data to the Clipboard.
+ /// The number of times to retry the operation if it fails.
+ /// The amount of time in milliseconds to wait between retries.
+ /// An indicating the success or failure of the operation.
+ internal static HRESULT SetData(
+ IComVisibleDataObject dataObject,
+ bool copy,
+ int retryTimes = OleRetryCount,
+ int retryDelay = OleRetryDelay)
+ {
+ TOleServices.EnsureThreadState();
+
+ ArgumentOutOfRangeException.ThrowIfNegative(retryTimes);
+ ArgumentOutOfRangeException.ThrowIfNegative(retryDelay);
+
+ using var iDataObject = ComHelpers.GetComScope(dataObject);
+
+ HRESULT result;
+ int retry = OleRetryCount;
+ while ((result = PInvokeCore.OleSetClipboard(iDataObject)).Failed)
+ {
+ if (--retry < 0)
+ {
+ return result;
+ }
+
+ Thread.Sleep(millisecondsTimeout: OleRetryDelay);
+ }
+
+ if (copy)
+ {
+ retry = retryTimes;
+ while ((result = PInvokeCore.OleFlushClipboard()).Failed)
+ {
+ if (--retry < 0)
+ {
+ return result;
+ }
+
+ Thread.Sleep(millisecondsTimeout: retryDelay);
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Attempts to retrieve data from the Clipboard.
+ ///
+ /// The proxy data object retrieved from the Clipboard.
+ /// The original object retrieved from the Clipboard, if available.
+ /// An indicating the success or failure of the operation.
+ public static HRESULT TryGetData(out ComScope proxyDataObject, out object? originalObject)
+ {
+ TOleServices.EnsureThreadState();
+
+ proxyDataObject = new(null);
+ originalObject = null;
+
+ int retryTimes = OleRetryCount;
+ HRESULT result;
+
+ while ((result = PInvokeCore.OleGetClipboard(proxyDataObject)).Failed)
+ {
+ if (--retryTimes < 0)
+ {
+ return result;
+ }
+
+ Thread.Sleep(millisecondsTimeout: OleRetryDelay);
+ }
+
+ // OleGetClipboard always returns a proxy. The proxy forwards all IDataObject method calls to the real data object,
+ // without giving out the real data object. If the data placed on the clipboard is not one of our CCWs or the clipboard
+ // has been flushed, a wrapper around the proxy for us to use will be given. However, if the data placed on
+ // the clipboard is one of our own and the clipboard has not been flushed, we need to retrieve the real data object
+ // pointer in order to retrieve the original managed object via ComWrappers if an IDataObject was set on the clipboard.
+ // To do this, we must query for an interface that is not known to the proxy e.g. IComCallableWrapper.
+ // If we are able to query for IComCallableWrapper it means that the real data object is one of our CCWs and we've retrieved it successfully,
+ // otherwise it is not ours and we will use the wrapped proxy.
+ using ComScope realDataObject = proxyDataObject.TryQuery(out HRESULT wrapperResult);
+
+ if (wrapperResult.Succeeded)
+ {
+ ComHelpers.TryUnwrapComWrapperCCW(realDataObject.AsUnknown, out originalObject);
+ }
+
+ return result;
+ }
+
+ ///
+ /// Checks if the specified is valid and compatible with the specified .
+ ///
+ ///
+ ///
+ /// This is intended to be used as a pre-validation step to give a more useful error to callers.
+ ///
+ ///
+ internal static bool IsValidTypeForFormat(Type type, string format)
+ {
+ if (string.IsNullOrWhiteSpace(format))
+ {
+ return false;
+ }
+
+ if (IsValidPredefinedFormatTypeCombination(format, type))
+ {
+ return true;
+ }
+
+ throw new NotSupportedException(string.Format(
+ SR.ClipboardOrDragDrop_InvalidFormatTypeCombination,
+ type.FullName,
+ format));
+
+ static bool IsValidPredefinedFormatTypeCombination(string format, Type type) => format switch
+ {
+ DataFormatNames.Text
+ or DataFormatNames.UnicodeText
+ or DataFormatNames.String
+ or DataFormatNames.Rtf
+ or DataFormatNames.Html
+ or DataFormatNames.OemText => typeof(string) == type,
+
+ DataFormatNames.FileDrop
+ or DataFormatNames.FileNameAnsi
+ or DataFormatNames.FileNameUnicode => typeof(string[]) == type,
+
+ _ => TOleServices.IsValidTypeForFormat(type, format)
+ };
+ }
+}
diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataObjectCore.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataObjectCore.cs
new file mode 100644
index 00000000000..1abb1fea435
--- /dev/null
+++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataObjectCore.cs
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using Windows.Win32.System.Com;
+using ComTypes = System.Runtime.InteropServices.ComTypes;
+
+namespace System.Private.Windows.Ole;
+
+internal static unsafe class DataObjectCore
+ where TRuntime : IRuntime
+ where TDataFormat : IDataFormat
+ where TDataObject : IComVisibleDataObject
+{
+ ///
+ /// 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 .
+ ///
+ internal static object TryJsonSerialize(string format, T data)
+ {
+ if (string.IsNullOrWhiteSpace(format.OrThrowIfNull()))
+ {
+ throw new ArgumentException(SR.DataObjectWhitespaceEmptyFormatNotAllowed, nameof(format));
+ }
+
+ data.OrThrowIfNull(nameof(data));
+
+ if (typeof(T) == typeof(TDataObject))
+ {
+ throw new InvalidOperationException(string.Format(SR.ClipboardOrDragDrop_CannotJsonSerializeDataObject, "SetData"));
+ }
+
+ return DataFormatNames.IsRestrictedFormat(format) || TRuntime.IsSupportedType()
+ ? data
+ : new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) };
+ }
+
+ internal static Composition CreateComposition() =>
+ Composition.CreateFromManagedDataObject(new DataStore());
+
+ internal static Composition CreateComposition(IDataObject* data) =>
+ Composition.CreateFromNativeDataObject(data);
+
+ internal static Composition CreateComposition(
+ object data,
+ Func adapterFactory)
+ {
+ if (data is IDataObjectInternal internalDataObject)
+ {
+ return Composition.CreateFromManagedDataObject(internalDataObject);
+ }
+ else if (data is TIDataObject iDataObject)
+ {
+ return Composition.CreateFromManagedDataObject(adapterFactory(iDataObject));
+ }
+ else if (data is ComTypes.IDataObject comDataObject)
+ {
+ return Composition.CreateFromRuntimeDataObject(comDataObject);
+ }
+
+ var composition = Composition.CreateFromManagedDataObject(new DataStore());
+ composition.SetData(data);
+ return composition;
+ }
+}
diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.DataStoreEntry.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.DataStoreEntry.cs
index 82f93dcf17d..f94c0de777c 100644
--- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.DataStoreEntry.cs
+++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.DataStoreEntry.cs
@@ -1,13 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.ComponentModel.DataAnnotations;
+
namespace System.Private.Windows.Ole;
internal sealed partial class DataStore
{
- private class DataStoreEntry
+ private readonly struct DataStoreEntry
{
+ [Required]
public object? Data { get; }
+
+ [Required]
public bool AutoConvert { get; }
public DataStoreEntry(object? data, bool autoConvert)
diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.cs
index a11334fe721..dc52dac5faf 100644
--- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.cs
+++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/DataStore.cs
@@ -48,7 +48,7 @@ private bool TryGetDataInternal(
bool TryGetData(string format, ref bool autoConvert, [NotNullWhen(true)] out T? data)
{
- if (_mappedData.TryGetValue(format, out DataStoreEntry? entry))
+ if (_mappedData.TryGetValue(format, out DataStoreEntry entry))
{
autoConvert |= entry.AutoConvert;
diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/IOleServices.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/IOleServices.cs
index 471d99fae59..6e05e09897d 100644
--- a/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/IOleServices.cs
+++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Ole/IOleServices.cs
@@ -28,6 +28,16 @@ static abstract bool TryGetBitmapFromDataObject(
IDataObject* dataObject,
[NotNullWhen(true)] out T data);
+ ///
+ /// Returns true if the given is a valid type for the given .
+ ///
+ ///
+ ///
+ /// Basic predefined formats that map to are checked before this call.
+ ///
+ ///
+ static abstract bool IsValidTypeForFormat(Type type, string format);
+
///
/// Allows the given to pass pre-validation without a resolver.
///
diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/Runtime.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/Runtime.cs
index 7ebe4b7afc7..bf1c4c01e07 100644
--- a/src/System.Private.Windows.Core/src/System/Private/Windows/Runtime.cs
+++ b/src/System.Private.Windows.Core/src/System/Private/Windows/Runtime.cs
@@ -37,4 +37,6 @@ static bool INrbfSerializer.TryGetObject(SerializationRecord record, [NotNullWhe
TNrbfSerializer.TryGetObject(record, out value);
static bool INrbfSerializer.TryWriteObject(Stream stream, object value) =>
TNrbfSerializer.TryWriteObject(stream, value);
+ static bool IOleServices.IsValidTypeForFormat(Type type, string format) =>
+ TOleServices.IsValidTypeForFormat(type, format);
}
diff --git a/src/System.Windows.Forms/src/GlobalSuppressions.cs b/src/System.Windows.Forms/src/GlobalSuppressions.cs
index ad51ed92d46..c589b0dab36 100644
--- a/src/System.Windows.Forms/src/GlobalSuppressions.cs
+++ b/src/System.Windows.Forms/src/GlobalSuppressions.cs
@@ -256,6 +256,7 @@
[assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Windows.Forms.ListBox.NativeAdd(System.Object)~System.Int32")]
[assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Windows.Forms.RichTextBox.Find(System.Char[],System.Int32,System.Int32)~System.Int32")]
[assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Windows.Forms.RichTextBox.CharRangeToString(Windows.Win32.UI.Controls.RichEdit.CHARRANGE)~System.String")]
+[assembly: SuppressMessage("Usage", "CA2201:Do not raise reserved exception types", Justification = "Compat", Scope = "member", Target = "~M:System.Windows.Forms.Clipboard.Clear")]
[assembly: SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Public API", Scope = "member", Target = "~F:System.Windows.Forms.BindingManagerBase.onCurrentChangedHandler")]
[assembly: SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Public API", Scope = "member", Target = "~F:System.Windows.Forms.BindingManagerBase.onPositionChangedHandler")]
[assembly: SuppressMessage("Design", "CA1051:Do not declare visible instance fields", Justification = "Public API", Scope = "member", Target = "~F:System.Windows.Forms.CurrencyManager.listposition")]
diff --git a/src/System.Windows.Forms/src/GlobalUsings.cs b/src/System.Windows.Forms/src/GlobalUsings.cs
index 99f1ed8ff69..6120c995eef 100644
--- a/src/System.Windows.Forms/src/GlobalUsings.cs
+++ b/src/System.Windows.Forms/src/GlobalUsings.cs
@@ -6,7 +6,23 @@
global using System.Private.Windows;
global using AppContextSwitches = System.Windows.Forms.Primitives.LocalAppContextSwitches;
-global using DragDropHelper = System.Private.Windows.Ole.DragDropHelper;
+
+// Having these as global usings reduces verbiage in code and avoids accidental mismatching when defining types.
+global using ClipboardCore = System.Private.Windows.Ole.ClipboardCore<
+ System.Windows.Forms.Ole.WinFormsOleServices>;
+global using Composition = System.Private.Windows.Ole.Composition<
+ System.Windows.Forms.WinFormsRuntime,
+ System.Windows.Forms.DataFormats.Format>;
+global using DataFormatsCore = System.Private.Windows.Ole.DataFormatsCore<
+ System.Windows.Forms.DataFormats.Format>;
+global using DataObjectCore = System.Private.Windows.Ole.DataObjectCore<
+ System.Windows.Forms.WinFormsRuntime,
+ System.Windows.Forms.DataFormats.Format,
+ System.Windows.Forms.DataObject,
+ System.Windows.Forms.IDataObject>;
+global using DragDropHelper = System.Private.Windows.Ole.DragDropHelper<
+ System.Windows.Forms.Ole.WinFormsOleServices,
+ System.Windows.Forms.DataFormats.Format>;
global using Windows.Win32;
global using Windows.Win32.Foundation;
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 bf1c9744aea..3c2b83c7250 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
@@ -9,7 +9,6 @@
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;
namespace System.Windows.Forms;
@@ -43,43 +42,15 @@ public static void SetDataObject(object data, bool copy) =>
///
public static unsafe void SetDataObject(object data, bool copy, int retryTimes, int retryDelay)
{
- if (Application.OleRequired() != ApartmentState.STA)
- {
- throw new ThreadStateException(SR.ThreadMustBeSTA);
- }
-
ArgumentNullException.ThrowIfNull(data);
- ArgumentOutOfRangeException.ThrowIfNegative(retryTimes);
- ArgumentOutOfRangeException.ThrowIfNegative(retryDelay);
// Wrap if we're not already a DataObject
DataObject dataObject = data as DataObject ?? new WrappingDataObject(data);
- using var iDataObject = ComHelpers.GetComScope(dataObject);
+ HRESULT result = ClipboardCore.SetData(dataObject, copy, retryTimes, retryDelay);
- HRESULT hr;
- int retry = retryTimes;
- while ((hr = PInvokeCore.OleSetClipboard(iDataObject)).Failed)
+ if (result.Failed)
{
- if (--retry < 0)
- {
- throw new ExternalException(SR.ClipboardOperationFailed, (int)hr);
- }
-
- Thread.Sleep(millisecondsTimeout: retryDelay);
- }
-
- if (copy)
- {
- retry = retryTimes;
- while ((hr = PInvokeCore.OleFlushClipboard()).Failed)
- {
- if (--retry < 0)
- {
- throw new ExternalException(SR.ClipboardOperationFailed, (int)hr);
- }
-
- Thread.Sleep(millisecondsTimeout: retryDelay);
- }
+ throw new ExternalException(SR.ClipboardOperationFailed, (int)result);
}
}
@@ -88,46 +59,30 @@ public static unsafe void SetDataObject(object data, bool copy, int retryTimes,
///
public static unsafe IDataObject? GetDataObject()
{
- if (Application.OleRequired() != ApartmentState.STA)
- {
- // Only throw if a message loop was started. This makes the case of trying to query the clipboard from the
- // finalizer or non-UI MTA thread silently fail, instead of making the application die.
- return Application.MessageLoop ? throw new ThreadStateException(SR.ThreadMustBeSTA) : null;
- }
+ HRESULT result = ClipboardCore.TryGetData(
+ out ComScope proxyDataObject,
+ out object? originalObject);
- int retryTimes = 10;
- using ComScope proxyDataObject = new(null);
- HRESULT hr;
- while ((hr = PInvokeCore.OleGetClipboard(proxyDataObject)).Failed)
+ // Need ensure we release the ref count on the proxy object.
+ using (proxyDataObject)
{
- if (--retryTimes < 0)
+ if (result.Failed)
{
- throw new ExternalException(SR.ClipboardOperationFailed, (int)hr);
+ Debug.Assert(proxyDataObject.IsNull);
+ throw new ExternalException(SR.ClipboardOperationFailed, (int)result);
}
- Thread.Sleep(millisecondsTimeout: 100);
- }
+ if (originalObject is DataObject dataObject
+ && dataObject.TryUnwrapUserDataObject(out IDataObject? userObject))
+ {
+ // We have an original user object that we want to return.
+ return userObject;
+ }
- // OleGetClipboard always returns a proxy. The proxy forwards all IDataObject method calls to the real data object,
- // without giving out the real data object. If the data placed on the clipboard is not one of our CCWs or the clipboard
- // has been flushed, a wrapper around the proxy for us to use will be given. However, if the data placed on
- // the clipboard is one of our own and the clipboard has not been flushed, we need to retrieve the real data object
- // pointer in order to retrieve the original managed object via ComWrappers if an IDataObject was set on the clipboard.
- // To do this, we must query for an interface that is not known to the proxy e.g. IComCallableWrapper.
- // If we are able to query for IComCallableWrapper it means that the real data object is one of our CCWs and we've retrieved it successfully,
- // otherwise it is not ours and we will use the wrapped proxy.
- var realDataObject = proxyDataObject.TryQuery(out hr);
-
- if (hr.Succeeded
- && ComHelpers.TryUnwrapComWrapperCCW(realDataObject.AsUnknown, out DataObject? dataObject)
- && dataObject.TryUnwrapUserDataObject(out IDataObject? userObject))
- {
- // An IDataObject was given to us to place on the clipboard. We want to unwrap and return it instead of a proxy.
- return userObject;
+ // Original data given wasn't an IDataObject, give the proxy value back.
+ // (Creating the DataObject will add a reference to the proxy.)
+ return new DataObject(proxyDataObject.Value);
}
-
- // Original data given wasn't an IDataObject, give the proxy value back.
- return new DataObject(proxyDataObject.Value);
}
///
@@ -135,23 +90,10 @@ public static unsafe void SetDataObject(object data, bool copy, int retryTimes,
///
public static unsafe void Clear()
{
- if (Application.OleRequired() != ApartmentState.STA)
+ HRESULT result = ClipboardCore.Clear();
+ if (result.Failed)
{
- throw new ThreadStateException(SR.ThreadMustBeSTA);
- }
-
- HRESULT hr;
- int retry = 10;
- while ((hr = PInvokeCore.OleSetClipboard(null)).Failed)
- {
- if (--retry < 0)
- {
-#pragma warning disable CA2201 // Do not raise reserved exception types
- throw new ExternalException(SR.ClipboardOperationFailed, (int)hr);
-#pragma warning restore CA2201
- }
-
- Thread.Sleep(millisecondsTimeout: 100);
+ throw new ExternalException(SR.ClipboardOperationFailed, (int)result);
}
}
@@ -369,7 +311,7 @@ public static bool ContainsText(TextDataFormat format)
{
data = default;
resolver.OrThrowIfNull();
- if (!DataObject.IsValidFormatAndType(format)
+ if (!ClipboardCore.IsValidTypeForFormat(typeof(T), format)
|| GetDataObject() is not { } dataObject)
{
// Invalid format or no object on the clipboard at all.
@@ -385,7 +327,7 @@ public static bool ContainsText(TextDataFormat format)
[NotNullWhen(true), MaybeNullWhen(false)] out T data)
{
data = default;
- if (!DataObject.IsValidFormatAndType(format)
+ if (!ClipboardCore.IsValidTypeForFormat(typeof(T), format)
|| GetDataObject() is not { } dataObject)
{
// Invalid format or no object on the clipboard at all.
diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs
index 3420670d3da..d47f9c7c694 100644
--- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs
+++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataFormats.cs
@@ -126,11 +126,11 @@ public static partial class DataFormats
public static Format GetFormat(string format)
{
ArgumentException.ThrowIfNullOrWhiteSpace(format);
- return DataFormatsCore.GetOrAddFormat(format);
+ return DataFormatsCore.GetOrAddFormat(format);
}
///
/// Gets a with the Windows Clipboard numeric ID and name for the specified ID.
///
- public static Format GetFormat(int id) => DataFormatsCore.GetOrAddFormat(id);
+ public static Format GetFormat(int id) => DataFormatsCore.GetOrAddFormat(id);
}
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 b0b7f0f35ff..017384b7444 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
@@ -3,15 +3,13 @@
using System.Collections.Specialized;
using System.Drawing;
+using System.Private.Windows.Ole;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text.Json;
using Com = Windows.Win32.System.Com;
using ComTypes = System.Runtime.InteropServices.ComTypes;
-using System.Private.Windows.Ole;
-using System.Windows.Forms.Nrbf;
-using System.Windows.Forms.Ole;
namespace System.Windows.Forms;
@@ -30,21 +28,20 @@ public unsafe partial class DataObject :
Com.IManagedWrapper,
IComVisibleDataObject
{
- private readonly Composition _innerData;
+ private readonly Composition _innerData;
///
/// Initializes a new instance of the class, with the raw
/// and the managed data object the raw pointer is associated with.
///
///
- internal DataObject(Com.IDataObject* data) => _innerData = Composition.CreateFromNativeDataObject(data);
+ internal DataObject(Com.IDataObject* data) => _innerData = DataObjectCore.CreateComposition(data);
///
/// Initializes a new instance of the class, which can store arbitrary data.
///
///
- public DataObject() =>
- _innerData = Composition.CreateFromManagedDataObject(new DataStore());
+ public DataObject() => _innerData = DataObjectCore.CreateComposition();
///
/// Initializes a new instance of the class, containing the specified data.
@@ -58,26 +55,8 @@ public DataObject() =>
/// if is not implemented.
///
///
- public DataObject(object data)
- {
- if (data is IDataObjectInternal internalDataObject)
- {
- _innerData = Composition.CreateFromManagedDataObject(internalDataObject);
- }
- else if (data is IDataObject iDataObject)
- {
- _innerData = Composition.CreateFromManagedDataObject(new DataObjectAdapter(iDataObject));
- }
- else if (data is ComTypes.IDataObject comDataObject)
- {
- _innerData = Composition.CreateFromRuntimeDataObject(comDataObject);
- }
- else
- {
- _innerData = Composition.CreateFromManagedDataObject(new DataStore());
- SetData(data);
- }
- }
+ public DataObject(object data) =>
+ _innerData = DataObjectCore.CreateComposition(data, DataObjectAdapter.Create);
///
/// Initializes a new instance of the class, containing the specified data and its
@@ -93,7 +72,7 @@ internal virtual bool TryUnwrapUserDataObject([NotNullWhen(true)] out IDataObjec
{
DataObject data => data,
DataObjectAdapter adapter => adapter.DataObject,
- DataStore => this,
+ DataStore => this,
_ => null
};
@@ -102,11 +81,13 @@ internal virtual bool TryUnwrapUserDataObject([NotNullWhen(true)] out IDataObjec
///
[RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")]
- public void SetDataAsJson(string format, T data) => SetData(format, TryJsonSerialize(format, data));
+ public void SetDataAsJson(string format, T data) =>
+ SetData(format, DataObjectCore.TryJsonSerialize(format, data));
///
[RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")]
- public void SetDataAsJson(T data) => SetData(typeof(T), TryJsonSerialize(typeof(T).FullName!, data));
+ public void SetDataAsJson(T data) =>
+ SetData(typeof(T), DataObjectCore.TryJsonSerialize(typeof(T).FullName.OrThrowIfNull(), data));
///
/// Stores the data in the specified format.
@@ -144,32 +125,8 @@ internal virtual bool TryUnwrapUserDataObject([NotNullWhen(true)] out IDataObjec
///
///
[RequiresUnreferencedCode("Uses default System.Text.Json behavior which is not trim-compatible.")]
- public void SetDataAsJson(string format, bool autoConvert, T data) => SetData(format, autoConvert, TryJsonSerialize(format, data));
-
- ///
- /// JSON serialize the data only if the format is not a restricted deserialization format and the data is not an intrinsic type.
- ///
- ///
- /// The passed in as is if the format is restricted. Otherwise the JSON serialized .
- ///
- private static object TryJsonSerialize(string format, T data)
- {
- if (string.IsNullOrWhiteSpace(format.OrThrowIfNull()))
- {
- throw new ArgumentException(SR.DataObjectWhitespaceEmptyFormatNotAllowed, nameof(format));
- }
-
- data.OrThrowIfNull(nameof(data));
-
- if (typeof(T) == typeof(DataObject))
- {
- throw new InvalidOperationException(string.Format(SR.ClipboardOrDragDrop_CannotJsonSerializeDataObject, nameof(SetData)));
- }
-
- return DataFormatNames.IsRestrictedFormat(format) || WinFormsNrbfSerializer.IsSupportedType()
- ? data
- : new JsonData() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) };
- }
+ public void SetDataAsJson(string format, bool autoConvert, T data) =>
+ SetData(format, autoConvert, DataObjectCore.TryJsonSerialize(format, data));
#region IDataObject
[Obsolete(
@@ -339,7 +296,7 @@ public virtual void SetText(string textData, TextDataFormat format)
{
data = default;
- if (!IsValidFormatAndType(format))
+ if (!ClipboardCore.IsValidTypeForFormat(typeof(T), format))
{
// Resolver implementation is specific to the overridden TryGetDataCore method,
// can't validate if a non-null resolver is required for unbounded types.
@@ -349,44 +306,6 @@ public virtual void SetText(string textData, TextDataFormat format)
return TryGetDataCore(format, resolver, autoConvert, out data);
}
- ///
- /// Verify if the specified format is valid and compatible with the specified type .
- ///
- internal static bool IsValidFormatAndType(string format)
- {
- if (string.IsNullOrWhiteSpace(format))
- {
- return false;
- }
-
- if (IsValidPredefinedFormatTypeCombination(format))
- {
- return true;
- }
-
- throw new NotSupportedException(string.Format(
- SR.ClipboardOrDragDrop_InvalidFormatTypeCombination,
- typeof(T).FullName, format));
-
- static bool IsValidPredefinedFormatTypeCombination(string format) => format switch
- {
- DataFormatNames.Text
- or DataFormatNames.UnicodeText
- or DataFormatNames.String
- or DataFormatNames.Rtf
- or DataFormatNames.Html
- or DataFormatNames.OemText => typeof(string) == typeof(T),
-
- DataFormatNames.FileDrop
- or DataFormatNames.FileNameAnsi
- or DataFormatNames.FileNameUnicode => typeof(string[]) == typeof(T),
-
- DataFormatNames.Bitmap or DataFormatNames.BinaryFormatBitmap =>
- typeof(Bitmap) == typeof(T) || typeof(Image) == typeof(T),
- _ => true
- };
- }
-
private static string ConvertToDataFormats(TextDataFormat format) => format switch
{
TextDataFormat.UnicodeText => DataFormatNames.UnicodeText,
diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectAdapter.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectAdapter.cs
index 88df8173016..83f29a5d3f4 100644
--- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectAdapter.cs
+++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/DataObjectAdapter.cs
@@ -15,6 +15,8 @@ internal sealed class DataObjectAdapter : IDataObjectInternal
public DataObjectAdapter(IDataObject dataObject) => DataObject = dataObject;
+ public static IDataObjectInternal Create(IDataObject dataObject) => new DataObjectAdapter(dataObject);
+
public object? GetData(string format, bool autoConvert) => DataObject.GetData(format, autoConvert);
public object? GetData(string format) => DataObject.GetData(format);
public object? GetData(Type format) => DataObject.GetData(format);
diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/WinFormsOleServices.cs b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/WinFormsOleServices.cs
index f3bf38fdf32..7b5a5076930 100644
--- a/src/System.Windows.Forms/src/System/Windows/Forms/OLE/WinFormsOleServices.cs
+++ b/src/System.Windows.Forms/src/System/Windows/Forms/OLE/WinFormsOleServices.cs
@@ -14,11 +14,20 @@ namespace System.Windows.Forms.Ole;
///
internal sealed class WinFormsOleServices : IOleServices
{
+ // Prevent instantiation
private WinFormsOleServices() { }
- static void IOleServices.EnsureThreadState()
+ public static void EnsureThreadState()
{
- if (Control.CheckForIllegalCrossThreadCalls && Application.OleRequired() != ApartmentState.STA)
+ // There were some cases historically where we would try not to throw to avoid user code crashing from
+ // attempting to call OLE code in a finalizer. There isn't a bullet proof way to know whether or not
+ // we're on the finalizer thread, this should be left to the finalizer implementers to handle (even if
+ // that might be us on behalf of the user).
+ //
+ // In one other case we were checking for Control.CheckForIllegalCrossThreadCalls, but that is also not
+ // a great idea, as it overloaded the meaning of the property. Try to keep this as simple and as consistent
+ // as possible.
+ if (Application.OleRequired() != ApartmentState.STA)
{
throw new ThreadStateException(SR.ThreadMustBeSTA);
}
@@ -90,56 +99,57 @@ static unsafe bool IOleServices.TryGetBitmapFromDataObject(Com.IDataObject* d
data = default!;
return false;
- }
-
- private static unsafe bool TryGetBitmapData(Com.IDataObject* dataObject, [NotNullWhen(true)] out Bitmap? data)
- {
- data = default;
- FORMATETC formatEtc = new()
+ static unsafe bool TryGetBitmapData(Com.IDataObject* dataObject, [NotNullWhen(true)] out Bitmap? data)
{
- cfFormat = (ushort)CLIPBOARD_FORMAT.CF_BITMAP,
- dwAspect = (uint)DVASPECT.DVASPECT_CONTENT,
- lindex = -1,
- tymed = (uint)TYMED.TYMED_GDI
- };
+ data = default;
- STGMEDIUM medium = default;
+ FORMATETC formatEtc = new()
+ {
+ cfFormat = (ushort)CLIPBOARD_FORMAT.CF_BITMAP,
+ dwAspect = (uint)DVASPECT.DVASPECT_CONTENT,
+ lindex = -1,
+ tymed = (uint)TYMED.TYMED_GDI
+ };
- if (dataObject->QueryGetData(formatEtc).Succeeded)
- {
- HRESULT hr = dataObject->GetData(formatEtc, out medium);
+ STGMEDIUM medium = default;
- // One of the ways this can happen is when we attempt to put binary formatted data onto the
- // clipboard, which will succeed as Windows ignores all errors when putting data on the clipboard.
- // The data state, however, is not good, and this error will be returned by Windows when asking to
- // get the data out.
- Debug.WriteLineIf(hr == HRESULT.CLIPBRD_E_BAD_DATA, "CLIPBRD_E_BAD_DATA returned when trying to get clipboard data.");
- }
+ if (dataObject->QueryGetData(formatEtc).Succeeded)
+ {
+ HRESULT hr = dataObject->GetData(formatEtc, out medium);
- try
- {
- // GDI+ doesn't own this HBITMAP, but we can't delete it while the object is still around. So we
- // have to do the really expensive thing of cloning the image so we can release the HBITMAP.
- if ((uint)medium.tymed == (uint)TYMED.TYMED_GDI
- && !medium.hGlobal.IsNull
- && Image.FromHbitmap(medium.hGlobal) is Bitmap clipboardBitmap)
+ // One of the ways this can happen is when we attempt to put binary formatted data onto the
+ // clipboard, which will succeed as Windows ignores all errors when putting data on the clipboard.
+ // The data state, however, is not good, and this error will be returned by Windows when asking to
+ // get the data out.
+ Debug.WriteLineIf(hr == HRESULT.CLIPBRD_E_BAD_DATA, "CLIPBRD_E_BAD_DATA returned when trying to get clipboard data.");
+ }
+
+ try
{
- data = (Bitmap)clipboardBitmap.Clone();
- clipboardBitmap.Dispose();
- return true;
+ // GDI+ doesn't own this HBITMAP, but we can't delete it while the object is still around. So we
+ // have to do the really expensive thing of cloning the image so we can release the HBITMAP.
+ if ((uint)medium.tymed == (uint)TYMED.TYMED_GDI
+ && !medium.hGlobal.IsNull
+ && Image.FromHbitmap(medium.hGlobal) is Bitmap clipboardBitmap)
+ {
+ data = (Bitmap)clipboardBitmap.Clone();
+ clipboardBitmap.Dispose();
+ return true;
+ }
+ }
+ finally
+ {
+ PInvokeCore.ReleaseStgMedium(ref medium);
}
- }
- finally
- {
- PInvokeCore.ReleaseStgMedium(ref medium);
- }
- return false;
+ return false;
+ }
}
- // Image is a special case because we are reading Bitmaps directly from the SerializationRecord.
- static bool IOleServices.AllowTypeWithoutResolver() => typeof(T) == typeof(Image);
+ static bool IOleServices.AllowTypeWithoutResolver() =>
+ // Image is a special case because we are reading Bitmaps directly from the SerializationRecord.
+ typeof(T) == typeof(Image);
static void IOleServices.ValidateDataStoreData(ref string format, bool autoConvert, object? data)
{
@@ -151,4 +161,11 @@ static void IOleServices.ValidateDataStoreData(ref string format, bool autoConve
format = autoConvert ? DataFormatNames.Bitmap : throw new NotSupportedException(SR.DataObjectDibNotSupported);
}
}
+
+ static bool IOleServices.IsValidTypeForFormat(Type type, string format) => format switch
+ {
+ DataFormatNames.Bitmap or DataFormatNames.BinaryFormatBitmap => type == typeof(Bitmap) || type == typeof(Image),
+ // All else should fall through as valid.
+ _ => true
+ };
}
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 89e96f20733..5c3977d831a 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
@@ -367,9 +367,9 @@ public void Clipboard_SetDataObject_NegativeRetryDelay_ThrowsArgumentOutOfRangeE
() => Clipboard.SetAudio(Array.Empty()),
() => Clipboard.SetAudio(new MemoryStream()),
() => Clipboard.SetData("format", data: null!),
- () => Clipboard.SetDataObject(null!),
- () => Clipboard.SetDataObject(null!, copy: true),
- () => Clipboard.SetDataObject(null!, copy: true, retryTimes: 10, retryDelay: 0),
+ () => Clipboard.SetDataObject(new DataObject()),
+ () => Clipboard.SetDataObject(new DataObject(), copy: true),
+ () => Clipboard.SetDataObject(new DataObject(), copy: true, retryTimes: 10, retryDelay: 0),
() => Clipboard.SetFileDropList(["filePath"]),
() => Clipboard.SetText("text"),
() => Clipboard.SetText("text", TextDataFormat.Text)
@@ -979,6 +979,7 @@ public void Clipboard_SetDataObject_WithJson_ReturnsExpected(bool copy)
ITypedDataObject returnedDataObject = Clipboard.GetDataObject().Should().BeAssignableTo().Subject;
returnedDataObject.TryGetData("testDataFormat", out SimpleTestData deserialized).Should().BeTrue();
deserialized.Should().BeEquivalentTo(testData);
+
// We don't expose JsonData in legacy API
var legacyResult = Clipboard.GetData("testDataFormat");
if (copy)