作者:夏群林 原创 2025.6.9

拖放的实现,和其他的 GestureRecognizer 不同,需要 DragGestureRecognizer 与 DropGestureRecognizer 相互配合,Drag / Drop 又是在不同的控件上附加的,数据传输和配置相对复杂,不太好理解。需要彻底阅读源代码,才能真的把握。我做了一个扩展方法,把复杂的配置包裹起来,在代码层面与要附加拖放功能的控件分离,用户只需关注拖放动作所支持的业务功能即可。

直接上代码。

一、核心架构与关键组件

1. 数据载体:DragDropPayload<TView>

解耦控件与业务逻辑,封装拖放所需的视图引用、附加数据和回调逻辑。

public interface IDragDropPayload
{
public View View { get; } // 拖放源/目标控件
public object? Affix { get; } // 任意附加数据(如文本、对象)
public Action? Callback { get; } // 拖放完成后的回调
}
public class DragDropPayload<TView> : IDragDropPayload where TView : View
{
public required TView View { get; init; }
public object? Affix { get; init; }
public Action? Callback { get; init; }
View IDragDropPayload.View => View;
}

关键点

  • View:强类型视图引用,确保拖放操作与具体控件绑定。
  • Affix:支持传递复杂数据,用于拖和放时,对源控件和目标控件进行处理所需附加的数据。 默认为 null。
  • Callback:用于执行拖放后的轻量化操作(如日志记录、UI 微更新),对源控件和目标控件分别处理。可得到 Affix 数据支持。默认为 null。即不处理。
  • 设计 IDragDropPayload 公共接口,配置协变,是本扩展方法保持精干而又多面的关键。

2. 消息传递:DragDropMessage<TSource, TTarget>

通过泛型消息明确拖放类型,实现跨层业务逻辑解耦。 这里也配置了协变,便于 WeakReferenceMessenger 引用。使用反射权衡后的妥协。

public interface IDragDropMessage
{
public IDragDropPayload SourcePayload { get; }
public IDragDropPayload TargetPayload { get; }
} public sealed class DragDropMessage<TSource, TTarget> : IDragDropMessage
where TSource : View
where TTarget : View
{
public required DragDropPayload<TSource> SourcePayload { get; init; }
public required DragDropPayload<TTarget> TargetPayload { get; init; } IDragDropPayload IDragDropMessage.SourcePayload => SourcePayload;
IDragDropPayload IDragDropMessage.TargetPayload => TargetPayload;
}

关键点

  • 类型安全:通过 TSourceTTarget 约束拖放的源/目标类型(如 LabelBorder)。
  • 数据透传:通过 DataPackagePropertySet 传递扩展属性,避免消息类字段膨胀。
  • 解耦业务:消息仅负责数据传递,具体逻辑由订阅者(如 MainPage)处理。

3. AsDraggable<TSource> 扩展方法

通过扩展方法为任意控件注入拖放能力,屏蔽手势识别细节。

    public static void AsDraggable<TSource>(this TSource source, object? sourceAffix = null, Action? sourceCallback = null)
where TSource : View
{
// 创建并存储 payload
var payload = new DragDropPayload<TSource>
{
View = source,
Affix = sourceAffix,
Callback = sourceCallback
}; // 覆盖现有 payload(如果存在)
dragPayloads.AddOrUpdate(source, payload); // 查找或创建 DragGestureRecognizer
var dragGesture = source.GestureRecognizers.OfType<DragGestureRecognizer>().FirstOrDefault();
if (dragGesture == null)
{
dragGesture = new DragGestureRecognizer { CanDrag = true };
source.GestureRecognizers.Add(dragGesture); // 只在首次添加手势时注册事件
dragGesture.DragStarting += (sender, args) =>
{
// 通过 dragPayloads 提取最新的 payload
if (dragPayloads.TryGetValue(source, out var dragPayload) && dragPayload is DragDropPayload<TSource> payload)
{
args.Data.Properties.Add("SourcePayload", payload);
source.Opacity = 0.5;
}
};
}
}

