Tuesday, January 4, 2011

[2011.01.04] Drag and Drop in WPF using MVVMLight [I/III]


downloadCode02
This is a multipart series outlining several topics is WPF
  1. Part 1: MVVM via the MVVMLight framework
  2. Part 2: Drag and Drop
  3. Part 3: The Adorner
  4. Part 4: DataTemplateSelector, StyleSelector, Unit Testing
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.

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
Going along with Mvvm, I will have a Model which is the data layer, the View which is the presentation layer and the ViewModel which sits between the View and Model. In Mvvm, the view does not have a direct reference to the model and references flow in a single direction:
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;
2
3 namespace DragDropUsingMvvmLight01.Model
4 {
5 /// <summary>
6 /// Contract depicting the state and behavior of what a Contact is in this context.
7 /// </summary>
8 public interface IContact
9 {
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;
2
3 namespace DragDropUsingMvvmLight01.Model
4 {
5 public class Contact : IContact
6 {
7 #region Private Fields
8 private ObservableCollection<Contact> _contacts;
9 private const string ImgPath = @"images/";
10 #endregion
11
12 #region Properties
13 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 #endregion
20
21 public Contact(){}
22
23 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 }
41
42 /// <summary>
43 /// Enumeration specifying which pirate crew each contact belongs to.
44 /// </summary>
45 public enum Faction
46 {
47 Strawhat,
48 Whitebeard
49 }
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>
18
19
20 <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>
31
32 <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>
60
61 <Border Grid.Row="2" Height="20" />
62
63 <TextBlock Grid.Row="3"
64 Background="White"
65 FontSize="18"
66 Text="Listbox to drop to" />
67
68 <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.ViewModel
2 {
3 /// <summary>
4 /// This class contains static references to all the view models in the
5 /// application and provides an entry point for the bindings.
6 /// </summary>
7 public class ViewModelLocator
8 {
9 private static MainViewModel _main;
10
11 /// <summary>
12 /// Initializes a new instance of the ViewModelLocator class. This instance will further be used to
13 /// create the MainViewModel
14 /// </summary>
15 public ViewModelLocator()
16 {
17 CreateMain();
18 }
19
20 /// <summary>
21 /// Gets the Main property.
22 /// </summary>
23 public static MainViewModel MainStatic
24 {
25 get
26 {
27 if (_main == null)
28 {
29 CreateMain();
30 }
31
32 return _main;
33 }
34 }
35
36 /// <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 Main
41 {
42 get
43 {
44 return MainStatic;
45 }
46 }
47
48 /// <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 }
56
57 /// <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 }
67
68 /// <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
An instance of the ViewModelLocator is created in the App.xaml file in the Resources section as shown in Listing 2.
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
Notice that the ViewModelLocator class exposes a Main property which is set at the DataContext of the MainWindow.
15 <Window.DataContext>
16 <Binding Path="Main" Source="{StaticResource Locator}" />
17 </Window.DataContext>
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.
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;
10
11 namespace DragDropUsingMvvmLight01.ViewModel
12 {
13 /// <summary>
14 /// This class contains properties that the main View can data bind to.
15 /// </summary>
16 public class MainViewModel : ViewModelBase
17 {
18 #region Private Fields
19 private ObservableCollection<Contact> _contacts;
20 private ObservableCollection<Contact> _targetContacts;
21 private readonly IContact _contact;
22
23 private ListBoxItem _listBoxItem; // adorned element
24 private Grid _topLevelGrid; // element to get adorner layer from
25 private DraggedAdorner _draggedAdorner;
26 #endregion
27
28 #region Public Properties
29 // collections
30 public ObservableCollection<Contact> Contacts
31 {
32 get { return _contacts; }
33 set
34 {
35 if (_contacts == value)
36 return;
37 _contacts = value;
38 RaisePropertyChanged("Contacts");
39 }
40 }
41 public ObservableCollection<Contact> TargetContacts
42 {
43 get { return _targetContacts; }
44 set
45 {
46 if (_targetContacts == value)
47 return;
48 _targetContacts = value;
49 RaisePropertyChanged("TargetContacts");
50 }
51 }
52
53 // commands
54 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 #endregion
60
61 /// <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>();
69
70 #region CommandInvocations
71
72 PreviewMouseLeftButtonDownCommand = new RelayCommand<MouseButtonEventArgs>(
73 e =>
74 {
75 // get dragged listbox
76 ListBox listBox = e.Source as ListBox;
77 ListBoxItem listBoxItem = VisualHelper.FindAncestor<ListBoxItem>((DependencyObject)e.OriginalSource);
78
79 // set up shared states
80 _listBoxItem = listBoxItem; // adorned element
81 _topLevelGrid = GetTopLevelGrid(e); // element to get an adorner layer from higher up the logical tree than the listboxitem
82
83 // Find the data behind the listBoxItem
84 if (listBox == null || listBoxItem == null) return;
85
86 Contact contact = (Contact)listBox.ItemContainerGenerator.ItemFromContainer(listBoxItem);
87
88 AddAdorner(listBoxItem, _topLevelGrid);
89
90 // Initialize the drag & drop operation
91 DataObject dragData = new DataObject("myContactData", contact);
92 DragDrop.DoDragDrop(listBoxItem, dragData, DragDropEffects.Move);
93 }
94 );
95
96 DragOverCommand = new RelayCommand<DragEventArgs>(
97 e =>
98 {
99 if (!e.Data.GetDataPresent("contact"))
100 {
101 e.Effects = DragDropEffects.None;
102 }
103
104 var currentMousePosition = e.GetPosition(_topLevelGrid);
105
106 if (_topLevelGrid != null && _draggedAdorner != null)
107 _draggedAdorner.UpdateAdornerPosition(_topLevelGrid, currentMousePosition);
108 }
109 );
110
111 DropTargetCommand = new RelayCommand<DragEventArgs>(
112 e =>
113 {
114 if(!e.Data.GetDataPresent("myContactData")) return;
115 Contact contact = e.Data.GetData("myContactData") as Contact;
116
117 _targetContacts.Add(contact); // add to new collection
118 _contacts.Remove(contact); // remove from source collection
119
120 // pass in the root grid since its adorner layer was used to add ListBoxItems adorners to
121 RemoveAdorner(_listBoxItem, _topLevelGrid);
122 });
123
124 // if dropping on the source list, remove the adorner
125 DropSourceCommand = new RelayCommand<DragEventArgs>(e => RemoveAdorner(_listBoxItem, _topLevelGrid));
126
127 ClickCommand = new RelayCommand<DragEventArgs>(
128 e =>
129 {
130 Contacts = _contact.GetContacts();
131 TargetContacts = new ObservableCollection<Contact>();
132 }
133 );
134
135 #endregion
136 }
137
138 private Grid GetTopLevelGrid(MouseEventArgs mouseEventArgs)
139 {
140 var sourceElement = mouseEventArgs.Source as DependencyObject;
141 var topLevelWindow = VisualHelper.FindAncestor<Grid>(sourceElement);
142 return topLevelWindow;
143 }
144
145 #region Helper Methods
146
147 /// <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 the
153 /// 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 to
159 var adornerLayer = AdornerLayer.GetAdornerLayer(elementToGetAdornerLayerFrom);
160 _draggedAdorner = new DraggedAdorner(elementToAdorn);
161 adornerLayer.Add(_draggedAdorner);
162 }
163
164 /// <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);
173
174 if (adorners == null) return;
175
176 var dragAdorner = adorners[0] as DraggedAdorner;
177 if (dragAdorner != null)
178 {
179 adornerLayer.Remove(dragAdorner);
180 }
181 }
182 #endregion
183 }
184 }
Listing 6: The MainViewModel where most of the logic between the view and model happen
Two properties are created to hold the collections of data for both of the dragging from and dropping to listboxes. Following that a number of RelayCommands are defined which are mapped to events related to the Drag and Drop infrastructure. In the constructor, the logic for the commands are exposed and can be easily followed from the documentation provided in the form of comments. Finally some adorner related helper methods are defined.
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: