Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@ramezgerges
Copy link
Contributor

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:

Other information ℹ️

Copilot AI review requested due to automatic review settings December 19, 2025 01:18
Copy link
Contributor

Copilot AI left a 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 AsyncHDropHandler class that uses IDataObjectAsyncCapability APIs
  • 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

Comment on lines +292 to +296
_lastAsyncHDropHandler?.Drop(dataObject);
*pdwEffect = (DROPEFFECT)_manager.ProcessReleased(src);
if (_lastAsyncHDropHandler != null)
{
_lastAsyncHDropHandler.DropEffect = *pdwEffect;
Copy link

Copilot AI Dec 19, 2025

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().

Suggested change
_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);

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +214
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;
}
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +290 to +291
// NewMethod(dataObject);

Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
// NewMethod(dataObject);

Copilot uses AI. Check for mistakes.
hResult3 = dataObject->QueryInterface(guidPtr, (void**)&localDataObject);
}
var asyncCapability = (IDataObjectAsyncCapability*)localDataObject;
if (!hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded && !isAsync)
Copy link

Copilot AI Dec 19, 2025

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'

Suggested change
if (!hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded && !isAsync)
if (!hResult3.Succeeded || !asyncCapability->GetAsyncMode(out var isAsync).Succeeded || !isAsync)

Copilot uses AI. Check for mistakes.
Comment on lines +572 to +590
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();
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +254
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);
}
}
}

Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
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 uses AI. Check for mistakes.
return HRESULT.S_OK;
}

private unsafe bool TryhandleAsyncHDrop(IDataObject* dataObject, FORMATETC[] formatEtcList, DataPackage package)
Copy link

Copilot AI Dec 19, 2025

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

Suggested change
private unsafe bool TryhandleAsyncHDrop(IDataObject* dataObject, FORMATETC[] formatEtcList, DataPackage package)
private unsafe bool TryHandleAsyncHDrop(IDataObject* dataObject, FORMATETC[] formatEtcList, DataPackage package)

Copilot uses AI. Check for mistakes.
Comment on lines +554 to +555
const int attempts = 100;
for (int i = 0; i < attempts; i++)
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
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++)

Copilot uses AI. Check for mistakes.
Comment on lines +555 to +562
for (int i = 0; i < attempts; i++)
{
if (dataObject->GetData(hdropFormat, out hdropMedium).Succeeded)
{
success = true;
break;
}
}
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +223
if (_lastAsyncHDropHandler is null)
{
throw new InvalidOperationException("Async HDrop handler is called too late.");
}

return await _lastAsyncHDropHandler.Task;
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants