Silverlight 2 的专业拖放管理器控件






4.93/5 (16投票s)
一篇关于灵活的 Silverlight 2 拖放管理器和 TemplateControl 的文章。

引言
Silverlight 2 提供了许多控件。不幸的是,它们中没有一个支持许多业务应用程序所需的拖放功能。本文提供了一个拖放管理器的实现,该管理器应足以满足大多数业务应用程序的需求。
特点
- 支持不同的拖放模式
- 移动模式
- 克隆模式
- 一次性克隆模式
- 通过单独的接口或嵌入的缩放变换进行完全缩放/缩放支持
- 拖放操作期间的视觉反馈(包括缩放/缩放支持)
- 支持任何
Panel
作为潜在的放置目标 - 支持任何派生自
FrameworkElement
的类型的拖放 - 通过模板控件支持样式
- 支持拖放行为的组合和继承
- 集成 Z 顺序和焦点(仅适用于派生自
Control
的元素)处理 - 拖放元素和放置目标的事件处理
- 完全支持 Expression Blend(依赖项属性/动画状态)
- 键盘支持(按 Escape 键取消)
背景
Silverlight 2 为 .NET 开发人员提供了一个非常好的开发环境。不幸的是,一些在大多数企业应用程序中必需的控件/功能缺失。其中一项缺失的功能就是拖放处理。
当今大多数 Silverlight 应用程序中实现拖放的方式是一种非常直接的方法,直接处理必要的鼠标事件,如 MouseMove
和 MouseLeftButtonUp
。不幸的是,为了在大多数场景中进行设置,需要进行大量繁琐的工作。
因此,本文将介绍一个拖放管理器,该管理器可用于各种解决方案,它处理了大多数不便的任务,如 Z 顺序映射或面板到面板的移动。此外,拖放管理器通过类的组合(装饰器模式)和创建新的支持拖放的样式模板控件来支持重构场景。
在解释此控件的用法之前,我想总结一下此组件对象的主要命名约定。
- Surface 描述放置目标。它可以是任何派生自
Panel
的对象。IDropSurface
和IZoomAbleSurface
这两个接口可以由任何Surface 实现。 DragAndDropElement
描述拖放操作中的拖动元素。- 枚举
DragAndDropMode
表示当DragAndDropElement
被拖动时发生的情况。
DragDropManager 组件的类图