4. AsDroppable<TSource, TTarget> 扩展方法

public static void AsDroppable<TTarget>(this TTarget target, object? targetAffix = null, Action? targetCallback = null)
where TTarget : View
{
AsDroppable<View, TTarget>(target, targetAffix, targetCallback);
} public static void AsDroppable<TSource, TTarget>(this TTarget target, object? targetAffix = null, Action? targetCallback = null)
where TSource : View
where TTarget : View
{
var dropGesture = target.GestureRecognizers.OfType<DropGestureRecognizer>().FirstOrDefault();
if (dropGesture is null)
{
dropGesture = new DropGestureRecognizer() { AllowDrop = true };
target.GestureRecognizers.Add(dropGesture); DragDropPayload<TTarget> defaultPayload = new()
{
View = target,
Affix = null,
Callback = null
}; _ = dropPayloads
.GetOrCreateValue(dropGesture)
.GetOrAdd(typeof(View).Name, _ => defaultPayload); dropGesture.DragOver += (sender, args) =>
{
bool isSupported = args.Data.Properties.TryGetValue("SourcePayload", out _);
target.BackgroundColor = isSupported ? Colors.LightGreen : Colors.Transparent;
}; dropGesture.DragLeave += (sender, args) =>
{
target.BackgroundColor = Colors.Transparent;
}; dropGesture.Drop += (s, e) => OnDroppablesMessage<TTarget>(target, dropGesture, e);
} DragDropPayload<TTarget> sourceSpecificDropPayload = new()
{
View = target,
Affix = targetAffix,
Callback = targetCallback
}; var payloadDict = dropPayloads.GetOrCreateValue(dropGesture);
_ = payloadDict.AddOrUpdate(typeof(TSource).Name, (s) => sourceSpecificDropPayload, (s, old) => sourceSpecificDropPayload);
}

核心机制

  • 手势识别器:使用 DragGestureRecognizerDropGestureRecognizer 捕获拖放事件。 保持实例唯一。
  • 类型映射表:静态存储器 dragPayloads / dropPayloads 存储可支持的拖、放对象及其附加的数据,保持最新。
  • 消息注册:为每种类型组合注册唯一的消息处理函数,确保消息精准投递。
  • 方法重载:AsDroppable ,无特殊数据和动作附加的,可简化处理,毋须逐一注册类型配对。

二、关键实现细节

1. ConditionalWeakTable

DragDropExtensions 中,我们使用两个 ConditionalWeakTable 实现状态管理,保证拖放事件发生时传递最新约定的数据。

ConditionalWeakTable 最大的好处是避免内存泄漏。用 View 或 GestureRecognizer 实例作为键,当该实例不再被别处引用时,内存回收机制会自动清除对应的键值对,无需用户专门释放内存。

private static readonly ConditionalWeakTable<View, IDragDropPayload> dragPayloads = [];
private static readonly ConditionalWeakTable<GestureRecognizer, ConcurrentDictionary<string, IDragDropPayload>> dropPayloads = [];

2. dropPayloads

为每个 DropGestureRecognizer 关联源类型映射和对应该源类型所预先配置目标类型 TargetPayload。

DragDropPayload<TTarget> sourceSpecificDropPayload = new()
{
View = target,
Affix = targetAffix,
Callback = targetCallback
}; var payloadDict = dropPayloads.GetOrCreateValue(dropGesture);
_ = payloadDict.AddOrUpdate(typeof(TSource).Name, (s) => sourceSpecificDropPayload, (s, old) => sourceSpecificDropPayload);

还贴心地预备好默认配置:

DragDropPayload<TTarget> defaultPayload = new()
{
View = target,
Affix = null,
Callback = null
}; _ = dropPayloads
.GetOrCreateValue(dropGesture)
.GetOrAdd(typeof(View).Name, _ => defaultPayload);

3 . dragPayloads

源类型 SourcePayload 配置表,在 DragGestureRecognizer 首次配置时注册,重复 AsDraggable 方法时更新。

