Interactive drag and drop panel in WPF

Reading Time: 6 minutes

One of my countless projects I do is a WPF application. Nothing really fancy, just a plain form based app. Doing some business logic, mostly acting as a simple front-end.

I still have not fully eradicated the famous engineer pride. So I often try to do things right. Like they supposed to be done.

I don’t always succeed, one sometimes has to be a pragmatic and choose business increment over the rules of art.

But this time I mostly succeeded. I managed to solve a technical problem of keeping MVVM approach in the really unpleasant case, so I thought I will tell you how I did it. Maybe some of you will benefit from it some day.

What’s the problem?

I will obviously not show you the real project and its problems here, I am loyal to my clients – they wouldn’t like me using their project like a dummy in a P. E. class.

Let’s say I have specific and customized list of images. You can drag an image file and drop it into this gray area. It would look something like this:

That’s a very simple control, it consists of a simple XAML view with very small code-behind, viewmodel with a few basic properties and the implementation of ICommand interface – that does all the UI logic for a viewmodel. #justWPFthings

Here is our view:

<Window x:Class="BlogPostApp.MainWindow"
        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:local="clr-namespace:BlogPostApp"
        xmlns:viewModels="clr-namespace:BlogPostApp.ViewModels"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance {x:Type viewModels:MainViewModel}}"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <DataTemplate x:Key="ImageListItemTemplate">
            <WrapPanel>              <Image Width="200" Height="200" Margin="3" Stretch="Uniform" Source="{Binding}" />
            </WrapPanel>
        </DataTemplate>
    </Window.Resources>
    <StackPanel>
        <StackPanel AllowDrop="True" x:Name="DragDropPanel" Background="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" MinHeight="200" VerticalAlignment="Top">
            <ListView x:Name="ListOfImages" ItemTemplate="{StaticResource ImageListItemTemplate}" ItemsSource="{Binding Images, ValidatesOnDataErrors=True}" Focusable="False" Foreground="{x:Null}" BorderBrush="{x:Null}" Background="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}">
                <ListView.ItemsPanel>
                    <ItemsPanelTemplate >
                        <VirtualizingStackPanel IsItemsHost="True" Orientation="Horizontal" CanHorizontallyScroll="True" />
                    </ItemsPanelTemplate>
                </ListView.ItemsPanel>
            </ListView>
            <Label x:Name="DragDropPanelLabel" Content="Drag and Drop image" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" Visibility="{Binding DropPanelLabelVisibility}" Foreground="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" FontFamily="Segoe UI Light" FontSize="36" />
        </StackPanel>
    </StackPanel>
</Window>
namespace BlogPostApp
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainViewModel ViewModel { get; set; }

        public MainWindow()
        {
            InitializeComponent();
            DataContext = ViewModel = new MainViewModel();
            DragDropPanel.Drop +=
                (sender, args) => ViewModel.DropImageCommand.Execute(args.Data.GetData(DataFormats.FileDrop));
            DragDropPanel.Drop += (sender, args) => DragDropPanel.Opacity = 1d;
            DragDropPanel.DragOver += (sender, args) => DragDropPanel.Opacity = 0.7d;
            DragDropPanel.DragLeave += (sender, args) => DragDropPanel.Opacity = 1d;
        }
    }
}

Here is viewmodel:

namespace BlogPostApp.ViewModels
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ICommand DropImageCommand { get; set; }
        public ObservableCollection<string> Images { get; set; }
        public Visibility DropPanelLabelVisibility => Images.Any() ? Visibility.Collapsed : Visibility.Visible;

        public MainViewModel()
        {
            DropImageCommand = new DropImageCommand(this);
            Images = new ObservableCollection<string>();
            Images.CollectionChanged += OnImagesCollectionChange;
        }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private void OnImagesCollectionChange(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
        {
            OnPropertyChanged(nameof(Images));
            OnPropertyChanged(nameof(DropPanelLabelVisibility));
        }
    }
}

 

And here is DropImageCommand:

namespace BlogPostApp.Commands
{
    public class DropImageCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        private readonly List<string> _supportedImageFormats;
        private readonly MainViewModel _viewModel;

