This is a multipart series outlining several topics is WPF
I was given a task to do a simple demo on Drag and Drop in WPF. That's it, just plain vanilla D&D - well except for using a DataTemplateSelector and a StyleSelector. However, the same time, I wanted to learn and add to it, namely using an existing MVVM framework and Test Driven Development(TDD) or at least some bastardization of it. I figure one unit test at least is better than none and was a step in the right direction. I hadn't done D&D before so I had to spend some time seeing what others had done before.
In this demo, I will basically drag from one ListBox to another. Both the source (item dragged from) and target (item dragged to) ListBoxes will have the their respective ItemTemplateSelector and ItemContainerStyleSelector properties set to the same ResourceKeys, respectively. That is, both boxes will look and behave the same.
In this demo, I will basically drag from one ListBox to another. Both the source (item dragged from) and target (item dragged to) ListBoxes will have the their respective ItemTemplateSelector and ItemContainerStyleSelector properties set to the same ResourceKeys, respectively. That is, both boxes will look and behave the same.
Design and Architecture
Basically, I will follow the Model-View-ViewModel patten here. I wouldn't spend time going into the details of the pattern as there is enough literature on it out there to sink a few ships. However, rather than re-invent the wheel at each and every step, I decided to use the MVVMLight framework on this demo. I chose MvvmLight because it is pretty popular and based on videos and podcasts with Laurent, I wanted to try it out as it provides some nice helpers to make the development of a Mvvm based application much smoother:- INotifyPropertyChanged is already implemented in a ViewModelBase class
- RelayCommand
- Designer support in WPF (wraps the GetIsInDesignMode() method in an IsInDesignMode property)
- EventToCommand behavior to pipe a non-click based Command to handlers in the ViewModel
- Messenger to communicate between multiple views and view models
- a deterministic way of cleaning up
View -> ViewModel -> Model. This means the model knows nothing of the view.
The Model
This is a simple, static piece of data in this case. The actual Contact implements the IContact interface which has a single method called GetContacts() which returns an ObservableCollection of Contacts. I used an interface here to add some flexibility to the code, specifically, to use Dependency Injection to inject an instance of a type that implements IContact in the constructor of the view model. I have not actually done so in this case as it was not necessary. These are shown in Listings 1 and 2.1 using System.Collections.ObjectModel;23 namespace DragDropUsingMvvmLight01.Model4 {5 /// <summary>6 /// Contract depicting the state and behavior of what a Contact is in this context.7 /// </summary>8 public interface IContact9 {10 /// <summary>11 /// Retrieves a collection of Contact types.12 /// </summary>13 /// <returns>An ObservableCollection of Conctact types.</returns>14 ObservableCollection<Contact> GetContacts();15 }16 }
Listing 1: The interface that is the basis for what a Contact is
1 using System.Collections.ObjectModel;23 namespace DragDropUsingMvvmLight01.Model4 {5 public class Contact : IContact6 {7 #region Private Fields8 private ObservableCollection<Contact> _contacts;9 private const string ImgPath = @"images/";10 #endregion1112 #region Properties13 public string Name { get; set; }14 public string Alias { get; set; }15 public string Race { get; set; }16 public string Status { get; set; }17 public string ProfileImageUrl { get; set; }18 public Faction Crew { get; set; }19 #endregion2021 public Contact(){}2223 public ObservableCollection<Contact> GetContacts()24 {25 return _contacts = new ObservableCollection<Contact>26 {27 new Contact{Name = "Monkey D. Luffy", Alias = "Straw-Hat", Race = "Human", Status = "Captain", ProfileImageUrl = ImgPath + @"luffyLogo.jpg", Crew = Faction.Strawhat},28 new Contact{Name = "Roronoa Zoro", Alias = "Pirate Hunter", Race = "Human", Status = "Swordsman", ProfileImageUrl = ImgPath + @"zoroLogo.jpg", Crew = Faction.Strawhat },29 new Contact{Name = "Nami", Alias = "Pirate Robber", Race = "Human", Status = "Navigator", ProfileImageUrl = ImgPath + @"namiLogo.jpg", Crew = Faction.Strawhat },30 new Contact{Name = "Usopp", Alias = "Tell Tale Man", Race = "Human", Status = "Sharpshooter", ProfileImageUrl = ImgPath + @"usoppLogo.jpg", Crew = Faction.Strawhat },31 new Contact{Name = "Sanji", Alias = "", Race = "Human", Status = "Cook", ProfileImageUrl = ImgPath + @"sanjiLogo.jpg", Crew = Faction.Strawhat },32 new Contact{Name = "Chopper", Alias = "Tony Chopper", Race = @"Reindeer-human", Status = "Doctor", ProfileImageUrl = ImgPath + @"chopperLogo.jpg", Crew = Faction.Strawhat },33 new Contact{Name = "Nico Robin", Alias = "Miss Sunday", Race = "Human", Status = "Archaeologist", ProfileImageUrl = ImgPath + @"robinLogo.jpg", Crew = Faction.Strawhat },34 new Contact{Name = "Cutty Flam", Alias = "Franky", Race = "Cyborg", Status = "Shipwright", ProfileImageUrl = ImgPath + @"frankyLogo.jpg", Crew = Faction.Strawhat },35 new Contact{Name = "Brook", Alias = "Dead Bones", Race = "Human", Status = "Musician", ProfileImageUrl = ImgPath + @"brookLogo.jpg", Crew = Faction.Strawhat },36 new Contact{Name = "Portgas D. Ace", Alias = "Firefist Ace", Race = "Human", Status = "Commander", ProfileImageUrl = ImgPath + @"aceLogo.jpg", Crew = Faction.Whitebeard },37 new Contact{Name = "Edward Newgate", Alias = "Whitebeard", Race = "Human", Status = "Captain", ProfileImageUrl = ImgPath + @"whitebeardLogo.jpg", Crew = Faction.Whitebeard },38 };39 }40 }4142 /// <summary>43 /// Enumeration specifying which pirate crew each contact belongs to.44 /// </summary>45 public enum Faction46 {47 Strawhat,48 Whitebeard49 }50 }
Listing 2: The Contact class which implements the IContact interface
The View
The view as you would guess, is independent of the model. There isn't much to say here except to note that the DataContext property is set to a resource value which defines an instance of the ViewModelLocator class that will be discussed shortly. The templates and styles used here are defined in a ResourceDictionary is part of the package available for download.Listing 3 shows the view for this project.
1 <Window x:Class="DragDropUsingMvvmLight01.MainWindow"2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"4 xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"6 xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"7 xmlns:local="clr-namespace:DragDropUsingMvvmLight01"8 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"9 Title="MainWindow" Height="400"10 mc:Ignorable="d">11 <Window.Resources>12 <local:FactionDataTemplateSelector x:Key="dataTemplateSelector" />13 <local:ListBoxStyleSelectors x:Key="styleSelector" />14 </Window.Resources>15 <Window.DataContext>16 <Binding Path="Main" Source="{StaticResource Locator}" />17 </Window.DataContext>181920 <Grid x:Name="LayoutRoot" Background="AliceBlue">21 <Grid.RowDefinitions>22 <RowDefinition Height="Auto" />23 <RowDefinition Height="Auto" />24 <RowDefinition Height="Auto" />25 <RowDefinition Height="Auto" />26 <RowDefinition Height="Auto" />27 <RowDefinition Height="Auto" />28 <RowDefinition Height="Auto" />29 <RowDefinition Height="Auto" />30 </Grid.RowDefinitions>3132 <TextBlock Grid.Row="0"33 Background="White"34 FontSize="18"35 Text="Listbox to drag from" />36 <ListBox x:Name="dragSource"37 Grid.Row="1"38 MinWidth="100"39 MinHeight="40"40 Margin="0,0,0,0"41 AllowDrop="True"42 Background="AliceBlue"43 ItemContainerStyleSelector="{DynamicResource styleSelector}"44 ItemsPanel="{DynamicResource SourceListBoxItemsPanelTemplate}"45 ItemsSource="{Binding Contacts}"46 ItemTemplateSelector="{DynamicResource dataTemplateSelector}"47 Style="{DynamicResource ListBoxStyle1}">48 <i:Interaction.Triggers>49 <i:EventTrigger EventName="PreviewMouseLeftButtonDown">50 <cmd:EventToCommand Command="{Binding PreviewMouseLeftButtonDownCommand, Mode=OneWay}" PassEventArgsToCommand="True" />51 </i:EventTrigger>52 <i:EventTrigger EventName="DragOver">53 <cmd:EventToCommand Command="{Binding DragOverCommand, Mode=OneWay}" PassEventArgsToCommand="True" />54 </i:EventTrigger>55 <i:EventTrigger EventName="Drop">56 <cmd:EventToCommand Command="{Binding DropSourceCommand, Mode=OneWay}" PassEventArgsToCommand="True" />57 </i:EventTrigger>58 </i:Interaction.Triggers>59 </ListBox>6061 <Border Grid.Row="2" Height="20" />6263 <TextBlock Grid.Row="3"64 Background="White"65 FontSize="18"66 Text="Listbox to drop to" />6768 <ListBox x:Name="dropTarget"69 Grid.Row="4"70 MinWidth="100"71 MinHeight="117"72 Margin="0,0,0,0"73 AllowDrop="True"74 Background="#FFE4F3FD"75 ItemContainerStyleSelector="{DynamicResource styleSelector}"76 ItemsPanel="{DynamicResource SourceListBoxItemsPanelTemplate}"77 ItemsSource="{Binding TargetContacts}"78 ItemTemplateSelector="{DynamicResource dataTemplateSelector}"79 Style="{DynamicResource ListBoxStyle1}">80 <i:Interaction.Triggers>81 <i:EventTrigger EventName="Drop">82 <cmd:EventToCommand Command="{Binding DropTargetCommand, Mode=OneWay}" PassEventArgsToCommand="True" />83 </i:EventTrigger>84 <i:EventTrigger EventName="DragOver">85 <cmd:EventToCommand Command="{Binding DragOverCommand, Mode=OneWay}" PassEventArgsToCommand="True" />86 </i:EventTrigger>87 </i:Interaction.Triggers>88 </ListBox>89 <Button Grid.Row="5"90 Content="Reset Collections"91 Template="{DynamicResource ResourceKey=ButtonTemplate}">92 <i:Interaction.Triggers>93 <i:EventTrigger EventName="Click">94 <cmd:EventToCommand Command="{Binding ClickCommand, Mode=OneWay}" />95 </i:EventTrigger>96 </i:Interaction.Triggers>97 </Button>98 </Grid>99 </Window>100
Listing 3: The main view of the application
The ViewModels
The MvvmLight provides a ViewModelBase class. When a new MvvmLight project is created, two view models are also created, the ViewModelLocator and the MainViewModel which derives from ViewModelBase which takes care of implementing INotifyPropertyChanged so you don't need to keep doing boilerplate work over again.The next interesting piece of generated code is the ViewModelLocator class which can
be thought of as a centralized place for dealing with view models. You can instantiate your ViewModels in the locator class and use the ViewModelLocator as the DataContext of the view. By doing this, you will have access to all view models defined and exposed as properties in the locator class. For instance, in this case, the MainViewModel is exposed as the Main property as shown below in Listing 4.
1 namespace DragDropUsingMvvmLight01.ViewModel2 {3 /// <summary>4 /// This class contains static references to all the view models in the5 /// application and provides an entry point for the bindings.6 /// </summary>7 public class ViewModelLocator8 {9 private static MainViewModel _main;1011 /// <summary>12 /// Initializes a new instance of the ViewModelLocator class. This instance will further be used to13 /// create the MainViewModel14 /// </summary>15 public ViewModelLocator()16 {17 CreateMain();18 }1920 /// <summary>21 /// Gets the Main property.22 /// </summary>23 public static MainViewModel MainStatic24 {25 get26 {27 if (_main == null)28 {29 CreateMain();30 }3132 return _main;33 }34 }3536 /// <summary>37 /// Gets the Main property.38 /// </summary>39 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This non-static member is needed for data binding purposes.")]40 public MainViewModel Main41 {42 get43 {44 return MainStatic;45 }46 }4748 /// <summary>49 /// Provides a deterministic way to delete the Main property.50 /// </summary>51 public static void ClearMain()52 {53 _main.Cleanup();54 _main = null;55 }5657 /// <summary>58 /// Provides a deterministic way to create the Main property.59 /// </summary>60 public static void CreateMain()61 {62 if (_main == null)63 {64 _main = new MainViewModel();65 }66 }6768 /// <summary>69 /// Cleans up all the resources.70 /// </summary>71 public static void Cleanup()72 {73 ClearMain();74 }75 }76 }
Listing 4: Everything here is automatically provided when creating a MvvmLight project
1 <Application x:Class="DragDropUsingMvvmLight01.App"2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"4 xmlns:vm="clr-namespace:DragDropUsingMvvmLight01.ViewModel"5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"6 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"7 StartupUri="MainWindow.xaml"8 mc:Ignorable="d">9 <Application.Resources>10 <ResourceDictionary>11 <ResourceDictionary.MergedDictionaries>12 <ResourceDictionary Source="ResourceDictionary1.xaml"/>13 </ResourceDictionary.MergedDictionaries>14 <!--Global View Model Locator-->15 <vm:ViewModelLocator x:Key="Locator"16 d:IsDataSource="True" />17 </ResourceDictionary>18 </Application.Resources>19 </Application>20
Listing 5: Instantiation of the ViewModelLocator
As the application is started, when the DataContext property is initialized, it is seen that it is binding and the application walks up the logical tree looking for the named resource which is eventually found in the App.xaml file. Properties in the MainViewModel can now be bound to as the Locator is used as the gateway to the rest of the properies in the view model. The plumbing of associating a view model with a view has been taken care of.15 <Window.DataContext>16 <Binding Path="Main" Source="{StaticResource Locator}" />17 </Window.DataContext>
A MainViewModel which derives from ViewModelBase is also created for you. This is where most of the logic for this application takes place as shown in Listing 6.
1 using System.Collections.ObjectModel;2 using System.Windows;3 using System.Windows.Controls;4 using System.Windows.Documents;5 using System.Windows.Input;6 using System.Windows.Media;7 using GalaSoft.MvvmLight;8 using GalaSoft.MvvmLight.Command;9 using DragDropUsingMvvmLight01.Model;1011 namespace DragDropUsingMvvmLight01.ViewModel12 {13 /// <summary>14 /// This class contains properties that the main View can data bind to.15 /// </summary>16 public class MainViewModel : ViewModelBase17 {18 #region Private Fields19 private ObservableCollection<Contact> _contacts;20 private ObservableCollection<Contact> _targetContacts;21 private readonly IContact _contact;2223 private ListBoxItem _listBoxItem; // adorned element24 private Grid _topLevelGrid; // element to get adorner layer from25 private DraggedAdorner _draggedAdorner;26 #endregion2728 #region Public Properties29 // collections30 public ObservableCollection<Contact> Contacts31 {32 get { return _contacts; }33 set34 {35 if (_contacts == value)36 return;37 _contacts = value;38 RaisePropertyChanged("Contacts");39 }40 }41 public ObservableCollection<Contact> TargetContacts42 {43 get { return _targetContacts; }44 set45 {46 if (_targetContacts == value)47 return;48 _targetContacts = value;49 RaisePropertyChanged("TargetContacts");50 }51 }5253 // commands54 public RelayCommand<MouseButtonEventArgs> PreviewMouseLeftButtonDownCommand { get; private set; }55 public RelayCommand<DragEventArgs> DragOverCommand { get; private set; }56 public RelayCommand<DragEventArgs> DropTargetCommand { get; private set; }57 public RelayCommand<DragEventArgs> DropSourceCommand { get; private set; }58 public RelayCommand<DragEventArgs> ClickCommand { get; private set; }59 #endregion6061 /// <summary>62 /// Initializes a new instance of the MainViewModel class.63 /// </summary>64 public MainViewModel()65 {66 _contact = new Contact();67 _contacts = _contact.GetContacts();68 _targetContacts = new ObservableCollection<Contact>();6970 #region CommandInvocations7172 PreviewMouseLeftButtonDownCommand = new RelayCommand<MouseButtonEventArgs>(73 e =>74 {75 // get dragged listbox76 ListBox listBox = e.Source as ListBox;77 ListBoxItem listBoxItem = VisualHelper.FindAncestor<ListBoxItem>((DependencyObject)e.OriginalSource);7879 // set up shared states80 _listBoxItem = listBoxItem; // adorned element81 _topLevelGrid = GetTopLevelGrid(e); // element to get an adorner layer from higher up the logical tree than the listboxitem8283 // Find the data behind the listBoxItem84 if (listBox == null || listBoxItem == null) return;8586 Contact contact = (Contact)listBox.ItemContainerGenerator.ItemFromContainer(listBoxItem);8788 AddAdorner(listBoxItem, _topLevelGrid);8990 // Initialize the drag & drop operation91 DataObject dragData = new DataObject("myContactData", contact);92 DragDrop.DoDragDrop(listBoxItem, dragData, DragDropEffects.Move);93 }94 );9596 DragOverCommand = new RelayCommand<DragEventArgs>(97 e =>98 {99 if (!e.Data.GetDataPresent("contact"))100 {101 e.Effects = DragDropEffects.None;102 }103104 var currentMousePosition = e.GetPosition(_topLevelGrid);105106 if (_topLevelGrid != null && _draggedAdorner != null)107 _draggedAdorner.UpdateAdornerPosition(_topLevelGrid, currentMousePosition);108 }109 );110111 DropTargetCommand = new RelayCommand<DragEventArgs>(112 e =>113 {114 if(!e.Data.GetDataPresent("myContactData")) return;115 Contact contact = e.Data.GetData("myContactData") as Contact;116117 _targetContacts.Add(contact); // add to new collection118 _contacts.Remove(contact); // remove from source collection119120 // pass in the root grid since its adorner layer was used to add ListBoxItems adorners to121 RemoveAdorner(_listBoxItem, _topLevelGrid);122 });123124 // if dropping on the source list, remove the adorner125 DropSourceCommand = new RelayCommand<DragEventArgs>(e => RemoveAdorner(_listBoxItem, _topLevelGrid));126127 ClickCommand = new RelayCommand<DragEventArgs>(128 e =>129 {130 Contacts = _contact.GetContacts();131 TargetContacts = new ObservableCollection<Contact>();132 }133 );134135 #endregion136 }137138 private Grid GetTopLevelGrid(MouseEventArgs mouseEventArgs)139 {140 var sourceElement = mouseEventArgs.Source as DependencyObject;141 var topLevelWindow = VisualHelper.FindAncestor<Grid>(sourceElement);142 return topLevelWindow;143 }144145 #region Helper Methods146147 /// <summary>148 /// Adds an adorner to an element.149 /// </summary>150 /// <param name="elementToAdorn">Element to add an adorner to.</param>151 /// <param name="elementToGetAdornerLayerFrom">Element to get the AdornerLayer from.</param>152 /// <example>In the case of the ListBox, if you want to adorn each ListBoxItem and the adorner layer of the153 /// containing ListBox is used, then the adorner object gets clipped as it is moved out of the ListBox. In this case,154 /// you need a layer that is higher up the visual tree to allow the adorner to be visible anywhere and not be clipped.155 /// </example>156 public void AddAdorner(UIElement elementToAdorn, Visual elementToGetAdornerLayerFrom)157 {158 // get the adorner layer to attach the adorner to159 var adornerLayer = AdornerLayer.GetAdornerLayer(elementToGetAdornerLayerFrom);160 _draggedAdorner = new DraggedAdorner(elementToAdorn);161 adornerLayer.Add(_draggedAdorner);162 }163164 /// <summary>165 /// Removes an adorner from an element.166 /// </summary>167 /// <param name="adornedElement">The element that has an adorner associated with it.</param>168 /// /// <param name="elementToGetAdornerLayerFrom">Element to get the AdornerLayer from.</param>169 public void RemoveAdorner(UIElement adornedElement, Visual elementToGetAdornerLayerFrom)170 {171 var adornerLayer = AdornerLayer.GetAdornerLayer(elementToGetAdornerLayerFrom);172 var adorners = adornerLayer.GetAdorners(adornedElement);173174 if (adorners == null) return;175176 var dragAdorner = adorners[0] as DraggedAdorner;177 if (dragAdorner != null)178 {179 adornerLayer.Remove(dragAdorner);180 }181 }182 #endregion183 }184 }
Listing 6: The MainViewModel where most of the logic between the view and model happen
Note: The constructor is also a good place to inject dependencies. For instance, if my Contact class was not of static data, but is coming from a database or something like that, then you could have passed in the IContact interface as a constructor parameter. By doing this, you can pass in any type that implements this interface which means that you can write a test to pass in a mock implementation. Perhaps I will add this functionality on later.
No comments:
Post a Comment