WPF 中的拖放






4.80/5 (42投票s)
本文介绍如何使用GongSolutions.Wpf.DragDrop库为WPF应用程序添加拖放功能。
引言
一段时间以来,我一直在抱怨WPF中缺乏像样的拖放支持。虽然WPF在桌面GUI编程方面比WinForms时代有了长足的进步,但拖放功能自我在Visual Basic 3.0开始用Windows编程以来一直没有改变。
特别是,人们必须在代码隐藏中充斥着拖放逻辑,这对于在MVVM时代任何有自尊的WPF开发人员来说都是不可接受的。
在忍受了这一切一段时间后,我偶然发现了一篇由Bea Stollnitz撰写的关于使用附加属性在XAML中为控件添加拖放逻辑的文章。然而,Bea的解决方案并没有达到我所需要的要求。我需要的是
- MVVM支持:任何比基本拖放操作更复杂的功能都需要逻辑,而代码隐藏不适合存放这些逻辑。关于什么可以被拖动以及什么可以被放到哪里,这些决定应该委托给ViewModels。
- 插入/拖入:有时当你将一个项目从一个地方拖到另一个地方时,你是在重新排序列表中的项目。有时,你是在将一个项目拖放到另一个项目上,就像在文件管理器中将文件拖到一个文件夹中一样。
- TreeView支持
- 多选
- 自动滚动
环顾四周,我没有找到任何能满足我需求的东西,所以我决定自己写一个。你可以在Google Code项目找到最新的源代码和示例。它是在BSD许可证下发布的,所以你可以随心所欲地使用它。
Using the Code
为了演示拖放框架的使用,让我们使用一个简单的学校示例。在这个例子中,我们有三个ViewModels
MainViewModel
:这仅包含学校的集合。SchoolViewModel
:一所学校有一个名字和学生集合。PupilViewModel
:一个学生有一个名字。
在我们的UI中,我们将显示两个列表框。第一个用于显示学校列表,第二个用于显示属于该学校的学生列表。我们的XAML如下所示
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0"
ItemsSource="{Binding Schools}"
DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True"/>
<ListBox Grid.Column="1"
ItemsSource="{Binding Schools.CurrentItem.Pupils}"
DisplayMemberPath="FullName"/>
</Grid>
以及由此产生的窗口如下所示
首先,我们希望能够重新排序第二个ListBox
中的学生。这可以通过在ListBox
上设置IsDragSource
和IsDropTarget
附加属性来完成
<ListBox Grid.Column="1"
ItemsSource="{Binding Schools.CurrentItem.Pupils}"
DisplayMemberPath="FullName"
dd:DragDrop.IsDragSource="True"
dd:DragDrop.IsDropTarget="True"/>
你现在将能够拖放项目在ListBox
内重新排序它们。现在,你可能会认为,如果你在第一个ListBox
上设置了IsDropTarget
属性,你就能将一个学生拖到一个学校里。然而,试试这样做,你不会被允许的。这是因为第一个ListBox
绑定到SchoolViewModel
对象的集合,而第二个ListBox
绑定到PupilViewModel
s的集合。这里应该发生什么?框架不知道,所以我们必须告诉它。
进入DropHandlers
为了给ListBox
添加一个拖放处理器,我们在第一个ListBox
上设置DropHandler
附加属性,并将其绑定到一个ViewModel。在这种情况下,我们将只把它绑定到MainViewModel
,如下所示
<ListBox Grid.Column="0"
ItemsSource="{Binding Schools}" DisplayMemberPath="Name"
IsSynchronizedWithCurrentItem="True"
dd:DragDrop.IsDropTarget="True" dd:DragDrop.DropHandler="{Binding}"/>
现在我们需要在MainViewModel
中添加处理代码。要做到这一点,我们实现IDropTarget
接口。这个接口定义了两个方法:DragOver
和Drop
。DragOver
在拖动过程中被调用,用于确定当前的拖放目标是否有效
void IDropTarget.DragOver(DropInfo dropInfo)
{
if (dropInfo.Data is PupilViewModel &&
dropInfo.TargetItem is SchoolViewModel)
{
dropInfo.DropTargetAdorner = DropTargetAdorners.Highlight;
dropInfo.Effects = DragDropEffects.Move;
}
}
在这里,我们正在检查被拖动的数据是否是PupilViewModel
,并且当前是拖动目标的项是否是SchoolViewModel
。
dropInfo.Data
属性包含被拖动的数据。如果拖动的源控件是一个绑定的控件(事实确实如此),它将是被拖动项绑定的对象。dropInfo.TargetItem
属性包含鼠标光标当前下的项所绑定的对象。
如果数据类型正确,我们将继续设置拖放装饰器。因为将学生拖放到学校会导致学生被添加到学校,所以我们选择Highlight
装饰器,它会高亮显示目标项。另一个可用的拖放目标装饰器是Insert
装饰器,它会在列表中的插入点绘制一个插入符号,并且当你重新排序第二个ListBox
中的学生时会看到它。
最后,我们将拖放效果设置为DragDropEffects.Move
。这告诉框架被拖动的数据可以在这里放下,并显示一个移动鼠标指针。如果我们不设置这个属性,就会使用默认值DragDropEffects.None
,这表示在该位置不允许放下。
接下来是Drop
方法
void IDropTarget.Drop(DropInfo dropInfo)
{
SchoolViewModel school = (SchoolViewModel)dropInfo.TargetItem;
PupilViewModel pupil = (PupilViewModel)dropInfo.Data;
school.Pupils.Add(pupil);
}
在这里,我们只是从DropInfo
参数中获取涉及拖放的SchoolViewModel
和PupilViewModel
,并将学生添加到学校。我们可以确定数据将是预期的类型,因为DragOver
方法已经过滤掉了其他所有情况。
哦!但是等等,当我们把学生拖放到新学校时,我们并没有把他从原来的学校移走,也就是说,我们是在复制而不是移动。为了从之前的学校移走学生,我们需要能够获取拖动开始的集合。这由DropInfo.DragInfo
对象提供。这个对象保存了与拖动源相关的信息。特别是,我们对SourceCollection
属性感兴趣
((IList)dropInfo.DragInfo.SourceCollection).Remove(pupil);
本文到此结束
这完成了这篇入门文章。你可以在Google Code项目中找到更多示例。一些值得关注的功能
- 拖放处理器
- 拖放装饰器
- 多选
如果这篇文章让你感兴趣,请加入我们!请发布到Google Groups,或为项目贡献代码,希望我们能做出一些能让拖放摆脱20世纪束缚的东西。