29

Currently I have in my XAML

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

All of my drag/drop code exists within the codebehind of my View, rather than within my ViewModel.

How can I handle drag/drop in my ViewModel without adding any dependencies on the View?

6 Answers 6

35

There are libraries for this such as gong and similar snippets on various blog articles.

However, you shouldn't get too hung up on having absolutely no code-behind. For example, this is still MVVM in my book:

void ButtonClicked(object sender, EventArgs e)
{
    ((MyViewModel) this.DataContext).DoSomething();
}

A command binding might be a better choice, but the logic is definitely in the viewmodel. With something like Drag and Drop, it's more variable where you want to draw the line. You can have code-behind interpret the Drag Args and call methods on the viewmodel when appropriate.

1
  • 5
    it's ok solution if you have a small and static model, but not if you need loose coupling and make use of dependency injection.
    – bitman
    Commented Nov 27, 2015 at 15:27
27

Here is some code I wrote that allows you to drag and drop files onto a control without violating MVVM. It could easily be modified to pass the actual object instead of a file.

/// <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");
        }
    }
}

Usage:

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

Ensure the DataContext inherits from IFileDragDropTarget and implements the OnFileDrop.

public class MyDataContext : ViewModelBase, IFileDragDropTarget
{
    public void OnFileDrop(string[] filepaths)
    {
        //handle file drop in data context
    }
}
4
  • Excellent work! Worked for me in VS2017 out of the box.
    – metal
    Commented Oct 4, 2018 at 13:18
  • Some reason cant get this to work on a <border>, anyone know why?
    – Alfie
    Commented Nov 6, 2018 at 10:29
  • 3
    @Alfie That's because Border doesn't inherit from Control, and the OnFileDragDropEnabled handler is specifically checking for that type. However, the drag and drop events are inherited from UIElement, which Border does inherit from. You could potentially modify the method to check for that so it includes more things. I'm not sure if there are any other impacts to consider with that though.
    – Herohtar
    Commented Nov 28, 2019 at 1:54
  • Any idea why I get "The attachable property was not found in type" error in my XAML for the dependency properties? Commented Mar 31, 2020 at 8:39
6

Here is a solution a bit more generic, out-of-the-box and easier than Mustafa's one, with a single DependencyProperty

  1. Copy this interface in your projet
public interface IFilesDropped
{
    void OnFilesDropped(string[] files);
}
  1. Make you ViewModel implement the interface
public class SomeViewModel : IFilesDropped
{
    public void OnFilesDropped(string[] files)
    {
        // Implement some logic here
    }
}
  1. Copy this generic extension in your project
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);
    }
}
  1. Enable the Drop files behavior to the UI components of you choice (here a TextBox)
<TextBox ns:DropFilesBehaviorExtension.IsEnabled ="True" />

Happy drops !

2
  • This is what I ended up using and it worked great. Where does one even begin with being able to do this on their own? I can follow the code for the most part but I would never have been able to come up with this on my own.
    – GarudaLead
    Commented Feb 10, 2022 at 12:07
  • Try MVVM with IoC - its better they said. Try MVVM with IoC its easier to maintain they said.. Jeez, drag n Drop was achieved with two lines of code before.. :) Thanks for the input though
    – Christian
    Commented Feb 7 at 8:44
2

This is just an additional answer that ports @Asheh's answer's to VB.NET for VB developers.

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
0

This might also be of some help to you. The attached command behavior library allows you to convert any event(s) into a command which will more closely adhere to the MVVM framework.

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

Using this is extremely easy. And has saved my bacon numerous times

Hope this helps

0

Based on @Kino101's answer, I created a WinUI 3 version. It is also available as a gist.

Helpers/DropFilesBehaviour.cs

using Microsoft.UI.Xaml;
using Windows.ApplicationModel.DataTransfer;

namespace App1.Helpers;

public interface IFilesDropped
{
    void OnFilesDropped(string[] files);
}

public class DropFilesBehavior
{
    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
        "IsEnabled", typeof(bool), typeof(DropFilesBehavior), PropertyMetadata.Create(default(bool), OnIsEnabledChanged));

    public static readonly DependencyProperty FileDropTargetProperty = DependencyProperty.RegisterAttached(
        "FileDropTarget", typeof(IFilesDropped), typeof(DropFilesBehavior), null);

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

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

    public static void SetFileDropTarget(DependencyObject obj, IFilesDropped value)
    {
        obj.SetValue(FileDropTargetProperty, value);
    }

    public static IFilesDropped GetFileDropTarget(DependencyObject obj)
    {
        return (IFilesDropped)obj.GetValue(FileDropTargetProperty);
    }

    private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var fe = d as FrameworkElement ?? throw new InvalidOperationException();
        
        if ((bool)e.NewValue)
        {
            fe.AllowDrop = true;
            fe.Drop += OnDrop;
            fe.DragOver += OnDragOver;
        }
        else
        {
            fe.AllowDrop = false;
            fe.Drop -= OnDrop;
            fe.DragOver -= OnDragOver;
        }
    }

    private static void OnDragOver(object sender, DragEventArgs e)
    {
        e.AcceptedOperation = DataPackageOperation.Move; // or Link/Copy
        e.Handled = true;
    }

    private static void OnDrop(object sender, DragEventArgs e)
    {
        var dobj = (DependencyObject)sender;
        var target = dobj.GetValue(FileDropTargetProperty);
        var filesDropped = target switch
        {
            IFilesDropped fd => fd,
            null => throw new InvalidOperationException("File drop target is not set."),
            _ => throw new InvalidOperationException($"Binding error, '{target.GetType().Name}' doesn't implement '{nameof(IFilesDropped)}'."),
        };

        if (filesDropped == null)
        {
            return;
        }

        var files = e.DataView.GetStorageItemsAsync().GetAwaiter().GetResult();
        if (files.Count == 0)
        {
            return;
        }

        filesDropped.OnFilesDropped(files.Select(f => f.Path).ToArray());
    }
}

Usage in your ViewModel, e.g. ViewModels/MainViewModel.cs

using App1.Helpers;

namespace App1.ViewModels;

public partial class MainViewModel : IFilesDropped
{
    public MainViewModel()
    {
    }

    public void OnFilesDropped(string[] files)
    {
        Debug.Print("---");
        foreach (var file in files)
        {
            Debug.Print($"{file}");
        }
    }
}

Usage in your view, e.g. Views/MainPage.xaml

<Page
    x:Class="App1.Views.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:helpers="using:App1.Helpers" 
    Background="{ThemeResource SolidBackgroundFillColorBaseBrush}"
    mc:Ignorable="d">

    <Grid x:Name="ContentArea" helpers:DropFilesBehavior.IsEnabled="True" helpers:DropFilesBehavior.FileDropTarget="{x:Bind ViewModel}">
                
    </Grid>
</Page>

Not the answer you're looking for? Browse other questions tagged or ask your own question.