如何在不违反 MVVM 原则的情况下处理拖放?

Posted

技术标签:

【中文标题】如何在不违反 MVVM 原则的情况下处理拖放?【英文标题】:How to handle drag/drop without violating MVVM principals? 【发布时间】:2011-08-20 10:39:53 【问题描述】:

目前我的 XAML 中有

<TabControl  
    AllowDrop="True"
    PreviewDragOver="DragOver"
    PreviewDrop="Drop" />

我的所有拖放代码都存在于我的 View 的代码隐藏中,而不是我的 ViewModel 中。

如何在 ViewModel 中处理拖放操作而不在 View 上添加任何依赖项?

【问题讨论】:

【参考方案1】:

在各种博客文章中都有类似的库,例如 gong 和类似的 sn-ps。

但是,您不应该过于拘泥于完全没有代码隐藏。例如,这仍然是我书中的 MVVM:

void ButtonClicked(object sender, EventArgs e)

    ((MyViewModel) this.DataContext).DoSomething();

命令绑定可能是更好的选择,但逻辑肯定在视图模型中。使用拖放之类的东西,你想在哪里画线就更容易改变了。您可以在适当的时候让代码隐藏解释 Drag Args 并调用视图模型上的方法。

【讨论】:

如果你有一个小而静态的模型,这是可以的解决方案,但如果你需要松散耦合并使用依赖注入。【参考方案2】:

这是我编写的一些代码,允许您在不违反 MVVM 的情况下将文件拖放到控件上。它可以很容易地修改为传递实际对象而不是文件。

/// <summary>
/// IFileDragDropTarget Interface
/// </summary>
public interface IFileDragDropTarget

    void OnFileDrop(string[] filepaths);


/// <summary>
/// FileDragDropHelper
/// </summary>
public class FileDragDropHelper

    public static bool GetIsFileDragDropEnabled(DependencyObject obj)
    
        return (bool)obj.GetValue(IsFileDragDropEnabledProperty);
    

    public static void SetIsFileDragDropEnabled(DependencyObject obj, bool value)
    
        obj.SetValue(IsFileDragDropEnabledProperty, value);
    

    public static bool GetFileDragDropTarget(DependencyObject obj)
    
        return (bool)obj.GetValue(FileDragDropTargetProperty);
    

    public static void SetFileDragDropTarget(DependencyObject obj, bool value)
    
        obj.SetValue(FileDragDropTargetProperty, value);
    

    public static readonly DependencyProperty IsFileDragDropEnabledProperty =
            DependencyProperty.RegisterAttached("IsFileDragDropEnabled", typeof(bool), typeof(FileDragDropHelper), new PropertyMetadata(OnFileDragDropEnabled));

    public static readonly DependencyProperty FileDragDropTargetProperty =
            DependencyProperty.RegisterAttached("FileDragDropTarget", typeof(object), typeof(FileDragDropHelper), null);

    private static void OnFileDragDropEnabled(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        if (e.NewValue == e.OldValue) return;
        var control = d as Control;
        if (control != null) control.Drop += OnDrop;
    

    private static void OnDrop(object _sender, DragEventArgs _dragEventArgs)
    
        DependencyObject d = _sender as DependencyObject;
        if (d == null) return;
        Object target = d.GetValue(FileDragDropTargetProperty);
        IFileDragDropTarget fileTarget = target as IFileDragDropTarget;
        if (fileTarget != null)
        
            if (_dragEventArgs.Data.GetDataPresent(DataFormats.FileDrop))
            
                fileTarget.OnFileDrop((string[])_dragEventArgs.Data.GetData(DataFormats.FileDrop));
            
        
        else
        
            throw new Exception("FileDragDropTarget object must be of type IFileDragDropTarget");
        
    

用法:

<ScrollViewer AllowDrop="True" Background="Transparent" utility:FileDragDropHelper.IsFileDragDropEnabled="True" utility:FileDragDropHelper.FileDragDropTarget="Binding"/>

确保 DataContext 继承自 IFileDragDropTarget 并实现 OnFileDrop。

public class MyDataContext : ViewModelBase, IFileDragDropTarget

    public void OnFileDrop(string[] filepaths)
    
        //handle file drop in data context
    

【讨论】:

出色的工作!开箱即用,在 VS2017 中为我工作。 某些原因无法在 &lt;border&gt; 上运行,有人知道为什么吗? @Alfie 那是因为Border 没有从Control 继承,而OnFileDragDropEnabled 处理程序专门检查该类型。但是,拖放事件是从 UIElement 继承的,Border 确实 继承自 Border。您可能会修改该方法以检查它,以便它包含更多内容。不过,我不确定是否还有其他影响需要考虑。 知道为什么我在依赖属性的 XAML 中收到“在类型中找不到可附加属性”错误吗?【参考方案3】:

这是一个比 Mustafa 的解决方案更通用、开箱即用且更简单的解决方案,只有一个 DependencyProperty

    将此界面复制到您的项目中
public interface IFilesDropped

    void OnFilesDropped(string[] files);

    让您的 ViewModel 实现接口
public class SomeViewModel : IFilesDropped

    public void OnFilesDropped(string[] files)
    
        // Implement some logic here
    

    在您的项目中复制此通用扩展
public class DropFilesBehaviorExtension

    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
        "IsEnabled", typeof(bool), typeof(DropFilesBehaviorExtension), new FrameworkPropertyMetadata(default(bool), OnPropChanged)
        
            BindsTwoWayByDefault = false,
        );

    private static void OnPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    
        if (!(d is FrameworkElement fe))
            throw new InvalidOperationException();
        if ((bool)e.NewValue)
        
            fe.AllowDrop = true;
            fe.Drop += OnDrop;
            fe.PreviewDragOver += OnPreviewDragOver;
        
        else
        
            fe.AllowDrop = false;
            fe.Drop -= OnDrop;
            fe.PreviewDragOver -= OnPreviewDragOver;
        
    

    private static void OnPreviewDragOver(object sender, DragEventArgs e)
    
        // NOTE: PreviewDragOver subscription is required at least when FrameworkElement is a TextBox
        // because it appears that TextBox by default prevent Drag on preview...
        e.Effects = DragDropEffects.Move;
        e.Handled = true;
    

    private static void OnDrop(object sender, DragEventArgs e)
    
        var dataContext = ((FrameworkElement)sender).DataContext;
        if (!(dataContext is IFilesDropped filesDropped))
        
            if (dataContext != null)
                Trace.TraceError($"Binding error, 'dataContext.GetType().Name' doesn't implement 'nameof(IFilesDropped)'.");
            return;
        

        if (!e.Data.GetDataPresent(DataFormats.FileDrop))
            return;

        if (e.Data.GetData(DataFormats.FileDrop) is string[] files)
            filesDropped.OnFilesDropped(files);
    

    public static void SetIsEnabled(DependencyObject element, bool value)
    
        element.SetValue(IsEnabledProperty, value);
    

    public static bool GetIsEnabled(DependencyObject element)
    
        return (bool)element.GetValue(IsEnabledProperty);
    

    为您选择的 UI 组件(此处为 TextBox)启用拖放文件行为
