Skip to content

Allow IDataObjectAsyncCapability #13431

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

Merged
merged 1 commit into from
May 19, 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
1 change: 1 addition & 0 deletions docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ Documentation for experimental features is available in the [Experimental Help](
| `WFO5001` | NET9.0 | | `System.Windows.Forms.Application.SetColorMode`(System.Windows.Forms.SystemColorMode) is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. |
| `WFO5001` | NET9.0 | | `System.Windows.Forms.SystemColorMode` is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. |
| `WFO5002` | NET9.0 | | `System.Windows.Forms.Form.ShowAsync` is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. |
| `WFO5003` | NET10.0 | | `System.Windows.Forms.IAsyncDropTarget` is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. |
7 changes: 7 additions & 0 deletions src/Common/tests/TestUtilities/AppContextSwitchNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ public const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwi
/// </summary>
public const string ClipboardDragDropEnableNrbfSerializationSwitchName
= "Windows.ClipboardDragDrop.EnableNrbfSerialization";

/// <summary>
/// When set to true, prevents the async capable drag/drop operations from being performed in a
/// synchronous manner.
/// </summary>
public const string DragDropDisableSyncOverAsyncSwitchName
= "Windows.DragDrop.DisableSyncOverAsync";
}
1 change: 1 addition & 0 deletions src/System.Private.Windows.Core/src/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ HRGN
HWND
HWND_*
IDataObject
IDataObjectAsyncCapability
IDI_*
IDispatchEx
IDragSourceHelper2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ internal static class CoreAppContextSwitches
{
// Enabling switches in Core is different from Framework. See https://learn.microsoft.com/dotnet/core/runtime-config/
// for details on how to set switches.
internal const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName = "Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization";
internal const string ClipboardDragDropEnableNrbfSerializationSwitchName = "Windows.ClipboardDragDrop.EnableNrbfSerialization";
internal const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwitchName =
"Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization";

internal const string ClipboardDragDropEnableNrbfSerializationSwitchName =
"Windows.ClipboardDragDrop.EnableNrbfSerialization";

internal const string DragDropDisableSyncOverAsyncSwitchName =
"Windows.DragDrop.DisableSyncOverAsync";

private static int s_clipboardDragDropEnableUnsafeBinaryFormatterSerialization;
private static int s_clipboardDragDropEnableNrbfSerialization;
private static int s_dragDropDisableSyncOverAsync;

private static bool GetCachedSwitchValue(string switchName, ref int cachedSwitchValue)
{
Expand Down Expand Up @@ -44,7 +51,7 @@ private static bool GetSwitchValue(string switchName, ref int cachedSwitchValue)
AppContext.TryGetSwitch("TestSwitch.LocalAppContext.DisableCaching", out bool disableCaching);
if (!disableCaching)
{
cachedSwitchValue = isSwitchEnabled ? 1 /*true*/ : -1 /*false*/;
cachedSwitchValue = isSwitchEnabled ? 1 /* true */ : -1 /* false */;
}
else if (!hasSwitch)
{
Expand Down Expand Up @@ -95,4 +102,21 @@ public static bool ClipboardDragDropEnableNrbfSerialization
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetCachedSwitchValue(ClipboardDragDropEnableNrbfSerializationSwitchName, ref s_clipboardDragDropEnableNrbfSerialization);
}

/// <summary>
/// If <see langword="true"/>, then async capable drag/drop operations will not be performed in a synchronous manner.
/// </summary>
/// <remarks>
/// <para>
/// Some drag sources only support async operations. Notably, Chromium-based applications with file drop (the
/// new Outlook is one example). To enable applications to accept filenames from these sources we use the interface
/// when available and just do the operation synchronously. This isn't expected to be a problem, but if it is we'll
/// provide a way to opt out of this behavior. The flag may also be useful for testing purposes.
/// </para>
/// </remarks>
public static bool DragDropDisableSyncOverAsync
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetCachedSwitchValue(DragDropDisableSyncOverAsyncSwitchName, ref s_dragDropDisableSyncOverAsync);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ internal static class DiagnosticIDs
// Experimental, number group 5000+
public const string ExperimentalDarkMode = "WFO5001";
public const string ExperimentalAsync = "WFO5002";
public const string ExperimentalAsyncDropTarget = "WFO5003";
}
2 changes: 2 additions & 0 deletions src/System.Windows.Forms/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[WFO5003]System.Windows.Forms.IAsyncDropTarget
[WFO5003]System.Windows.Forms.IAsyncDropTarget.OnAsyncDragDrop(System.Windows.Forms.DragEventArgs! e) -> void
115 changes: 106 additions & 9 deletions src/System.Windows.Forms/System/Windows/Forms/OLE/DropTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,24 +179,121 @@ HRESULT OleIDropTarget.Interface.Drop(Com.IDataObject* pDataObj, MODIFIERKEYS_FL
return HRESULT.E_INVALIDARG;
}

if (CreateDragEventArgs(pDataObj, grfKeyState, pt, *pdwEffect) is { } dragEvent)
// Some drag sources only support async operations. Notably, Chromium-based applications with file drop (the
// new Outlook is one example). The async interface is primarily a feature check and ref counting mechanism.
// To enable applications to accept filenames from these sources we use the interface when available and just
// do the operation synchronously. When we add new async API we would defer to the async interface.
//
// While initial investigations show that this is not a problem, we'll still provide a way to opt out should
// this prove blocking for some unknown scenario.
//
// https://learn.microsoft.com/windows/win32/shell/datascenarios#dragging-and-dropping-shell-objects-asynchronously

IDataObjectAsyncCapability* asyncCapability = null;
HRESULT result = HRESULT.S_OK;

bool enableSyncOverAsync = !CoreAppContextSwitches.DragDropDisableSyncOverAsync;
#pragma warning disable WFO5003 // Type is for evaluation purposes only
IAsyncDropTarget? asyncDropTarget = _owner as IAsyncDropTarget;
#pragma warning restore WFO5003
if (asyncDropTarget is not null || enableSyncOverAsync)
{
if (_lastDragEventArgs?.DropImageType > DropImageType.Invalid)
result = pDataObj->QueryInterface(out asyncCapability);
if (result.Succeeded
&& asyncCapability is not null
&& asyncCapability->GetAsyncMode(out BOOL isAsync).Succeeded
&& isAsync)
{
ClearDropDescription();
DragDropHelper.Drop(dragEvent);
result = asyncCapability->StartOperation();
if (result.Failed)
{
return result;
}
}
}

_owner.OnDragDrop(dragEvent);
*pdwEffect = (DROPEFFECT)dragEvent.Effect;
*pdwEffect = DROPEFFECT.DROPEFFECT_NONE;

try
{
if (CreateDragEventArgs(pDataObj, grfKeyState, pt, *pdwEffect) is { } dragEvent)
{
if (_lastDragEventArgs?.DropImageType > DropImageType.Invalid)
{
ClearDropDescription();
DragDropHelper.Drop(dragEvent);
}

result = HandleOnDragDrop(dragEvent, asyncCapability, pdwEffect);
asyncCapability = null;
}

_lastEffect = DragDropEffects.None;
_lastDataObject = null;
}
else
finally
{
if (asyncCapability is not null)
{
// We weren't successful in completing the operation, so we need to end it with no drop effect.
// There isn't clear guidance on expected errors here, so we'll just use E_UNEXPECTED.
result = asyncCapability->EndOperation(HRESULT.E_UNEXPECTED, null, (uint)DROPEFFECT.DROPEFFECT_NONE);
asyncCapability->Release();
}
}

return result;
}