// 创建并存储 payload
var payload = new DragDropPayload<TSource>
{
View = source,
Affix = sourceAffix,
Callback = sourceCallback
}; // 覆盖现有 payload(如果存在)
dragPayloads.AddOrUpdate(source, payload);

4 . IDragDropMessage / WeakReferenceMessenger

反射获取分类拖放消息,但需要统一发送:

// 构建泛型类型
Type genericMessageType = typeof(DragDropMessage<,>);
Type constructedMessageType = genericMessageType.MakeGenericType(sourceType, typeof(TTarget)); // 创建实例
object? message = Activator.CreateInstance(constructedMessageType);
if (message is null)
{
return;
} // 设置属性
PropertyInfo sourceProp = constructedMessageType.GetProperty("SourcePayload")!;
PropertyInfo targetProp = constructedMessageType.GetProperty("TargetPayload")!;
sourceProp.SetValue(message, sourcePayload);
targetProp.SetValue(message, targetPayload); // 核心动作
_ = WeakReferenceMessenger.Default.Send<IDragDropMessage>((IDragDropMessage)message);

三、 反射的优化

尝试了很多办法,还是采用反射技术,最为直接。

我并不喜欢使用反射。消耗大不说,现在 Microsoft 大力推进 Native AOT( Ahead Of Time)编译,将.NET 代码提前编译为本机代码,对反射的使用有约束,如果代码中反射模式导致 AOT 编译器无法静态分析,就会产生裁剪警告,甚至可能导致编译失败或运行时异常。

因此,在 .NET MAUI 的 AOT 编译环境下,对反射泛型类型的创建需要特殊处理。这里通过 预编译委托缓存 + 静态类型注册 的组合方案,实现了AOT 的泛型消息工厂。高效是肯定的,目前看来,是兼容的。

使用 ConcurrentDictionary<string, HashSet<Type>> 存储注册的源类型和目标类型,通过 "Source""Target" 两个键区分不同角色的类型集合, HashSet<Type> 确保类型唯一性,避免重复注册。

private static readonly ConcurrentDictionary<string, HashSet<Type>> registeredTypes = new();

自动配对机制:当新类型注册时,自动与已注册的对立类型(源→目标,目标→源)创建所有可能的配对组合(静态),确保 AOT 环境下反射可用。

private static void RegisterType(string role, Type type)
{
// 获取或创建对应角色的类型集合
var types = registeredTypes.GetOrAdd(role, _ => []); // 添加类型并判断是否为新增(返回true表示新增)
if (types.Add(type))
{
// 新注册的类型,补全所有可能的配对组合
if (role == "Source")
{
// 源类型:与所有已注册的目标类型配对
if (registeredTypes.TryGetValue("Target", out var targetTypes))
{
foreach (var targetType in targetTypes)
{
RegisterMessageFactory(type, targetType);
}
}
}
else if (role == "Target")
{
// 目标类型:与所有已注册的源类型配对
if (registeredTypes.TryGetValue("Source", out var sourceTypes))
{
foreach (var sourceType in sourceTypes)
{
RegisterMessageFactory(sourceType, type);
}
}
}
}
}

反射泛型工厂:每个类型组合仅反射一次,生成的委托被缓存

private static readonly ConcurrentDictionary<(Type source, Type target), Func<IDragDropPayload, IDragDropPayload, IDragDropMessage>> messageFactories = new();

private static void RegisterMessageFactory(Type sourceType, Type targetType)
{
var key = (sourceType, targetType);
messageFactories.GetOrAdd(key, _ => {
// 仅首次执行反射
var messageType = typeof(DragDropMessage<,>).MakeGenericType(sourceType, targetType);
return (sourcePayload, targetPayload) => {
var message = Activator.CreateInstance(messageType)!;
// 设置属性...
return (IDragDropMessage)message;
};
});
}

反射优化策略:后续调用直接执行委托,避免重复反射

// 通过预注册的工厂创建消息实例
var key = (sourceType, typeof(TTarget));
if (messageFactories.TryGetValue(key, out var factory))
{
var message = factory(sourcePayload, targetPayload); // 核心动作
_ = WeakReferenceMessenger.Default.Send<IDragDropMessage>(message);
}

