WPF Drag and Drop - Get original source info from DragEventArgs - c#

I am trying write Drag and Drop functionality using MVVM which will allow me to drag PersonModel objects from one ListView to another.
This is almost working but I need to be able to get the ItemsSource of the source ListView from the DragEventArgs which I cant figure out how to do.
private void OnHandleDrop(DragEventArgs e)
{
if (e.Data != null && e.Data.GetDataPresent("myFormat"))
{
var person = e.Data.GetData("myFormat") as PersonModel;
//Gets the ItemsSource of the source ListView
..
//Gets the ItemsSource of the target ListView and Adds the person to it
((ObservableCollection<PersonModel>)(((ListView)e.Source).ItemsSource)).Add(person);
}
}
Any help would be greatly appreciated.
Thanks!

I found the answer in another question
The way to do it is to pass the source ListView into the DragDrow.DoDragDrop method ie.
In the method which handles the PreviewMouseMove for the ListView do-
private static void List_MouseMove(MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (e.Source != null)
{
DragDrop.DoDragDrop((ListView)e.Source, (ListView)e.Source, DragDropEffects.Move);
}
}
}
and then in the OnHandleDrop method change the code to
private static void OnHandleDrop(DragEventArgs e)
{
if (e.Data != null && e.Data.GetDataPresent("System.Windows.Controls.ListView"))
{
//var person = e.Data.GetData("myFormat") as PersonModel;
//Gets the ItemsSource of the source ListView and removes the person
var source = e.Data.GetData("System.Windows.Controls.ListView") as ListView;
if (source != null)
{
var person = source.SelectedItem as PersonModel;
((ObservableCollection<PersonModel>)source.ItemsSource).Remove(person);
//Gets the ItemsSource of the target ListView
((ObservableCollection<PersonModel>)(((ListView)e.Source).ItemsSource)).Add(person);
}
}
}

Related

How to move List View Item from one list to another with drag and drop? UWP C#

