Introduction
This article started out as a WPF DataGrid
demo and a short article explaining how to accomplish basic tasks with the DataGrid
. I have been digging into the WPF DataGrid
over the past couple of days, and I am pleasantly surprised by what I have found. It is pretty easy to work with, and it produces pretty good results. I put together a demo to document the basic operations that I will need to perform with the DataGrid
on an upcoming project, and I thought I would share it with the CodeProject community. In particular, I wanted to share my approach to drawing a drop-line on the grid while a row drag was in process.
As I wrote this article, it emerged that most of the way in which I deal with the DataGrid
is driven by the Model-View-ViewModel pattern, and the article ended up being as much about MVVM as about the DataGrid
. I think you will find it particularly helpful if you are trying to get a handle on MVVM, although even experienced hands will find the DataGrid
discussion to be useful.
The demo app posted with this article gathers “How-To’s” in one place for the basic operations of:
- Styling the
DataGrid
- Getting data to and from the
DataGrid
- Adding items to the
DataGrid
- Removing items from the
DataGrid
- Moving items by dragging and dropping them within the
DataGrid
As you will see in going through the article, MVVM heavily influences almost every aspect of working with the DataGrid
. For many developers, including myself, it is simply the way that WPF is done.
Running the Demo App
For the demo app, I needed a simple model with no natural order, so I borrowed the family grocery list from last Friday, and I made an object model out of that. It is the sort of list that one might use to perform all of the DataGrid
tasks listed above. I haven't included data persistence in the demo, since there are so many ways of doing that. Instead, I create the object model at runtime.
When you start the demo, you will see a grocery list on a DataGrid
. The grid has two columns:
Sequence
: The sequence number of each item on the list. I include this column only so that I can verify that resequencing the list (which I discuss below) is being done when an item is moved.
Item
: The grocery list item.
Here is how to perform the basic DataGrid
operations described above:
- Add item to list: Click in the blank row at the bottom of the list. The
DataGrid
will create a new, empty item and assign it the next sequence number. Type in a name for your item and hit enter.
- Remove an item from the list: Select an item and click the Delete button above the
DataGrid
. The item will be removed, and the remaining items will be resequenced.
- Move an item: Hold the Shift key and click on an item. Still holding the shift key, drag the item to another row, then release it. While you are dragging the item, a drop-line will appear on the grid to show you where the item will be inserted if you drop it. When you drop the item, the list will be resequenced.
And that’s about all there is to the demo. As I said initially, it is pretty simple.
Model-View-ViewModel Implementation
The demo is built around the Model-View-ViewModel (MVVM) pattern. I use a fairly conventional implementation of MVVM. There are several different flavors of MVVM in the wild these days, and a lot of the literature on MVVM doesn't make a distinction that I consider to be vital.
Two flavors of MVVM: MVVM evolved from two other patterns, Model-View-Controller (MVC) and Model-View-Presenter (MVP). Both patterns are approaches for moving business and control logic out of a UI (the View). In both, a coordinator class is placed between the UI and the domain model. The coordinator class communicates with the View, and it contains the business and control logic that otherwise would have gone into code-behind in the View.
However similar the two patterns may be, they are not the same. I personally draw the following distinction between the two patterns:
- In MVC, the Controller is not supposed to know about its View. It simply exposes properties and methods that a View can use. It is indifferent to the View that actually consumes its services.
- In MVP, the Presenter typically has intimate knowledge of its View; the View is really just an adjunct to the Presenter. In an MVP app, it is not unusual for the View to be a property of the Presenter, to facilitate easy communication between the two.
I have a strong personal preference for the MVC flavor of MVVM. I believe that the MVC approach gives me greater flexibility to change the View, without causing unintended consequences in the ViewModel. In other words, I believe it does a better job of promoting Separation of Concerns and isolating the View and the ViewModel from each other, while allowing easy communication between the two. I discuss the subject at greater length in my CodeProject article on MVC, which can be found here.
Accordingly, my ViewModel knows nothing about the View that uses it. The ViewModel is passed to the View by App.xaml.cs as a part of the application bootstrap process:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mainWindow = new MainWindow();
var viewModel = new MainWindowViewModel();
mainWindow.DataContext = viewModel;
mainWindow.Show();
}
The view model is instantiated and set as the DataContext of the main window, which serves as the demo application’s View. From there, the XAML can access the properties of the view model using WPF data binding.
Elements of the View Model
In other respects, my implementation of MVVM is pretty simple. The view model is made up of the following classes:
- MainWindowViewModel.cs: This is the main class of the view model.
- DeleteItemCommand.cs: An
ICommand
class that deletes objects from the DataGrid
.
- SequencingService.cs: A service class that re-sequences the grocery list when an item is moved on the
DataGrid
.
- IgnoreNewItemPlaceHolderConverter.cs: A value converter to work around a nasty bug in the WPF
DataGrid
. More on this below.
This composition of a view model using a main class, commands, services, and value converters is pretty typical of my MVVM implementations.
The MVVM pattern is designed to facilitate the use of WPF data binding. I don't know if the WPF implementation can be considered Version 3.0 of data binding, but Microsoft absolutely got it right in this version. WPF data binding dramatically simplifies the process of wiring a view to its view model. Just about everything is done as a simple binding:
<toolkit:DataGrid x:Name="MainGrid"
ItemsSource="{Binding GroceryList}"
SelectedItem="{Binding SelectedItem, ...}" />
Even commands are wired up through data binding:
<Button Command="{Binding DeleteItem}" ... />
The process of wiring up a view is simpler and quicker than it is in WinForms. That alone would justify a move to WPF.
View model properties: My own personal style of programming is to group items and to make heavy use of regions to do the grouping. In the demo app, the view model properties are grouped into two different categories, which is pretty typical of my MVVM implementations:
- Command properties: These are
ICommand
objects which do the actual work of the view model. These properties are generally bound to the Command
property of buttons and similar controls. The demo app has one command property, DeleteItem
, which deletes an item from the DataGrid
.
- Data properties: These are what we traditionally think of as properties. The demo app has two properties;
GroceryItem
and SelectedItem
.
In some projects, I will include a third category of MVVM properties, UtilityProperties
. These properties hold flags and other values that the view may need to provide to the view model. I didn't need them in the demo app, so it only has the two types of properties listed above.
Command Properties
In my MVVM implementations, the ViewModel does very little actual work. Instead, it acts as a coordinator object, and it delegates the work to command and service objects. Command
objects are a standard element of the MVVM pattern, and again there are a couple of ways of implementing them. In my implementations, I like to put the main logic for each task in a command object. So, my command properties are all ICommand
objects, and each is linked to a unique class that implements the ICommand
interface.
The real benefit of the ICommand
interface is that it provides a convenient framework for maintaining the enabled/disabled status of controls that are displayed in the view. For example, the demo app displays a button that deletes the selected item from the grid. The button is bound to the DeleteItem
command property of the view model:
<Button Command="{Binding DeleteItem}" ... >
That property is linked to an ICommand
class, DeleteItemCommand.cs. Every ICommand
class must implement a CanExecute()
method, which determines whether the command can be executed at any given point in time. Here is the DeleteItemCommand.cs implementation of the method:
public bool CanExecute(object parameter)
{
return (m_ViewModel.SelectedItem != null);
}
.NET polls this method for all ICommand
objects on a regular basis. If the method returns true
, .NET enables any control bound to the command. If the method returns false
, then .NET disables the bound control. In DeleteItemCommand.cs, the CanExecute()
method simply checks the view model’s SelectedItem
property to see if anything is selected in the grid. If so, the method returns true
, and the button is enabled. Otherwise, the method returns false
, and the button is disabled automatically by .NET. That’s another strong argument in favor of WPF.
Note that DeleteItemCommand.cs holds a reference to its parent view model, in the command’s m_ViewModel
member variable. This reference was set when the view model initialized its command properties:
private void Initialize()
{
this.DeleteItem = new DeleteItemCommand(this);
...
}
The view model instantiates the ICommand
objects to which its command properties are linked, and it passes a reference to itself to the constructor of each ICommand
class. This reference makes it easy for the command to check view model properties as it carries out its work.
The actual work of an ICommand
is performed in its Execute()
method. In the demo app, the work is very simple. The Execute()
method gets the currently-selected item from the view model and deletes the item:
public void Execute(object parameter)
{
var selectedItem = m_ViewModel.SelectedItem;
m_ViewModel.GroceryList.Remove(selectedItem);
}
Data Properties
Data properties are pretty straightforward, because they resemble the object properties that we have used in .NET from the beginning. The only twist is the WPF requirement that data properties in the view model raise the PropertyChanged
event, so that the view can be updated when a view model property changes.
In the demo app, we fulfill this requirement in two ways. First, we derive the view model from an abstract
class, ViewModelBase.cs, that is located in the UtilityClasses folder of the demo app. The class implements the INotifyPropertyChanged
interface, a .NET interface that requires implementing classes to provide the PropertyChanged
event. The ViewModelBase
class also provides a method, RaisePropertyChangedEvent()
, to raise the event when a property changes.
Our view model uses simple auto-properties for command properties, like this:
public ICommand DeleteItem { get; set; }
But it uses more elaborate property declarations for its data properties, like this:
public ObservableCollection<GroceryItem> GroceryList
{
get { return p_GroceryList; }
set
{
p_GroceryList = value;
base.RaisePropertyChangedEvent("GroceryList");
}
}
The form of the data property declaration allows us to invoke the RaisePropertyChangedEvent()
in the view model base class when a data property changes. That’s what keeps the view and the view model in sync with each other.
Service Classes
There are some tasks that are either:
- Used by several commands
- Lengthy enough that I don't want to clutter up command objects with their code, or
- Invoked by the view model, rather than a control in the view
I use service classes, which I treat as part of the view model, for this code. In my MVVM implementations, service classes are generally static
.
NHibernate quirkiness: In the demo app, we have one service class, SequencingService.cs. My applications generally use NHibernate for data persistence. I am a big fan of NHibernate; it drastically reduces the time spent on creating code to load objects from a database and save them back. But it does have a few quirks.
One of the things I like best about NHibernate is the fact that it does automatic ‘dirty checking’. It will save only objects that have changed, which speeds up data persistence considerably. But for this feature to work, we have to be fairly careful to preserve the identity of the objects we read in from the database. For example, if we reorder a collection we have loaded from a database, NHibernate will consider the reordered collection to be a new collection, and we will lose automatic dirty checking. Fortunately, we can add objects to the collection, and delete objects from it, without losing dirty checking.
But it turns out that NHibernate also has an aversion to the ObservableCollection<T>
collection type that MVVM is built upon. To get around this limitation, I created an application that wraps plain vanilla IList<T>
objects, which NHibernate likes, in ObservableCollection<T>
wrappers, which WPF likes. You can find the application and documentation here.
The demo app included with this article doesn't involve NHibernate, but my upcoming production app, for which the demo app serves as a pilot project, does. In that app, I am going to need to save a sequence number for each object in the collections displayed by the view, so that the objects can be loaded in the correct order the next time they are read in. That turns out to be a fairly simple task.
When we use wrapper objects as described above, then when we drag and drop items in a data grid, it’s the wrapper objects that get reordered, not the domain objects. The domain objects remain in their original order in the domain collection. All we need to do is resequence the wrapper collection after a move, an add, or a delete.
The SequencingService class and the ISequencedObject interface: The SequencingService
class takes care of that. It has one method, SetCollectionSequence()
, which simply runs through the list and renumbers each item according to its current index:
public static ObservableCollection<T> SetCollectionSequence<T>(ObservableCollection<T>
targetCollection) where T : ISequencedObject
{
var sequenceNumber = 1;
foreach (ISequencedObject sequencedObject in targetCollection)
{
sequencedObject.SequenceNumber = sequenceNumber;
sequenceNumber++;
}
return targetCollection;
}
The signature of this method warrants some explanation. In my production application, I will have several collections that will be displayed by the app’s views. These collections will not necessarily derive from the same base class, so I have created an interface, ISequencedObject
, so that I can pass any of these collections to this service method. The interface file is located in the UtilityClasses folder of the demo app. It contains a single property:
public interface ISequencedObject
{
int SequenceNumber { get; set; }
}
As a result, the SetCollectionSequence()
method can process any ObservableCollection<T>
, so long as the elements of the collection implement the ISequencedObject
interface. That is what the where clause at the end of the method signature signifies.
Service classes are frequently invoked from ICommand
objects. However, the demo app shows a different reason for using a service class. Resequencing a collection is not triggered by a button or other control. Instead, it is invoked when the grocery list changes. That is, when an item is added to the list, removed from it, or moved within the list. In its Initialize()
method, the view model subscribes to the CollectionChanged
event of the grocery list:
p_GroceryList.CollectionChanged += OnGroceryListChanged;
When the event fires, an event handler in the view model invokes the sequencing service:
void OnGroceryListChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
...
SequencingService.SetCollectionSequence(this.GroceryList);
}
We could have simply placed the resequencing code in the event handler, but in a complex production app, a view model can get cluttered pretty quickly. And in my upcoming app, the code will be called by a number of different view models. Those requirements call for a service class that is accessible by whoever needs it.
Keep in mind that the sequencing service is not required for either the WPF or MVVM. Its need stems from my fairly unique requirements, which stem from using NHibernate with the WPF DataGrid
. Nonetheless, even assuming these requirements do not apply to your application, the service class demonstrates one approach to where and how to use services in an MVVM application. It is an approach that has worked very well for me, and I have no hesitation in recommending it for general use.
Value Converters
Value converters are another frequently-used element of the MVVM pattern. WPF generally communicates in terms of strings, and we use value converters to convert object values to their string representations. The classic example of a value converter is a date converter, which might convert a DateTime
object, which has a default string representation of “9/20/2009 12:00:00 AM”, to a more user-friendly representation, such as “Sunday, September 20”.
We don't have any naturally-occurring value conversion needs in the demo app, but we do have a very important use for a value converter nonetheless. The version of the WPF DataGrid
included in the Pre-C# 4.0 WPF Toolkit has a fairly subtle, but very nasty bug. If you data bind the DataGrid
’s SelectedItem
property to a view model (as the demo app does), you can no longer use the DataGrid
’s ability to add a new item to the grid by clicking on the blank row at the bottom of the grid. If you try to add a new item that way, you will get a FormatException
with practically no explanation of what happened or where it happened.
Thanks to Nigel Spencer, who documented this bug and provided a clever fix. It seems the problem lies with the value that the DataGrid
generates for a new item. The problem can be avoided by using a value converter that simply ignores that return value. That’s what the value converter in the demo app does; it is a direct copy of the one suggested by Nigel. You can read more about the bug, and Nigel’s solution, here.
Note that we have to declare the value converter in code—see the class file IgnoreNewItemPlaceHolderConverter.cs. But we also need to reference the value converter in the XAML of each window with a DataGrid
. In the demo app, we do that in the <Window.Resources>
section of the main Window XAML:
<Window.Resources>
...
-->
<vc:IgnoreNewItemPlaceHolderConverter x:Key="ignoreNewItemPlaceHolderConverter"/>
...
</Window.Resources>
As you will see below, it is more usual to place resources such as this in a ResourceDictionary
, where they are accessible to multiple windows. With that, let’s turn to the DataGrid
itself.
Basic DataGrid Operations
As I discussed at the outset, there are three DataGrid
operations that most applications need to support: adding items to the grid, removing items from it, and moving items up and down in the grid. Before we get to those operations, let’s take a look at styling the grid.
Styling the DataGrid: The DataGrid
is styled in XAML. In the demo app, we place the style in the <Window.Resources>
section of the main Window XAML. But in production apps, it is more usual to place the style in a ResourceDictionary
, where it can be shared among several windows. And in a complex application, where grids may be located in many different assemblies, the styles may be placed in a special resource dictionary located in a ‘shared resources’ assembly. You can read more about that here.
The styling itself is pretty straightforward. The main window contains three styles:
<Window.Resources>
...
-->
<LinearGradientBrush x:Key="BlueLightGradientBrush" StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#FFEAF3FF"/>
<GradientStop Offset="0.654" Color="#FFC0DEFF"/>
<GradientStop Offset="1" Color="#FFC0D9FB"/>
</LinearGradientBrush>
-->
<Style TargetType="{x:Type toolkit:DataGrid}">
<Setter Property="Margin" Value="5" />
<Setter Property="Background" Value="{StaticResource BlueLightGradientBrush}" />
<Setter Property="BorderBrush" Value="#FFA6CCF2" />
<Setter Property="RowBackground" Value="White" />
<Setter Property="AlternatingRowBackground" Value="#FDFFD0" />
<Setter Property="HorizontalGridLinesBrush" Value="Transparent" />
<Setter Property="VerticalGridLinesBrush" Value="#FFD3D0" />
<Setter Property="RowHeaderWidth" Value="0" />
</Style>
-->
<Style TargetType="{x:Type toolkit:DataGridRow}">
<Setter Property="AllowDrop" Value="True" />
</Style>
</Window.Resources>
The first defines a gradient brush that is used as the grid background. The second style defines the appearance of the DataGrid
, and it references the first. The third style is used for dragging and dropping items within the DataGrid
, and we will discuss it further below.
By and large, the styles are plain-vanilla XAML, and they do not warrant a lengthy discussion here. If you need more explanation, MSDN and most WPF books have pretty good discussions of the subject.
Adding items to the DataGrid: This task is about as simple as it gets. When you click on the blank row at the bottom of a DataGrid, the control creates a new, empty item for you and adds it to its collection. Thanks to the magic of WPF data-binding, the change is automatically propagated to the view model collection to which the DataGrid
is bound. In the demo app, that collection is the GroceryList
property. As you add an item to the grid, note the sequence numbers in the first column and the item count at the bottom of the window. Both of these values are being taken from the view model property, not from the grid itself.
The DataGrid
is bound to the GroceryList
property through the grid’s ItemsSource
property:
<toolkit:DataGrid x:Name="MainGrid" ItemsSource="{Binding
GroceryList}" ...>
The Sequence
column of the DataGrid
is bound to the SequenceNumber
property of each GroceryItem
object in the collection:
<toolkit:DataGrid x:Name="MainGrid"
...
<toolkit:DataGrid.Columns>
<toolkit:DataGridTextColumn ... Binding="{Binding SequenceNumber}" ... />
...
</toolkit:DataGrid.Columns>
</toolkit:DataGrid>
In short, all we have to do is set up the data bindings; the DataGrid
does the rest of the work for us.
Removing items from the grid: This task requires just a little more work. We have already discussed it above, in connection with the MVVM implementation. The Delete Item button, located in the main window just above the DataGrid
, is bound to the DeleteItem
command property in the view model. That property is linked to the DeleteItemCommand.cs class, whose Execute()
method performs the actual work.
Moving items within the DataGrid: This is about the only task that requires any explanation. The implementation in the demo app, is taken from a code sample published on MSDN by Ben Carter, a member of the WPF development team. The demo app adds drop-lines to Ben’s implementation, but otherwise, it is taken straight from his code. When you drag an item to another row within the grid, you will notice a blue line that follows your cursor up and down the grid, indicating where a dropped row will be inserted. I had previously added that feature to a WinForms data grid, and that implementation required some fairly complicated owner-draw code. In WPF, it takes just a couple of lines of simple formatting code. It’s just one more reason to love WPF.
No code-behind: Before we get into the specifics of how the drag-and-drop functionality works, take a look at the code in MainWindow.xaml.cs. It appears that I have broken one of the cardinal rules of MVVM:
A view should delegate its work to its view model, rather than relying on code-behind. A well-designed MVVM application has almost no code-behind in its views.
Obviously, in the demo app, the main window of the demo app has quite a bit of code-behind. You might well wonder what that is all about.
I am a big believer in the “no-code-behind” principle, and I don't think I have violated that principle in the demo app. The idea behind no-code-behind is to move control and business logic out of the view to a controller or coordinator layer that mediates between a view and the domain model.
Now take another look at the code-behind in the main window. You will notice that all of the code is related to the drag-and-drop functionality of the data grid. That functionality is a display concern, and it is therefore the province of the view. None of the code deals with business logic or control issues. Accordingly, Separation of Concerns is preserved.
In a larger application, with multiple views and numerous data grids, you might want to remove the guts of the code to a service class for all views, which would be invoked by skeletal event handlers in each view. That approach would eliminate duplication and provide a single point of change for updating the code. But in a simple application like the demo app, there is absolutely nothing wrong with placing view-support code of this sort into code-behind.
So as you develop applications using MVVM, take the no-code-behind rule with a grain of salt. If you need to program display logic that cannot be expressed in XAML, then code-behind is a perfectly acceptable place to do it.
Implementing drag-and-drop: The drag-and-drop implementation is really very straightforward. It has three elements. First, we add the events that we need for drag-and-drop to the DataGrid
’s XAML:
<toolkit:DataGrid ...
MouseMove="OnMainGridMouseMove"
DragEnter="OnMainGridCheckDropTarget"
DragLeave="OnMainGridCheckDropTarget"
DragOver="OnMainGridCheckDropTarget"
Drop="OnMainGridDrop"
DataContextChanged="OnMainGridDataContextChanged">
These events follow normal .NET conventions for drag-and-drop operations. If you need more explanation, MSDN covers the subject pretty well.
The second group of elements of the drag-and-drop implementation is the event handlers for the events we added in XAML. These event handlers appear in the main window code-behind:
#region Static Methods
private static T FindVisualParent<T>(UIElement element) where T : UIElement
{
var parent = element;
while (parent != null)
{
var correctlyTyped = parent as T;
if (correctlyTyped != null)
{
return correctlyTyped;
}
parent = VisualTreeHelper.GetParent(parent) as UIElement;
}
return null;
}
#endregion
#region Event Handlers
private void OnMainGridCheckDropTarget(object sender, DragEventArgs e)
{
var row = FindVisualParent<DataGridRow>(e.OriginalSource as UIElement);
if ((row == null) || !(row.Item is GroceryItem))
{
e.Effects = DragDropEffects.None;
}
else
{
var currentIndex = row.GetIndex();
if (m_OldRow != null) m_OldRow.BorderThickness = new Thickness(0);
int direction = (currentIndex - m_OriginalIndex);
if (direction < 0) row.BorderThickness = new Thickness(0, 2, 0, 0);
else if (direction > 0) row.BorderThickness = new Thickness(0, 0, 0, 2);
m_OldRow = row;
}
}
private void OnMainGridDrop(object sender, DragEventArgs e)
{
e.Effects = DragDropEffects.None;
e.Handled = true;
var row = FindVisualParent<DataGridRow>(e.OriginalSource as UIElement);
if (row != null)
{
m_TargetItem = row.Item as GroceryItem;
if (m_TargetItem != null)
{
e.Effects = DragDropEffects.Move;
}
}
if (m_OldRow != null) m_OldRow.BorderThickness = new Thickness(0, 0, 0, 0);
}
private void OnMainGridMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed) return;
if (Keyboard.Modifiers != ModifierKeys.Shift) return;
var row = FindVisualParent<DataGridRow>(e.OriginalSource as FrameworkElement);
m_OriginalIndex = row.GetIndex();
if ((row != null) && row.IsSelected)
{
var selectedItem = (GroceryItem) row.Item;
var finalDropEffect = DragDrop.DoDragDrop
(row, selectedItem, DragDropEffects.Move);
if ((finalDropEffect == DragDropEffects.Move) && (m_TargetItem != null))
{
var oldIndex = m_ViewModel.GroceryList.IndexOf(selectedItem);
var newIndex = m_ViewModel.GroceryList.IndexOf(m_TargetItem);
if (oldIndex != newIndex) m_ViewModel.GroceryList.Move(oldIndex, newIndex);
m_TargetItem = null;
}
}
}
#endregion
Again, this code follows the general .NET pattern for drag-and-drop, so we won't discuss it at length.
Implementing a drop-line on the grid: What is worth noting is the implementation of a moving drop-line, which indicates where a dropped row will be inserted. The demo app implements a drop-line by setting either the top or the bottom border of a data row when the cursor enters that row, and then clearing the border when the cursor enters a new row.
When a drop is made, the WPF DataGrid
will insert a dropped item either above or below the row under the mouse pointer at the time of the drop, depending on the direction of the drag:
If the item is being dragged... |
When dropped, the item will be inserted... |
...above its original row |
...above the row under the mouse pointer |
...below its original row |
...below the row under the mouse pointer |
So, the first order of business is to determine the direction of the drag. We get the original row index from the OnMainGridMouseMove()
event handler:
private void OnMainGridMouseMove(object sender, MouseEventArgs e)
{
...
var row = FindVisualParent<DataGridRow>(e.OriginalSource as FrameworkElement);
m_OriginalIndex = row.GetIndex();
...
}
We store the index in a member variable in the window class. Next, we get the current row index as we drag the row upwards or downwards. We get that from the OnMainGridCheckDropTarget()
event handler (see below). Then we compare the two values. If the result is negative, the drag is upward, and we draw a border on the top of the row under the mouse pointer. If the result is positive, the drag is downward, and we draw a border on the bottom of that row.
private void OnMainGridCheckDropTarget(object sender, DragEventArgs e)
{
var row = FindVisualParent<DataGridRow>(e.OriginalSource as UIElement);
...
var currentIndex = row.GetIndex();
if (m_OldRow != null) m_OldRow.BorderThickness = new Thickness(0);
var direction = (currentIndex - m_OriginalIndex);
if (direction < 0) row.BorderThickness = new Thickness(0, 2, 0, 0);
else if (direction > 0) row.BorderThickness = new Thickness(0, 0, 0, 2);
m_OldRow = row;
...
}
The final element in the drag-and-drop implementation is a grid style that enables the rows of the grid to be used as drop targets:
<Style TargetType="{x:Type toolkit:DataGridRow}">
<Setter Property="AllowDrop" Value="True" />
</Style>
As with the other elements of the implementation, there is nothing really unique or special here, so we will leave it without further comment.
Remember that the Shift key must be held down during a drag-and-drop operation. The demo implements that feature in the OnMainGridMouseMove()
method:
if (e.LeftButton != MouseButtonState.Pressed) return;
if (Keyboard.Modifiers != ModifierKeys.Shift) return;
This feature was added to the demo app to simplify the process of distinguishing between two types of drags:
- A drag meant to select multiple rows, and
- A drag meant to move a row to a new location.
If the Shift key is pressed, the selected row will be dragged. If the Shift key is not pressed, multiple rows will be selected.
As you can see, basic grid operations are surprisingly easy to implement in the WPF DataGrid
. Not even the code for the drop-line is very complicated
Conclusion
As we noted at the beginning of this article, MVVM is a very scalable pattern, lending itself well to simple applications, such as the demo app, and to much more complex, multi-assembly applications. However, that does not mean that MVVM can support complex enterprise applications on its own. As an application grows larger, the essential problem becomes how to partition the application into manageable pieces, and how to integrate those pieces into a single, functioning whole.
I am very enthusiastic about the solution that Microsoft has provided in Composite Application Guidance for WPF, otherwise known as Prism. Prism is an application framework that allows a developer to partition an application into modules, which load into a shell that serves as the main window of an application. Prism uses some very clever mechanisms to allow the modules to communicate with each other and with the shell without becoming tangled up with each other.
As of this writing, Prism is now in Version 2.0. Unlike the earlier Composite Application Block library, Prism is not an all-or-nothing proposition. You can use as much or as little of it as you like. But Prism integrates so well with MVVM, and it is so well designed, that I find myself using just about everything it has to offer. It is definitely the next step to take after you gain a comfort level with MVVM.
And with that, I'll put down my pen. I invite any comments or suggestions you may have for this article; I find that I learn as much from reader comments as I do from writing the article itself. If you haven't spent a lot of time with MVVM, I recommend it to you. And if you haven't yet used the WPF DataGrid
, it is a well-designed, easy to use control that integrates well with MVVM. I hope it simplifies your development chores as it has done for me.