创建拖放管理器
演示程序分为两个不同的部分。上半部分通过组合创建拖放管理器。另一部分使用 DragAndDropControl
,它构成了支持样式和动画的模板控件。
通过组合创建拖放管理器
在许多场景中,可能需要重构已创建的控件元素。在这些情况下,使用嵌入的 DragAndDropBuilder
可能是最方便的方式。此生成器将创建 DragAndDropManager
对象并挂接到相应的事件。Panel
对象的可枚举(allowedDropSurfaces
)表示可以放置 DragAndDropElement
的 Panel
对象。如果 startDraggingOnMouseClick
为 true
,它还将 DragAndDropElement
的 MouseLeftButtonDown
事件与拖动开始挂钩。
请注意,生成器不支持将 DragAndDropMode CloningOnce
设置与 startDraggingOnMouseClick
结合使用。如果需要使用 DragAndDropMode CloningOnce
组合 DragAndDropElement
,请将 startDraggingOnMouseClick
设置为 false
并在应用程序中编写必要的代码。例如,请参阅 DragAndDropControl
的实现。
static public class DragAndDropBuilder
{
public static DragAndDropManager Create(
FrameworkElement dragAndDropElement,
IEnumerable allowedDropSurfaces,
DragAndDropMode mode,
bool startDraggingOnMouseClick)
{
bool cloningEnabled = false;
bool cloningOnlyOnce = false;
switch (mode)
{
case DragAndDropMode.Moving:
break;
case DragAndDropMode.CloningOnce:
cloningEnabled = true;
cloningOnlyOnce = true;
break;
case DragAndDropMode.Cloning:
cloningEnabled = true;
break;
}
var dadm = new DragAndDropManager(
dragAndDropElement,
allowedDropSurfaces,
cloningEnabled,
cloningOnlyOnce);
Debug.Assert(
!(mode == DragAndDropMode.CloningOnce && startDraggingOnMouseClick),
"Unfortunately there is no support " +
"of the CloningOnce mode in combination with an " +
"automatical dragging handler. " +
"Set startDraggingOnMouseClick to false.");
if (startDraggingOnMouseClick)
{
dragAndDropElement.MouseLeftButtonDown +=
new MouseButtonEventHandler(dadm.StartDragging);
if (mode == DragAndDropMode.Cloning || mode == DragAndDropMode.CloningOnce)
dadm.ClonedDragDropElement += new
EventHandler<clonedeventargs<frameworkelement>
(dadm_ClonedDragDropElement);
}
return dadm;
}
static void dadm_ClonedDragDropElement(object sender,
ClonedEventArgs<frameworkelement /> e)
{
e.ClonedElement.MouseLeftButtonDown +=
new MouseButtonEventHandler(((DragAndDropManager)sender).StartDragging);
}
...
}
在主应用程序中的用法(Page.xaml.cs)
var allowedSurfaces_1 = new Panel[] { this.CanvasDandD_1 };
DragAndDropBuilder.Create(
this.EllipseMoving,
allowedSurfaces_1,
DragAndDropMode.Moving,
true);
通过 DragAndDropControl 创建拖放管理器
使用 DragAndDropControl
时,DragAndDropManager
的创建完全在后台进行。因此,当从头开始构建支持拖放的应用程序或实现其他支持拖放的控件时,很可能也会使用此控件。DragAndDropControl
被构建为模板控件,并支持各种视觉状态(用于动画)和依赖项属性。因此,它完全支持在 Expression Blend 中进行自定义。
控件模板(XAML 代码)
<Style TargetType="local:DragAndDropControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DragAndDropControl">
<Grid Background="{TemplateBinding Background}" x:Name="LayoutRoot">
<!-- State manager -->
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualStateGroup.Transitions>
</vsm:VisualStateGroup.Transitions>
<vsm:VisualState x:Name="Normal">
<Storyboard />
</vsm:VisualState>
<vsm:VisualState x:Name="MouseOver">
<Storyboard />
</vsm:VisualState>
<vsm:VisualState x:Name="Disabled">
<Storyboard />
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="DraggingStates">
<vsm:VisualStateGroup.Transitions>
</vsm:VisualStateGroup.Transitions>
<vsm:VisualState x:Name="DragStarted">
<Storyboard />
</vsm:VisualState>
<vsm:VisualState x:Name="Dropped">
<Storyboard />
</vsm:VisualState>
</vsm:VisualStateGroup>
<vsm:VisualStateGroup x:Name="SurfaceStates">
<vsm:VisualState x:Name="SurfaceEnter">
<Storyboard />
</vsm:VisualState>
<vsm:VisualState x:Name="SurfaceLeave">
<Storyboard />
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<!-- Content -->
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}">
<ContentPresenter
x:Name="contentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
VerticalAlignment="{TemplateBinding
VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding
HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
演示程序展示了一个非常简单的示例,说明如何自定义 DragAndDropControl
的样式。然后可以使用此样式,并按以下代码片段设置内容。
<DragDropManager:DragAndDropControl BorderBrush="#FF000000"
Foreground="#FF000000" Height="50"
Margin="30,61,225,90" Width="100" BorderThickness="2,2,2,2"
Style="{StaticResource DragAndDropControlStyle}"
d:LayoutOverrides="HorizontalAlignment, VerticalAlignment"
x:Name="ControlMoving" Padding="2,2,2,2"
Content="Moving">
唯一可能在代码中完成的部分是分配有效的 Surface
var allowedSurfaces_2 = new Panel[] { this.GridDandD_2, this.StackPanelDandD_3 };
this.ControlMoving.AllowedDropSurfaces = allowedSurfaces_2;
DragAndDropMode
枚举 DragAndDropMode
用于设置 DragAndDropManager
对象的不同初始状态。
Moving
:DragAndDropElement
从一个地方移动到另一个地方。Cloning
:在元素可以移动到其他地方之前,会克隆DragAndDropElement
。克隆体继承了克隆行为。CloningOnce
:DragAndDropElement
仅在第一次时克隆。任何克隆的元素之后只能移动。
Surface 接口(缩放行为和通知)
该组件支持任何用作潜在目标的 Panel
对象继承两个相关的、可选的接口。第一个接口 IZoomableSurface
实现了一个成员 ZoomFactor
,并表示该对象的缩放因子。当鼠标悬停在 Surface 上时,将使用此缩放因子并相应地缩放 DragAndDropElement
。或者,Surface 也可以使用任何类型的缩放变换。DragAndDropElement
在拖动操作期间也会识别此缩放变换。在大多数情况下,只有当 Surface 通过不受支持的矩阵变换进行变换,或者当 Surface 元素的缩放需要不同于 DragAndDropElement
的缩放时,才可能使用 IZoomableSurface
接口。
第二个接口使 Surface 元素能够响应各种拖放状态(例如,OnDropped
、OnDragStart
等)。请参阅以下代码,这是一个如何使用这些接口的简单示例。
public class MyDropStackPanel : StackPanel, IDropSurface, IZoomAbleSurface
{
public static TextBlock NotificationTextBlock { get; set; }
public MyDropStackPanel()
{
this.ZoomFactor = 1.5d;
}
#region IDropSurface Members
public void OnDropping(FrameworkElement droppingObject)
{
droppingObject.Margin = new Thickness(0.0d);
}
public void OnDropped(FrameworkElement droppedObject)
{
droppedObject.RenderTransform = null;
droppedObject.VerticalAlignment = VerticalAlignment.Center;
}
public void OnDragStart(FrameworkElement draggedObject)
{
NotificationTextBlock.Text = "Dragging started";
}
public void OnDragMouseOver(FrameworkElement draggedObject)
{
NotificationTextBlock.Text = "Mouse moved while element is dragged.
Dragged element name: " + draggedObject.Name;
}
public void OnElementSurfaceEnter(FrameworkElement enterObject)
{
NotificationTextBlock.Text = "Mouse entered surface while element is dragged.
Dragged element name: " + enterObject.Name;
}
public void OnElementSurfaceLeave(FrameworkElement leaveObject)
{
NotificationTextBlock.Text = "Mouse left surface while element is dragged.
Dragged element name: " + leaveObject.Name;
}
#endregion
#region IZoomAbleSurface Members
public double ZoomFactor
{
get; set;
}
#endregion
}
DragDropManager 和 DragAndDropControl 事件
响应特定状态的另一种方法是通过 DragAndDropManager
事件。请注意,模板控件 DragAndDropControl
实现完全相同的事件。这些事件及其用法应该很直观。因此,我不再赘述。但是,在 DragAndDropControl
内部实现了一个可用的示例。
public event EventHandler<ClonedEventArgs<FrameworkElement>> ClonedDragDropElement;
public event EventHandler<DroppedEventArgs> Dropped;
public event EventHandler<DragStartedEventArgs> DragStarted;
public event EventHandler<DraggingEventArgs> Dragging;
public event EventHandler<SurfaceEnterLeaveEventArgs<FrameworkElement>> SurfaceEnter;
public event EventHandler<SurfaceEnterLeaveEventArgs<FrameworkElement>> SurfaceLeave;
幕后
在接下来的部分,我将讨论 DragAndDropManager
对象的一些内部工作原理。在 Silverlight 中开发支持拖放功能的应用程序时,我们首先需要确定在屏幕上移动对象的最佳通用方法。总的来说,我认为有两种选择。第一种选择是设置 Canvas.Left
和 Canvas.Top
依赖项属性。此解决方案的负面影响是拖放仅在派生自 Canvas
的对象上有效。第二种选择是使用 TranslateTransform
。此选项的优点是可以用于任何派生自 Panel
的 Surface。因此,为该组件选择了第二种选项。
以下代码用于 DragAndDropManager
对象内部,并启动拖动行为。我想特别提一下 SetDragDropElementLayoutToDefaultAndStore()
函数,它设置了 DragAndDropManager
元素默认的布局行为。它忽略 Canvas
和 Margin
设置,并将默认方向设置为左和上。一方面,这使得界面设计人员可以相当自由地布局初始位置。另一方面,它强制在目标区域中采用特定的布局行为。如果需要自定义此行为,则应在适当的 DragAndDropManager
事件中实现,或通过在放置目标中实现 IDropSurface
接口来实现。
[SuppressMessageAttribute(
"Microsoft.Security",
"CA2109",
Justification = "Can be used by external components " +
"in order to start dragging on mouse clicks")]
public void StartDragging(object sender, MouseButtonEventArgs e)
{
if (!this.StartDragging(e))
throw new InvalidOperationException("Unfortunately " +
"the dragging could not be initialized successfully.");
}
public bool StartDragging(MouseButtonEventArgs e)
{
// Only initialize dragging mode if mouse was not captured
if (this.mouseCaptured)
return false;
InitDraggingMode(e);
return true;
}
private void InitDraggingMode(MouseButtonEventArgs e)
{
CloneDragDropItem();
SetDragDropElementLayoutToDefaultAndStore();
CallIDropSurfaceEvent_OnDragStart();
InitDragAndDropElementLayout(e);
InitMouseForDragging();
this.initialZIndex =
SilverlightExtensions.GetZIndexAndSetMaxZIndex(this.DragAndDropElement);
Debug.Assert(
this.initialZIndex < short.MaxValue - 1,
"draggingInitialZIndex is supposed to have a smaller z value");
FocusDragAndDropElement();
// Send drag started event
CallEvent_DragStarted();
}
private void CloneDragDropItem()
{
if (this.CloningEnabled)
{
// --> Extension method: SilverlightExtensions.CloneObject(this);
var dragClone = this.DragAndDropElement.Clone();
CallEvent_Cloned(dragClone);
((Panel)this.DragAndDropElement.Parent).Children.Add(dragClone);
// Disable cloning from future cloning?
if (this.CloningOnlyOnce)
{
this.CloningEnabled = false;
this.removeWhenDropCancelled = true;
}
}
}
private void SetDragDropElementLayoutToDefaultAndStore()
{
// Store any kind of settings which might change during a drag drop operation
this.dragInitPos = new Point(
Canvas.GetLeft(this.DragAndDropElement),
Canvas.GetTop(this.DragAndDropElement));
this.dragInitMargin = this.DragAndDropElement.Margin;
this.dragInitHorizontalAlignment = this.DragAndDropElement.HorizontalAlignment;
this.dragInitVerticalAlignment = this.DragAndDropElement.VerticalAlignment;
this.dragInitRenderTransform = this.DragAndDropElement.RenderTransform;
this.dragInitRenderTransformOrigin = this.DragAndDropElement.RenderTransformOrigin;
// Ignore any kind of canvas settings, margins and alignment
Canvas.SetLeft(this.DragAndDropElement, 0.0);
Canvas.SetTop(this.DragAndDropElement, 0.0);
this.DragAndDropElement.Margin = new Thickness(0.0);
this.DragAndDropElement.HorizontalAlignment = HorizontalAlignment.Left;
this.DragAndDropElement.VerticalAlignment = VerticalAlignment.Top;
this.DragAndDropElement.RenderTransformOrigin = new Point(0.5, 0.5);
this.dragStartPanel = (Panel)this.DragAndDropElement.Parent;
// Get object transform except translating
this.dragInitTransformExcludeTranslate =
this.DragAndDropElement.RenderTransform.ExcludeTransform(
typeof(TranslateTransform));
}
private void InitDragAndDropElementLayout(MouseButtonEventArgs e)
{
// Get highest parent panel
Panel highestPanel =
SilverlightExtensions.GetFirstParentPanel(this.DragAndDropElement);
// Move this object to highest panel in order to be moved anywhere
MoveDragDropElementToPanel(highestPanel);
// Set correct X and Y position
var transform =
this.CombineTransformations(
e.GetPosition(highestPanel),
null,
1.0,
1.0);
this.DragAndDropElement.RenderTransform = transform;
}
private void MoveDragDropElementToPanel(Panel highestPanel)
{
SilverlightExtensions.RemoveElementFromParentPanel(this.DragAndDropElement);
if (highestPanel != null)
highestPanel.Children.Add(this.DragAndDropElement);
}
private void InitMouseForDragging()
{
this.mouseCaptured = true;
if (!this.DragAndDropElement.CaptureMouse())
throw new InvalidOperationException("Mouse could not be captured");
this.DragAndDropElement.Cursor = Cursors.Hand;
}
private void FocusDragAndDropElement()
{
// DragAndDrop manager should not require a Control derived type
// but supports Focusing
if (this.DragAndDropElement.GetType().GetAllBaseTypes().Contains(typeof(Control)))
((Control)this.DragAndDropElement).Focus();
}
DragAndDropManager
中的其他代码处理不同类型的变换(缩放、定位和组合)、对所述接口的支持(通过反射)、取消拖放操作以及事件通知。我非常希望得到关于这些实现的反馈。但是,我认为解释所有这些小功能有点啰嗦。因此,此时我想引用 DragAndDropManager
类本身。
已知限制/潜在问题
尽管我已尽力最大程度地减少了大部分缺点,但我仍想解释已知的潜在合规性。
- 如果对象的名称不唯一,则会抛出消息为“值不在预期范围内”的异常。当同一
Panel
中的两个对象具有相同的名称(如果它们有名称)时,Silverlight 会抛出这个非常直观的异常。这种情况在拖放场景中很容易发生。 - 绑定必须在所有潜在的 Surface 中都有效。例如,如果您为一个特殊的 Surface 创建了一个用户控件,并在该用户控件中设置了
DataContext
属性以绑定DragAndDropElement
的依赖项属性,则该绑定在拖动模式或将元素放置到另一个 Surface 后将无效。因此,DragAndDropElement
的绑定策略应包含不同的潜在父级。 - Surface 需要背景色。否则,它们不会被暴露用于命中测试(https://silverlight.net/forums/p/34201/104012.aspx)。
- 在开始拖动时,
DragAndDropElement
应具有固定的宽度和高度。 DragAndDropBuilder
不支持完全创建支持CloningOnce
的DragAndDropElement
。- 派生自
Panel
的对象应该是“主”父级。开箱即用,这通常是一个名为“LayoutRoot
”的Grid
。 DragAndDropElement
目前不支持矩阵变换。
结论
到目前为止,我已将此组件用于两个 Silverlight 项目。其中一个项目是一种图形模板编辑器,支持拖放。它在这些项目中运行得相当不错。
由于目前没有进一步的结论,我想说的是,我非常感谢您对这个控件和/或本文的任何评论、想法和批评。
参考文献
- Tamir Khason:http://blogs.microsoft.co.il/blogs/tamir/archive/2008/05/06/drawingbrush-and-deep-clone-in-silverlight.aspx。关于如何克隆依赖项属性的非常好的代码片段。他的代码片段在此文章中稍作了重构。
- 我的博客:http://coding-sos.spaces.live.com/blog/cns!6BCC7E85FC29F07F!184.entry。虽然不太体面,但我还是想在我的博客中简要介绍一下为什么克隆在 Silverlight 应用程序中不支持事件。由于这一缺点,必须进行一些更改才能支持
CloningOnce
行为。
历史
- 2009 年 2 月 12 日 - 版本 1.0 已发布