I am creating a custom ComboBox control with Google Search results implementation but I am running into some problem using the arrow keys.
Details of the problem
Please watch this video as I demonstrate the problem with UP and DOWN arrow keys and take a look on the output panel as well.
http://screencast.com/t/DFkmlDKR
In the video, I tried searching for "a" (just a test) and returns the Google search results that starts with "a" then I am pressing the DOWN and UP arrow keys. In the output panel, it shows the highlighted item as I press these keys.
Then I typed the next letter "l". Now If you noticed on the output panel, the selection is now "null" but I'm still pressing the UP & DOWN arrow keys.
And when you hover on the item again using the mouse pointer, it will start working again.
I am stuck with this problem for days and haven't figured out the solution.
I have uploaded the test version of this control so you can play with it as well. Here it is GoogleSuggestionComboBox
My goal here is to make the UP & DOWN arrow keys work all the time.
In this section of the code
I tried adding
SelectedIndex = 0;
after the ForEach statement so everytime the collection is repopulated with the new results, it will select the first result. Unfortunately, it did not work.
You can download the test code so you can play and test the problem. http://sdrv.ms/1eWV3Bc and here's the code for the ComboBox as well.
using GoogleSuggestionComboBox.Model;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace GoogleSuggestionComboBox
{
public class ComboBoxExC : ComboBox
{
GoogleSuggest google;
TextBox textbox;
string _text = string.Empty;
string _last_text = string.Empty;
public ComboBoxExC()
{
if (DesignerProperties.GetIsInDesignMode(this))
{
}
else
{
this.Loaded += ComboBoxExC_Loaded;
google = new GoogleSuggest();
google.OnGoogleSuggestAvailable += google_OnGoogleSuggestAvailable;
// since we have OnSelectionChanged "disabled"
// we need a way to know if the item in ComboBox is selected using mouse
EventManager.RegisterClassHandler(typeof(ComboBoxItem), ComboBoxItem.MouseDownEvent, new MouseButtonEventHandler(OnItemMouseDown));
}
}
void ComboBoxExC_Loaded(object sender, RoutedEventArgs e)
{
this.textbox = (TextBox)Template.FindName("PART_EditableTextBox", this);
}
private void OnItemMouseDown(object sender, MouseButtonEventArgs e)
{
var comboBoxItem = sender as ComboBoxItem;
if (comboBoxItem != null && comboBoxItem.IsHighlighted)
{
Model_SuggestedQueries m = (Model_SuggestedQueries)comboBoxItem.Content;
Go(m.Query);
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
// don't do anything so the .Text value won't change.
//base.OnSelectionChanged(e);
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
this.IsDropDownOpen = true;
base.OnPreviewKeyDown(e);
d("key: " + e.Key.ToString());
if (this.SelectedItem != null)
{
Model_SuggestedQueries m = (Model_SuggestedQueries)this.SelectedItem;
d("selected: " + m.Query);
}
else
{
d("null");
}
}
protected override void OnPreviewKeyUp(KeyEventArgs e)
{
base.OnPreviewKeyUp(e);
if (e.Key == Key.Enter)
{
if (this.SelectedItem == null)
{
Go(this.Text);
}
else
{
Model_SuggestedQueries m = (Model_SuggestedQueries)this.SelectedItem;
Go(m.Query);
}
}
else
{
if (this.Text != this._last_text)
{
google.LookForSuggestion(this.Text);
this._last_text = this.Text;
}
}
}
void google_OnGoogleSuggestAvailable(object sender, List<Model.Model_SuggestedQueries> suggestions)
{
this.Items.Clear();
suggestions.ForEach((a) =>
{
this.Items.Add(a);
});
}
void d(object a)
{
Debug.WriteLine(">>> " + a);
}
void Go(string query)
{
Process.Start("https://www.google.com.ph/search?q=" + query);
// clear suggestions
this.Items.Clear();
}
}
}
MainWindow.xaml
<Window x:Class="GoogleSuggestionComboBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:GoogleSuggestionComboBox"
Title="MainWindow" Height="133" Width="261" WindowStartupLocation="CenterScreen"
>
<Grid>
<StackPanel Margin="10">
<TextBlock Text="Search" />
<l:ComboBoxExC
IsEditable="True"
IsTextSearchEnabled="False"
TextSearch.TextPath="Query"
>
<l:ComboBoxExC.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Query}" />
</DataTemplate>
</l:ComboBoxExC.ItemTemplate>
</l:ComboBoxExC>
</StackPanel>
</Grid>
</Window>
Thank you,
Jayson
This is a pretty old question but I ran into about the same problem. The main issue for me was after selecting an item or clearing selection (setting it to -1) I could not go through the items with the arrow keys anymore until I hovered over them with my mouse.
After some days of struggling I found a fix for this:
KeyboardNavigation.SetDirectionalNavigation( this, KeyboardNavigationMode.Cycle );
this is the combobox, and this line is in the constructor of a sub classed combobox.
I don't know the exact issue, but here is a suggestion:
Don't subclass the ComboBox - you don't need to.
Just drop one on the MainForm, and then work with it from the code behind - it is much easier to work with it like that. You only need to subclass if you need to change the way a combo-box works which is not typical.
I tried downloading the test project, but it is not a zip file - it is a .7z which I could not open.
Also, you main form is a Window. That may not be an issue, but try creating a new project and just working with the Main form the project gives you. There is some wiring up that is done in the App that calls the MainForm that will catch exceptions etc, and that may give you more data as to what the issue is.
Greg
Related
I'm trying to get WPF Slider value after the user finishing to drag the thumb or clicking to move to specific point.
i want to save the new value in db by listening to some kind of event. how can i do this?
i tried the solutions on this question but i end up with nothing -
WPF: Slider with an event that triggers after a user drags
each time it enters the event lots of times
thanks.
xaml:
<Slider Value="{Binding VoipVolume}" MouseLeftButtonUp="slider_MouseLeftButtonUp"/>
codebehind:
public double VoipVolume
{
get { return (double)GetValue(VoipVolumeProperty); }
set
{
SetValue(VoipVolumeProperty, value);
VolumeChanged(value);
}
}
private void VolumeChanged(object value)
{
StationViewModel viewModel = this.DataContext as StationViewModel;
if (viewModel != null)
{
if (end)
{
viewModel.OnVolumeChange((float)VoipVolume);
end = false;
}
}
}
bool end = false;
private void slider_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
end = true;
}
Possibly duplicate. hope this may help you out: stackoverflow.com/a/723547/1611490
<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
and under MySlider_DragCompleted event, update your property of the VM or get the value.
if your are using FW 4.5 or above, try using the "Dalay" property.
http://www.jonathanantoine.com/2011/09/21/wpf-4-5-part-4-the-new-bindings-delay-property/
We didn't have a choice, in the end we use TickFrequency because we need to save the changes in the slider to db, and we don't want it to happen for every 0.001..
thanks anyway..
I am making a Universal Windows App, which includes inserting text into a textbox. I want my app to suggest text from a file to insert to the textbox. But I could not find that property. I have added the textbox in the MainPage.xaml through XAML tags. I believe there is a property for this operation in WPF API. I am just not sure if I can do this in UWP.
I recommend using the AutoSuggestBox control for UWP. The auto-suggest results list populates automatically once the user starts to enter text. The results list can appear above or below the text entry box.
<AutoSuggestBox PlaceholderText="Search" QueryIcon="Find" Width="200"
TextChanged="AutoSuggestBox_TextChanged"
QuerySubmitted="AutoSuggestBox_QuerySubmitted"
SuggestionChosen="AutoSuggestBox_SuggestionChosen"/>
private void AutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
// Only get results when it was a user typing,
// otherwise assume the value got filled in by TextMemberPath
// or the handler for SuggestionChosen.
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
//Set the ItemsSource to be your filtered dataset
//sender.ItemsSource = dataset;
}
}
private void AutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
{
// Set sender.Text. You can use args.SelectedItem to build your text string.
}
private void AutoSuggestBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
if (args.ChosenSuggestion != null)
{
// User selected an item from the suggestion list, take an action on it here.
}
else
{
// Use args.QueryText to determine what to do.
}
}
Here is the link to the GitHub repo for a complete UI basics sample.
Hope this helps.
This may not apply for UAP but with WPF there's a trick that allows a "dropdown suggestion list". You can replace text box with a combobox and populate it's items when user types. This can be achieved by doing bindings like so:
Text={ Binding Path=meCurrentValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged }
ItemsSource={Binding Path=meFilteredListOfSuggestions, Mode=TwoWay }
Then within your viewmodel you can simply do:
public string meCurrentValue
{
get { return _mecurrentvalue; }
set {
_mecurrentvalue = value;
updateSuggestionsList();
NotifyPropertyChanged("meCurrentValue");
NotifyPropertyChanged("meFilteredListOfSuggestions"); // notify that the list was updated
ComboBox.Open(); // use to open the combobox list
}
public List<string> meFilteredListOfSuggestions
{
get{return SuggestionsList.Select( e => e.text.StartsWith(_mecurrentvalue));}
}
EDIT:
Remember to set the editable value of the combobox to TRUE, this way it will act like a normal textbox.
I have a series(10) of selectable TextBoxes. I need to be able to click on one of them and select all the text boxes on which the mouse is moved until the click is released.
I used the following code but I am unable to hit the MouseMove on the other TextBoxes. It always hits the TextBox on which the Click was made.
class SelectableTextBox: TextBox
{
public Boolean IsSelected { get; protected set; }
public void select(Boolean value)
{
this.IsSelected = value;
if (value)
{
this.Background = System.Windows.Media.Brushes.Aqua;
}
else
{
this.Background = System.Windows.Media.Brushes.White;
}
}
}
private void onPreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
this.SelectionStartedRight = !textBox.IsSelected;
textBox.select(!textBox.IsSelected);
}
private void onPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
if (this.SelectionStartedRight)
{
textBox.select(true);
}
}
private void onPreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
this.SelectionStartedRight = false;
}
Try using MouseEnter event instead of MouseMove. Attach MouseEnter to your selectable textboxes. That would ensure that only they trigger the desired event handler and its code.
If you decide to stay with the global handler, be careful when you convert sender to a specific control type. You need to account for those times when it's not the "expected" control:
SelectableTextBox textBox = sender as SelectableTextBox;
if (textBox != null)
{
// The rest of the code here...
}
select is a Linq keyword, so you might want to rename that method to avoid any conflicts down the road. While I don't think it'd be causing any issues, I would change it.
You don't have to follow this convention, but in C# its customary to use an uppercase for the first letter of a method. Also note that the default access modifier for a class is internal... just want to make sure you're aware of that, as you continue your development.
Your last method, onPreviewMouseLeftButtonUp(...) has the following code in it:
SelectableTextBox textBox = (SelectableTextBox)sender;
Not only is it unsafe, as I've described above, but it also does absolutely nothing.
Lastly... and this is just me, I would probably move the code that handles changing the state of a selectable textbox (selected or not selected) into its class, since it belongs there. Why should anything else be in charge of how it handles its state. Keep things where they belong and you'll have a much easier time testing, debugging and maintaining your code. Don't fall into the "I'll refactor it later" trap... it'll rarely happen.
Here's my crude example. I let the class handle its MouseEnter event and simply check if the Mouse.LeftButton is down at that time. You would have to expand on it, but it should be a solid start:
Made some edits per OP's requests in the comments.
Preview:
XAML:
<Window x:Class="SelectableTextBoxes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SelectableTextBoxes"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
</StackPanel>
</Window>
C# (Pardon me for putting the SelectableTextBox into the same file...):
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SelectableTextBoxes
{
// Move this class into its own file (it's here for prototyping).
public class SelectableTextBox : TextBox
{
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
// If the value changes, sets new color.
SetSelectionColor();
}
}
}
public SelectableTextBox()
{
// For handling an initial click if it happens in the textbox.
PreviewMouseDown += SelectableTextBox_PreviewMouseDown;
// For handling selection when mouse enters the textbox
// and left mouse button is down.
MouseEnter += SelectableTextBox_MouseEnter;
// To handle mouse capture (release it).
GotMouseCapture += SelectableTextBox_GotMouseCapture;
}
// Handles the mouse down event within the textbox.
void SelectableTextBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (!IsSelected)
{
IsSelected = true;
}
// If one of the Shift keys is down, return, since
// we don't want to deselect others.
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
return;
}
// This part makes a poor assumption that the parent is
// always going to be a Panel... expand on this code to
// cover other types that may contain more than one element.
var parent = VisualTreeHelper.GetParent(this) as Panel;
if (parent != null)
{
foreach (var child in parent.Children)
{
// If a child is not of a correct type, it'll be null.
var tbx = child as SelectableTextBox;
// This is where we check to see if it's null or this instance.
if (tbx != null && tbx != this)
{
tbx.IsSelected = false;
}
}
}
}
// When textbox receives focus, this event fires... we need to release
// the mouse to continue selection.
void SelectableTextBox_GotMouseCapture(object sender, MouseEventArgs e)
{
ReleaseMouseCapture();
}
// Sets selection state to true if the left mouse button is
// down while entering.
void SelectableTextBox_MouseEnter(object sender, MouseEventArgs e)
{
if (Mouse.LeftButton == MouseButtonState.Pressed)
{
IsSelected = true;
}
}
// Sets the background color based on selection state.
private void SetSelectionColor()
{
if (IsSelected)
{
Background = Brushes.LightCyan;
}
else
{
Background = Brushes.White;
}
}
}
// Window code... should be on its own, but I placed the two
// classes together while prototyping.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
I am porting my program from WinForms to WPF and have ran into some issues with the drag and drop. It should allow for dragging from a TreeView (it is like a file explorer) to a textbox which opens the file. However, the WPF version acts like a copy-and-paste of the TreeViewItem's header text automatically. I think I just have something mixed up? Possibly the DataObject stuff.
The fully functional, relevant WinForms code:
private void treeView1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
TreeNode node = treeView1.GetNodeAt(e.Location);
if (node != null) treeView1.DoDragDrop(node, DragDropEffects.Move);
}
textbox[i].DragDrop += (o, ee) =>
{
if (ee.Data.GetDataPresent(typeof(TreeNode)))
{
TreeNode node = (TreeNode)ee.Data.GetData(typeof(TreeNode));
((Textbox)o).Text = File.ReadAllLines(pathRoot + node.Parent.FullPath);
...
The WPF code that should do the same thing:
private void TreeView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
TreeViewItem item = e.Source as TreeViewItem;
if (item != null)
{
DataObject dataObject = new DataObject();
dataObject.SetData(DataFormats.StringFormat, GetFullPath(item));
DragDrop.DoDragDrop(item, dataObject, DragDropEffects.Move);
}
}
//textbox[i].PreviewDrop += textbox_Drop;
private void textbox_Drop(object sender, DragEventArgs e)
{
TreeViewItem node = (TreeViewItem)e.Data.GetData(typeof(TreeViewItem)); //null?
((Textbox)sender).Text = "";
//this is being executed BUT then the node's header text is being pasted
//also, how do I access the DataObject I passed?
}
Problem: In my WPF version, I am setting the textbox's text to empty (as a test), which occurs, but afterwards the TreeViewItem's header text is being pasted which is not what I want.
Questions: What is the correct way to port this WinForms code to WPF? Why is the text being pasted in the WPF version? How do I prevent that? Am I using the correct events? How do I access the DataObject in textbox_Drop so that I can open the file like I did in the WinForms version? Why is TreeViewItem node always null in the WPF version?
Ah, what the heck, I'll expand my comment to an answer:
The link to read, as mentioned, is this:
http://msdn.microsoft.com/en-us/library/hh144798.aspx
Short story, the TextBox-derived controls already implement most of the "guts" for basic drag/drop operations, and it is recommended that you extend that rather than provide explicit DragEnter/DragOver/Drop handlers.
Assuming a tree "data" structure like:
public class TreeThing
{
public string Description { get; set; }
public string Path { get; set; }
}
The handlers might look something like this:
this.tb.AddHandler(UIElement.DragOverEvent, new DragEventHandler((sender, e) =>
{
e.Effects = !e.Data.GetDataPresent("treeThing") ?
DragDropEffects.None :
DragDropEffects.Copy;
}), true);
this.tb.AddHandler(UIElement.DropEvent, new DragEventHandler((sender, e) =>
{
if (e.Data.GetDataPresent("treeThing"))
{
var item = e.Data.GetData("treeThing") as TreeThing;
if (item != null)
{
tb.Text = item.Path;
// TODO: Actually open up the file here
}
}
}), true);
And just for giggles, here's a quick-and-dirty test app that is pure showboating in it's use of the Reactive Extensions (Rx) for the drag start stuff:
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TreeView x:Name="tree" Grid.Column="0" ItemsSource="{Binding TreeStuff}" DisplayMemberPath="Description"/>
<TextBox x:Name="tb" Grid.Column="1" AllowDrop="True" Text="Drop here" Height="30"/>
</Grid>
</Window>
Nasty code-behind (too lazy to MVVM this):
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
TreeStuff = new ObservableCollection<TreeThing>()
{
new TreeThing() { Description="file 1", Path = #"c:\temp\test.txt" },
new TreeThing() { Description="file 2", Path = #"c:\temp\test2.txt" },
new TreeThing() { Description="file 3", Path = #"c:\temp\test3.txt" },
};
var dragStart =
from mouseDown in
Observable.FromEventPattern<MouseButtonEventHandler, MouseEventArgs>(
h => tree.PreviewMouseDown += h,
h => tree.PreviewMouseDown -= h)
let startPosition = mouseDown.EventArgs.GetPosition(null)
from mouseMove in
Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(
h => tree.MouseMove += h,
h => tree.MouseMove -= h)
let mousePosition = mouseMove.EventArgs.GetPosition(null)
let dragDiff = startPosition - mousePosition
where mouseMove.EventArgs.LeftButton == MouseButtonState.Pressed &&
(Math.Abs(dragDiff.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(dragDiff.Y) > SystemParameters.MinimumVerticalDragDistance)
select mouseMove;
dragStart.ObserveOnDispatcher().Subscribe(start =>
{
var nodeSource = this.FindAncestor<TreeViewItem>(
(DependencyObject)start.EventArgs.OriginalSource);
var source = start.Sender as TreeView;
if (nodeSource == null || source == null)
{
return;
}
var data = (TreeThing)source
.ItemContainerGenerator
.ItemFromContainer(nodeSource);
DragDrop.DoDragDrop(nodeSource, new DataObject("treeThing", data), DragDropEffects.All);
});
this.tb.AddHandler(UIElement.DragOverEvent, new DragEventHandler((sender, e) =>
{
e.Effects = !e.Data.GetDataPresent("treeThing") ?
DragDropEffects.None :
DragDropEffects.Copy;
}), true);
this.tb.AddHandler(UIElement.DropEvent, new DragEventHandler((sender, e) =>
{
if (e.Data.GetDataPresent("treeThing"))
{
var item = e.Data.GetData("treeThing") as TreeThing;
if (item != null)
{
tb.Text = item.Path;
// TODO: Actually open up the file here
}
}
}), true);
this.DataContext = this;
}
private T FindAncestor<T>(DependencyObject current)
where T:DependencyObject
{
do
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
}
while (current != null);
return null;
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<TreeThing> TreeStuff { get; set; }
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class TreeThing
{
public string Description { get; set; }
public string Path { get; set; }
}
}
You've got more than one problem, enough to make this difficult. First issue is that you got the drag object wrong, you are dragging a string but still checking for a TreeViewItem. Just use the same approach as you used in Winforms, dragging the node. Second problem is that TextBox already implements D+D support and that gets in the way of your code. And the reason you saw the text show up after the drop.
Let's tackle the start of the drag first. You'll need to do a bit of extra work since the way you started the drag interferes with the normal usage of the TreeView, it gets very hard to select a node. Only start the drag when the mouse was moved far enough:
private Point MouseDownPos;
private void treeView1_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
MouseDownPos = e.GetPosition(treeView1);
}
private void treeView1_PreviewMouseMove(object sender, MouseEventArgs e) {
if (e.LeftButton == MouseButtonState.Released) return;
var pos = e.GetPosition(treeView1);
if (Math.Abs(pos.X - MouseDownPos.X) >= SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(pos.Y - MouseDownPos.Y) >= SystemParameters.MinimumVerticalDragDistance) {
TreeViewItem item = e.Source as TreeViewItem;
if (item != null) DragDrop.DoDragDrop(item, item, DragDropEffects.Copy);
}
}
Now the drop, you will need to implement the DragEnter, DragOver and Drop event handlers to avoid the default D+D support built into TextBox from getting in the way. Setting the e.Handled property to true is necessary:
private void textBox1_PreviewDragEnter(object sender, DragEventArgs e) {
if (e.Data.GetDataPresent(typeof(TreeViewItem))) e.Effects = e.AllowedEffects;
e.Handled = true;
}
private void textBox1_PreviewDrop(object sender, DragEventArgs e) {
var item = (TreeViewItem)e.Data.GetData(typeof(TreeViewItem));
textBox1.Text = item.Header.ToString(); // Replace this with your own code
e.Handled = true;
}
private void textBox1_PreviewDragOver(object sender, DragEventArgs e) {
e.Handled = true;
}
Problem: In my WPF version, I am setting the textbox's text to empty (as a test), which occurs, but afterwards the TreeViewItem's header text is being pasted which is not what I want.
I think a parent UI element is handling (and therefore overriding) the Drop event so you're not getting the results you expect. As a matter of fact, when trying to recreate your issue, I couldn't even get my TextBox.Drop event to fire. However, using the TextBox's PreviewDrop event, I was able to get what (I think) is your expected result. Try this:
private void textBox1_PreviewDrop(object sender, DragEventArgs e)
{
TextBox tb = sender as TextBox;
if (tb != null)
{
// If the DataObject contains string data, extract it.
if (e.Data.GetDataPresent(DataFormats.StringFormat))
{
string fileName = e.Data.GetData(DataFormats.StringFormat) as string;
using (StreamReader s = File.OpenText(fileName))
{
((TextBox)sender).Text = s.ReadToEnd();
}
}
}
e.Handled = true; //be sure to set this to true
}
I think that code snippet should answer most of the questions you posed except for this one:
Why is TreeViewItem node always null in the WPF version?
The DataObject you are passing in the DragDrop event does not support passing a TreeViewItem. In your code (and mine) we specify that the data format will be DataFormats.StringFormat which cannot be cast to a TreeViewItem.
GetFullPath seems to be outputting a wrong value. What you want to drag/drop is the Header and you can get it directly from item. Also bear in mind that the method below is associated with the MouseMove Event of the TreeView.
private void TreeView_MouseMove(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed) return;
TreeViewItem item = e.Source as TreeViewItem;
if (item != null)
{
DataObject dataObject = new DataObject();
dataObject.SetData(DataFormats.StringFormat, item.Header);
DragDrop.DoDragDrop(item, dataObject, DragDropEffects.Move);
}
}
I did create the drop part based on text rather than on the TreeViewItem (e.Data.GetData(typeof(string)).ToString()) but the most surprising thing is that it isn't even required. If you open a new C# WPF project, put a TreeView and a TextBox on it (, update the XAML part) and copy the code above, you can drop text from the TreeView into the TextBox without doing anything else!! The text is copied into the TextBox without accounting for the Drop handling.
Am I using the correct events?:
I think you are using the correct events, but I think you have several problems in your code.
I assume you have set the DataContext of your treeview to the real items and you use binding.
How do I access the DataObject in textbox_Drop ? -->
For getting the DataObject you have to get the real item by recursion (other solutions possible)
DependencyObject k = VisualTreeHelper.HitTest(tv_treeView, DagEventArgs.GetPosition(lv_treeView)).VisualHit;
while (k != null)
{
if (k is TreeViewItem)
{
TreeViewItem treeNode = k as TreeViewItem;
// Check if the context is your desired type
if (treeNode.DataContext is YourType)
{
// save the item
targetTreeViewItem = treeNode;
return;
}
}
else if (k == tv_treeview)
{
Console.WriteLine("Found treeview instance");
return;
}
// Get the parent item if no item from YourType was found
k = VisualTreeHelper.GetParent(k);
}
Why is the text being pasted in the WPF version? -->
The Header is displayed because (I assume) it is like the tostring method on your items. If for a complex item the binding is not specified, the ToString Method is executed.
Try not to set the Text directly in the handler of the drop event. Set the data context to your item (to the item you found in 1. point) and then specify the binding path via XAML. (for displaying)
In my VS 2012 WPF project, I have a customized textbox class called TextBoxDX that adds an AutoSelect feature. No problem. I have another class based on TextBoxDX called IntBox which only allows integers. That's where our story begins. Both classes are used in binding situations like so:
<local:TextBoxDX Grid.Row="0" Grid.Column="1" x:Name="txtBox_singlesName" Width="320" HorizontalAlignment="left" Text="{Binding SelectedItem.name, ElementName=listBoxSingles, Mode=OneWay}"/>
<local:IntBox x:Name="intBox_heightin" Width="60" AllowZeroValue="True" MaxLength="2" Text="{Binding SelectedItem.heightin, ElementName=listBoxSingles, Mode=OneWay}" MinVal="0" MaxVal="11"/>
Pretty sure most of that is irrelevant except for the binding. Both work fine in that they change their text corresponding to ListBox selections. But in the IntBox class, I had to add code for allowing only integers. In order to do that, I tapped into the TextChanged event in my IntBox class. The end result is this:
using System;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Herculese
{
public class IntBox : TextBoxDX
{
//INIT PROPERTIES
private int _MaxVal = 0;
private int _MinVal = 0;
private bool _AllowZeroValue = false;
//INIT STRING TO KEEP TRACK OF TEXT BEFORE CHANGES
private string originalText;
public IntBox()
{
//ADD TO TEXTCHANGED HANDLER
TextChanged += new TextChangedEventHandler(My_OnTextChanged);
//STORE ORIGINAL TEXT
originalText = this.Text;
}
//EVENT HANDLER WHEN TEXT IS CHANGED
private void My_OnTextChanged(object sender, EventArgs e)
{
//IF THERE IS TEXT IN THE BOX,
MessageBox.Show("yee");
if (this.Text.Length > 0)
{
//REMOVE SPACES AND LEADING ZEROS FROM STRING
if (!_AllowZeroValue)
this.Text = this.Text.TrimStart('0');
this.Text = Regex.Replace(this.Text, #"\s+", "");
//IF VALUE ISN'T NUMERICAL OR NOTHING IS LEFT AFTER REMOVING ZEROS AND SPACES, CHANGE TEXT BACK TO ORIGINAL
Regex regex = new Regex("[^0-9]+");
if (regex.IsMatch(this.Text) || this.Text.Length < 1)
{
this.Text = originalText;
System.Media.SystemSounds.Beep.Play();
}
//IF VALUE IS NUMERICAL,
else
{
//MAKE SURE VALUE IS WITHIN ACCEPTED RANGE. IF NOT, CHANGE IT TO HIGHEST/LOWEST AVAILABLE RESPECTIVELY
int intText;
intText = Convert.ToInt32(this.Text);
if (intText > _MaxVal)
{
this.Text = _MaxVal.ToString();
System.Media.SystemSounds.Beep.Play();
}
else if (intText < _MinVal)
{
this.Text = _MinVal.ToString();
System.Media.SystemSounds.Beep.Play();
}
//SUCCESS! UPDATE ORIGINAL TEXT WITH NEW VALID VALUE.
else originalText = this.Text;
}
}
}
//PROVIDE GET/SET PROPERTIES
public int MaxVal
{
get { return _MaxVal; }
set { _MaxVal = value; }
}
public int MinVal
{
get { return _MinVal; }
set { _MinVal = value; }
}
public bool AllowZeroValue
{
get { return _AllowZeroValue; }
set { _AllowZeroValue = value; }
}
}
}
As you can see, I had a great time in the TextChanged of my IntBox. Wild parties, you name it. Suddenly I realized that binding for IntBox wasn't working anymore. I could manually change the text just fine. It only accepted integers and worked like a charm. But changing the ListBox selection no longer updated the text. If I removed the code in My_OnTextChanged, the binding worked again. I figured my code was causing the problem. So I came back today with a fresh head and realized something weird.
If I remove the code in My_OnTextChanged and replace it with a MessageBox, the binding works and the message box appears. Makes perfect sense and furthers the idea that my code is causing the issue. Now for the weirdness: if I put the code back into the event after the MessageBox code, binding is broken again and the MessageBox never shows meaning the event never fires. All I can say to that is... HUUUUH?! I've recreated this several times just to be sure I'm not crazy. The only other thing I could think of is a conflict with the TextBoxDX that it inherits from so I made it inherit directly from TextBox and got the same results... Anybody got a clue on this one?
You are assigning a new value to Text which is removing the original binding as you have replaced it with a new string.
Instead of using this.Text = "Somthing" try using base.SetCurrentValue(TextProperty, value);
Example:
//IF VALUE ISN'T NUMERICAL OR NOTHING IS LEFT AFTER REMOVING ZEROS AND SPACES, CHANGE TEXT BACK TO ORIGINAL
Regex regex = new Regex("[^0-9]+");
if (regex.IsMatch(this.Text) || this.Text.Length < 1)
{
// this.Text = originalText;
base.SetCurrentValue(TextProperty, originalText);
System.Media.SystemSounds.Beep.Play();
}
SetCurrentValue Sets the value of a dependency property without changing its value source.