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)