AOT 兼容性保障

预编译委托缓存方案,支持任意类型组合,仅首次注册时有反射开销,平衡灵活性和性能,但需要在编译前静态注册所有可能的类型组合,避免运行时动态生成未知类型组合。

必要的话,可使用 [assembly: Preserve] 属性保留泛型类型及其成员。暂时没采用这种方法,寄希望于 Microsoft 自行保证兼容性。

四、使用示例

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Zhally.DragDrop.MainPage"
Title="拖放示例"> <StackLayout Spacing="20" Padding="30">
<Label Text="高级拖放示例"
FontSize="22"
FontAttributes="Bold"
HorizontalOptions="Center" /> <HorizontalStackLayout
HorizontalOptions="Center">
<Label x:Name="DragLabel"
Text="拖放示例文本"
BackgroundColor="LightBlue"
Padding="12"
HorizontalOptions="Center"
FontSize="16" /> <BoxView x:Name="DragBoxView"
HeightRequest="60"
WidthRequest="120"
BackgroundColor="LightPink"
HorizontalOptions="Center" /> <ContentView x:Name="DragContentView"
HeightRequest="60"
WidthRequest="120"
BackgroundColor="LightCyan"
HorizontalOptions="Center" />
</HorizontalStackLayout> <Border x:Name="DropBorder"
BackgroundColor="LightGreen"
Padding="20"
Margin="10"
HorizontalOptions="Center"
WidthRequest="200"
HeightRequest="100">
<Label Text="放置目标区域" HorizontalOptions="Center" />
</Border> <Label x:Name="ResultLabel"
Text="等待拖放操作..."
HorizontalOptions="Center"
FontAttributes="Italic"
TextColor="Gray" />
</StackLayout>
</ContentPage>

MainPage.xaml.cs

using CommunityToolkit.Mvvm.Messaging;
using System.Diagnostics;
using Zhally.DragDrop.Controls; namespace Zhally.DragDrop; public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
SetupDragDrop();
} private void SetupDragDrop()
{
// 设置可拖动元素(携带 Payload 数据)
DragLabel.AsDraggable<Label>(
sourceAffix: new { Type = "文本数据", Value = "Hello World" },
sourceCallback: () => Debug.WriteLine("拖动源回调")
);
DragLabel.AsDraggable<Label>(
sourceAffix: new { Type = "文本数据", Value = "Hello World agian" },
sourceCallback: () => Debug.WriteLine("拖动源回调 again")
);
DragBoxView.AsDraggable<BoxView>(
sourceAffix: new { Type = "BoxView数据", Value = "BoxView" },
sourceCallback: () => Debug.WriteLine("按钮拖动回调")
);
DragContentView.AsDraggable<ContentView>(
sourceAffix: new { Type = "ContentView数据", Value = "ContentView" },
sourceCallback: () => Debug.WriteLine("按钮拖动回调")
); // 设置可放置元素(携带目标数据)
DropBorder.AsDroppable<Label, Border>(
targetAffix: new { Type = "目标数据", Value = "Label Drop Zone" },
targetCallback: () => Debug.WriteLine("放置目标回调")
); DropBorder.AsDroppable<BoxView, Border>(
targetAffix: new { Type = "目标数据", Value = "BoxView Drop Zone" },
targetCallback: () => Debug.WriteLine("放置目标回调")
); // 设置可放置元素(通用,非必须,在携带目标数据时有用)
DropBorder.AsDroppable<Border>(
targetAffix: new { Type = "目标数据", Value = "Generic Drop Zone" },
targetCallback: () => Debug.WriteLine("放置目标回调")
);
} protected override void OnAppearing()
{
base.OnAppearing(); WeakReferenceMessenger.Default.Register<IDragDropMessage>(this, HandleBorderDragDropMessage);
} protected override void OnDisappearing()
{
base.OnDisappearing();
WeakReferenceMessenger.Default.UnregisterAll(this);
} private void HandleBorderDragDropMessage(object recipient, IDragDropMessage message)
{
if (message.SourcePayload.View == null || message.TargetPayload.View == null)
{
return;
} switch (message.SourcePayload.View)
{
case Label label:
HandleLabelDrop(label, message);
break; case BoxView boxView:
HandleBoxViewDrop(boxView, message);
break; case ContentView contentView:
HandleContentViewDrop(contentView, message);
break; default:
HandleDefaultDrop(message);
break;
}
} private void HandleDefaultDrop(IDragDropMessage message) => HandleBorderMessage(message);
private void HandleLabelDrop(Label label, IDragDropMessage message) => HandleBorderMessage(message);
private void HandleBoxViewDrop(BoxView boxView, IDragDropMessage message) => HandleBorderMessage(message);
private void HandleContentViewDrop(ContentView contentView, IDragDropMessage message) => HandleBorderMessage(message);
private void HandleBorderMessage(IDragDropMessage message)
{ MainThread.BeginInvokeOnMainThread(() =>
{
ResultLabel.Text = $"拖放成功!\n" +
$"源类型: {message.SourcePayload.View.GetType()}\n" +
$"源数据: {message.SourcePayload.Affix}\n" +
$"目标数据: {message.TargetPayload.Affix}";
}); // 执行回调
MainThread.BeginInvokeOnMainThread(() =>
{
message.SourcePayload.Callback?.Invoke(); // 执行源回调
}); // 执行回调
MainThread.BeginInvokeOnMainThread(() =>
{
message.TargetPayload.Callback?.Invoke(); // 执行目标回调
});
} }