I have many List Views in an UWP application and I would want to be able to move one item from one List View to another List View
I know about the AllowDrop or CanDragItems properties and that you need to handle some events for drag and drop to work, although I just don't know how to do it.
If you want to add ListView controls by clicking Add button and move items between ListView controls, please check the following code as a sample. The following code is different from the offical sample, it use ObservableCollection to complete the drag and drop operation. You could drag an item from source ListView item to target ListView, and also drag an item from original target ListView to original source ListView.
You could click the Add button twice and then two ListView with two items are added. You can drag any item from any ListView control to another. If you want to keep the dragged item in the source ListView control, just comment the code dragCollection.Remove(dragedItem as string); .
For example:
private ObservableCollection<string> dragCollection;
private ObservableCollection<string> dropCollection;
private object dragedItem;
private ListView dragListView;
private ListView dropListView;
……
private void AddButton_Click(object sender, RoutedEventArgs e)
{
ListView listView = new ListView();
listView.CanDragItems = true;
listView.CanDrag = true;
listView.AllowDrop = true;
listView.ReorderMode = ListViewReorderMode.Enabled;
listView.CanReorderItems = true;
listView.ItemsSource = new ObservableCollection<string>() { "item1","item2" };
listView.DragItemsStarting += ListView_DragItemsStarting;
//listView.DropCompleted += ListView_DropCompleted;
listView.DragEnter += ListView_DragEnter;
listView.Drop += ListView_Drop;
listView.DragOver += ListView_DragOver;
listView.BorderBrush = new SolidColorBrush(Colors.Red);
listView.BorderThickness = new Thickness(1);
stackPanel.Children.Add(listView);
}
private void ListView_DragOver(object sender, DragEventArgs e)
{
e.AcceptedOperation = DataPackageOperation.Move;
}
private void ListView_Drop(object sender, DragEventArgs e)
{
dropListView = sender as ListView;
if(dropListView!=null)
{
dropCollection = dropListView.ItemsSource as ObservableCollection<string>;
if (dragedItem != null)
{
dropCollection.Add(dragedItem as string);
//If you need to delete the draged item in the source ListView, then use the following code
dragCollection.Remove(dragedItem as string);
dragedItem = null;
}
}
}
private void ListView_DragEnter(object sender, DragEventArgs e)
{
e.AcceptedOperation = (e.DataView.Contains(StandardDataFormats.Text) ? DataPackageOperation.Move : DataPackageOperation.None);
}
private void ListView_DropCompleted(UIElement sender, DropCompletedEventArgs args)
{
var listView = sender as ListView;
if (listView != null)
{
dropListView = listView;
dropCollection = listView.ItemsSource as ObservableCollection<string>;
if(dropListView==dragListView)
{
return;
}
}
}
private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
var listView = sender as ListView;
if(listView!=null)
{
dragListView = listView;
dragCollection = listView.ItemsSource as ObservableCollection<string>;
if (dropListView == dragListView)
{
return;
}
if(e.Items.Count==1)
{
dragedItem = e.Items[0];
e.Data.RequestedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
}
}
}
For more information about dragging and dropping, you could refer to the document(https://learn.microsoft.com/en-us/windows/uwp/design/input/drag-and-drop).
Any concerns about the code, please feel free to contact me.
To implement dragging, you must set CanDragItems on the source ListView and AllowDrop on the target ListView. Then, you must handle DragItemsStarting event on the source list. Within this handler you can store the dragged data inside the DragItemsStartingEventArgs.Data property. Afterwards, you handle Drop event on the target list and retrieve the stored item values from the DataPackage using DragEventArgs.DataView.
To see all the moving parts of this in action, I recommend the official UWP samples for drag & drop which are available on GitHub. The first scenario of this sample show dragging items from and to a ListView including reordering support.

Resort DataGridView after adding to the bound list

I have a DataGridView that is bound to a BindingSource that is bound to a BindingList. I can sort the DataGridView by clicking on a column header, as expected. But when I drop an item onto the DataGridView, it appears at the bottom of the list rather than in its correct sorted position. I found code in a StackOverflow grid to check the grid's SortOrder and SortedColumn properties, and if they are set, then to resort the grid. However, when I put a breakpoint in the DragDrop handler, I find that SortOrder is set to None and SortedColumn is null.
Here's the DragDrop handler:
private void dgvCoils_DragDrop(object sender, DragEventArgs e)
{
int? coilStack = m_draggedCoil.Stack;
m_currentCharge.RemoveCoil(m_draggedCoil);
if (coilStack.HasValue)
{
DisplayStack(coilStack.Value);
}
if (!m_inventoryList.Any(coil => coil.Coil_id == m_draggedCoil.Coil_id))
{
m_inventoryList.Add(m_draggedCoil);
if (dgvCoils.SortOrder != SortOrder.None && dgvCoils.SortedColumn != null)
{
ListSortDirection dir = ListSortDirection.Ascending;
if (dgvCoils.SortOrder == SortOrder.Descending) dir = ListSortDirection.Descending;
dgvCoils.Sort(dgvCoils.SortedColumn, dir);
}
}
}
And here is the code where I create the list and bind the source to it:
InventorySet coilSet = new InventorySet(m_db);
coilSet.FilterOnField(coilSet.m_archived, 0);
coilSet.Open();
// coilBindingSource.DataSource = coilSet.UnderlyingTable;
while (!coilSet.IsEOF())
{
m_inventoryList.Add(coilSet.ExportData());
coilSet.MoveNext();
}
coilBindingSource.DataSource = m_inventoryList;
coilBindingSource.ResetBindings(false);
dgvCoils.Refresh();
I am not setting any Sort columns in my BindingSource object. Do I need to?
And do I need to use a BindingSource if the underlying list in a BindingList?
Edit:
In looking back through my source code, I remembered that I had to add code to my ColumnHeaderMouseClick event handler to get sorting to work. Here is that handler:
private void dgvCoils_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
string strColumnName = dgvCoils.Columns[e.ColumnIndex].DataPropertyName;
SortOrder strSortOrder = GetSortOrder(dgvCoils, e.ColumnIndex);
if (strSortOrder == SortOrder.Ascending)
{
m_inventoryList = new BindingList<InventorySetData>(m_inventoryList.OrderBy(x => typeof(InventorySetData).GetProperty(strColumnName).GetValue(x, null)).ToList());
}
else
{
m_inventoryList = new BindingList<InventorySetData>(m_inventoryList.OrderByDescending(x => typeof(InventorySetData).GetProperty(strColumnName).GetValue(x, null)).ToList());
}
dgvCoils.DataSource = m_inventoryList;
dgvCoils.Columns[e.ColumnIndex].HeaderCell.SortGlyphDirection = strSortOrder;
}
I am guessing that I am doing too much programmatically, and not letting the DataGridView do what it was designed to do.

How to access the Parent of the Source in a Drop Event?

