diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md
index c82ae72e584..2003c1cb5af 100644
--- a/docs/list-of-diagnostics.md
+++ b/docs/list-of-diagnostics.md
@@ -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. |
diff --git a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs
index 65b93fbacd7..043a6b195f8 100644
--- a/src/Common/tests/TestUtilities/AppContextSwitchNames.cs
+++ b/src/Common/tests/TestUtilities/AppContextSwitchNames.cs
@@ -33,4 +33,11 @@ public const string ClipboardDragDropEnableUnsafeBinaryFormatterSerializationSwi
///
public const string ClipboardDragDropEnableNrbfSerializationSwitchName
= "Windows.ClipboardDragDrop.EnableNrbfSerialization";
+
+ ///
+ /// When set to true, prevents the async capable drag/drop operations from being performed in a
+ /// synchronous manner.
+ ///
+ public const string DragDropDisableSyncOverAsyncSwitchName
+ = "Windows.DragDrop.DisableSyncOverAsync";
}
diff --git a/src/System.Private.Windows.Core/src/NativeMethods.txt b/src/System.Private.Windows.Core/src/NativeMethods.txt
index 9b54312efdc..56722d01258 100644
--- a/src/System.Private.Windows.Core/src/NativeMethods.txt
+++ b/src/System.Private.Windows.Core/src/NativeMethods.txt
@@ -150,6 +150,7 @@ HRGN
HWND
HWND_*
IDataObject
+IDataObjectAsyncCapability
IDI_*
IDispatchEx
IDragSourceHelper2
diff --git a/src/System.Private.Windows.Core/src/System/Private/Windows/CoreAppContextSwitches.cs b/src/System.Private.Windows.Core/src/System/Private/Windows/CoreAppContextSwitches.cs
index 77a3333c78d..b0781eb009c 100644
--- a/src/System.Private.Windows.Core/src/System/Private/Windows/CoreAppContextSwitches.cs
+++ b/src/System.Private.Windows.Core/src/System/Private/Windows/CoreAppContextSwitches.cs
@@ -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)
{
@@ -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)
{
@@ -95,4 +102,21 @@ public static bool ClipboardDragDropEnableNrbfSerialization
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetCachedSwitchValue(ClipboardDragDropEnableNrbfSerializationSwitchName, ref s_clipboardDragDropEnableNrbfSerialization);
}
+
+ ///
+ /// If , then async capable drag/drop operations will not be performed in a synchronous manner.
+ ///
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ public static bool DragDropDisableSyncOverAsync
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => GetCachedSwitchValue(DragDropDisableSyncOverAsyncSwitchName, ref s_dragDropDisableSyncOverAsync);
+ }
}
diff --git a/src/System.Windows.Forms.Analyzers/src/System/Windows/Forms/Analyzers/Diagnostics/DiagnosticIDs.cs b/src/System.Windows.Forms.Analyzers/src/System/Windows/Forms/Analyzers/Diagnostics/DiagnosticIDs.cs
index 7d8ad147fb6..3e76d6efbc9 100644
--- a/src/System.Windows.Forms.Analyzers/src/System/Windows/Forms/Analyzers/Diagnostics/DiagnosticIDs.cs
+++ b/src/System.Windows.Forms.Analyzers/src/System/Windows/Forms/Analyzers/Diagnostics/DiagnosticIDs.cs
@@ -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";
}
diff --git a/src/System.Windows.Forms/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/PublicAPI.Unshipped.txt
index e69de29bb2d..f3f661e6293 100644
--- a/src/System.Windows.Forms/PublicAPI.Unshipped.txt
+++ b/src/System.Windows.Forms/PublicAPI.Unshipped.txt
@@ -0,0 +1,2 @@
+[WFO5003]System.Windows.Forms.IAsyncDropTarget
+[WFO5003]System.Windows.Forms.IAsyncDropTarget.OnAsyncDragDrop(System.Windows.Forms.DragEventArgs! e) -> void
\ No newline at end of file
diff --git a/src/System.Windows.Forms/System/Windows/Forms/OLE/DropTarget.cs b/src/System.Windows.Forms/System/Windows/Forms/OLE/DropTarget.cs
index a8fef520645..28c065e2329 100644
--- a/src/System.Windows.Forms/System/Windows/Forms/OLE/DropTarget.cs
+++ b/src/System.Windows.Forms/System/Windows/Forms/OLE/DropTarget.cs
@@ -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;
}
diff --git a/src/System.Windows.Forms/System/Windows/Forms/OLE/IAsyncDropTarget.cs b/src/System.Windows.Forms/System/Windows/Forms/OLE/IAsyncDropTarget.cs
new file mode 100644
index 00000000000..f94c6baa859
--- /dev/null
+++ b/src/System.Windows.Forms/System/Windows/Forms/OLE/IAsyncDropTarget.cs
@@ -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;
+
+///
+/// Interface for a drop target that supports asynchronous processing.
+///
+///
+///
+/// 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.
+///
+///
+[Experimental(DiagnosticIDs.ExperimentalAsyncDropTarget, UrlFormat = DiagnosticIDs.UrlFormat)]
+public interface IAsyncDropTarget : IDropTarget
+{
+ ///
+ /// When supporting this interface, this method will be callled if the drop source supports asynchronous processing.
+ ///
+ ///
+ ///
+ /// Similar to , 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.
+ ///
+ ///
+ /// Avoid dispatching the back to the UI thread as invoking
+ /// on the UI thread will block it until the data is available. If existing code needs
+ /// consider creating a new instance with a new that has extracted the data you're looking for.
+ ///
+ ///
+ void OnAsyncDragDrop(DragEventArgs e);
+}