创建可编辑的xml文档(之三)执行拖放操作

来源:岁月联盟 编辑:zhu 时间:2004-07-02
执行托放操作

定义了treeview 显示得内容以后,现在你应该准备处理如何四处移动元素了,大多数得开发人员在处理拖放操作时得通用观念都是很相似得,无论使用visual c++ visual basic 或者任何一种.net 语言,所以我一直用下面的四个方法处理这个操作:

MouseDown-----用户选择得内容

DragEnter---用户开始拖动选中得项目

DragOver ---用户拖动选中得项目经过另一个项目

DragDrop---用户在某个地方放下选择得项目

执行这些方法适当得给用户针对可以和不可以处理的得操作分别给予视觉反馈,同时告诉用户他们是怎样被执行的,并且不用管给定的上下文的细节操作,所以就有三个直接的问题需要被考虑:

1. 你如何使treeview 控件中的一个节点和底层xml文档中的节点进行匹配

2. 为了物理节点能够跟随图形进行转换,用户如何操作xml文档

3. 你如何有效地执行大的xml文档。如果这样的转变要不得不加强时,你不想把没有必要的东西绑定到用户界面

清单1

A TreeNode's position maps to an XML node using an XPath query.

Private Sub XmlTreeView_MouseDown(ByVal sender As Object, ByVal e As _
System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown
' First check whether we've clicked on a node in the tree view; if not,
' just return
Dim pt As New Point(e.X, e.Y)
drag_node = Me.GetNodeAt(pt)

If drag_node Is Nothing Then Return

' Highlight the node and build an xpath query so that we can remove it later
xpath_remove_query = buildXPathQuery(drag_node)
Me.SelectedNode = drag_node

' Decide whether we're going to perform an intra-folder rearrangement (right
' mouse button) or a genuine drag-and-drop (left mouse button);
' we do this in the MouseDown rather than DragEnter method, since by the time
' DragEnter fires, the mouse may well have been dragged to a different node
If e.Button = System.Windows.Forms.MouseButtons.Right Then
right_mouse_drag = True
Else
right_mouse_drag = False
End If
End Sub

Private Function buildXPathQuery(ByVal node As System.Windows.Forms.TreeNode) As String
Dim query As String = ""

Do
query = "*[" & xpath_filter & "][" & (node.Index + 1) & "]/" & query
node = node.Parent
Loop While Not (node Is Nothing)

Return query.Substring(0, query.Length - 1)
End Function




显示了MouseDown 句柄 和它调用的帮助方法buildXPathQuery,首先代码检查一个被选中的节点,接着通过使用事先定义好的筛选, 存储TreeNode (drag_node) 和使它关联到xml文档根节点的Xpath 查询(xpath_remove_query)。 例如下面的查询确定了树的根节点的第二个孩子有五个孩子文件夹,一个文件夹可以用查询"attribute::id." 唯一确定

*[attribute::id][2]/*[attribute::id][5]
当用户拖动一个节点到另外一个位置时,代码列表1提供了移动treenode 和treenode相关联的xmlNode的足够信息。你也许认为你能够得到相同的效果,而完全没有必要引用筛选,并且简单的指定像“托动文档根节点的第二个孩子到第一个孩子节点内部”这样的事情,但是这里不是你认为的那样,应该是筛选器强迫treeview 的节点层次和xml文档一一对应的,没有了它 ,这样的直接使用可能是不明智的,例如假设筛选器匹配下面的结构:

<contact>
<email />
<city />
<country />
</contact>
这样的约束意味着Xpath 筛选器将contacts.xml的层次作为一个简单的子元素列表看待

[0] <contacts>
[0] <contact="Alex">
[1] <contact="Rebekah">
[2] <contact="Justin">
然而,treeview 将相同的文档看作一个节点的层次列表

[0] <contacts>
[0] <contact="Alex">
[0] <email>
[1] <city>
[2] <country>
[1] <contact="Rebekah">
只要联系点从不和另一个联系点嵌套,你就能保持treeview 和 xml文档保持同步而没有必要求助于筛选器,例如 如果你想交换"Alex"和"Rebekah"联系点入口,你可以很容易的这么做:
指令: 移除 node[0], child[0];在node[0], child[0]之后重新插入它
treeview: 移除叫做"Alex"的"contact"节点,在叫做"Rebekah" 的"contact"节点之后从新插入它
xml文档:移除叫做"Alex"的"contact"节点,在叫做"Rebekah" 的"contact"节点之后从新插入
但是嵌套的contacts,相同的指令会引起TreeView表示和xml文档表示对不准。例如 假设你试图移动在下面treeview表示中嵌套的"Rebekah":
[0] <contacts>
[0] <contact="Alex">
[0] <contact="Rebekah">
[1] <contact="Justin">
在用不同方法表现节点的xml文档中
[0] <contacts>
[0] <contact="Alex">
[0] <contact="Rebekah">
[1] <contact="Justin">

一个对treeview 表现真正有意义的指令没有必要和xml文档执行相通的工作:
指令:Remove node[0], child[0], child[0]
treeview: Remove "contact" node called "Rebekah"
xml文档:从一个叫做“ALex”的节点上错误的移动了“Email”节点
我们可以借助一个筛选器,筛选器应该能够用离散的实体区分contacts,而不是通过简单的树节点的路径进行区分。这样你就没有必要在担心如何contact "Rebekah"放到它的父节点”alex”内部的正确位置了,因此你就可以保证自己的安全设置
假设一个用户决定要拖动其中一个contact,下一步就是对用户操作的内容给予反馈,一个DragEnter检测操作被拖动的项目是一个treeview 节点,然后记录发生的拖拉操作。对于一个想要执行它自己的应用程序来说这个控制又很大的用处。因此变量drag_drop_active作为DragDropActive的属性直接公开
[C#]
private void XmlTreeView_DragEnter(object sender, System.Windows.Forms.DragEventArgs e)
{
// Allow the user to drag tree nodes within a
// tree
if (e.Data.GetDataPresent(
"System.Windows.Forms.TreeNode", true ))
{
e.Effect = DragDropEffects.Move;
drag_drop_active = true;
}
else
e.Effect = DragDropEffects.None;
}



[VB]
Private Sub XmlTreeView_DragEnter( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.DragEventArgs) _
Handles MyBase.DragEnter
' Allow the user to drag tree nodes within a tree
If e.Data.GetDataPresent( _
"System.Windows.Forms.TreeNode", True) Then
e.Effect = DragDropEffects.Move
drag_drop_active = True
Else
e.Effect = DragDropEffects.None
End If
End Sub
当用户拖动文件夹时DragOver被不断的调用
Private Sub XmlTreeView_DragOver(ByVal sender As Object, ByVal e As
System.Windows.Forms.DragEventArgs) Handles MyBase.DragOver
' Fired continuously while a tree node is dragged. We need to override this to
' provide appropriate feedback
If e.Data.GetDataPresent("System.Windows.Forms.TreeNode", True) Then
' Determine which node we are dragging over
Dim pt As Point = Me.PointToClient(New Point(e.X, e.Y))
Dim drop_node As TreeNode = Me.GetNodeAt(pt)

' If it's the same as the one we last dragged over, take no further action
If drop_node Is last_drop_node Then
Return
End If

' Otherwise highlight the node as a potential drop target
Me.SelectedNode = drop_node
last_drop_node = drop_node

' If the drop node and drag node are the same, indicate that the drag is
' disallowed and take no further action (as per Explorer)
If drag_node Is drop_node Then
e.Effect = DragDropEffects.None
Return
End If

If right_mouse_drag Then
' Right mouse drag-and-drop operations constitute intra-folder
' rearrangements which provide continuous graphical feedback
' We need to cache the drop node's parent, since it will
' be inaccessible if we remove it from the tree
Dim drop_parent As TreeNode = drop_node.Parent

' Check if it's at the same level as the node being dragged
If drag_node.Parent Is drop_parent Then
' Temporarily remove the drop node's siblings from the tree; then add
' them back in a different order
Dim siblings(drop_parent.Nodes.Count) As System.Windows.Forms.TreeNode
Dim count As Integer = siblings.Length - 1

Dim item As Integer
For item = 0 To count - 1
siblings(item) = drop_parent.Nodes(0)
drop_parent.Nodes(0).Remove()
Next

For item = 0 To count - 1
If siblings(item) Is drop_node Then
drop_parent.Nodes.Add(drag_node)
Else
If siblings(item) Is drag_node Then
drop_parent.Nodes.Add(drop_node)
Else
drop_parent.Nodes.Add(siblings(item))
End If
End If
Next

' Highlight the new node
last_drop_node = drag_node
e.Effect = DragDropEffects.Move
Me.SelectedNode = drag_node
Else
e.Effect = DragDropEffects.None
End If
Else
' If the user is left-button dragging, disallow (pointless) attempts
' to drag a node into its parent's folder (as per Explorer)
If drag_node.Parent Is drop_node Then
e.Effect = DragDropEffects.None
Else
e.Effect = DragDropEffects.Move
End If
End If
End If
End Sub
出于执行效率的原因,代码首先检查自从上次调用dragover 以后被拖动的文件是否发生了变化,如果发生了变化,代码接着判断处理中的拖动类型。以前我必须允许用户最后可以重新排序和设置层次,我在这里选择类似windows的行为(只要它被定义),在其他的地方使用我的方案。因此让用户使用左键复制或者移动文件夹是很不自然的,我们应该让用户使用右键进行处理文件夹的操作。然而这样做会产生一个小问题,因为这两个拖动将会用不同的方法进行处理:左键的拖动直到拖动结束时,而右键拖动将不断的反馈状态,即使不断的拖动,直到用户松开鼠标的按键前,文档不发生物理位置上的改变
既然这样,代码检查被拖动的节点是否是节点的兄弟节点,如果是的话,父节点的所有子节点被从树中分离出来,然后进行拖放操作交换节点位置,然后再把这些子节点添加回去。结果是:释放操作完成时,底层数据源根据当前的可视化表达方式进行更新,隐藏的底层数据和数据的可视化表达就可以保持同步。更好的处理方法是:不断的显示更新操作,因此用户可以立刻得到关于拖动的反馈,xml文档只需在拖动完成时更新一次。鼠标左键的拖放操作不需要特殊的代码,drag/drop API 可以适当的处理反馈.
用户通过松开鼠标键来完成拖放操作, 参考下面的代码列表3
Listing 3. XMLTreeView_DragDrop and Helper Methods:
The XMLTreeView_DragDrop and its helper swapXmlDocumentNodes methods provide logic to decide where a node belongs among its siblings
Private Sub XmlTreeView_DragDrop(ByVal sender As Object, ByVal e As _
System.Windows.Forms.DragEventArgs) Handles MyBase.DragDrop
' Cancel drag/drop
drag_drop_active = False

' Check that we are dropping nodes within the same tree view
If e.Data.GetDataPresent("System.Windows.Forms.TreeNode", True) = False Then
Return
End If
' If it's a right-mouse drag-and-drop operation, the tree view will already
' show the updated hierarchy; so it's just a matter of updating the xml
' document to match the tree view
If right_mouse_drag Then
swapXmlDocumentNodes()
drag_node = Nothing
last_drop_node = drag_node
xpath_remove_query = ""
Else
' Determine which node we are dropping onto
Dim pt As Point = Me.PointToClient(New Point(e.X, e.Y))
Dim drop_node As TreeNode = Me.GetNodeAt(pt)

' Do nothing if the drag and drop target are the same node
If drag_node Is drop_node Then
Return
End If
If drop_node Is Nothing Then
' Don't allow the user to drag nodes off the tree. Though the tree view
' wouldn't complain, any attempt to create an xml document with 2 roots
' would cause problems
Return
End If

' Add the new node where it was dropped
drag_node.Remove()
drop_node.Nodes.Add(drag_node)

' And update the xml document to match the new tree view hierarchy
swapXmlDocumentNodes()
drag_node = Nothing
last_drop_node = drag_node
xpath_remove_query = ""
End If
End Sub


Private Sub swapXmlDocumentNodes()
' This method updates the xml document bound to the tree view so that the two node
' hierarchies are the same; it determines appropriate xpath queries to remove and
' reinsert the node in question by comparing the tree view's structure before and
' after the drag/drop operation took place
Dim node As System.Xml.XmlNode
node = xml_document.DocumentElement.SelectSingleNode(xpath_remove_query)
node.ParentNode.RemoveChild(node)

' Create a query to determine where the node should be reinserted
Dim xpath_insert_query As String = buildXPathQuery(drag_node)

' We are only interested in the parent portion of the insert query
xpath_insert_query = xpath_insert_query.Substring(0, xpath_insert_query.LastIndexOf("/"))
Dim insert_parent As System.Xml.XmlNode = xml_document.DocumentElement.SelectSingleNode(xpath_insert_query)

If drag_node.Parent.Nodes.Count = 1 Then
' Special case: if as a result of the drag/drop operation some parent without
' previous children gained a child, just add the child to the parent.
insert_parent.AppendChild(node)
Else
' Otherwise we need to insert the child at its appropriate position; XmlNode
' does not have an Index property, so we need to do this by hand
Dim child As Integer
For child = 0 To insert_parent.ChildNodes.Count - 1
If child = drag_node.Index Then
insert_parent.InsertBefore(node, insert_parent.ChildNodes(child))
Return
End If
Next

' If we've reached here, the node to be reinserted must be the last child
insert_parent.AppendChild(node)
End If
End Sub
那样的话,一些简单的代码就可以完成在文档中移除树节点和它相关的文件夹,还可以通过创建适当的Xpath 查询来在新的位置上重新插入文件夹。需要特别指出的是:当用户在一个没有子节点的文件夹下面插入一个文件夹时,只能通过创建一个新的子节点来实现。