65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (16投票s)

2009年2月11日

CPOL

8分钟阅读

viewsIcon

82776

downloadIcon

1701

一篇关于灵活的 Silverlight 2 拖放管理器和 TemplateControl 的文章。

引言

Silverlight 2 提供了许多控件。不幸的是,它们中没有一个支持许多业务应用程序所需的拖放功能。本文提供了一个拖放管理器的实现,该管理器应足以满足大多数业务应用程序的需求。

特点

  • 支持不同的拖放模式
    • 移动模式
    • 克隆模式
    • 一次性克隆模式
  • 通过单独的接口或嵌入的缩放变换进行完全缩放/缩放支持
  • 拖放操作期间的视觉反馈(包括缩放/缩放支持)
  • 支持任何 Panel 作为潜在的放置目标
  • 支持任何派生自 FrameworkElement 的类型的拖放
  • 通过模板控件支持样式
  • 支持拖放行为的组合和继承
  • 集成 Z 顺序和焦点(仅适用于派生自 Control 的元素)处理
  • 拖放元素和放置目标的事件处理
  • 完全支持 Expression Blend(依赖项属性/动画状态)
  • 键盘支持(按 Escape 键取消)

背景

Silverlight 2 为 .NET 开发人员提供了一个非常好的开发环境。不幸的是,一些在大多数企业应用程序中必需的控件/功能缺失。其中一项缺失的功能就是拖放处理。

当今大多数 Silverlight 应用程序中实现拖放的方式是一种非常直接的方法,直接处理必要的鼠标事件,如 MouseMoveMouseLeftButtonUp。不幸的是,为了在大多数场景中进行设置,需要进行大量繁琐的工作。

因此,本文将介绍一个拖放管理器,该管理器可用于各种解决方案,它处理了大多数不便的任务,如 Z 顺序映射或面板到面板的移动。此外,拖放管理器通过类的组合(装饰器模式)和创建新的支持拖放的样式模板控件来支持重构场景。

在解释此控件的用法之前,我想总结一下此组件对象的主要命名约定。

  • Surface 描述放置目标。它可以是任何派生自 Panel 的对象。IDropSurfaceIZoomAbleSurface 这两个接口可以由任何Surface 实现。
  • DragAndDropElement 描述拖放操作中的拖动元素。
  • 枚举 DragAndDropMode 表示当 DragAndDropElement 被拖动时发生的情况。

DragDropManager 组件的类图

创建拖放管理器

演示程序分为两个不同的部分。上半部分通过组合创建拖放管理器。另一部分使用 DragAndDropControl,它构成了支持样式和动画的模板控件。

通过组合创建拖放管理器

在许多场景中,可能需要重构已创建的控件元素。在这些情况下,使用嵌入的 DragAndDropBuilder 可能是最方便的方式。此生成器将创建 DragAndDropManager 对象并挂接到相应的事件。Panel 对象的可枚举(allowedDropSurfaces)表示可以放置 DragAndDropElementPanel 对象。如果 startDraggingOnMouseClicktrue,它还将 DragAndDropElementMouseLeftButtonDown 事件与拖动开始挂钩。

请注意,生成器不支持将 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 对象的不同初始状态。

  • MovingDragAndDropElement 从一个地方移动到另一个地方。
  • Cloning:在元素可以移动到其他地方之前,会克隆 DragAndDropElement。克隆体继承了克隆行为。
  • CloningOnceDragAndDropElement 仅在第一次时克隆。任何克隆的元素之后只能移动。

Surface 接口(缩放行为和通知)

该组件支持任何用作潜在目标的 Panel 对象继承两个相关的、可选的接口。第一个接口 IZoomableSurface 实现了一个成员 ZoomFactor,并表示该对象的缩放因子。当鼠标悬停在 Surface 上时,将使用此缩放因子并相应地缩放 DragAndDropElement。或者,Surface 也可以使用任何类型的缩放变换。DragAndDropElement 在拖动操作期间也会识别此缩放变换。在大多数情况下,只有当 Surface 通过不受支持的矩阵变换进行变换,或者当 Surface 元素的缩放需要不同于 DragAndDropElement 的缩放时,才可能使用 IZoomableSurface 接口。

第二个接口使 Surface 元素能够响应各种拖放状态(例如,OnDroppedOnDragStart 等)。请参阅以下代码,这是一个如何使用这些接口的简单示例。

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.LeftCanvas.Top 依赖项属性。此解决方案的负面影响是拖放仅在派生自 Canvas 的对象上有效。第二种选择是使用 TranslateTransform。此选项的优点是可以用于任何派生自 Panel 的 Surface。因此,为该组件选择了第二种选项。

以下代码用于 DragAndDropManager 对象内部,并启动拖动行为。我想特别提一下 SetDragDropElementLayoutToDefaultAndStore() 函数,它设置了 DragAndDropManager 元素默认的布局行为。它忽略 CanvasMargin 设置,并将默认方向设置为左和上。一方面,这使得界面设计人员可以相当自由地布局初始位置。另一方面,它强制在目标区域中采用特定的布局行为。如果需要自定义此行为,则应在适当的 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 不支持完全创建支持 CloningOnceDragAndDropElement
  • 派生自 Panel 的对象应该是“主”父级。开箱即用,这通常是一个名为“LayoutRoot”的 Grid
  • DragAndDropElement 目前不支持矩阵变换。

结论

到目前为止,我已将此组件用于两个 Silverlight 项目。其中一个项目是一种图形模板编辑器,支持拖放。它在这些项目中运行得相当不错。

由于目前没有进一步的结论,我想说的是,我非常感谢您对这个控件和/或本文的任何评论、想法和批评。

参考文献

历史

  • 2009 年 2 月 12 日 - 版本 1.0 已发布
© . All rights reserved.