WPF TextBox Binding does not update with Drag and Drop - c#

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!

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.

How to force update of validation error indicator (red line)

In App.xaml.cs I've registered a validation error event handler, to revert all my TextBox controls to previous value (value in VM property) when a validation error occurs (e.g. if a TextBox bound to a double property is entered a string value).
public App()
{
EventManager.RegisterClassHandler(typeof(TextBox), Validation.ErrorEvent,
new RoutedEventHandler(TextBox_ValidationErrorEventHandler));
}
private void TextBox_ValidationErrorEventHandler(object sender, RoutedEventArgs e)
{
var tb = (TextBox)sender;
DependencyProperty prop = TextBox.TextProperty;
BindingExpression binding = BindingOperations.GetBindingExpression(tb, prop);
if (binding != null) { binding.UpdateTarget(); }
}
This works fine. But the red line around the control remains and is never removed again when I enter legal values. How can I force update of the validation, so that the red line is removed?
You just need to "clean" the value of the Validation.ErrorTemplate property of the TextBox, by colling the SetErrorTemplate method:
private void TextBox_ValidationErrorEventHandler(object sender, RoutedEventArgs e)
{
TextBox tb = sender as TextBox;
if (tb != null)
{
DependencyProperty prop = TextBox.TextProperty;
BindingExpression binding = BindingOperations.GetBindingExpression(tb, prop);
if (binding != null)
{
binding.UpdateTarget();
Validation.SetErrorTemplate(tb, null);
}
}
}
I hope it can help you.

How to set binding property to null if DatePickerTextBox is left empty

My DatePickerTextBox is binded to a property of type DateTime? (which allows null)
I would like to set this property to null if the DatePickerTextBox is left empty.
My current approach:
private void TextChanged_Handler(object sender, TextChangedEventArgs e)
{
var dateTimePickerTextBox = (DatePickerTextBox)sender;
if (dateTimePickerTextBox.Text == string.Empty)
this.MyBindingObj.MyDate = null;
}
This works, but it has the disadvantage that the DatePickerTextBox UI is marked as red (error), because the binding between the text and DateTime was not successful. Although behind the scenes everything works fine.
I wonder if there is a cleaner way to do this.
I found a workaround, by simply adding dateTimePickerTextBox.Text = null; to the previous code:
private void TextChanged_Handler(object sender, TextChangedEventArgs e)
{
var dateTimePickerTextBox = (DatePickerTextBox)sender;
if (dateTimePickerTextBox.Text == string.Empty)
{
this.MyBindingObj.MyDate = null;
// Added this to avoid validation error:
dateTimePickerTextBox.Text = null;
}
}

Cancel or Save changes made to a custom object in a DataGrid

I have a DataGrid that has an ItemsSource set to a ObservableCollection of custom objects, that is passed from a window to the usercontrol that contains the DataGrid, and while this works quite well and changes are registered in both directions I would like to have the ability to cancel any changes made in my UserControl if i click a cancel button.
Is there some way to defer any changes I make in my DataGrid to my ItemSource in my parent window? or conversely some way to cancel changes if I so choose. Any help would be greatly appreciated, here is my current code;
public UserControl1(ObservableCollection<ContourControl.RectangleContour> list1)
{
InitializeComponent();
itemCollectionViewSource = (CollectionViewSource)(FindResource("ItemCollectionViewSource"));
itemCollectionViewSource.Source = list1;
//here i create some columns and bind the data
DataGridViewer.ItemsSource = list1;
DataGridViewer.AutoGenerateColumns = false;
}
private void Cancel(object sender, RoutedEventArgs e)
{
//if i click this i would like any changes to list1 to be reverted or ignored
Window parentWindow = Window.GetWindow((DependencyObject)sender);
if (parentWindow != null)
{
parentWindow.Close();
}
}
private void OK(object sender, RoutedEventArgs e)
{
//if i click this i would like the changes to carry on to my list, as it currently does instantly
Window parentWindow = Window.GetWindow((DependencyObject)sender);
if (parentWindow != null)
{
parentWindow.Close();
}
}
and here is the code that launches my usercontrol and passes the original list i have the data stored in.
Window window = new Window
{
Height = 200,
Width = 765,
Content = new UserControl1(list1: rectContourList),
};
window.ShowDialog();
You can set UpdateSourceTrigger=Explicitto your textbox.
<TextBox Name="tb1" Text="{Binding tbval, UpdateSourceTrigger=Explicit}"/>
Then on "ok" button click update the source
private void OK_Button_Click(object sender, RoutedEventArgs e)
{
BindingExpression binding = tb1.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();
}
And on "cancel" just set the previous property back if you want to show the previous value on the textbox, else you can just leave it empty.
private void Cancel_Button_Click_1(object sender, RoutedEventArgs e)
{
//tbval is the property binded to Textbox
tbval = tbval;
}
https://msdn.microsoft.com/en-us/library/system.windows.data.binding.updatesourcetrigger.aspx

WPF Drag and Drop - Get original source info from DragEventArgs

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);
}
}
}

Categories

Resources