I am trying to handle some drag and drop functionality for values in my TreeView.
I am specifically targeting the Drop method. I want to be able to access the Parent object of the element being dragged.
The
TreeView.SelectedItem
is the property of the TreeView that I suspect I will need. However, this property is of type string, so accessing the Parent object is proposing a challenge.
I tried using the
VisualTreeHelper.GetParent(DependencyObject)
method but there doesn't seem to exist a conversion from string to DependencyObject and vice versa.
I am able to access the ParentControl of the target, but not the source and I can't understand why.
This is my Drop Method, as well as the MouseMove. Any help would be appreciated.
private void TreeViewItem_Drop(object sender, DragEventArgs e)
{
if (sender is TreeViewItem target)
{
string source = e.Data.GetData(typeof(string)) as string;
string dropTarget = target.DataContext as string;
var parentControl = VisualTreeHelper.GetParent((TreeViewItem)(sender));
var parentElement = ((VirtualizingStackPanel)(parentControl)).DataContext;
if (parentElement is GroupDecision decision)
{
if (decision.Decisions.Contains(source) && (decision.Decisions.Contains(dropTarget)))
{
int sourceIndex = decision.Decisions.IndexOf(source);
int targetIndex = decision.Decisions.IndexOf(dropTarget);
SwapElements(sourceIndex, targetIndex, decision.Decisions);
}
else
{
if (!decision.Decisions.Contains(source))
{
decision.Decisions.Add(source);
// Remove from old
}
}
}
}
private void TreeViewItem_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (TreeView.SelectedItem != null)
{
DragDrop.DoDragDrop(this, TreeView.SelectedItem, DragDropEffects.Move);
}
}
}
The treeview.selecteditem in your dodragdrop there is what ends up in your data when you do your e.Data.GetData. Since you cast it to a string, it's just a string floating about in memory. Adrift on an ocean of not-in-the-visual-tree. It has no parent control, because it's a string.
The sender is whatever control you dropped it on, so that's not the thing you dragged from. It's what you're dragging to.
But... what you pass as data can be whatever you give it.
That can be a simple string or a complicated business object with many properties on it.
Whatever you want when you drop, is best stashed away there in that data.
In the answer here:
WPF drag and drop and data types
That's a viewmodel.
I don't really follow what you want out of this but instead of
DragDrop.DoDragDrop(this, TreeView.SelectedItem, DragDropEffects.Move);
You should put whatever is required as that second parameter.
DragDrop.DoDragDrop(this, AmoreComplexObjectWithMoreData, DragDropEffects.Move);
Define a class which has properties matching whatever you need. New it up and set those as you start dragging. When you drop, you have everything you need then. Cast your drag data back to said class.
A string has no visual ancestor. Maybe you want to pass the TreeViewItem container of the selected item to the DoDragDrop method?:
private void TreeViewItem_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (TreeView.SelectedItem != null)
{
DragDrop.DoDragDrop(this,
TreeView.ItemContainerGenerator.ContainerFromItem(TreeView.SelectedItem) as TreeViewItem,
DragDropEffects.Move);
}
}
}
You could then get a reference to the visual container in the Drop event handler and traverse the visual tree as you like:
TreeViewItem source = e.Data.GetData(typeof(TreeViewItem)) as TreeViewItem;
string sourceObject = source.DataContext as string;
//...

WPF TextBox Binding does not update with Drag and Drop

