Implement Drag and Drop Between TreeViews
The goal of this tutorial is to show how to implement drag and drop between two different RadTreeViews bound to heterogeneous data. The first RadTreeView represents a local machine files tree and it displays a hierarchy of business objects containing images and their URIs. The second one represents an application files tree and displays hierarchy of another type of business objects that also containing images and in addition a title. The drop will be allowed only from the local machine tree to the application tree.
The picture below demonstrates the final result:
Setting the RadTreeViews in XAML
First you can define the controls in your view. As the purpose of this tutorial is to demonstrate how to implement drag and drop operations, we won't focus on the definitions of the controls in xaml. However, please note to set the RadTreeView IsDragDropEnabled property to true.
<Grid>
<Grid.Resources>
<DataTemplate x:Key="mediaFilesTemplate">
<StackPanel Orientation="Horizontal">
<Image Width="16"
Height="16"
Source="Images/DefaultIcons/photos.png"
Stretch="Fill" />
<TextBlock Text="{Binding ImageFilePath}" />
</StackPanel>
</DataTemplate>
<HierarchicalDataTemplate x:Key="localMachineTemplate"
ItemTemplate="{StaticResource mediaFilesTemplate}"
ItemsSource="{Binding MediaFiles}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
<DataTemplate x:Key="resourceTemplate">
<StackPanel Orientation="Horizontal">
<Image Width="16"
Height="16"
Source="{Binding ImageFilePath}"
Stretch="Fill" />
<TextBlock Text="{Binding Title}" />
</StackPanel>
</DataTemplate>
<HierarchicalDataTemplate x:Key="applicationTemplate"
ItemTemplate="{StaticResource resourceTemplate}"
ItemsSource="{Binding Resources}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="" />
<ColumnDefinition Width="" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="File/Folders" />
<telerik:RadTreeView x:Name="xLocalMachineTree"
Grid.Row="1"
Grid.Column="0"
IsDragDropEnabled="True"
ItemTemplate="{StaticResource localMachineTemplate}" />
<TextBlock Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="MyApplication" />
<telerik:RadTreeView x:Name="xApplicationTree"
Grid.Row="1"
Grid.Column="1"
IsDragDropEnabled="True"
ItemTemplate="{StaticResource applicationTemplate}" />
</Grid>
Create the View Models
For the first RadTreeView we can create a business class called MediaFile that will hold information about images.
public class MediaFile
{
public string ImageTitle { get; set; }
public string ImageFilePath { get; set; }
}
Public Class MediaFile
Public Property ImageTitle() As String
Public Property ImageFilePath() As String
End Class
Then we can define a business class called PartitionViewModel which will hold a collection of MediaFiles and the name of a partition in the RadTreeView that represents the local machine tree.
public class PartitionViewModel
{
public PartitionViewModel()
{
this.MediaFiles = new ObservableCollection<MediaFile>();
}
public string Name { get; set; }
public ObservableCollection<MediaFile> MediaFiles { get; set; }
}
Public Class PartitionViewModel
Public Sub New()
Me.MediaFiles = New ObservableCollection(Of MediaFile)()
End Sub
Public Property Name() As String
Get
Return m_Name
End Get
Set(value As String)
m_Name = Value
End Set
End Property
Private m_Name As String
Public Property MediaFiles() As ObservableCollection(Of MediaFile)
Get
Return m_MediaFiles
End Get
Set(value As ObservableCollection(Of MediaFile))
m_MediaFiles = Value
End Set
End Property
Private m_MediaFiles As ObservableCollection(Of MediaFile)
End Class
For our second RadTreeView we can create a class called Resource that will hold the information about the images in it.
public class Resource
{
public BitmapImage ImageFilePath { get; set; }
public string Title { get; set; }
}
Public Class Resource
Public Property ImageFilePath() As BitmapImage
Public Property Title() As String
End Class
Then we can define a class called ApplicationViewModel which will hold a collection of Resources and the name of an application.
public class ApplicationViewModel
{
public ApplicationViewModel()
{
this.Resources = new ObservableCollection<Resource>();
}
public string Name { get; set; }
public ObservableCollection<Resource> Resources { get; set; }
}
Public Class ApplicationViewModel
Public Sub New()
Me.Resources = New ObservableCollection(Of Resource)()
End Sub
Public Property Name() As String
Public Property Resources() As ObservableCollection(Of Resource)
End Class
Next we can define a MainViewModel class that contains the collections which we will use to populate the ItemsSource property of both RadTreeViews For the first RadTreeView will be populated with collection of PartitionViewModel objects, whereas the second RadTreeView will use a collection of ApplicationViewModel objects.
public class MainViewModel
{
public MainViewModel()
{
this.LocalMachinePartitions = new ObservableCollection<PartitionViewModel>();
this.Applications = new ObservableCollection<ApplicationViewModel>();
this.GenetareSampleData();
}
public ObservableCollection<PartitionViewModel> LocalMachinePartitions
{
get;
set;
}
public ObservableCollection<ApplicationViewModel> Applications
{
get;
set;
}
private void GenetareSampleData()
{
string defaultImagePath = "Images/MediaFiles/{0}";
ObservableCollection<MediaFile> firstPartishionFiles = new ObservableCollection<MediaFile>();
firstPartishionFiles.Add(new MediaFile() { ImageTitle = "1PersonalFolders.png", ImageFilePath = string.Format(defaultImagePath, "Images/1PersonalFolders.png") });
firstPartishionFiles.Add(new MediaFile() { ImageTitle = "2DeletedItems.png", ImageFilePath = string.Format(defaultImagePath, "Images/2DeletedItems.png") });
firstPartishionFiles.Add(new MediaFile() { ImageTitle = "3Drafts.png", ImageFilePath = string.Format(defaultImagePath, "Images/3Drafts.png") });
this.LocalMachinePartitions.Add(new PartitionViewModel()
{
Name = @"C:/Images",
MediaFiles = firstPartishionFiles
});
ObservableCollection<MediaFile> secondPartishionFiles = new ObservableCollection<MediaFile>();
secondPartishionFiles.Add(new MediaFile() { ImageTitle = "beach_small.png", ImageFilePath = string.Format(defaultImagePath, "Photos/beach_small.png") });
secondPartishionFiles.Add(new MediaFile() { ImageTitle = "forest_small.png", ImageFilePath = string.Format(defaultImagePath, "Photos/forest_small.png") });
this.LocalMachinePartitions.Add(new PartitionViewModel()
{
Name = @"D:/Photos",
MediaFiles = secondPartishionFiles
});
this.Applications.Add(new ApplicationViewModel() { Name = "Web Client" });
this.Applications.Add(new ApplicationViewModel() { Name = "Desktop Client" });
}
}
Public Class MainViewModel
Public Sub New()
Me.LocalMachinePartitions = New ObservableCollection(Of PartitionViewModel)()
Me.Applications = New ObservableCollection(Of ApplicationViewModel)()
Me.GenetareSampleData()
End Sub
Public Property LocalMachinePartitions() As ObservableCollection(Of PartitionViewModel)
Get
Return m_LocalMachinePartitions
End Get
Set(value As ObservableCollection(Of PartitionViewModel))
m_LocalMachinePartitions = Value
End Set
End Property
Private m_LocalMachinePartitions As ObservableCollection(Of PartitionViewModel)
Public Property Applications() As ObservableCollection(Of ApplicationViewModel)
Get
Return m_Applications
End Get
Set(value As ObservableCollection(Of ApplicationViewModel))
m_Applications = Value
End Set
End Property
Private m_Applications As ObservableCollection(Of ApplicationViewModel)
Private Sub GenetareSampleData()
Dim defaultImagePath As String = "Images/MediaFiles/{0}"
Dim firstPartishionFiles As New ObservableCollection(Of MediaFile)()
firstPartishionFiles.Add(New MediaFile() With { _
.ImageTitle = "1PersonalFolders.png", _
.ImageFilePath = String.Format(defaultImagePath, "Images/1PersonalFolders.png") _
})
firstPartishionFiles.Add(New MediaFile() With { _
.ImageTitle = "2DeletedItems.png", _
.ImageFilePath = String.Format(defaultImagePath, "Images/2DeletedItems.png") _
})
firstPartishionFiles.Add(New MediaFile() With { _
.ImageTitle = "3Drafts.png", _
.ImageFilePath = String.Format(defaultImagePath, "Images/3Drafts.png") _
})
Me.LocalMachinePartitions.Add(New PartitionViewModel() With { _
.Name = "C:/Images", _
.MediaFiles = firstPartishionFiles _
})
Dim secondPartishionFiles As New ObservableCollection(Of MediaFile)()
secondPartishionFiles.Add(New MediaFile() With { _
.ImageTitle = "beach_small.png", _
.ImageFilePath = String.Format(defaultImagePath, "Photos/beach_small.png") _
})
secondPartishionFiles.Add(New MediaFile() With { _
.ImageTitle = "forest_small.png", _
.ImageFilePath = String.Format(defaultImagePath, "Photos/forest_small.png") _
})
Me.LocalMachinePartitions.Add(New PartitionViewModel() With { _
.Name = "D:/Photos", _
.MediaFiles = secondPartishionFiles _
})
Me.Applications.Add(New ApplicationViewModel() With { _
.Name = "Web Client" _
})
Me.Applications.Add(New ApplicationViewModel() With { _
.Name = "Desktop Client" _
})
End Sub
End Class
Implement the drag and drop logic
As was mentioned in the beginning of this article, the drop will be forbidden in the local machine tree (the first one). In order to do so we can subscribe for the DragOver and the Drop events of the DragDropManager and implement the custom logic there. We will start with configuring the RadTreeViews as a participants in drag and drop operations.
Now let’s add the handlers for the DragDropManager events listed above. We’ll do that in the code-behind.
public MainPage()
{
InitializeComponent();
MainViewModel viewModelData = new MainViewModel();
this.xLocalMachineTree.ItemsSource = viewModelData.LocalMachinePartitions;
this.xApplicationTree.ItemsSource = viewModelData.Applications;
DragDropManager.AddDragOverHandler(this.xLocalMachineTree, OnLocalMachineTreeDragOver, true);
DragDropManager.AddDragOverHandler(this.xApplicationTree, OnApplicationTreeDragOver, true);
DragDropManager.AddDropHandler(this.xApplicationTree, OnApplicationTreeDrop, true);
}
Public Sub New()
InitializeComponent()
Dim viewModelData As New MainViewModel()
Me.xLocalMachineTree.ItemsSource = viewModelData.LocalMachinePartitions
Me.xApplicationTree.ItemsSource = viewModelData.Applications
DragDropManager.AddDragOverHandler(Me.xLocalMachineTree, OnLocalMachineTreeDragOver, True)
DragDropManager.AddDragOverHandler(Me.xApplicationTree, OnApplicationTreeDragOver, True)
DragDropManager.AddDropHandler(Me.xApplicationTree, OnApplicationTreeDrop, True)
End Sub
Once we do so, we can start implementing the drag/drop handlers. For the purpose of this example we can only drop items to the RadTreeView with x:Name property set to xApplicationTree.
RadTreeView drag operation creates an object of type TreeViewDragDropOptions that holds all information related to the drag. You can read more about the properties exposed by the type in the Drag and Drop documentation article.
Next, we have to handle the Drop event for the xApplicationTree. When we drop an item in the second RadTreeView we create a new item of Resource type.
private void OnApplicationTreeDrop(object sender, Telerik.Windows.DragDrop.DragEventArgs e)
{
var options = DragDropPayloadManager.GetDataFromObject(e.Data, TreeViewDragDropOptions.Key) as TreeViewDragDropOptions;
if (options == null)
return;
MediaFile draggedItem = options.DraggedItems.FirstOrDefault() as MediaFile;
if (draggedItem == null)
return;
RadTreeViewItem dropTargetItem = options.DropTargetItem;
if (dropTargetItem == null)
return;
var dropItemModel = dropTargetItem.DataContext;
if (dropItemModel == null)
return;
var dropTree = sender as RadTreeView;
if (dropTree != null)
{
if (dropItemModel is Resource && options.DropAction == DropAction.None)
{
e.Handled = true;
return;
}
if (dropItemModel is ApplicationViewModel || dropItemModel is Resource)
{
options.DropAction = DropAction.Copy;
options.UpdateDragVisual();
ApplicationViewModel destinationFolder = null;
if (dropItemModel is ApplicationViewModel)
{
destinationFolder = dropItemModel as ApplicationViewModel;
}
else
{
destinationFolder = options.DropTargetItem.ParentItem.DataContext as ApplicationViewModel;
}
if (destinationFolder == null)
return;
Resource file = new Resource()
{
ImageFilePath = new System.Windows.Media.Imaging.BitmapImage(new Uri(draggedItem.ImageFilePath, UriKind.RelativeOrAbsolute)),
Title = draggedItem.ImageTitle
};
destinationFolder.Resources.Add(file);
}
}
}
Private Sub OnApplicationTreeDrop(sender As Object, e As Telerik.Windows.DragDrop.DragEventArgs)
Dim options = TryCast(DragDropPayloadManager.GetDataFromObject(e.Data, TreeViewDragDropOptions.Key), TreeViewDragDropOptions)
If options Is Nothing Then
Return
End If
Dim draggedItem As MediaFile = TryCast(options.DraggedItems.FirstOrDefault(), MediaFile)
If draggedItem Is Nothing Then
Return
End If
Dim dropTargetItem As RadTreeViewItem = options.DropTargetItem
If dropTargetItem Is Nothing Then
Return
End If
Dim dropItemModel = dropTargetItem.DataContext
If dropItemModel Is Nothing Then
Return
End If
Dim dropTree = TryCast(sender, RadTreeView)
If dropTree IsNot Nothing Then
If TypeOf dropItemModel Is Resource AndAlso options.DropAction = DropAction.None Then
e.Handled = True
Return
End If
If TypeOf dropItemModel Is ApplicationViewModel OrElse TypeOf dropItemModel Is Resource Then
options.DropAction = DropAction.Copy
options.UpdateDragVisual()
Dim destinationFolder As ApplicationViewModel = Nothing
If TypeOf dropItemModel Is ApplicationViewModel Then
destinationFolder = TryCast(dropItemModel, ApplicationViewModel)
Else
destinationFolder = TryCast(options.DropTargetItem.ParentItem.DataContext, ApplicationViewModel)
End If
If destinationFolder Is Nothing Then
Return
End If
Dim file As New Resource() With { _
.ImageFilePath = New System.Windows.Media.Imaging.BitmapImage(New Uri(draggedItem.ImageFilePath, UriKind.RelativeOrAbsolute)), _
.Title = draggedItem.ImageTitle _
}
destinationFolder.Resources.Add(file)
End If
End If
End Sub
In order to deny the nesting of Resource files and update the visual representation of the DropAction in the application tree, we can subscribe for the DragOver event of the DragDropManager.
private void OnApplicationTreeDragOver(object sender, Telerik.Windows.DragDrop.DragEventArgs e)
{
var options = DragDropPayloadManager.GetDataFromObject(e.Data, TreeViewDragDropOptions.Key) as TreeViewDragDropOptions;
if (options == null)
return;
RadTreeViewItem dropTargetItem = options.DropTargetItem;
var draggedItem = options.DraggedItems.First();
if (dropTargetItem == null ||
(dropTargetItem != null &&
options.DropTargetItem.DataContext is Resource &&
options.DropPosition == DropPosition.Inside) ||
draggedItem is PartitionViewModel)
{
options.DropAction = DropAction.None;
}
options.UpdateDragVisual();
}
Private Sub OnApplicationTreeDragOver(sender As Object, e As Telerik.Windows.DragDrop.DragEventArgs)
Dim options = TryCast(DragDropPayloadManager.GetDataFromObject(e.Data, TreeViewDragDropOptions.Key), TreeViewDragDropOptions)
If options Is Nothing Then
Return
End If
Dim dropTargetItem As RadTreeViewItem = options.DropTargetItem
Dim draggedItem = options.DraggedItems.First()
If dropTargetItem Is Nothing OrElse (dropTargetItem IsNot Nothing AndAlso TypeOf options.DropTargetItem.DataContext Is Resource AndAlso options.DropPosition = DropPosition.Inside) OrElse TypeOf draggedItem Is PartitionViewModel Then
options.DropAction = DropAction.None
End If
options.UpdateDragVisual()
End Sub
To ensure that we cannot drop in the local machine tree, we can subscribe for the DragOver event.
private void OnLocalMachineTreeDragOver(object sender, Telerik.Windows.DragDrop.DragEventArgs e)
{
var options = DragDropPayloadManager.GetDataFromObject(e.Data, TreeViewDragDropOptions.Key) as TreeViewDragDropOptions;
if (options != null)
{
options.DropAction = DropAction.None;
options.UpdateDragVisual();
e.Handled = true;
}
}
Private Sub OnLocalMachineTreeDragOver(sender As Object, e As Telerik.Windows.DragDrop.DragEventArgs)
Dim options = TryCast(DragDropPayloadManager.GetDataFromObject(e.Data, TreeViewDragDropOptions.Key), TreeViewDragDropOptions)
If options IsNot Nothing Then
options.DropAction = DropAction.None
options.UpdateDragVisual()
e.Handled = True
End If
End Sub
Find a runnable project of the previous example in the WPF Samples GitHub repository.