        public DropImageCommand(MainViewModel viewModel)
        {
            _viewModel = viewModel;
            _supportedImageFormats = new List<string>
            {
                ".jpg",
                ".jpeg",
                ".png"
            };
        }

        public bool CanExecute(object parameter)
        {
            var imagePaths = ToArrayOfFilePaths(parameter);
            return imagePaths.Length != 0 &&
                   imagePaths.Any(imagePath => _supportedImageFormats.Contains(Path.GetExtension(imagePath)));
        }

        public void Execute(object parameter)
        {
            var imagePaths = ToArrayOfFilePaths(parameter);
            var filteredImagePaths = FilterNotSupportedFormats(imagePaths);

            foreach (var imagePath in filteredImagePaths)
            {
                _viewModel.Images.Add(imagePath);
            }
        }

        private string[] ToArrayOfFilePaths(object givenParameter)
        {
            return givenParameter as string[] ?? new string[] { };
        }

        private string[] FilterNotSupportedFormats(string[] filePaths)
        {
            return filePaths.Where(filePath => _supportedImageFormats.Contains(Path.GetExtension(filePath))).ToArray();
        }
    }
}

See? Nothing scary, it literally took me 2 minutes to spit this code out of me. WPF is easy.

Now, how do I give it a bit of live? Let’s turn a simple image list element into an independent, strong entity of my application!

I want each of the photos I drop into that gray hole of sorrow to have an on-hover overlay with some weird caption that depends on something that I do with this picture.

For example let it have a default caption when the image is freshly dropped and a context menu with two options. Each option modifies the caption somehow.

Why? Well, let’s say I have my business reasons – why do you care?

Unleash the coder!

Rawr!

The key thing is to keep as much MVVM compliant as possible, as it seems to be the most acceptable way in the WPF world.

I have a working drag and drop panel so far. I drop the image, and it appears in the panel. That’s it. Good, let’s start with modifing the view a bit.

Why start with the view? Well, normally I start with the end of the logic so I know what exactly I have to implement. I am a lazy man, why should I think about all the things I could need in the front-end when I can just code the front-end and then I know everything I need. Whether we’re talking about XAML code, HTML or unit tests – it’s always a good place to start to define your goals.

Alright, it could look something like this:

<DataTemplate x:Key="ImageListItemTemplate">
            <WrapPanel>
                <WrapPanel.ContextMenu>
                    <ContextMenu>
                        <MenuItem Header="Do something" Command="{Binding DoSomethingCommand}"  CommandParameter="{Binding}" Visibility="{Binding DidSomething}" />
                        <MenuItem Header="Do something else" Command="{Binding DoSomethingElseCommand}"  CommandParameter="{Binding}" Visibility="{Binding DidSomethingElse}" />
                    </ContextMenu>
                </WrapPanel.ContextMenu>
                <Grid x:Name="ImageGrid">
                    <Image Width="200" Height="200" Margin="3" Stretch="Uniform" Source="{Binding Path}" />
                    <TextBlock Width="200" Text="{Binding Caption}"/>
                </Grid>
            </WrapPanel>
        </DataTemplate>

Right? Looks ok? Sure it does. Visual Studio will flash lights and colors into my face, because it does not recognize half of the stuff that’s written in there. Doesn’t matter – it will be fine soon.

So I know a need a viewmodel with certain properties. Let’s code it.

namespace BlogPostApp.ViewModels
{
    public class ImageListItemViewModel : INotifyPropertyChanged
    {
        public ICommand DoSomethingCommand { get; set; }
        public ICommand DoSomethingElseCommand { get; set; }
        public string Path { get; set; }

        public string Caption => DidSomething
            ? "Did Something"
            : DidSomethingElse
                ? "Did Something Else"
                : "Nothing has been done";

        public bool DidSomething { get; private set; }
        public bool DidSomethingElse { get; private set; }

        public ImageListItemViewModel()
        {
            DoSomethingCommand = new DoSomethingCommand();
            DoSomethingElseCommand = new DoSomethingElseCommand();
        }