I have a wpf project where I have a bound text box that has AllowDrop set to true. If I type directly into this text box and leave the box, the bound property for this text box changes as expected.
However, if I create a drop event and set the text value for the text box to the filename value, the text box bound property does not change. I have to click into the text box and tab out of it.
I must be misunderstanding how bound properties should work. My thinking was that if the text of the box changes that it should update the bound property as well.
As it stands now, I have to have the code behind update the property rather than rely upon the binding. Below is from a sample project I created.
XAML:
<Window.DataContext>
<local:XmlFile x:Name="XmlFileInfo"/>
</Window.DataContext>
...
<StackPanel Orientation="Horizontal" Grid.Row="0">
<Label Content="XML File:"/>
<TextBox x:Name="xmlFilePath" Text="{Binding XMLFile}" Height="25" VerticalAlignment="Top" MinWidth="300" AllowDrop="True" PreviewDragOver="xmlFilePath_PreviewDragOver" Drop="xmlFilePath_Drop"/>
</StackPanel>
And below is my viewmodel
public class XmlFile
{
private string _xmlfile;
private string _xmlElementName;
public string XMLFile
{
get { return _xmlfile; }
set
{
if (value != _xmlfile)
{
_xmlfile = value;
_xmlElementName = SetElementNameFromFileName();
}
}
}
...
}
And finally my code behind for XAML
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Exit_Click(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown();
}
private void SetControlState()
{
FileTest.IsEnabled = false;
if (!string.IsNullOrEmpty(XmlFileInfo.XMLFile))
{
if(XmlFileInfo.IsValidXml(XmlFileInfo.XMLFile))
{
FileTest.IsEnabled = true;
}
}
}
private void xmlFilePath_PreviewDragOver(object sender, DragEventArgs e)
{
e.Handled = true;
}
private void xmlFilePath_Drop(object sender, DragEventArgs e)
{
var filenames = (string[])e.Data.GetData(DataFormats.FileDrop);
if (filenames == null) return;
var filename = filenames.FirstOrDefault();
if (filename == null) return;
//XmlFileInfo.XMLFile = filename; <-- This bypasses the binding
(sender as TextBox).Text = filename; // This should trigger updating binding
SetControlState(); //<-- enables a button control if file is valid
}
}
I have tried setting the binding mode to twoway, and other binding settings without any change in behavior. What I would like to do is figure out how to get the drop functionality to act just like manually typing into the box and leaving the box without having to bypass my binding and directly setting the property.
Make your ViewModel implement INotifyPropertyChanged.
Instead of directly changing the text of your textbox change the fileName in your viewModel. That will do the trick.
You also have to call OnPropertyChanged in the setter of your XMLFile-Property
public class XmlFile : INotifyPropertyChanged
{
private string _xmlfile;
private string _xmlElementName;
public string XMLFile
{
get { return _xmlfile; }
set
{
if (value != _xmlfile)
{
_xmlfile = value;
_xmlElementName = SetElementNameFromFileName();
OnPropertyChanged(nameof(XMLFile); //this tells your UI to update
}
}
}
...
}
private void xmlFilePath_Drop(object sender, DragEventArgs e)
{
var filenames = (string[])e.Data.GetData(DataFormats.FileDrop);
if (filenames == null) return;
var filename = filenames.FirstOrDefault();
if (filename == null) return;
//XmlFileInfo.XMLFile = filename; <-- This bypasses the binding
var viewModel = (XmlFile)this.DataContext;
viewModel.XMLFile = filename;
SetControlState(); //<-- enables a button control if file is valid
}
You should have a look at how DataBinding works.
What you're looking for is the following:
var textBox = sender as TextBox;
if (textBox == null)
return;
// Sets the value of the DependencyProperty without overwriting the value, this will also update the ViewModel/XmlFile that TextBox is bound to.
textBox.SetCurrentValue(TextBox.TextProperty, "My Test Value");
You can also force the binding to update the target(XmlFile) by the following:
var textBinding = textBox.GetBindingExpression(TextBox.TextProperty);
// This will force update the target (rather than wait until control loses focus)
textBinding?.UpdateTarget();
I had a situation where I needed to populate multiple textboxes with filepaths. I used the ideas from akjoshi presented here: Update ViewModel from View. I implemented it in the following way:
private void Text_Drop(object sender, DragEventArgs e)
{
TextBox temp = sender as TextBox;
if(temp == null)
return;
string[] tempArray = (string[])e.Data.GetData(DataFormats.FileDrop, false);
temp.Text = tempArray[0];
sender = temp;
BindingExpression bind = BindingOperations.GetBindingExpression(temp, TextBox.TextProperty);
bind.UpdateSource();
}
Hope this helps!

WPF Datagrid - deselect selected item(s) when clicking whitespace in the DataGrid

The default behavior is to use CTRL+Click to deselect items in the Datagrid
I want to be able to mouse click (left or right button) the whitespace in the grid and have it deselect any selected items.
I've googled it to death and found some incredibly complex workarounds, but i'm hoping for a simple solution.
Edit:
I'm now using a listview instead, and still havent found a solution. It's slightly less annoying with a listview though because they are styled better.
I had the same question and found a solution. This should be built in behaviour:
private void dataGrid1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender != null)
{
DataGrid grid = sender as DataGrid;
if (grid != null && grid.SelectedItems != null && grid.SelectedItems.Count == 1)
{
DataGridRow dgr = grid.ItemContainerGenerator.ContainerFromItem(grid.SelectedItem) as DataGridRow;
if (!dgr.IsMouseOver)
{
(dgr as DataGridRow).IsSelected = false;
}
}
}
}
A simple
<DataGrid MouseDown="DataGrid_MouseDown">
is not what you want?
private void DataGrid_MouseDown(object sender, MouseButtonEventArgs e)
{
(sender as DataGrid).SelectedItem = null;
}
The only disadvantage is that a click without CTRL on a selected item deselects all too.
I am not sure whether you mean white space or gray space. In the latter case the following does the job:
private void dataViewImages_MouseUp(object sender, MouseEventArgs e)
{
DataGridView.HitTestInfo hit = dataViewImages.HitTest(e.X, e.Y);
if (hit.Type != DataGridViewHitTestType.Cell)
dataViewImages.ClearSelection();
}
This is what I use to deselect all cells by clicking in the gray space.
private void dg_IsKeyboardFocusWithinChanged
(object sender, DependencyPropertyChangedEventArgs e)
{
if (dg.SelectedItem != null) {
dg.UnselectAll();
}
}
If you have SelectionUnit="FullRow" you have to use UnselectAllCells() instead UnselectAll().

Categories

Resources