五、总结

本方案实现了 MAUI 控件拖放能力的动态扩展。核心设计遵循以下原则:

  1. 解耦:拖放逻辑与控件分离,通过消息系统连接业务层。
  2. 类型安全:泛型约束确保拖放类型匹配,编译期暴露潜在问题。
  3. 可扩展:通过字典映射和消息订阅,轻松支持新的拖放类型组合。

此方案已在实际项目中验证,适用于文件管理、列表排序、数据可视化等场景,为 MAUI 应用提供了灵活高效的拖放解决方案。

本方案源代码开源,按照 MIT 协议许可。地址:xiaql/Zhally.Toolkit: Dynamically attach draggable and droppable capability to controls of View in MAUI

Maui 实践:为控件动态扩展 DragDrop 能力的更多相关文章

  1. TextBoxFor控件的扩展---Bootstrap在mvc上的应用

    TextBoxFor控件的问题: 1:自带了样式,再用bootstrap样式会有冲突. 2:要加水印,js事件,限制输入长度比较麻烦. 因此需要对textboxfor控件进行扩展. 目标: 1:能使用 ...

  2. 验证控件插图扩展控件ValidatorCalloutExtender(用于扩展验证控件)和TextBoxWatermarkExtender

    <asp:ScriptManager ID="ScriptManager1" runat="server">  </asp:ScriptMan ...

  3. C# DataGridView控件 动态添加新行

    DataGridView控件在实际应用中非常实用,特别需要表格显示数据时.可以静态绑定数据源,这样就自动为DataGridView控件添加相应的行.假如需要动态为DataGridView控件添加新行, ...

  4. Delphi7 第三方控件1stClass4000的TfcImageBtn按钮控件动态加载jpg图片例子

    Delphi7 第三方控件1stClass4000的TfcImageBtn按钮控件动态加载jpg图片例子 procedure TForm1.Button1Click(Sender: TObject); ...

  5. 不在界面上用控件 动态创建idhttp,IdAntiFreeze来用

    不在界面上用控件 动态创建idhttp,IdAntiFreeze来用 var IdHTTP: Tidhttp; IdAntiFreeze: TidAntiFreeze; begin IdAntiFre ...

  6. C# DataGridView控件动态添加新行

    C# DataGridView控件动态添加新行 DataGridView控件在实际应用中非常实用,特别需要表格显示数据时.可以静态绑定数据源,这样就自动为DataGridView控件添加相应的行.假如 ...

  7. 【C#】使用IExtenderProvider为控件添加扩展属性,像ToolTip那样

    申明: - 本文适用于WinForm开发 - 文中的“控件”一词是广义上的说法,泛指包括ToolStripItem.MenuItem在内单个界面元素,并不特指继承自Control类的狭义控件 用过To ...

  8. DotNetBar RibbonControl 控件动态添加项

    想做个插件式开发,界面用Dotnetbar的RibbonControl,需要通过代码动态的向RibbonControl控件添加项 示例代码如下: RibbonTabItem rti = new Rib ...

  9. MFC中List控件动态填充数据(LVN_GETDISPINFO)

    在使用List控件的过程中,有时候List控件中需要添加大量的数据,如果使用InsertItem填充,会一次性将数据全部添加进List控件中,比较耗时.这里记录下如何动态添加List控件数据. 步骤 ...

  10. UniGUI的 TUniPageControl控件动态拖动tabsheet的实现方法

    https://blog.csdn.net/shuiying/article/details/54932518 实现可以用鼠标动态拖动tabsheet,共三个步骤: 1.在ServerModule中, ...

