Browse Source

Allow IDataObjectAsyncCapability (#13431)

Chromium based apps don't support file drop without using IDataObjectAsyncCapability. This includes the new Outlook.

To support this we'll look for this interface in our current code paths and utilize it. This makes the async operation sync, which works, but isn't ideal. Chromium will pop a dialog that will leave WinForms modal as well until it is responded to.

If this behavior creates an issue it can be disabled with the appcontext switch: "Windows.DragDrop.DisableSyncOverAsync"

In order to truly support async we're also introducing a new interface to allow calling back off of the UI thread. This will be shipped as experimental for .NET 10 as there is a small risk we'll want to change the API based on real-world feedback. See #13422.
pull/13479/head
Jeremy Kuhne 2 weeks ago
committed by GitHub
parent
commit
4c32f29dda
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      docs/list-of-diagnostics.md
  2. 7
      src/Common/tests/TestUtilities/AppContextSwitchNames.cs
  3. 1
      src/System.Private.Windows.Core/src/NativeMethods.txt
  4. 30
      src/System.Private.Windows.Core/src/System/Private/Windows/CoreAppContextSwitches.cs
  5. 1
      src/System.Windows.Forms.Analyzers/src/System/Windows/Forms/Analyzers/Diagnostics/DiagnosticIDs.cs
  6. 2
      src/System.Windows.Forms/PublicAPI.Unshipped.txt
  7. 115
      src/System.Windows.Forms/System/Windows/Forms/OLE/DropTarget.cs
  8. 36
      src/System.Windows.Forms/System/Windows/Forms/OLE/IAsyncDropTarget.cs

1
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. |

7
src/Common/tests/TestUtilities/AppContextSwitchNames.cs

@ -33,4 +33,11 @@ public static class AppContextSwitchNames
/// </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
src/System.Private.Windows.Core/src/NativeMethods.txt

@ -150,6 +150,7 @@ HRGN
HWND
HWND_*
IDataObject
IDataObjectAsyncCapability
IDI_*
IDispatchEx
IDragSourceHelper2

30
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 @@ internal static class CoreAppContextSwitches
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 @@ internal static class CoreAppContextSwitches
[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);
}
}

1
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";
}

2
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

115
src/System.Windows.Forms/System/Windows/Forms/OLE/DropTarget.cs

@ -179,24 +179,121 @@ internal unsafe class DropTarget : OleIDropTarget.Interface, IManagedWrapper<Ole
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;
}

36
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;
/// <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);
}
Loading…
Cancel
Save