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)
Related
I have been working on this issue for a month now but can't seem to solve it. I am trying to drag an item from RadListView1 to RadListView2. I don't actually want to move or copy this item to RadListView2. I want to combine the item with an item that I am over when I release the mouse button over RadListView2 and display it in a RadListView3. Example. Drag a song name over to a singer and it combine the singer and songname into the RadListView3.
I just can't figure out the drag events and how to get the item I dragged to in RadListView2. Hopefully I didn't confuse the heck out of you.
Any help appreciated. I've looked at the Telerik docs forever but can't get it.
Thanks.
I can't even get this to work
private void DragDropService_PreviewDragOver(object sender, RadDragOverEventArgs e)
{
e.CanDrop = e.HitTarget is DetailListViewDataCellElement || e.HitTarget is DetailListViewElement;
Debug.WriteLine("PreviewDragOver triggered");
}
private void DragDropService_PreviewDragDrop(object sender, RadDropEventArgs e)
{
BaseListViewVisualItem draggedItem = e.DragInstance as BaseListViewVisualItem;
DetailListViewDataCellElement cellElement = e.HitTarget as DetailListViewDataCellElement;
DetailListViewElement viewElement = e.HitTarget as DetailListViewElement;
if (cellElement == null && viewElement == null)
{
Debug.WriteLine("All NULL...returning");
return;
}
e.Handled = true;
if (viewElement != null) Debug.WriteLine("Over ViewElement");
if (cellElement != null) Debug.WriteLine("Over cellElement");
}
See this article, in the PreviewDragDrop event, you have the DragInstance and the HitTarget.
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 have a headered items control bound to an observable collection. The items in the collection are associated with a data template. The data template is pretty simple in that it only contains two text boxes. The part I am having difficulty with is setting focus to the text boxes programatically.
When the items control is first displayed it always has one item in it. It may or may not have a value that is bound to the first text box. If it has a value, the focus needs to be set to the second text box. I've done this using the following:
protected override void OnActivated(EventArgs e)
{
base.OnActivated(e);
if (_activated) return;
_activated = true;
var dc = DataContext as MyViewModel;
if (null != dc)
{
var cp = theItemsControl.ItemContainerGenerator.ContainerFromIndex(0) as ContentPresenter;
var tb = FindVisualChildren<TextBox>(cp);
if (tb != null)
{
if (!String.IsNullOrEmpty(dc.TheCollection.First().FirstValue))
{
var t = tb.FirstOrDefault(box => box.Name == "SecondTextBox");
if (null != t) t.Focus();
}
else
{
var t = tb.FirstOrDefault(box => box.Name == "FirstTextBox");
if (null != t) t.Focus();
}
}
}
}
This seems to work fine. However, now, when a new item is added to the collection I need to set focus to the FirstTextBox. I've tried setting focus in the datatemplate using the FocusManager, but that prohibits the above code from working correctly. I've also tried the following in the view. The method gets called from the ViewModel after the new item has been added to the ObservableCollection. The code runs, but it fails to find any textboxes (ie. tb == null). Have the controls been created at this point?
public void SetFocusToNewItem(int index)
{
var dc = DataContext as MyViewModel;
if (null != dc)
{
var cp = theItemsControl.ItemContainerGenerator.ContainerFromIndex(index) as ContentPresenter;
var tb = FindVisualChildren<TextBox>(cp);
if (tb != null)
{
var t = tb.FirstOrDefault(box => box.Name == "FirstTextBox");
if (null != t) t.Focus();
}
}
}
Are there other solutions to set focus after the item has been added?
Sometimes the following trick works for me:
// Replace such call:
SetFocusToNewItem(newIndex);
// By this:
Dispatcher.BeginInvoke((Action) (() =>
{
SetFocusToNewItem(newIndex);
}));
The problem is when you add an item in collection UI elements is not created, so you can't get it. This code above delays your function executing and you method should be executed after UI is created.
PS: You can move Dispatcher call inside of your method.
EDIT:
This may help too (works for me):
var cp = theItemsControl.ItemContainerGenerator.ContainerFromIndex(index) as ContentPresenter;
cp.Loaded += (s, e2) =>
{
var tb = FindVisualChildren<TextBox>(e2.Source as UIElement)
.FirstOrDefault();
if (tb != null)
tb.Focus();
};
The reason is the same: UI element was not created.
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
I have 2 LookUpEdit controls from DevExpress on my form. Both use an ObservableCollection as it's datasource, one being of type string and the other of type double. The LookUpEdit control has an event called ProcessNewValue which fires when, you guessed it, a new value is entered in the control. I've added some code in this event to add the newly added value to the ObservableCollection and it automatically selects it once done. This works as expected for the string LooUpEdit but when I try it with the double LookUpEdit`, it adds it to the collection but then it clears out the control.
Here's the code to load the controls, which gets called in Form_Load():
void InitControls()
{
double[] issueNumbers = new double[5];
issueNumbers[0] = 155;
issueNumbers[1] = 156;
issueNumbers[2] = 157;
issueNumbers[3] = 158;
issueNumbers[4] = 159;
ObservableCollection<double> issues = new ObservableCollection<double>(issueNumbers);
lookupIssues.Properties.DataSource = issues;
DevExpress.XtraEditors.Controls.LookUpColumnInfoCollection colInfo = lookupIssues.Properties.Columns;
colInfo.Clear();
colInfo.Add(new DevExpress.XtraEditors.Controls.LookUpColumnInfo("Column"));
colInfo[0].Caption = "Issue ID's";
string[] stringNumbers = Array.ConvertAll<double, string>(issueNumbers, Convert.ToString);
ObservableCollection<string> issuesString = new ObservableCollection<string>(stringNumbers);
lookupStringValue.Properties.DataSource = issuesString;
colInfo.Clear();
colInfo.Add(new DevExpress.XtraEditors.Controls.LookUpColumnInfo("Column"));
colInfo[0].Caption = "String Issue ID's";
}
And here's the ProcessNewValue event for both (I've renamed them to try to make it easier to see which does what):
private void OnProcessNewValue_Double(object sender, DevExpress.XtraEditors.Controls.ProcessNewValueEventArgs e)
{
ObservableCollection<double> source = (ObservableCollection<double>)(sender as LookUpEdit).Properties.DataSource;
if (source != null)
{
if ((sender as LookUpEdit).Text.Length > 0)
{
source.Add(Convert.ToDouble((sender as LookUpEdit).Text));
(sender as LookUpEdit).Refresh();
}
}
e.Handled = true;
}
private void OnProcessNewValue_String(object sender, DevExpress.XtraEditors.Controls.ProcessNewValueEventArgs e)
{
ObservableCollection<string> source = (ObservableCollection<string>)(sender as LookUpEdit).Properties.DataSource;
if (source != null)
{
if ((sender as LookUpEdit).Text.Length > 0)
{
source.Add((sender as LookUpEdit).Text);
(sender as LookUpEdit).Refresh();
}
}
e.Handled = true;
}
As you can see, the code it identical with the exception of one converting text to a double before adding it to the collection.
Anyone know why the double value gets added to the collection but the control doesn't automatically select it like it does with a string collection? I've even tried to hard-code the newly added value right after e.Handled = true; but it still doesn't select it. What's weird is that if I run it through the debugger, I can step through and see that the lookupIssues control indeed gets the newly added value AND it's Text property is set to it, but as soon as the event terminates, the control clears it out.....really strange.
Any help is greatly appreciated!
BTW, I can add a link to a sample project that duplicates the problem but you would need to have DevExpress v12.2.6 controls installed in order to compile the project.
I posted this to the DevExpress team as well and they were gracious enough to provide the solution:
I agree that this discrepancy appears confusing as-is. The reason for the discrepancy is LookUpEdit.ProcessNewValueCore makes a call to RepositoryItemLookUpEdit.GetKeyValueByDisplayValue which returns a null value from the LookUpListDataAdapter because no implicit conversion exists from double to string. You may resolve the discrepancy with the following change to your ProcessNewValue handler:
private void OnProcessNewValue_Double(object sender, DevExpress.XtraEditors.Controls.ProcessNewValueEventArgs e)
{
ObservableCollection<double> source = (ObservableCollection<double>)(sender as LookUpEdit).Properties.DataSource;
if (source != null) {
if ((sender as LookUpEdit).Text.Length > 0) {
double val = Convert.ToDouble((sender as LookUpEdit).Text);
source.Add(val);
e.DisplayValue = val;
(sender as LookUpEdit).Refresh();
}
}
e.Handled = true;
}
The control now behaves as expected. I hope this can help someone else out :)