Avalonia icon indicating copy to clipboard operation
Avalonia copied to clipboard

New DataTransfer API does not allow passing of object references

Open pixsperdavid opened this issue 1 month ago • 7 comments

Describe the bug

The updated DataTransfer (replacing DataObject) makes it impossible to pass arbitrary object references. This is very much required for internal Drag and Drop.

To Reproduce

The current DataFormat constructor is protected, and the currently available factory methods only allow formats of either string, byte[], IStorageItem or Bitmap types. This makes it impossible to create a custom DataFormat for an arbitrary class or struct type.

Expected behavior

Either the DataFormat constructor should be unprotected, or a new generic factory method should be created.

Avalonia version

11.3.9

OS

No response

Additional context

No response

pixsperdavid avatar Nov 19 '25 11:11 pixsperdavid

This is actually by design.

The first iteration of the feature allowed arbitrary types to be defined for DataFormat.

However, a common pain point is that people are expecting those formats to be placed onto the system clipboard or platform drag/drop object and to round-trip properly. It's how it worked in the past in WPF, with transparent serialization through BinaryFormatter.

Instead, we want to only accept formats we know are valid, with the serialization being controlled by the user (except for universal formats that are handled by Avalonia itself).

That being said, the in-process drag-and-drop is a valid scenario, and that's not the first time it's coming up. Note that it is possible right now: you need to define your own IDataTransfer implementation and store whatever you want there without using the DataFormat capabilities.

We might consider adding a new DataFormat.CreateInProcessFormat<T>() that makes it clear that the specified data format won't be placed onto the system clipboard and won't be available through cross-process drag-and-drop.

MrJul avatar Nov 19 '25 11:11 MrJul

You can create a byte array containing the data you need like this:

var bytes = BitConverter.GetBytes(Environment.ProcessId).Concat(BitConverter.GetBytes(GCHandle.ToIntPtr(handle))).ToArray();

Then read it back like this:

if (BitConverter.ToInt32(bytes, 0) != Environment.ProcessId)
{
	return;
}

var handle = GCHandle.FromIntPtr((nint) BitConverter.ToInt64(bytes, sizeof(int)));
if (handle is { IsAllocated: true, Target: MyType myValue })
{
	// do stuff
}

The tricky part is knowing when to free the GCHandle. For a drag/drop this is easy, but if you are copying to the clipboard then the scope is less clear.

TomEdwardsEnscape avatar Nov 20 '25 12:11 TomEdwardsEnscape

Bug or By Design ? In any case, useful functionality of in-app DnD for any object is gone...

Will we get an update before the old API is removed ?

LaurentInSeattle avatar Nov 20 '25 18:11 LaurentInSeattle

Please don't pass GC handle pointers around when it's possible to just define a custom type.

In any case, useful functionality of in-app DnD for any object is gone...

It isn't gone, see my reply above:

Note that it is possible right now: you need to define your own IDataTransfer implementation and store whatever you want there without using the DataFormat capabilities.

Example:

<StackPanel>

  <Border Width="100" Height="20" Background="DodgerBlue" PointerPressed="OnDragPointerPressed">
    <TextBlock Text="Drag" />
  </Border>

  <Border Width="100" Height="20" Background="OrangeRed" DragDrop.AllowDrop="True" DragDrop.Drop="OnDrop">
    <TextBlock Text="Drop" />
  </Border>

</StackPanel>
private void OnDragPointerPressed(object? sender, PointerPressedEventArgs e)
{
    var data = new CustomData { Foo = "Bar" };
    DragDrop.DoDragDropAsync(e, new CustomDataTransfer { CustomData = data }, DragDropEffects.Copy);
}

private void OnDrop(object? sender, DragEventArgs e)
{
    var customData = (e.DataTransfer as CustomDataTransfer)?.CustomData;
    Console.WriteLine($"Foo is {customData?.Foo}");
}

class CustomDataTransfer : IDataTransfer
{
    public CustomData? CustomData { get; set; }

    IReadOnlyList<DataFormat> IDataTransfer.Formats => [];
    IReadOnlyList<IDataTransferItem> IDataTransfer.Items => [];
    void IDisposable.Dispose() { }
}

class CustomData
{
    public string? Foo { get; set; }
}

Could it be better? Yes, of course, the code above isn't very intuitive and that's why I'm proposing adding DataFormat.CreateInProcessFormat<T>(). However, no scenario should be blocked.

MrJul avatar Nov 20 '25 18:11 MrJul

It's counter-intuitive that CustomDataTransfer implements an interface but does nothing at all with any of its properties or methods. An IDataTransfer which provided no formats or items sounds like it would be invalid and have no effect, and the interface's XML docs support this idea.

If this behaviour were wrapped up in the proposed CreateInProcessFormat then of course there would be no confusion. I assume that the generic type for this method would be unconstrained?

TomEdwardsEnscape avatar Nov 20 '25 21:11 TomEdwardsEnscape

I assume that the generic type for this method would be unconstrained

That's correct. There's no reason to constrain it if it doesn't cross process boundaries.

MrJul avatar Nov 21 '25 08:11 MrJul

Notes from the API review meeting: The API has been approved:

 class DataFormat
 {
+    public static DataFormat<T> CreateInProcessFormat<T>(string identifier) where T : class;
 }

 public enum DataFormatKind
 {
+    InProcess
 }

Note: T would still be constrained to class because DataFormat<T> requires it.

MrJul avatar Nov 26 '25 09:11 MrJul