- UI组件
- 前端
【免费下载链接】ant-design-blazor
🌈A rich set of enterprise-class UI components based on Ant Design and Blazor.
导读
在 Ant Design Blazor 的Tree组件中,仅开启Draggable属性只能实现"全节点可拖、全节点可放"的粗粒度交互。本文以官方示例 Droppable.razor 为骨架,深入讲解DraggableExpression与DroppableExpression两个表达式参数:前者按节点判定"谁可以被拖动",后者按"被拖节点 + 落点节点 + 落点方位"三元组判定"谁可以接收",并结合 TreeNodeTitle.razor.cs 与 TreeNode.razor.cs 的源码调用链,说明拖放判定、目标高亮、数据移动与事件回调的完整执行过程。读完本文,你将能在组织架构树、文件目录树等场景中实现"只有指定类型节点可拖、只有指定目标可接收"的精细化拖放控制。
一、从整体拖放到精确控制
Ant Design Blazor 的Tree组件(源码位于 components/tree)通过一个布尔参数开启拖放能力:
[Parameter] public bool Draggable { get; set; }该参数定义于 Tree.razor.cs,开启后组件根元素会附加draggable-tree样式类(见 SetClassMapper)。一旦开启,所有节点默认都既"可拖"也"可放",这在需要约束拖放范围(例如只允许拖动叶节点、只允许放入指定层级)的业务场景中远远不够。
为此,Tree组件额外提供了两个表达式参数,用于在节点维度和目标维度上做精细化控制:
| 参数 | 类型 | 作用 |
|---|---|---|
DraggableExpression | Func<TreeNode<TItem>, bool> | 传入待判定的节点,返回true表示该节点允许被拖动 |
DroppableExpression | Func<TreeNode<TItem>, TreeNode<TItem>, bool, bool> | 传入(被拖节点、落点节点、是否落在落点下方),返回true表示允许此次拖放 |
两者的签名定义见 Tree.razor.cs。值得注意的是DroppableExpression的第三个参数dropBelow:true表示被拖节点将插到落点节点的下方(成为其兄弟节点),false表示被拖节点将放入落点节点内部(成为其子节点)。这一"方位"参数让规则可以区分"可做子节点"与"只能插在下方"两种场景,是整个精确控制的关键。
二、官方示例全解析:组织架构树
官方文档 Droppable.md 对应的完整示例位于同目录的 Droppable.razor,其业务场景是一棵"公司 — 部门 — 员工"三层组织架构树。核心思路是:只有"员工"节点可以被拖动;"员工"节点不能接收"放入其内部"的拖放。完整代码:
<Tree Style="width:100%;" DataSource="treeData" Draggable OnDrop="onDrop" DraggableExpression="node => node.DataItem.Type == NodeType.ntEmployeee" DroppableExpression="IsDroppable" TItem="Data" Selectable BlockNode DefaultExpandAll ChildrenExpression="node => node.DataItem.Children" TitleExpression="node => node.DataItem.Title" KeyExpression="node => node.DataItem.Key" IsLeafExpression="node => !node.DataItem.Children.Any()"> </Tree> @code { List<Data> treeData = new List<Data> { new() { Title = "Company", Key = "1", Type = NodeType.ntCompany, Children = new List<Data> { new() { Title = "Department1", Key = "11", Type = NodeType.ntDepartment, Children = new List<Data> { new() { Title = "Tom", Key = "111", Type = NodeType.ntEmployeee }, new() { Title = "John", Key = "112", Type = NodeType.ntEmployeee } } }, new() { Title = "Department2", Key = "12", Type = NodeType.ntDepartment, Children = new List<Data> { new() { Title = "Jim", Key = "121", Type = NodeType.ntEmployeee }, new() { Title = "Lili", Key = "122", Type = NodeType.ntEmployeee } } } } } }; public enum NodeType { ntCompany, ntDepartment, ntEmployeee } public class Data { public string Key { get; set; } public string Title { get; set; } public NodeType Type { get; set; } public List<Data> Children { get; set; } = new(); } private bool IsDroppable(TreeNode<Data> draggingNode, TreeNode<Data> dropNode, bool dropBelow) { if (dropNode.Key == "111") return false; if (dropBelow && dropNode.GetParentNode() == null) return false; return dropNode.DataItem.Type != NodeType.ntEmployeee || dropBelow; } private void onDrop(TreeEventArgs<Data> e) { } }2.1 数据模型与节点类型
示例使用TItem="Data"泛型参数将树与自定义实体绑定,通过TitleExpression、KeyExpression、ChildrenExpression、IsLeafExpression四个表达式把实体属性映射为树的标题、键、子节点与叶子判定。实体内部的Type枚举(ntCompany/ntDepartment/ntEmployeee)是拖放规则的判据来源——这正是DraggableExpression/DroppableExpression可以自由读取node.DataItem的原因:表达式收到的TreeNode<TItem>对象持有DataItem属性,开发者可以在表达式闭包内访问业务实体的任意字段。
2.2 拖放规则拆解
DraggableExpression="node => node.DataItem.Type == NodeType.ntEmployeee"表示:只有Type == ntEmployeee的节点(Tom、John、Jim、Lili)显示为可拖动,公司节点与部门节点均不可拖。
IsDroppable方法则实现了三条递进规则:
if (dropNode.Key == "111") return false;—— 键为"111"的节点(Tom)拒绝任何拖放落到它身上,相当于"锁定"该节点;if (dropBelow && dropNode.GetParentNode() == null) return false;—— 若要把节点插到"根节点下方"(即插到树的顶层,成为公司节点的兄弟),一律拒绝。GetParentNode()是 TreeNode.razor.cs 提供的公开 API,根节点返回null;return dropNode.DataItem.Type != NodeType.ntEmployeee || dropBelow;—— 若落点节点本身是员工(非叶子容器),只允许"插到它下方"(dropBelow == true),不允许"放入它内部"(员工节点之下不应再挂子节点);若落点是公司或部门,则两种方位都允许。
这三条规则叠加后的效果:员工可以被拖到部门/公司内部或任意节点下方,但不能拖到 Tom 身上,不能拖到树的顶层,也不能把员工塞进另一个员工内部。
三、DraggableExpression:可拖判定的源码链路
DraggableExpression的值最终落到每个TreeNode<TItem>的Draggable属性上。TreeNode的该属性定义于 TreeNode.razor.cs:
private bool _draggable = true; [Parameter] public bool Draggable { get => TreeComponent.Draggable && _draggable; set => _draggable = value; }可见节点级可拖状态是"树的Draggable开关 && 节点自身_draggable"的合取结果——即使表达式放行,只要树未开启Draggable也不会生效。表达式的结果在节点初始化时计算并写入,见 TreeNode.razor.cs:
if (TreeComponent.DraggableExpression != null) Draggable = TreeComponent.DraggableExpression(this);渲染层面,TreeNodeTitle.razor 将CanDraggable映射为 HTML5 拖放属性并绑定全部拖放事件:
draggable="@(CanDraggable ? "true" : "false")" aria-grabbed="@(CanDraggable ? "true" : "false")" @ondragover:preventDefault="@(TreeComponent.DragItem != null)" @ondragover="OnDragOver" @ondragleave:preventDefault="@(TreeComponent.DragItem != null)" @ondragleave="OnDragLeave" @ondragenter:preventDefault="@(TreeComponent.DragItem != null)" @ondragenter="OnDragEnter" @ondrop:preventDefault="@(TreeComponent.DragItem != null)" @ondrop="OnDrop" @ondragstart="e => { if (CanDraggable) OnDragStart(e); }" @ondragend="e => { if (CanDraggable) OnDragEnd(e); }"拖动开始时,OnDragStart 把当前节点写入TreeComponent.DragItem(拖拽起点节点),随后自动折叠该节点并触发OnDragStart回调;拖动期间TreeComponent.DragItem即"正在被拖动的节点",后续所有落点判定都以它为准。
四、DroppableExpression:落点判定与目标高亮
4.1 判定时机与调用链
DroppableExpression并非只在松手时调用一次,而是在拖动悬停期间被反复调用,用于决定目标节点是否显示可放置高亮。其核心入口是 TreeNodeTitle.razor.cs 的CanDrop:
private bool CanDrop(TreeNode<TItem> draggingNode, TreeNode<TItem> dropNode, bool dropBelow) { if (TreeComponent.DroppableExpression != null) return TreeComponent.DroppableExpression(draggingNode, dropNode, dropBelow); return true; }若未设置DroppableExpression,默认所有目标都放行(返回true)。
4.2 方位判定:OffSETX 阈值
OnDragOver(TreeNodeTitle.razor.cs)根据鼠标在目标节点标题上的横向位移决定"放入内部"还是"插到下方",阈值常量为OffSETX = 25(像素):
private const double OffSETX = 25; // ... if (e.ClientX - _dragTargetClientX > OffSETX) { if (!CanDrop(TreeComponent.DragItem, SelfNode, false)) return; SelfNode.SetTargetBottom(); SelfNode.SetParentTargetContainer(); _ = SelfNode.Expand(true); // 视为子节点容器时自动展开 } else { if (!CanDrop(TreeComponent.DragItem, SelfNode, true)) return; SelfNode.SetTargetBottom(true); SelfNode.SetParentTargetContainer(true); } SelfNode.DragTarget = true;含义:拖入目标节点后继续向右移动超过 25px,则意图是"成为该节点的子节点"(dropBelow == false);否则意图是"插到该节点下方"(dropBelow == true)。两种意图分别调用一次CanDrop,只要对应方向的判定返回false,本次落点高亮就被否决。这也解释了示例规则三的写法:员工节点在"向右拖入内部"方向(dropBelow == false)被拒绝,而"插到下方"方向(dropBelow == true)被放行。
4.3 目标高亮的样式证据
节点是否成为合法落点,通过 TreeNode.razor.cs 的 ClassMapper 反映到 DOM:
.If("drop-target", () => DragTarget) .If("drag-over-gap-bottom", () => DragTarget && DragTargetBottom) .If("drag-over", () => DragTarget && !DragTargetBottom) .If("drop-container", () => TargetContainer)其中DragTargetBottom(TreeNode.razor.cs)记录"落点在下方"的方位状态;同时 TreeNodeTitle.razor 在合法落点渲染一条ant-tree-drop-indicator指示线。视觉反馈与判定逻辑一一对应:用户看到的高亮,正是DroppableExpression刚刚放行的结果。
五、松手后的数据移动与事件回调
当拖拽在合法目标上松手,OnDrop 根据落点方位执行节点数据移动:
private void OnDrop(DragEventArgs e) { if (TreeComponent.DragItem == null || !SelfNode.DragTarget) return; SelfNode.DragTarget = false; SelfNode.SetParentTargetContainer(); if (SelfNode.DragTargetBottom) TreeComponent.DragItem.DragMoveDown(SelfNode); // 插到目标下方 else TreeComponent.DragItem.DragMoveInto(SelfNode); // 放入目标内部 if (TreeComponent.OnDrop.HasDelegate) TreeComponent.OnDrop.InvokeAsync(new TreeEventArgs<TItem>( TreeComponent, TreeComponent.DragItem, e, SelfNode.DragTargetBottom) { TargetNode = SelfNode }); }两种移动的内部实现见 TreeNode.razor.cs:
DragMoveInto(treeNode):先从原父节点Remove(),再treeNode.AddChildNode(DataItem)挂到目标下,并将目标标记为非叶子(treeNode.IsLeaf = false)后自动展开;DragMoveDown(treeNode):先Remove(),再treeNode.AddNextNode(DataItem)插到目标节点的下一个兄弟位置。
落点事件参数TreeEventArgs<TItem>(TreeEventArgs.cs)携带完整上下文:Node(被拖节点)、TargetNode(落点节点)、DropBelow(是否落在下方)、Tree(组件实例)以及OriginalEvent(原始MouseEventArgs)。开发者可在OnDrop回调中读取这些信息做后续业务处理(如持久化新的父子关系),示例中的onDrop(TreeEventArgs<Data> e)即该回调的占位实现。
拖放结束后,OnDragEnd(TreeNodeTitle.razor.cs)在可拖节点上触发,源码注释特别提醒"该回调必须设置"(见 Tree.razor.cs),用于清理拖动产生的临时状态。与此相关的完整事件家族为:OnDragStart、OnDragEnter、OnDragLeave、OnDrop、OnDragEnd(Tree.razor.cs)。
六、实战要点与边界提醒
- 表达式是纯判定、不负责数据变更:
DraggableExpression/DroppableExpression只决定"能否拖 / 能否放",实际的数据移动由组件内部完成。若移动后还需同步后端,请在OnDrop中依据e.Node、e.TargetNode、e.DropBelow自行处理持久化。 - 注意
dropBelow的两种语义:true= 插到目标节点下方(兄弟关系),false= 放入目标节点内部(父子关系)。写规则时建议同时覆盖两个方向,否则会出现"拖得动但放不下"或"放得下但方向不符合预期"的现象。 - 根节点与顶层约束:示例用
dropNode.GetParentNode() == null判断根节点。由于GetParentNode()返回null表示该节点位于树顶层,可用于禁止把节点拖到顶层或禁止根节点接收子节点。 - 判定时机频繁:
DroppableExpression在dragenter/dragover期间会被高频调用(见 OnDragEnter 与 OnDragOver),表达式应保持轻量,避免在其中执行耗时逻辑。 - 与其它节点参数协同:
Draggable属性与DraggableExpression是"与"关系(TreeNode.razor.cs),表达式之外,节点本身的Disabled等状态也会影响交互,组合使用时注意叠加效果。
七、结语
DraggableExpression与DroppableExpression把 HTML5 原生拖放抽象成了两个可读性极强的判定函数:一个回答"这个节点能不能被拖走",一个回答"拖到这儿、以这种方位落下去行不行"。配合OnDrop回调与TreeEventArgs<TItem>的事件上下文,ant-design-blazor 的Tree组件可以轻松承载组织架构调整、菜单排序、文件归类等需要严格约束拖放边界的业务。官方完整示例可直接参考 Droppable.razor,对应的基础拖放示例见 Draggable.razor,组件实现细节可在 components/tree 目录下继续深挖。
- UI组件
- 前端
【免费下载链接】ant-design-blazor
🌈A rich set of enterprise-class UI components based on Ant Design and Blazor.
相关推荐
ant-design-blazor 树组件拖放精确控制:DraggableExpression 与 DroppableExpression 实战解析
ant design blazor 树组件拖放精确控制:DraggableExpression 与 DroppableExpression 实战解析 本篇指南基
前端UI组件设计系统Ant Design Blazor Segmented 分段控制器组件实战指南
Ant Design Blazor Segmented 分段控制器组件实战指南 本文以 ant design blazor 开源仓库中的 Segmented(分
UI组件前端Ant Design Blazor 树形控件增强:实现节点级选中状态绑定
Ant Design Blazor 树形控件增强:实现节点级选中状态绑定 在 Ant Design Blazor 组件库的树形控件开发中,开发者 GoldSuc
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考