private HRESULT HandleOnDragDrop(DragEventArgs e, IDataObjectAsyncCapability* asyncCapability, DROPEFFECT* pdwEffect)
{
#pragma warning disable WFO5003 // Type is for evaluation purposes only
if (asyncCapability is not null && _owner is IAsyncDropTarget asyncDropTarget)
#pragma warning restore WFO5003
{
// We have an implemented IAsyncDropTarget and the drag source supports async operations, push to a
// worker thread to allow the drop to complete without blocking the UI thread.
Task.Run(() =>
{
DROPEFFECT effect = DROPEFFECT.DROPEFFECT_NONE;

try
{
asyncDropTarget.OnAsyncDragDrop(e);
effect = (DROPEFFECT)e.Effect;
}
finally
{
HRESULT result = asyncCapability->EndOperation(HRESULT.S_OK, null, (uint)effect);
asyncCapability->Release();
}
});

// It isn't clear what we're supposed to do with the effect here as the actual result comes from
// EndOperation. Perhaps DROPEFFECT_COPY would be a better default?
*pdwEffect = DROPEFFECT.DROPEFFECT_NONE;
return HRESULT.S_OK;
}

// We don't have the IAsyncDropTarget or the drag source doesn't support async operations, so just call
// the normal OnDragDrop.

DROPEFFECT effect = DROPEFFECT.DROPEFFECT_NONE;

try
{
_owner.OnDragDrop(e);
effect = (DROPEFFECT)e.Effect;
}
finally
{
if (asyncCapability is not null)
{
HRESULT result = asyncCapability->EndOperation(HRESULT.S_OK, null, (uint)effect);
asyncCapability->Release();
}
}

_lastEffect = DragDropEffects.None;
_lastDataObject = null;
return HRESULT.S_OK;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Windows.Forms.Analyzers.Diagnostics;

namespace System.Windows.Forms;

/// <summary>
/// Interface for a drop target that supports asynchronous processing.
/// </summary>
/// <remarks>
/// <para>
/// This is currently marked as experimental as there is some uncertainty around the API that might need
/// to be addressed in the future. With additional scenario feedback, we will make changes if needed.
/// </para>
/// </remarks>
[Experimental(DiagnosticIDs.ExperimentalAsyncDropTarget, UrlFormat = DiagnosticIDs.UrlFormat)]
public interface IAsyncDropTarget : IDropTarget
{
/// <summary>
/// When supporting this interface, this method will be callled if the drop source supports asynchronous processing.
/// </summary>
/// <remarks>
/// <para>
/// Similar to <see cref="IDropTarget.OnDragDrop"/>, but this method is called when a drop operation supports
/// asyncronous processing. It will not block the UI thread, any UI updates will need to be invoked to occur
/// on the UI thread.
/// </para>
/// <para>
/// Avoid dispatching the <see cref="DragEventArgs"/> back to the UI thread as invoking <see cref="DragEventArgs.Data"/>
/// on the UI thread will block it until the data is available. If existing code needs <see cref="DragEventArgs"/>
/// consider creating a new instance with a new <see cref="DataObject"/> that has extracted the data you're looking for.
/// </para>
/// </remarks>
void OnAsyncDragDrop(DragEventArgs e);
}