-
Notifications
You must be signed in to change notification settings - Fork 834
feat(win32): add support IDataObjectAsyncCapability Drag&Drop operations #22182
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
base: master
Are you sure you want to change the base?
feat(win32): add support IDataObjectAsyncCapability Drag&Drop operations #22182
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for IDataObjectAsyncCapability to enable asynchronous drag-and-drop operations on Win32 platforms, specifically for file drop (HDROP) operations. The feature addresses scenarios where file information becomes available asynchronously during drag-and-drop, such as when dragging files from cloud storage providers.
Key Changes:
- Refactored
Win32ClipboardExtension.GetFileDropList()to return a list instead of directly modifying the DataPackage, enabling reuse in async scenarios - Implemented async HDROP handling through a new
AsyncHDropHandlerclass that usesIDataObjectAsyncCapabilityAPIs - Added support for querying and displaying custom clipboard format names for improved logging
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 13 comments.
| File | Description |
|---|---|
src/Uno.UI.Runtime.Skia.Win32/ApplicationMode/DataTransfer/Win32ClipboardExtension.cs |
Refactored GetFileDropList to return List<IStorageItem>? instead of void, enabling reuse for async file drop operations |
src/Uno.UI.Runtime.Skia.Win32/ApplicationMode/DataTransfer/DragDrop/Win32DragDropExtension.cs |
Added async capability support with TryHandleAsyncHDrop method, AsyncHDropHandler class, and lifecycle management for async file drops |
src/Uno.UI.Runtime.Skia.Win32.Support/NativeMethods.txt |
Added IDataObjectAsyncCapability interface and GetClipboardFormatName function to native method declarations |
| _lastAsyncHDropHandler?.Drop(dataObject); | ||
| *pdwEffect = (DROPEFFECT)_manager.ProcessReleased(src); | ||
| if (_lastAsyncHDropHandler != null) | ||
| { | ||
| _lastAsyncHDropHandler.DropEffect = *pdwEffect; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Race condition: The DropEffect property is being set after ProcessReleased is called (line 293), but the AsyncHDropHandler's thread may already be executing and calling EndOperation with the old value (DROPEFFECT_NONE from line 526). The thread starts executing as soon as GetData succeeds (line 557-590), and line 296 sets DropEffect afterward. This timing issue could cause the wrong drop effect to be reported. Consider setting DropEffect before calling Drop(), or redesigning the flow to pass the effect as a parameter to Drop().
| _lastAsyncHDropHandler?.Drop(dataObject); | |
| *pdwEffect = (DROPEFFECT)_manager.ProcessReleased(src); | |
| if (_lastAsyncHDropHandler != null) | |
| { | |
| _lastAsyncHDropHandler.DropEffect = *pdwEffect; | |
| var effect = (DROPEFFECT)_manager.ProcessReleased(src); | |
| *pdwEffect = effect; | |
| if (_lastAsyncHDropHandler != null) | |
| { | |
| _lastAsyncHDropHandler.DropEffect = effect; | |
| _lastAsyncHDropHandler.Drop(dataObject); |
| private unsafe bool TryhandleAsyncHDrop(IDataObject* dataObject, FORMATETC[] formatEtcList, DataPackage package) | ||
| { | ||
| var formatEtcNullable = formatEtcList.Cast<FORMATETC?>().FirstOrDefault(f => f!.Value.cfFormat == (int)CLIPBOARD_FORMAT.CF_HDROP, null); | ||
| if (formatEtcNullable is null) | ||
| { | ||
| return false; | ||
| } | ||
| var formatEtc = formatEtcNullable.Value; | ||
|
|
||
| HRESULT hResult3; | ||
| var localDataObject = dataObject; | ||
| fixed (Guid* guidPtr = &_asyncCapabilityGuid) | ||
| { | ||
| hResult3 = dataObject->QueryInterface(guidPtr, (void**)&localDataObject); | ||
| } | ||
| var asyncCapability = (IDataObjectAsyncCapability*)localDataObject; | ||
| if (!hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded && !isAsync) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| _lastAsyncHDropHandler = new AsyncHDropHandler(formatEtc); | ||
| package.SetDataProvider(StandardDataFormats.StorageItems, DelayRenderer); | ||
| return true; | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new IDataObjectAsyncCapability support lacks test coverage. While the repository has existing drag-and-drop tests (e.g., in Uno.UI.RuntimeTests), none of these tests cover the new async file drop functionality. Consider adding runtime tests that verify: (1) async drag-and-drop operations with IDataObjectAsyncCapability, (2) proper handling of the async handler lifecycle, (3) correct behavior when dropping files asynchronously, and (4) error handling in the async path.
| // NewMethod(dataObject); | ||
|
|
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This commented-out method call should be removed. Dead or commented-out code should not be committed to the codebase as it reduces code clarity and maintainability.
| // NewMethod(dataObject); |
| hResult3 = dataObject->QueryInterface(guidPtr, (void**)&localDataObject); | ||
| } | ||
| var asyncCapability = (IDataObjectAsyncCapability*)localDataObject; | ||
| if (!hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded && !isAsync) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The operator precedence in this condition is incorrect. The expression evaluates as '!(hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded) && !isAsync' due to operator precedence, which is likely not the intended logic. The condition should be properly parenthesized to check: (1) if QueryInterface failed, OR (2) if GetAsyncMode failed, OR (3) if async mode is disabled. Consider rewriting as: '!hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded || !isAsync'
| if (!hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded && !isAsync) | |
| if (!hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded || !isAsync) |
| new Thread(() => | ||
| { | ||
| using var _2 = Disposable.Create(() => | ||
| { | ||
| PInvoke.ReleaseStgMedium(ref hdropMedium); | ||
| asyncCapability->EndOperation(HRESULT.S_OK, null, (uint)DropEffect); | ||
| dispose = false; | ||
| }); | ||
|
|
||
| var files = Win32ClipboardExtension.GetFileDropList(hdropMedium.u.hGlobal); | ||
| if (files is null) | ||
| { | ||
| tcs.SetException(new InvalidOperationException("Failed to retrieve file drop list from HDROP.")); | ||
| } | ||
| else | ||
| { | ||
| tcs.SetResult(files); | ||
| } | ||
| }).Start(); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a bare Thread using 'new Thread().Start()' is not recommended in modern .NET code. This creates an unmanaged thread that doesn't participate in the ThreadPool and can lead to resource management issues. Consider using Task.Run or ThreadPool.QueueUserWorkItem instead, which provides better resource management and integration with the async infrastructure. If a dedicated thread is truly required, document why this is necessary.
| private static unsafe void NewMethod(IDataObject* dataObject) | ||
| { | ||
| IntPtr ptr = 0; | ||
| Guid asyncCapabilityGuid = new Guid(0x3D8B0590, 0xF691, 0x11D2, 0x8E, 0xA9, 0x00, 0x60, 0x97, 0xDF, 0x5B, 0xD4); | ||
| HRESULT hResult3 = dataObject->QueryInterface(&asyncCapabilityGuid, (void**)&ptr); | ||
| var asyncCapability = (IDataObjectAsyncCapability*)ptr; | ||
| if (hResult3.Succeeded && asyncCapability is not null && asyncCapability->GetAsyncMode(out BOOL isAsync).Succeeded && isAsync) | ||
| { | ||
| if (asyncCapability->StartOperation().Succeeded) | ||
| { | ||
| var hdropFormat = new FORMATETC() | ||
| { | ||
| cfFormat = (ushort)CLIPBOARD_FORMAT.CF_HDROP, | ||
| dwAspect = 1, | ||
| lindex = -1, | ||
| ptd = null, | ||
| tymed = (uint)TYMED.TYMED_HGLOBAL | ||
| }; | ||
| STGMEDIUM hdropMedium; | ||
| while (!dataObject->GetData(hdropFormat, out hdropMedium).Succeeded) | ||
| { | ||
| } | ||
|
|
||
| var files = Win32ClipboardExtension.GetFileDropList(hdropMedium.u.hGlobal); | ||
| asyncCapability->EndOperation(HRESULT.S_OK, null, (uint)DROPEFFECT.DROPEFFECT_COPY); | ||
| } | ||
| } | ||
| } | ||
|
|
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method appears to be unused dead code that should be removed. It's not called anywhere in the codebase (line 290 shows it's commented out), and it duplicates logic that's now properly implemented in the AsyncHDropHandler class. Keeping this code reduces maintainability and could cause confusion.
| private static unsafe void NewMethod(IDataObject* dataObject) | |
| { | |
| IntPtr ptr = 0; | |
| Guid asyncCapabilityGuid = new Guid(0x3D8B0590, 0xF691, 0x11D2, 0x8E, 0xA9, 0x00, 0x60, 0x97, 0xDF, 0x5B, 0xD4); | |
| HRESULT hResult3 = dataObject->QueryInterface(&asyncCapabilityGuid, (void**)&ptr); | |
| var asyncCapability = (IDataObjectAsyncCapability*)ptr; | |
| if (hResult3.Succeeded && asyncCapability is not null && asyncCapability->GetAsyncMode(out BOOL isAsync).Succeeded && isAsync) | |
| { | |
| if (asyncCapability->StartOperation().Succeeded) | |
| { | |
| var hdropFormat = new FORMATETC() | |
| { | |
| cfFormat = (ushort)CLIPBOARD_FORMAT.CF_HDROP, | |
| dwAspect = 1, | |
| lindex = -1, | |
| ptd = null, | |
| tymed = (uint)TYMED.TYMED_HGLOBAL | |
| }; | |
| STGMEDIUM hdropMedium; | |
| while (!dataObject->GetData(hdropFormat, out hdropMedium).Succeeded) | |
| { | |
| } | |
| var files = Win32ClipboardExtension.GetFileDropList(hdropMedium.u.hGlobal); | |
| asyncCapability->EndOperation(HRESULT.S_OK, null, (uint)DROPEFFECT.DROPEFFECT_COPY); | |
| } | |
| } | |
| } |
| return HRESULT.S_OK; | ||
| } | ||
|
|
||
| private unsafe bool TryhandleAsyncHDrop(IDataObject* dataObject, FORMATETC[] formatEtcList, DataPackage package) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Method name has incorrect casing. Should be 'TryHandleAsyncHDrop' following PascalCase convention with proper capitalization of 'Handle'.
| private unsafe bool TryhandleAsyncHDrop(IDataObject* dataObject, FORMATETC[] formatEtcList, DataPackage package) | |
| private unsafe bool TryHandleAsyncHDrop(IDataObject* dataObject, FORMATETC[] formatEtcList, DataPackage package) |
| const int attempts = 100; | ||
| for (int i = 0; i < attempts; i++) |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number '100' for retry attempts lacks explanation. This value determines how many times the code will poll for HDROP data before giving up, but there's no indication of why 100 attempts is appropriate or what the expected timing is. Consider extracting this to a named constant with a descriptive comment explaining the rationale, or better yet, implement a timeout-based approach instead of a fixed retry count.
| const int attempts = 100; | |
| for (int i = 0; i < attempts; i++) | |
| // Limit the number of synchronous polling attempts for HDROP data to avoid a potential | |
| // infinite loop if the async drag source never provides the data. The value 100 preserves | |
| // existing behavior while providing a reasonable upper bound on retries. | |
| const int MaxHdropRetrievalAttempts = 100; | |
| for (int i = 0; i < MaxHdropRetrievalAttempts; i++) |
| for (int i = 0; i < attempts; i++) | ||
| { | ||
| if (dataObject->GetData(hdropFormat, out hdropMedium).Succeeded) | ||
| { | ||
| success = true; | ||
| break; | ||
| } | ||
| } |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tight polling loop with no delay between attempts could consume excessive CPU resources. The loop repeatedly calls GetData until it succeeds (up to 100 times) without any sleep or delay. Consider adding a small delay (e.g., Thread.Sleep or Task.Delay) between attempts to avoid spinning the CPU, or implement a more efficient waiting mechanism.
| if (_lastAsyncHDropHandler is null) | ||
| { | ||
| throw new InvalidOperationException("Async HDrop handler is called too late."); | ||
| } | ||
|
|
||
| return await _lastAsyncHDropHandler.Task; |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DelayRenderer method accepts a CancellationToken parameter but never uses it. Either the cancellation token should be respected by checking for cancellation and propagating it to the underlying async operation, or the parameter should be removed if cancellation is not supported. Not respecting cancellation tokens can lead to resource leaks and unresponsive UI.
| if (_lastAsyncHDropHandler is null) | |
| { | |
| throw new InvalidOperationException("Async HDrop handler is called too late."); | |
| } | |
| return await _lastAsyncHDropHandler.Task; | |
| ct.ThrowIfCancellationRequested(); | |
| var handler = _lastAsyncHDropHandler; | |
| if (handler is null) | |
| { | |
| throw new InvalidOperationException("Async HDrop handler is called too late."); | |
| } | |
| return await handler.Task.WaitAsync(ct).ConfigureAwait(false); |
GitHub Issue: closes https://github.com/unoplatform/kahua-private/issues/390
PR Type:
What is the current behavior? π€
What is the new behavior? π
PR Checklist β
Please check if your PR fulfills the following requirements:
Screenshots Compare Test Runresults.Other information βΉοΈ