Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Clipboard and DataObject code to Core #12868

Merged
merged 3 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/System.Private.Windows.Core/src/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

// 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")]

Expand All @@ -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")]
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Contains platform-agnostic clipboard operations.
/// </summary>
internal static unsafe class ClipboardCore<TOleServices>
where TOleServices : IOleServices
{
/// <summary>
/// The number of times to retry OLE clipboard operations.
/// </summary>
private const int OleRetryCount = 10;

/// <summary>
/// The amount of time in milliseconds to sleep between retrying OLE clipboard operations.
/// </summary>
private const int OleRetryDelay = 100;

/// <summary>
/// Removes all data from the Clipboard.
/// </summary>
/// <returns>An <see cref="HRESULT"/> indicating the success or failure of the operation.</returns>
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;
}

/// <summary>
/// Attempts to set the specified data on the Clipboard.
/// </summary>
/// <param name="dataObject">The data object to set on the Clipboard.</param>
/// <param name="copy">Indicates whether to copy the data to the Clipboard.</param>
/// <param name="retryTimes">The number of times to retry the operation if it fails.</param>
/// <param name="retryDelay">The amount of time in milliseconds to wait between retries.</param>
/// <returns>An <see cref="HRESULT"/> indicating the success or failure of the operation.</returns>
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<IDataObject>(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;
}

/// <summary>
/// Attempts to retrieve data from the Clipboard.
/// </summary>
/// <param name="proxyDataObject">The proxy data object retrieved from the Clipboard.</param>
/// <param name="originalObject">The original object retrieved from the Clipboard, if available.</param>
/// <returns>An <see cref="HRESULT"/> indicating the success or failure of the operation.</returns>
public static HRESULT TryGetData(out ComScope<IDataObject> 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<IComCallableWrapper> realDataObject = proxyDataObject.TryQuery<IComCallableWrapper>(out HRESULT wrapperResult);

if (wrapperResult.Succeeded)
{
ComHelpers.TryUnwrapComWrapperCCW(realDataObject.AsUnknown, out originalObject);
}

return result;
}

/// <summary>
/// Checks if the specified <paramref name="format"/> is valid and compatible with the specified <paramref name="type"/>.
/// </summary>
/// <remarks>
/// <para>
/// This is intended to be used as a pre-validation step to give a more useful error to callers.
/// </para>
/// </remarks>
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)
};
}
}
Original file line number Diff line number Diff line change
@@ -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<TRuntime, TDataFormat, TDataObject, TIDataObject>
where TRuntime : IRuntime<TDataFormat>
where TDataFormat : IDataFormat<TDataFormat>
where TDataObject : IComVisibleDataObject
{
/// <summary>
/// JSON serialize the data only if the format is not a restricted deserialization format and the data is not an intrinsic type.
/// </summary>
/// <returns>
/// The passed in <paramref name="data"/> as is if the format is restricted. Otherwise the JSON serialized <paramref name="data"/>.
/// </returns>
internal static object TryJsonSerialize<T>(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<T>()
? data
: new JsonData<T>() { JsonBytes = JsonSerializer.SerializeToUtf8Bytes(data) };
}

internal static Composition<TRuntime, TDataFormat> CreateComposition() =>
Composition<TRuntime, TDataFormat>.CreateFromManagedDataObject(new DataStore<TRuntime>());

internal static Composition<TRuntime, TDataFormat> CreateComposition(IDataObject* data) =>
Composition<TRuntime, TDataFormat>.CreateFromNativeDataObject(data);

internal static Composition<TRuntime, TDataFormat> CreateComposition(
object data,
Func<TIDataObject, IDataObjectInternal> adapterFactory)
{
if (data is IDataObjectInternal internalDataObject)
{
return Composition<TRuntime, TDataFormat>.CreateFromManagedDataObject(internalDataObject);
}
else if (data is TIDataObject iDataObject)
{
return Composition<TRuntime, TDataFormat>.CreateFromManagedDataObject(adapterFactory(iDataObject));
}
else if (data is ComTypes.IDataObject comDataObject)
{
return Composition<TRuntime, TDataFormat>.CreateFromRuntimeDataObject(comDataObject);
}

var composition = Composition<TRuntime, TDataFormat>.CreateFromManagedDataObject(new DataStore<TRuntime>());
composition.SetData(data);
return composition;
}
}
Original file line number Diff line number Diff line change
@@ -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<TOleServices>
{
private class DataStoreEntry
private readonly struct DataStoreEntry
{
[Required]
public object? Data { get; }

[Required]
public bool AutoConvert { get; }

public DataStoreEntry(object? data, bool autoConvert)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private bool TryGetDataInternal<T>(

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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ static abstract bool TryGetBitmapFromDataObject<T>(
IDataObject* dataObject,
[NotNullWhen(true)] out T data);

/// <summary>
/// Returns true if the given <paramref name="type"/> is a valid type for the given <paramref name="format"/>.
/// </summary>
/// <remarks>
/// <para>
/// Basic predefined formats that map to <see langword="string"/> are checked before this call.
JeremyKuhne marked this conversation as resolved.
Show resolved Hide resolved
/// </para>
/// </remarks>
static abstract bool IsValidTypeForFormat(Type type, string format);

/// <summary>
/// Allows the given <typeparamref name="T"/> to pass pre-validation without a resolver.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
1 change: 1 addition & 0 deletions src/System.Windows.Forms/src/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
18 changes: 17 additions & 1 deletion src/System.Windows.Forms/src/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<System.Windows.Forms.Ole.WinFormsOleServices, System.Windows.Forms.DataFormats.Format>;

// Having these as global usings reduces verbiage in code and avoids accidental mismatching when defining types.
JeremyKuhne marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down
Loading