        public void DoSomething()
        {
            DidSomething = true;
            DidSomethingElse = false;
            OnPropertyChanged(nameof(Caption));
        }

        public void DoSomethingElse()
        {
            DidSomething = false;
            DidSomethingElse = true;
            OnPropertyChanged(nameof(Caption));
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Yes. So depending on what do I click in the context menu of the image, the image caption will adapt. And thanks to INotifyPropertyChanged interface kindly provided by Microsoft my view will know about a change that happened somewhere lower.

Now I know I need two commands for my viewmodel: one that does something and the other that does something else. Cool, let’s code them.

namespace BlogPostApp.Commands
{
    public class DoSomethingCommand : ICommand
    {
        public bool CanExecute(object parameter)
        {
            if (parameter is ImageListItemViewModel imageViewModel)
            {
                return !imageViewModel.DidSomething;
            }

            return false;
        }

        public void Execute(object parameter)
        {
            if (parameter is ImageListItemViewModel imageViewModel)
            {
                imageViewModel.DoSomething();
            }
        }

        public event EventHandler CanExecuteChanged
        {
            add => CommandManager.RequerySuggested += value;
            remove => CommandManager.RequerySuggested -= value;
        }
    }
}
namespace BlogPostApp.Commands
{
    public class DoSomethingElseCommand : ICommand
    {
        public bool CanExecute(object parameter)
        {
            if (parameter is ImageListItemViewModel imageViewModel)
            {
                return !imageViewModel.DidSomethingElse;
            }

            return false;
        }

        public void Execute(object parameter)
        {
            if (parameter is ImageListItemViewModel imageViewModel)
            {
                imageViewModel.DoSomethingElse();
            }
        }

        public event EventHandler CanExecuteChanged
        {
            add => CommandManager.RequerySuggested += value;
            remove => CommandManager.RequerySuggested -= value;
        }
    }
}

Look, I know there are some code duplications, but can we pretend those commands really do different stuff? Come on man, it’s just a proof of concept.

Anyway, we have our commands – seems almost ready! Just a topping. Let’s give it a little style.

<Style x:Key="AnnotationStyle" TargetType="{x:Type TextBlock}">
            <Setter Property="Visibility" Value="Collapsed" />
            <Setter Property="Foreground" Value="WhiteSmoke"/>
            <Setter Property="Padding" Value="0,10"/>
            <Setter Property="HorizontalAlignment" Value="Center"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="TextAlignment" Value="Center"/>
            <Setter Property="TextWrapping" Value="Wrap"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorLevel=1, AncestorType={x:Type Grid}}}" Value="True" >
                    <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" />
                    <Setter Property="Visibility" Value="Visible" />
                </DataTrigger>
            </Style.Triggers>
        </Style>

Added it into window resources at the top. Now I can do this:

<TextBlock Width="200" Style="{StaticResource AnnotationStyle}" Text="{Binding Caption}"/>

And let’s deal with all those warnings in XAML with adding a namespace attribute to the Window element:

xmlns:viewModels="clr-namespace:BlogPostApp.ViewModels"

And DataContext to the list item template:

<DataTemplate x:Key="ImageListItemTemplate">
            <WrapPanel d:DataContext="{d:DesignInstance {x:Type viewModels:ImageListItemViewModel}}">
                <WrapPanel.ContextMenu>

Let’s run this!

That’s about it

My schedule is on the edge lately. As my sanity is. I have quite a stack of the projects for my clients to develop and a few of mine. The technology spread is huge: wpf, react, asp.net, php etc. Also a few trainings and presentations to conduct. With the weather we had for a last few days/weeks (extreme heat) it’s amazingly hard not to feel exhausted.

One of the ways to deal with it for me, is to create something simple and satisfying. Then show others how really simple that is and hope someone will find it useful.

So yes, you are my break in this mayhem of responsibilities. Thanks guys for keeping me sane, if you want to have a look or take some of the code – feel free to. As always you will find it on my github repo.

Now, time to get to work. See ya!

Leave a Reply

Your email address will not be published. Required fields are marked *