<TextBox ns:DropFilesBehaviorExtension.IsEnabled ="True" />

滴滴快乐!

【讨论】:

这是我最终使用的,效果很好。一个人从哪里开始能够自己做到这一点?我大部分时间都可以按照代码进行操作,但我永远无法自己想出这个。【参考方案4】:

这只是为 VB 开发人员将 @Asheh 的答案移植到 VB.NET 的附加答案。

Imports System.Windows

Interface IFileDragDropTarget

    Sub OnFileDrop(ByVal filepaths As String())

End Interface

Public Class FileDragDropHelper

    Public Shared Function GetIsFileDragDropEnabled(ByVal obj As DependencyObject) As Boolean
        Return CBool(obj.GetValue(IsFileDragDropEnabledProperty))
    End Function

    Public Shared Sub SetIsFileDragDropEnabled(ByVal obj As DependencyObject, ByVal value As Boolean)
        obj.SetValue(IsFileDragDropEnabledProperty, value)
    End Sub

    Public Shared Function GetFileDragDropTarget(ByVal obj As DependencyObject) As Boolean
        Return CBool(obj.GetValue(FileDragDropTargetProperty))
    End Function

    Public Shared Sub SetFileDragDropTarget(ByVal obj As DependencyObject, ByVal value As Boolean)
        obj.SetValue(FileDragDropTargetProperty, value)
    End Sub

    Public Shared ReadOnly IsFileDragDropEnabledProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsFileDragDropEnabled", GetType(Boolean), GetType(FileDragDropHelper), New PropertyMetadata(AddressOf OnFileDragDropEnabled))

    Public Shared ReadOnly FileDragDropTargetProperty As DependencyProperty = DependencyProperty.RegisterAttached("FileDragDropTarget", GetType(Object), GetType(FileDragDropHelper), Nothing)

    Shared WithEvents control As Windows.Controls.Control
    Private Shared Sub OnFileDragDropEnabled(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
        If e.NewValue = e.OldValue Then Return
        control = TryCast(d, Windows.Controls.Control)
        If control IsNot Nothing Then
            AddHandler control.Drop, AddressOf OnDrop
        End If
    End Sub

    Private Shared Sub OnDrop(ByVal _sender As Object, ByVal _dragEventArgs As DragEventArgs)
        Dim d As DependencyObject = TryCast(_sender, DependencyObject)
        If d Is Nothing Then Return
        Dim target As Object = d.GetValue(FileDragDropTargetProperty)
        Dim fileTarget As IFileDragDropTarget = TryCast(target, IFileDragDropTarget)
        If fileTarget IsNot Nothing Then
            If _dragEventArgs.Data.GetDataPresent(DataFormats.FileDrop) Then
                fileTarget.OnFileDrop(CType(_dragEventArgs.Data.GetData(DataFormats.FileDrop), String()))
            End If
        Else
            Throw New Exception("FileDragDropTarget object must be of type IFileDragDropTarget")
        End If
    End Sub
End Class

【讨论】:

【参考方案5】:

这也可能对您有所帮助。附带的命令行为库允许您将任何事件转换为更符合 MVVM 框架的命令。

http://marlongrech.wordpress.com/2008/12/13/attachedcommandbehavior-v2-aka-acb/

使用它非常容易。还救了我无数次的培根

希望对你有帮助

【讨论】:

以上是关于如何在不违反 MVVM 原则的情况下处理拖放?的主要内容,如果未能解决你的问题,请参考以下文章

在不违反 SRP、OCP、DRY 的情况下编写测试

如何在不实际拖放的情况下使用 jQuery UI Droppable 触发 Drop 事件?

当前的 MVVM 视图模型是不是违反了单一职责原则?

如何在不违反主键约束的情况下插入具有循环引用的实体框架

如何在不违反命令和查询端分离的情况下基于元数据在 CQRS 中构建查询

如何在不违反 CSP 的情况下绑定剃须刀视图中的 onchange 等事件