Browse Source

Implement DataFormat.CreateInProcessFormat<T>() (#21076)

* test: add DataFormat.CreateInProcessFormat tests

Cover the new InProcess format API: kind, identifier, null/empty
validation, non-ASCII identifiers, HasSystemName for all four kinds,
ToSystemName throwing, equality/inequality, DataTransferItem
integration, and coexistence with other formats in DataTransfer.

* feat: implement DataFormat.CreateInProcessFormat<T> for in-process drag/drop

Add DataFormatKind.InProcess and DataFormat.CreateInProcessFormat<T>()
so users can pass arbitrary object references during in-process
drag-and-drop without crossing serialization boundaries.

- Add HasSystemName property to indicate whether ToSystemName() is valid
- Update ToSystemName to throw for InProcess (same as Universal)
- Guard all 7 platform backends (Win32, macOS, X11, Android, Browser,
  iOS) to skip InProcess formats during clipboard/drag-drop enumeration

Closes #20097

* fix: remove HasSystemName API per review feedback
release/12.0.1
Nathan Nguyen 6 months ago
committed by Julien Lebosquain
parent
commit
162de6d7cf
  1. 4
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  2. 19
      src/Avalonia.Base/Input/DataFormat.cs
  3. 14
      src/Avalonia.Base/Input/DataFormatKind.cs
  4. 5
      src/Avalonia.Base/Input/DataFormatOfT.cs
  5. 4
      src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs
  6. 3
      src/Avalonia.X11/Clipboard/X11Clipboard.cs
  7. 3
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  8. 3
      src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs
  9. 3
      src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs
  10. 110
      tests/Avalonia.Base.UnitTests/Input/DataFormatTests.cs

4
src/Android/Avalonia.Android/Platform/ClipboardImpl.cs

@ -39,6 +39,7 @@ namespace Avalonia.Android.Platform
return;
var mimeTypes = dataTransfer.Formats
.Where(f => f.Kind != DataFormatKind.InProcess)
.Select(AndroidDataFormatHelper.DataFormatToMimeType)
.ToArray();
@ -80,6 +81,9 @@ namespace Avalonia.Android.Platform
// Create the item from the first format returning a supported value.
foreach (var dataFormat in item.Formats)
{
if (dataFormat.Kind == DataFormatKind.InProcess)
continue;
hasFormats = true;
if (DataFormat.Text.Equals(dataFormat))

19
src/Avalonia.Base/Input/DataFormat.cs

@ -62,7 +62,7 @@ public abstract class DataFormat : IEquatable<DataFormat>
{
DataFormatKind.Application => applicationPrefix + Identifier,
DataFormatKind.Platform => Identifier,
_ => throw new InvalidOperationException($"Cannot get system name for universal format {Identifier}")
_ => throw new InvalidOperationException($"Cannot get system name for {Kind} format {Identifier}")
};
}
@ -137,6 +137,23 @@ public abstract class DataFormat : IEquatable<DataFormat>
public static DataFormat<string> CreateStringApplicationFormat(string identifier)
=> CreateApplicationFormat<string>(identifier);
/// <summary>
/// Creates a new format that stays within the current process and is never serialized to a platform clipboard or drag-and-drop operation.
/// </summary>
/// <typeparam name="T">The data type. Can be any reference type.</typeparam>
/// <param name="identifier">
/// The format identifier. This value is only used for equality comparisons within the process
/// and is never passed to the underlying platform.
/// </param>
/// <returns>A new <see cref="DataFormat{T}"/>.</returns>
public static DataFormat<T> CreateInProcessFormat<T>(string identifier)
where T : class
{
ThrowHelper.ThrowIfNullOrEmpty(identifier);
return new(DataFormatKind.InProcess, identifier);
}
private static DataFormat<T> CreateApplicationFormat<T>(string identifier)
where T : class
{

14
src/Avalonia.Base/Input/DataFormatKind.cs

@ -42,5 +42,17 @@ public enum DataFormatKind
/// It is not possible to create such a format directly.
/// </para>
/// </summary>
Universal
Universal,
/// <summary>
/// <para>
/// The data format is only usable within the current process.
/// It never crosses process or serialization boundaries.
/// </para>
/// <para>
/// Such a format is created using <see cref="DataFormat.CreateInProcessFormat{T}"/>.
/// </para>
/// </summary>
/// <seealso cref="DataFormat.CreateInProcessFormat{T}"/>
InProcess
}

5
src/Avalonia.Base/Input/DataFormatOfT.cs

@ -10,8 +10,9 @@ namespace Avalonia.Input;
/// This class cannot be instantiated directly.
/// Use universal formats such as <see cref="DataFormat.Text"/> and <see cref="DataFormat.File"/>,
/// or create custom formats using <see cref="DataFormat.CreateBytesApplicationFormat"/>,
/// <see cref="DataFormat.CreateStringApplicationFormat"/>, <see cref="DataFormat.CreateBytesPlatformFormat"/>
/// or <see cref="DataFormat.CreateStringPlatformFormat"/>.
/// <see cref="DataFormat.CreateStringApplicationFormat"/>, <see cref="DataFormat.CreateBytesPlatformFormat"/>,
/// <see cref="DataFormat.CreateStringPlatformFormat"/>,
/// or <see cref="DataFormat.CreateInProcessFormat{T}"/>.
/// </remarks>
[SuppressMessage("ReSharper", "UnusedTypeParameter", Justification = "Used to resolve typed overloads.")]
public sealed class DataFormat<T> : DataFormat

4
src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs

@ -18,7 +18,7 @@ internal sealed class DataTransferItemToAvnClipboardDataItemWrapper(IDataTransfe
private readonly IDataTransferItem _item = item;
IAvnStringArray IAvnClipboardDataItem.ProvideFormats()
=> new AvnStringArray(_item.Formats.Select(ClipboardDataFormatHelper.ToNativeFormat));
=> new AvnStringArray(_item.Formats.Where(f => f.Kind != DataFormatKind.InProcess).Select(ClipboardDataFormatHelper.ToNativeFormat));
IAvnClipboardDataValue? IAvnClipboardDataItem.GetValue(string format)
{
@ -63,6 +63,8 @@ internal sealed class DataTransferItemToAvnClipboardDataItemWrapper(IDataTransfe
for (var i = 0; i < count; i++)
{
var format = formats[i];
if (format.Kind == DataFormatKind.InProcess)
continue;
if (ClipboardDataFormatHelper.ToNativeFormat(format) == nativeFormat)
return format;
}

3
src/Avalonia.X11/Clipboard/X11Clipboard.cs

@ -253,6 +253,9 @@ namespace Avalonia.X11.Clipboard
{
foreach (var format in dataTransfer.Formats)
{
if (format.Kind == DataFormatKind.InProcess)
continue;
foreach (var atom in ClipboardDataFormatHelper.ToAtoms(format, _textAtoms, _x11.Atoms))
atoms.Add(atom);
}

3
src/Browser/Avalonia.Browser/ClipboardImpl.cs

@ -40,6 +40,9 @@ internal sealed class ClipboardImpl : IClipboardImpl
{
foreach (var format in dataTransferItem.Formats)
{
if (format.Kind == DataFormatKind.InProcess)
continue;
var formatString = ToBrowserFormat(format);
if (!IsClipboardFormatSupported(formatString))
continue;

3
src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs

@ -193,6 +193,9 @@ internal class DataTransferToOleDataObjectWrapper(IDataTransfer dataTransfer)
foreach (var dataFormat in DataTransfer.Formats)
{
if (dataFormat.Kind == DataFormatKind.InProcess)
continue;
if (DataFormat.Bitmap.Equals(dataFormat))
{
// We add extra formats for bitmaps

3
src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs

@ -65,6 +65,9 @@ internal sealed class ClipboardImpl(UIPasteboard pasteboard)
foreach (var dataFormat in dataTransferItem.Formats)
{
if (dataFormat.Kind == DataFormatKind.InProcess)
continue;
var data = await TryGetFoundationDataAsync(dataTransferItem, dataFormat);
if (data is null)
continue;

110
tests/Avalonia.Base.UnitTests/Input/DataFormatTests.cs

@ -0,0 +1,110 @@
using System;
using Avalonia.Input;
using Xunit;
namespace Avalonia.Base.UnitTests.Input;
public sealed class DataFormatTests
{
[Fact]
public void CreateInProcessFormat_Returns_Format_With_InProcess_Kind()
{
var format = DataFormat.CreateInProcessFormat<string>("my-format");
Assert.Equal(DataFormatKind.InProcess, format.Kind);
}
[Fact]
public void CreateInProcessFormat_Returns_Format_With_Correct_Identifier()
{
var format = DataFormat.CreateInProcessFormat<string>("my-format");
Assert.Equal("my-format", format.Identifier);
}
[Fact]
public void CreateInProcessFormat_Throws_On_Null_Identifier()
{
Assert.Throws<ArgumentNullException>(() => DataFormat.CreateInProcessFormat<string>(null!));
}
[Fact]
public void CreateInProcessFormat_Throws_On_Empty_Identifier()
{
Assert.Throws<ArgumentException>(() => DataFormat.CreateInProcessFormat<string>(string.Empty));
}
[Fact]
public void CreateInProcessFormat_Allows_Non_ASCII_Identifiers()
{
var format = DataFormat.CreateInProcessFormat<string>("日本語フォーマット");
Assert.Equal("日本語フォーマット", format.Identifier);
}
[Fact]
public void ToSystemName_Throws_For_InProcess()
{
var format = DataFormat.CreateInProcessFormat<string>("test");
Assert.Throws<InvalidOperationException>(() => format.ToSystemName("prefix."));
}
[Fact]
public void InProcess_Format_Equality_Same_Identifier()
{
var format1 = DataFormat.CreateInProcessFormat<string>("my-format");
var format2 = DataFormat.CreateInProcessFormat<string>("my-format");
Assert.Equal(format1, format2);
Assert.True(format1 == format2);
}
[Fact]
public void InProcess_Format_Inequality_Different_Identifier()
{
var format1 = DataFormat.CreateInProcessFormat<string>("format-a");
var format2 = DataFormat.CreateInProcessFormat<string>("format-b");
Assert.NotEqual(format1, format2);
Assert.True(format1 != format2);
}
[Fact]
public void InProcess_Format_Inequality_Different_Kind_Same_Identifier()
{
var inProcess = DataFormat.CreateInProcessFormat<string>("test-format");
var application = DataFormat.CreateStringApplicationFormat("test-format");
Assert.NotEqual<DataFormat>(inProcess, application);
}
[Fact]
public void InProcess_Format_Works_With_DataTransferItem_Set_And_Get()
{
var format = DataFormat.CreateInProcessFormat<string>("my-inprocess");
var item = new DataTransferItem();
item.Set(format, "hello");
var value = item.TryGetValue(format);
Assert.Equal("hello", value);
}
[Fact]
public void InProcess_Format_Coexists_With_Other_Formats_In_DataTransfer()
{
var inProcessFormat = DataFormat.CreateInProcessFormat<string>("my-inprocess");
var item = new DataTransferItem();
item.SetText("plain text");
item.Set(inProcessFormat, "in-process data");
var dataTransfer = new DataTransfer();
dataTransfer.Add(item);
Assert.Contains(DataFormat.Text, dataTransfer.Formats);
Assert.Contains(inProcessFormat, (System.Collections.Generic.IEnumerable<DataFormat>)dataTransfer.Formats);
Assert.Equal("plain text", item.TryGetValue(DataFormat.Text));
Assert.Equal("in-process data", item.TryGetValue(inProcessFormat));
}
}
Loading…
Cancel
Save