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); +}