随机推荐

  1. ModuleNotFoundError: No module named '_lzma' when building python

    前言 运行 python 报错:ModuleNotFoundError: No module named '_lzma' when building python 解决 sudo apt-get in ...

  2. Mac 安装php Swoole扩展出现 Enable openssl support, require openssl library 或者fatal error: 'openssl/ssl.h' file not found

    Mac 安装php Swoole扩展时出现 Enable openssl support, require openssl library 或者fatal error: 'openssl/ssl.h' ...

  3. HTML5 给网站添加图标

    1.首先将图标上传到对应的目录下 2.在网页的index.html,添加已下代码到<head>标签里 <link rel="icon" href="i_ ...

  4. MySQL-InnoDB行锁

    InnoDB的锁类型 InnoDB存储引擎支持行锁,锁类型有两种: 共享锁(S锁) 排他锁(X锁) S和S不互斥,其他均互斥. 除了这两种锁以外,innodb还支持一种锁,叫做意向锁. 那么什么是意向 ...

  5. 【Yuexingfei_qwq的原创游戏】

    好的其实标题里的Yuexingfei_qwq指的是我. 不定时持续更新ing-- 有bug及时回复或私信我哈-- 本文同步发表在以下Blog: 洛谷:https://www.luogu.com.cn/ ...

  6. 经过几天的努力Biwen.AutoClassGen终于实现了DTO复杂属性的生成

    前言 距写上一篇 https://www.cnblogs.com/vipwan/p/18535459 生成DTO已经有一段时间了, 最初没有考虑复杂二级属性嵌套的实现,而是直接使用排除使用自定义的方式 ...

  7. 11. RabbitMQ 消息队列 Federation (Exchange 交换机和 Queue队列) + Shovel 同步的搭建配置

    11. RabbitMQ 消息队列 Federation (Exchange 交换机和 Queue队列) + Shovel 同步的搭建配置 @ 目录 11. RabbitMQ 消息队列 Federat ...

  8. 测试工作中用到的Redis命令

    由于项目测试的需要,经常需要连接Redis数据库修改某些键值,无奈最近Redis的客户端连接工具使用不了 只有使用命令行来操作了,现总结如下: 1.远程连接Redis redis-cli -h hos ...

  9. MySQL 事务的二阶段提交是什么?

    MySQL 事务的二阶段提交是什么? 二阶段提交(Two-Phase Commit, 2PC)是分布式事务中的一种协调协议,用于确保多个资源(如数据库或数据节点)在事务提交时保持一致性.MySQL 在 ...

  10. Java 中的 young GC、old GC、full GC 和 mixed GC 的区别是什么?

    Java 中的 young GC.old GC.full GC 和 mixed GC 的区别 在 Java 中,垃圾回收(GC)可以分为几种不同类型,包括 young GC.old GC.full G ...