Maui 实践:不要把 DataPackagePropertySetView 看作一层皮
—— 再论为控件动态扩展 DragDrop 能力
夏群林 原创 2025.7.18
一、Drag / Drop 之间传递的参数
前文提到,拖放的实现需要 DragGestureRecognizer 与 DropGestureRecognizer 在不同的控件上相互配合,数据传输和配置复杂。主要有三个事件参数:DragStartingEventArgs,DragEventArgs 和 DropEventArgs。还有一个 DropCompletedEventArgs,不涉及实体数据传递,这里不讨论。
Drag / Drop 操作,本质上是把依存于源控件的数据,与依存于目标控件的数据,挑选出来,组合,供业务流程调用。
DragStartingEventArgs 是数据的起点,它打包了 DataPackage 类型的 Data,以及源控件的位置数据。位置数据暂且不论,我们聚焦在业务层面的实体数据上。去掉枝枝叶叶,在 Maui 中 DragStartingEventArgs 源代码是这样:
public class DragStartingEventArgs : EventArgs
{
public bool Handled { get; set; }
public bool Cancel { get; set; }
public DataPackage Data { get; } = new DataPackage();
public virtual Point? GetPosition(Element? relativeTo) =>_getPosition?.Invoke(relativeTo);
}
展开 DataPackage,不神秘,就是一个在应用程序中封装和传递数据的容器,它的只读属性 Properties,一个键值对词典,Dictionary<string, object> _propertyBag,是载体,用 DataPackagePropertySet 类包装。object 类型的值,你可以在这里放任何你想传递的数据。所以,简单,但强大。
public class DataPackage
{
public DataPackagePropertySet Properties { get; }
public ImageSource Image { get; set; }
public string Text { get; set; }
public DataPackageView View => new DataPackageView(this.Clone());
}
public class DataPackagePropertySet : IEnumerable
{
// 这里是数据保持处
Dictionary<string, object> _propertyBag;
public IEnumerable<string> Keys => _propertyBag.Keys;
public IEnumerable<object> Values => _propertyBag.Values;
public void Add(string key, object value)=> _propertyBag.Add(key, value);
public bool ContainsKey(string key) => _propertyBag.ContainsKey(key);
public bool TryGetValue(string key, out object value) =>
_propertyBag.TryGetValue(key, out value);
}
DragEventArgs 是数据中间站,当拖动源控件经停目标控件时,平台会比对两个控件,是否有缘。属于 Drag / Drop 对相同阵营的,
public class DragEventArgs : EventArgs
{
public DragEventArgs(DataPackage dataPackage)
{
Data = dataPackage;
}
public DataPackage Data { get; }
public DataPackageOperation AcceptedOperation { get; set; } = DataPackageOperation.Copy;
}
就会允许 DataPackage 接收下来打包转发。 AcceptedOperation 决定要不要 Copy。事实上,枚举类型 DataPackageOperation 只有两个值:Copy / None 。同样,我们这里忽略了位置数据的讨论。
最后,DropEventArgs 将 DragStartingEventArgs 传来的 DataPackage 蒙上面纱,以 DataPackageView 面目示人。
public class DropEventArgs
{
public DataPackageView Data { get; }
public bool Handled { get; set; }
public virtual Point? GetPosition(Element? relativeTo) =>
_getPosition?.Invoke(relativeTo);
}
public class DataPackageView
{
public DataPackagePropertySetView Properties { get; }
public Task<ImageSource> GetImageAsync()
{
return Task.FromResult(DataPackage.Image);
}
public Task<string> GetTextAsync()
{
return Task.FromResult(DataPackage.Text);
}
}
DataPackageView 的数据载体是 DataPackagePropertySetView,后者是 DataPackagePropertySet 的只读包装:
public class DataPackagePropertySetView : IReadOnlyDictionary<string, object>
{
public DataPackagePropertySet _dataPackagePropertySet;
public object this[string key] => _dataPackagePropertySet[key];
public IEnumerable<string> Keys => _dataPackagePropertySet.Keys;
public IEnumerable<object> Values => _dataPackagePropertySet.Values;
public int Count => _dataPackagePropertySet.Count;
public bool ContainsKey(string key) => _dataPackagePropertySet.ContainsKey(key);
public bool TryGetValue(string key, out object value) => _dataPackagePropertySet.TryGetValue(key, out value);
}
观察 DataPackage / DataPackageView,会发现,除了核心的用户数据字典外,还有一个字符串数据,string Text,一个图像数据,ImageSource Image。拖放操作,在 DragStartingEventArgs 时准备数据。最后在 DropEventArgs 处获取数据:
public Task<ImageSource> GetImageAsync()
{
return Task.FromResult(DataPackage.Image);
}
public Task<string> GetTextAsync()
{
return Task.FromResult(DataPackage.Text);
}
你可以把 Text / Image 看作常用数据快捷通道。我的实践,就是利用这个快捷通道。
二、DataPackagePropertySetView 的核心价值:不止于包装
我相信,初学者大多会有我当初那样的困惑:用 DataPackagePropertySetView 包装 DataPackagePropertySet,是否多此一举?既然底层实质数据一样,用同一个数据类型岂不方便?何必要加 DataPackagePropertySetView 这层皮?
原因是,Maui 为我们带来跨平台数据标准化便利的同时,也带来了跨平台数据传递打包解包的繁杂,以及额外开销。
由于 Maui 控件的实现,最终会转化成应用所在平台的本机实现,Drag / Drop 操作所携带的数据,会一层层转换为本机要求的结构,再一层层转换回 Maui。这里的事情,不简单。
在 MAUI 拖放机制中,DataPackagePropertySetView 绝非简单的字典包装,而是跨平台数据传输的核心枢纽,其设计蕴含三大关键价值:
跨平台数据标准化。MAUI需要将数据转换为不同平台的原生格式(如Android的ClipData、iOS的NSItemProvider),而
DataPackagePropertySetView通过标准化属性(如Title、Description、Keywords)屏蔽了底层差异。类型安全与延迟加载。强类型访问,避免通过字符串键强制转换类型的风险(如
(string)properties["Title"]);仅在调用GetTextAsync()等方法时才实际传输数据,延迟加载,减少无效开销。安全隔离机制。作为只读视图,
DataPackagePropertySetView防止拖放目标意外修改源数据,同时通过平台适配器确保数据传输的安全性(如跨进程场景的序列化/反序列化)。
不需要更细节的理解,但是我做了决定,能绕开依赖本机数据转换而传递数据的,最好在 Maui 层面直接处理。这也是当初我开发 AsDroppable/ AsDraggable 扩展方法 (参阅: Maui 实践:为控件动态扩展 DragDrop 能力 )。
自定义数据传递,通常我们会建立全局缓存管理器,在拖放源缓存对象并传递 ID,然后在拖放目标通过 ID 获取对象。因为数据产生于拖放源,如果拖放源不再被引用,其所产生的数据也应该销毁,否则会造成内存泄漏。
问题在于,我们使用 AsDraggable 方法为拖放源配置拖放数据时,不知道该拖放源在程序逻辑中,是否要释放,何时会释放。于是想到用弱引用管理器避免内存泄漏。
ConditionalWeakTable<TKey, TValue> 是 .NET 框架中的一个特殊集合类,在两个对象之间建立弱关联关系,同时确保不会阻止垃圾回收(GC)对这些对象的回收。当 TKey 类型的对象(键)被垃圾回收时,对应的 TValue 类型的值也会被自动从表中移除,不会因为键值对的存在而延长对象的生命周期。这样,我们可以缓存与特定拖 / 放源关联的数据,同时不影响这些对象的垃圾回收。完美。
不过,针对我的应用情形,完美之中有瑕疵。我们的全局缓存管理器在拖放源缓存对象并传递 ID,这个 ID,我直接选用 Guid 类型,可以唯一性区别无限个数据,简洁。但 Guid 是值类型,不符合 ConditionalWeakTable<TKey, TValue> 对 TKey 为引用类型的要求。
我设计了一个包装类,把 Guid 包装成引用类:
public sealed class GuidToken
{
public Guid Id { get; } = Guid.NewGuid();
public string Token => Id.ToString();
}
然后用 GuidToken 作为键,欺骗 ConditionalWeakTable:
private static readonly ConditionalWeakTable<GestureRecognizer, GuidToken> guidTokens = [];
private static readonly ConditionalWeakTable<GuidToken, DragDropPayload> dragDropPayloads = [];
这里,拖 / 放源为键关联 GuidToken 值,再以 GuidToken 为键关联数据 DragDropPayload。这样,我们就实现了在拖放过程中,当平台与本机做着复杂的交互时,只需传递简单的 guid 字符串,还保证不会内存泄漏。
我们还要做点额外的工作:为 GuidToken 建立一个生命期可控的强引用:
private static readonly ConcurrentDictionary<string, WeakReference<GuidToken>> tokenCache = new();
否则,GuidToken 不知何时会被 GC,其代表的数据亦不知何时被 GC。手动控制 GuidToken 生命期的方式,结合在 AsDraggable / AsDroppable 扩展方法中,后面一并讲到。
三、进阶:DynamicGesturesExtension 改进
根据前面的讨论,我对自己先前开发的 AsDraggable / AsDroppable 扩展方法予以改进。AsDraggable / AsDroppable 是通用方法,本想通过泛型的方式,源控件类型/目标控件类型的组合,来区分应该采取的拖放后续操作。为此,还回避不了头疼的反射技术。这次我顺手把它去掉了,区分拖放后续操作,只需要通过拖/放控件关联的数据类型 DragDropPayload 的组合,即可确认。我也简化了 DragDropPayload 数据结构,消除协变和逆变的顾忌。
public class DragDropPayload
{
public required View View { get; init; } // 拖放源/目标控件
public object? Affix { get; init; } // 任意附加数据(如文本、对象)
public Action<View, object?>? Callback { get; init; } // 拖放完成后的回调
public View? Anchor { get; set; } = null; // 拖放源/目标控件的 recognizer 依附 View 组件
public SourceTypeEnum SourceType { get; set; } // 标识。源/目标之间标识有交集者才能交互
}
1. 注册数据
private static string RegisterPayload(this GestureRecognizer recognizer, DragDropPayload payload)
{
ArgumentNullException.ThrowIfNull(recognizer);
ArgumentNullException.ThrowIfNull(payload);
var guidToken = guidTokens.GetOrCreateValue(recognizer);
dragDropPayloads.AddOrUpdate(guidToken, payload);
tokenCache[guidToken.Token] = new WeakReference<GuidToken>(guidToken);
return guidToken.Token;
}
注册数据在指定源控件 AsDragble 时完成:
public static void AsDraggable<TSourceAnchor, TSource>(this TSourceAnchor anchor, TSource source, Func<TSourceAnchor, TSource, DragDropPayload> payloadCreator)
where TSourceAnchor : View
where TSource : View
{
AttachDragGestureRecognizer(anchor, source, payloadCreator); // 覆盖现有 payload(如果存在)
}
private static void AttachDragGestureRecognizer<TSourceAnchor, TSource>(TSourceAnchor anchor, TSource source, Func<TSourceAnchor, TSource, DragDropPayload> payloadCreator)
where TSourceAnchor : View
where TSource : View
{
anchor.Undraggable();
DragGestureRecognizer dragGesture = new() { CanDrag = true };
anchor.GestureRecognizers.Add(dragGesture);
dragGesture.DragStarting += (sender, args) =>
{
DragDropPayload dragPayload = payloadCreator(anchor, source);
_ = dragGesture.RegisterPayload(dragPayload);
args.Data.Text = guidTokens.GetOrCreateValue(dragGesture).Token;
anchor.Opacity = 0.5;
};
dragGesture.DropCompleted += (sender, args) =>
{
guidTokens.GetOrCreateValue(dragGesture).Token.RemovePayload();
};
}
2. 匹配数据,在 DragLeave 事件中处理
dropGesture.DragOver += (sender, e) =>
{
string token = e.Data.Text;
if (token.TryAssociatedPayload(out DragDropPayload? dragPayload) &&
guidTokens.TryGetValue(dropGesture, out GuidToken? dropToken) && dropToken is not null &&
dropToken.Token.TryAssociatedPayload(out DragDropPayload? dropPayload) &&
(dragPayload.SourceType & dropPayload.SourceType) != 0)
{
e.AcceptedOperation = DataPackageOperation.Copy;
}
else
{
e.AcceptedOperation = DataPackageOperation.None;
}
};
public static bool TryAssociatedPayload(this string token, [NotNullWhen(true)] out DragDropPayload? payload)
{
payload = null;
if (!token.IsValidGuid())
{
return false;
}
if (tokenCache.TryGetValue(token, out var weakGuidToken) &&
weakGuidToken.TryGetTarget(out var guidToken) &&
dragDropPayloads.TryGetValue(guidToken, out payload))
{
return true;
}
_ = tokenCache.TryRemove(token, out _); // 尝试清理缓存
return false;
}
注意上面尝试清理缓存,顺手做的。在DropCompleted事件处理中,此时数据传递使命完成,会专门移除缓存数据:
dragGesture.DropCompleted += (sender, args) =>
{
guidTokens.GetOrCreateValue(dragGesture).Token.RemovePayload();
};
public static void RemovePayload(this string token)
{
if (!token.IsValidGuid() || !tokenCache.TryGetValue(token, out var weakToken))
{
return;
}
if (weakToken.TryGetTarget(out var guidToken))
{
_ = dragDropPayloads.Remove(guidToken);
}
_ = tokenCache.TryRemove(token, out _);
}
3. 发送数据组合,OnDroppablesMessageAsync
public static void AsDroppable<TTargetAnchor, TTarget>(this TTargetAnchor anchor, DragDropPayload payload)
where TTargetAnchor : View
where TTarget : View
{
anchor.Undroppable();
DropGestureRecognizer dropGesture = new() { AllowDrop = true };
anchor.GestureRecognizers.Add(dropGesture);
_ = dropGesture.RegisterPayload(payload);
// ... ...
dropGesture.Drop += async (s, e) =>
{
await OnDroppablesMessageAsync<TTargetAnchor>(anchor, dropGesture, e);
// ... ...
};
}
private static async Task OnDroppablesMessageAsync<TTargetAnchor>(TTargetAnchor anchor, DropGestureRecognizer dropGesture, DropEventArgs e)
where TTargetAnchor : View
{
string token = await e.Data.GetTextAsync();
// ... ...
_ = WeakReferenceMessenger.Default.Send<DragDropMessage>(new DragDropMessage()
{
SourcePayload = sourcePayload,
TargetPayload = targetPayload
});
// ... ...
}
五、总结:从"数据传输"到"对象生命周期管理"
MAUI拖放功能的核心挑战不仅在于数据传递,更在于对象生命周期的安全管理。DataPackagePropertySetView通过标准化接口屏蔽了平台差异,而ConditionalWeakTable则解决了复杂对象传输的性能与内存问题。
本 DynamicGesturesExtension 改进方案已在实际项目中验证,源代码开源,按照 MIT 协议许可。地址:xiaql/Zhally.Toolkit: Dynamically attach draggable and droppable capability to controls of View in MAUI
Maui 实践:不要把 DataPackagePropertySetView 看作一层皮的更多相关文章
- 手工走一次OPENSTACK安装,掉一层皮啊
掉皮也是值得的,对OS的了解慢慢加深. 最近加入CS的Q群也学到不少.
- 朱晔的互联网架构实践心得S1E2:屡试不爽的架构三马车
朱晔的互联网架构实践心得S1E2:屡试不爽的架构三马车 [下载本文PDF进行阅读] 这里所说的三架马车是指微服务.消息队列和定时任务.如下图所示,这里是一个三驾马车共同驱动的一个立体的互联网项目的架构 ...
- LINUX内核分析第四周学习总结——扒开系统调用的“三层皮”
LINUX内核分析第四周学习总结--扒开系统调用的"三层皮" 标签(空格分隔): 20135321余佳源 余佳源 原创作品转载请注明出处 <Linux内核分析>MOOC ...
- Linux内核分析——期末总结
Linux内核学习总结 首先非常感谢网易云课堂这个平台,让我能够在课下学习,课上加强,体会翻转课堂的乐趣.孟宁老师的课程循序渐进,虽然偶尔我学习地不是很透彻,但能够在后续的课程中进一步巩固学习,更加深 ...
- 【转】opencv检测运动物体的基础_特征提取
特征提取是计算机视觉和图像处理中的一个概念.它指的是使用计算机提取图像信息,决定每个图像的点是否属于一个图像特征.特征提取的结果是把图像上的点分为不同的子集,这些子集往往属于孤立的点.连续的曲线或者连 ...
- 《STL源码剖析》相关面试题总结
原文链接:http://www.cnblogs.com/raichen/p/5817158.html 一.STL简介 STL提供六大组件,彼此可以组合套用: 容器容器就是各种数据结构,我就不多说,看看 ...
- 写书好累 <HTTP抓包实战>终于出版
我的新书<HTTP抓包实战>终于开始在京东销售了.内容是关于HTTP包,Fiddler抓包,JMeter发包,适合任何IT工程师阅读.我将自己十年所学的知识,融会贯通总结为一本书.阅读后肯 ...
- 面试题总结(三)、《STL源码剖析》相关面试题总结
声明:本文主要探讨与STL实现相关的面试题,主要参考侯捷的<STL源码剖析>,每一个知识点讨论力求简洁,便于记忆,但讨论深度有限,如要深入研究可点击参考链接,希望对正在找工作的同学有点帮助 ...
- STL笔试面试题总结(干货)(转)
STL笔试面试题总结 一.STL有哪些组件? STL提供六大组件彼此此可以组合套用: 1.容器容器就是各种数据结构,我就不多说,看看下面这张图回忆一下就好了,从实现角度看,STL容器是一种class ...
- Java 异常面试题(2020 最新版)
Java异常架构与异常关键字 Java异常简介 Java异常是Java提供的一种识别及响应错误的一致性机制. Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程 ...
随机推荐
- 开源项目YtyMark文本编辑器--UI界面相关功能(关于设计模式的实战运用)
开源项目地址 GitHub 开源地址(YtyMark-java) 欢迎提交 PR.Issue.Star ️! 1. 简述 YtyMark-java项目分为两大模块: UI界面(ytyedit-mark ...
- Sentinel源码—9.限流算法的实现对比
大纲 1.漏桶算法的实现对比 (1)普通思路的漏桶算法实现 (2)节省线程的漏桶算法实现 (3)Sentinel中的漏桶算法实现 (4)Sentinel中的漏桶算法与普通漏桶算法的区别 (5)Sent ...
- chrome “从 Google 获取图片说明”
右键菜单"从 Google 获取图片说明"多余去掉. 设置-高级-使用硬件加速模式(如果可用)-关闭 在用户使用上firefox完胜chrome,但是firefox的开发人员工具相 ...
- Java的"伪泛型"变"真泛型"后,会对性能有帮助吗?
泛型存在于Java源代码中,在编译为字节码文件之前都会进行泛型擦除(type erasure),因此,Java的泛型完全由Javac等编译器在编译期提供支持,可以理解为Java的一颗语法糖,这种方式实 ...
- Strands Agents(一)Strands Agents 介绍
Strands Agent AWS 最新开源的 Strands Agents SDK 是一款采用模型驱动架构的 AI 代理开发框架,旨在通过极简开发方式,帮助开发者快速构建和部署 AI 代理.它将代理 ...
- python C3算法
Python MRO C3算法是python当中计算类继承顺序的一个算法,从python2.3以后就一直使用此算法了. c3 linearization算法称为c3线性化算法 C3算法原理 首先定义几 ...
- mysql字符集插入中文报错
org.apache.ibatis.exceptions.PersistenceException: ### Error updating database. Cause: java.sql.SQLE ...
- 你了解CAS吗?有什么问题吗?如何解决?
什么是CAS? CAS全称Compare And Swap,比较与交换,是乐观锁的主要实现方式.CAS在不使用锁的情况下实现多线程之间的变量同步.ReentrantLock内部的AQS和原子类内部都使 ...
- 修改AndroidStudio的Boot Java Runtime for the IDE后,IDE打开报错无法运行
修改AndroidStudio的Boot Java Runtime for the IDE后,IDE打开报错无法运行,解决方法 一.问题 我想在AndroidStudio里使用markdown支持 ...
- Visual Studio 2022 中的 EF Core 反向工程和模型可视化扩展插件
前言 在 EF 6 及其之前的版本数据库优先模式(Database First)是可以在 Visual Studio 中通过可视化界面来操作完成的,但是到了 EF Core 之后就不再支持了(因为模型 ...