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.
Related
I'm trying to find the extents of a UI element in a ListBox. I use WPF / c# for this.
The code is roughly as follows:
ObservableCollection<SomeUserControl> TheCollection = new ObservableCollection<SomeUserControl>();
// setup
ListBox lb = new ListBox();
// code omitted here that setup the listbox with 2 columns
lb.ItemsSource = TheCollection;
// code omitted here that populates TheCollection with UI elements
Then I register a mouse event and I would like to know when a specific point is bounded by a specific element in the listbox. For this I have the following means to do it:
UIElement GetObjectHit(Point P) {
if (!lb.IsVisible) return null;
foreach(var i in TheCollection) {
FrameworkElement item = i as FrameworkElement;
if (item == null || !item.IsVisible)
continue;
Point P00 = item.TranslatePoint(new Point(0,0), lb);
Point P11 = item.TranslatePoint(new Point(item.ActualWidth, item.ActualHeight), lb);
if (IsBounded(P, P00, P11)
return item;
}
return null;
}
UIElement GetObjectHit(MouseButtonEventArgs e) {
return GetObjectHit(e.GetPosition(lb));
}
This works great with one exception...
The different user controls I have have varying sizes and the ListBox will then extend each item to the maximum element size currently in list and then center the smaller UserControls that are in the list.
So what's the problem?
Well when the position is in the white area which is inside the ListBoxItem area but outside the UserControl area the code above fails to find the intersection.
So my question is as follows:
Do I have to do a separate loop first to get the maximum extends of each UI element or is there any way to get the current extent of the ListBoxItem in the ListBox (even though the UserControl extent is smaller)?
In the unlikely event if anyone needs the answer to this I managed to get this to work as follows:
UIElement GetObjectHit(Point P) {
if (!lb.IsVisible) return null;
foreach(var i in TheCollection) {
var item = i as SomeUserControl;
ListBoxItem lbi = lb.ContainerFromElement(item) as ListBoxItem;
if (lbi == null || !lbi.IsVisible)
continue;
Point P00 = lbi.TranslatePoint(new Point(0,0), lb);
Point P11 = lbi.TranslatePoint(new Point(lbi.ActualWidth, lbi.ActualHeight), lb);
if (IsBounded(P, P00, P11)
return i;
}
return null;
}
I am making a Winforms application. Because I want to redraw some borders I want to loop through the controls and check which controls have a border. Unfortunately I have no idea how to accomplish this.
I know panels and textboxes, etc. have a property BorderStyle but I can not access it while looping through Controls. I use the function from this link : https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.controls?view=netframework-4.8 , to loop through the controls.
If you have a panel you can foreach on the panel. I use form load as an event
private void Form1_Load(object sender, EventArgs e)
{
foreach (var item in this.Controls)
{
switch (item.GetType().Name)
{
case "Label":
if (((Label)item).BorderStyle == BorderStyle.None)
{
//Your commands
}
break;
case "TextBox":
if (((TextBox)item).BorderStyle == BorderStyle.None)
{
//Your commands
}
break;
}
}
}
Or you can check them dynamic
I recommend you to use the dynamic way.
in this way, your app wouldn't encounter exceptions or errors
foreach (var item in this.Controls)
{
//Get item from form
dynamic element = item;
//check if there is any property called Borderstyle exists
var res = item.GetType().GetProperties().Where(p => p.Name.Equals("BorderStyle")).FirstOrDefault();
//if it exists and value is not equal None(Control have border)
if (res !=null && !(res.GetValue(item).Equals("None")))
{
res.SetValue(item, BorderStyle.FixedSingle, null);
//your other commands
}
}
You could use a foreach for every type of control you use (TextBox, Label, etc.)
var controlsWithoutBorders = new List<Control>();
// Check textboxes
foreach (var control in controls.OfType<TextBox>())
{
if (control.BorderStyle != BorderStyle.None)
{
controlsWithoutBorders.Add(control);
}
}
//Check labels
foreach (var control in controls.OfType<Label>())
{
if (control.BorderStyle != BorderStyle.None)
{
controlsWithoutBorders.Add(control);
}
}
Alternatively, you could use a single foreach on all the controls, and try to cast the control to each type. The cast will be successful if the control is actually what you want to cast it to, otherwise it will return null (e.g. trying to cast a Control that is a TextBox to a Label will return null)
var controlsWithoutBorders = new List<Control>();
foreach (var control in controls)
{
var controlAsTextBox = control as TextBox;
var controlAsLabel = control as Label;
if (controlAsTextBox != null && controlAsTextBox.BorderStyle != BorderStyle.None)
{
controlsWithBorders.Add(control);
}
if (controlAsLabel != null && controlAsLabel.BorderStyle != BorderStyle.None)
{
controlsWithBorders.Add(control);
}
}
Though this method is not the fastest at run time, your problem could be cleanly solved with the use of C#'s dynamic typing feature. Consider the below snippet of code.
public void DealWithBorder(List<Control> lsControls) {
foreach(var element in lsControls){
dynamic dynamicElement = element;
try{
BorderStyle style = dynamicElement.BorderStyle;
// Handle if property does exist in the control
}
catch{
// Handle if property doesnt exist in the control
}
}
}
In English, it will try to act as if the property exists in the object but if it does not, an exception will thrown. For more info on dynamic typing, click here.
I'm trying to come up with a System.Windows.Interactivity.Behaviour that, when applied to a WPF DataGrid, adds a context menu (or items to an existing context menu) that allow users to show or hide columns.
I came up with a solution that almost works very well.
Everything works just as expected - until you hide and then re-show a column. Once becoming visible again, the contextmenu just seems to disappear, right-clicking on the column doesn't do anything anymore.
Code is below, verbalyl what I'm doing:
On attaching the behavior, I start listening to the DataGrid "Loaded" event
In the Loaded event, I find all DataGridColumnHeader descendants of the DataGrid
For each of these, I generate an individual context menu and attach it to the DataGridColumnHeader
For each context menu, I generate one menu item per column, and assign a command to it that, upon execution, sets the DataGridColumn's visibility to Visible or Hidden
I've stripped down the code to a minimum example for the simplest case: To test this, simply apply that behavior to a DataGrid that doesn't have a ContextMenu assigned currently.
public class DgColumnBehavior : Behavior<DataGrid>
{
protected ICommand ToggleColumnVisibilityCmd;
protected DataGrid _AssociatedObject;
protected override void OnAttached()
{
this.ToggleColumnVisibilityCmd = new DelegateCommand<DataGridColumn>(ToggleColumnVisibilityCmdExecute);
this._AssociatedObject = (DataGrid)this.AssociatedObject;
Observable.FromEventPattern(this._AssociatedObject, "Loaded")
.Take(1)
.Subscribe(x => _AssociatedObject_Loaded());
base.OnAttached();
}
void _AssociatedObject_Loaded()
{
var columnHeaders = this._AssociatedObject.SafeFindDescendants<DataGridColumnHeader>(); // see second code piece for the SafeFindDescendants extension method
foreach (var columnHeader in columnHeaders)
{
EnsureSeparateContextMenuFor(columnHeader);
if (columnHeader.ContextMenu.ItemsSource != null)
{
// ContextMenu has an ItemsSource, so need to add items to that -
// ommitted though as irrelevant for example
}
else
{
// No ItemsSource assigned to the Menu, so we can just add directly
foreach (var item in CreateMenuItemsFor(columnHeader))
columnHeader.ContextMenu.Items.Add(item);
}
}
}
/// Ensures that the columnHeader ...
/// A) has a ContextMenu, and
/// B) that is has an individual context menu, i.e. one that isn't shared with any other DataGridColumnHeaders.
///
/// I'm doing that as in practice, I'm adding some further items that are specific to each column, so I can't have a shared context menu
private void EnsureSeparateContextMenuFor(DataGridColumnHeader columnHeader)
{
if (columnHeader.ContextMenu == null)
{
columnHeader.ContextMenu = new ContextMenu();
}
else
{
// clone the existing menu
// ommitted as irrelevant for example
}
}
/// Creates one menu item for each column of the underlying DataGrid to toggle that column's visibility
private IEnumerable<FrameworkElement> CreateMenuItemsFor(DataGridColumnHeader columnHeader)
{
foreach (var column in _AssociatedObject.Columns)
{
var item = new MenuItem();
item.Header = String.Format("Toggle visibility for {0}", column.Header);
item.Command = ToggleColumnVisibilityCmd;
item.CommandParameter = column;
yield return item;
}
}
// Gets executed when the user clicks on one of the ContextMenu items
protected void ToggleColumnVisibilityCmdExecute(DataGridColumn column)
{
bool isVisible = (column.Visibility == Visibility.Visible);
Visibility newVisibility = (isVisible) ? Visibility.Hidden : Visibility.Visible;
column.Visibility = newVisibility;
}
}
The SafeFindDescendants extension method is heavily based on the one from here: DataGridColumnHeader ContextMenu programmatically
public static class Visual_ExtensionMethods
{
public static IEnumerable<T> SafeFindDescendants<T>(this Visual #this, Predicate<T> predicate = null) where T : Visual
{
if (#this != null)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(#this);
for (int i = 0; i < childrenCount; i++)
{
var currentChild = VisualTreeHelper.GetChild(#this, i);
var typedChild = currentChild as T;
if (typedChild == null)
{
var result = ((Visual)currentChild).SafeFindDescendants<T>(predicate);
foreach (var r in result)
yield return r;
}
else
{
if (predicate == null || predicate(typedChild))
{
yield return typedChild;
}
}
}
}
}
}
I can't figure out what's going on. Why does the context menu seem to be removed after hiding/re-showing a column?!
Appreciate any ideas! Thanks.
I've come up with a quick and dirty Fix. It works, but it isn't pretty. Maybe someone can think of a better solution.
Essentially, each time the visibility of a DataGridColumn item changes to hidden/collapsed, I retrieve its DataGridColumnHeader and store the associated context menu in a cache.
And each time the visibility changes back to visible, I'm listening to the next DataGrid LayoutUpdated event (to ensure the visual tree has been built), retrieve the DataGridColumnHeader again - which will incoveniently be a different instance than the original one - and set its context menu to the cached one.
protected IDictionary<DataGridColumn, ContextMenu> _CachedContextMenues = new Dictionary<DataGridColumn, ContextMenu>();
protected void ToggleColumnVisibilityCmdExecute(DataGridColumn column)
{
bool isVisible = (column.Visibility == Visibility.Visible);
Visibility newVisibility = (isVisible) ? Visibility.Hidden : Visibility.Visible;
if(newVisibility != Visibility.Visible)
{
// We're hiding the column, so we'll cache its context menu so for re-use once the column
// becomes visible again
var contextMenu = _AssociatedObject.SafeFindDescendants<DataGridColumnHeader>(z => z.Column == column).Single().ContextMenu;
_CachedContextMenues.Add(column, contextMenu);
}
if(newVisibility == Visibility.Visible)
{
// The column just turned visible again, so we set its context menu to the
// previously cached one
Observable
.FromEventPattern(_AssociatedObject, "LayoutUpdated")
.Take(1)
.Select(x => _AssociatedObject.SafeFindDescendants<DataGridColumnHeader>(z => z.Column == column).Single())
.Subscribe(x =>
{
var c = x.Column;
var cachedMenu = _CachedContextMenues[c];
_CachedContextMenues.Remove(c);
x.ContextMenu = cachedMenu;
});
}
column.Visibility = newVisibility;
}
Have you already found a better solution?
I have a new DataGrid class, so "this" is the actual Instance of a DataGrid!
This is my solution (I'm also listen on the LayoutUpdated event):
this.LayoutUpdated += (sender, args) =>
{
foreach (DataGridColumnHeader columnHeader in GetVisualChildCollection<DataGridColumnHeader>(this))
{
if(columnHeader.ContextMenu == null)
ContextMenuService.SetContextMenu(columnHeader, _ContextMenu);
}
};
public static List<T> GetVisualChildCollection<T>(object parent) where T : Visual
{
List<T> visualCollection = new List<T>();
GetVisualChildCollection(parent as DependencyObject, visualCollection);
return visualCollection;
}
private static void GetVisualChildCollection<T>(DependencyObject parent, List<T> visualCollection) where T : Visual
{
int count = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is T)
{
visualCollection.Add(child as T);
}
else if (child != null)
{
GetVisualChildCollection(child, visualCollection);
}
}
}
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)
I am working on a Windows 8 Metro Newsreader-App (with C# and XAML). I show the feed-items on a Grouped Items Page (template). A click forwards the user to a detail-page, which I implemented as a Split Page. Therefore, I have an Image-Gallery where the user can navigate from this DetailPage (and back). This works fine. On the ItemDetailPage I have to assign the Data in the LoadState function. The template offers me the following solution:
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
// TODO: Assign a bindable group to this.DefaultViewModel["Group"]
// TODO: Assign a collection of bindable items to this.DefaultViewModel["Items"]
if (pageState == null)
{
// When this is a new page, select the first item automatically unless logical page
// navigation is being used (see the logical page navigation #region below.)
if (!this.UsingLogicalPageNavigation() && this.itemsViewSource.View != null)
{
this.itemsViewSource.View.MoveCurrentToFirst();
}
}
else
{
// Restore the previously saved state associated with this page
if (pageState.ContainsKey("SelectedItem") && this.itemsViewSource.View != null)
{
// TODO: Invoke this.itemsViewSource.View.MoveCurrentTo() with the selected
// item as specified by the value of pageState["SelectedItem"]
}
}
}
What I did was the following:
protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
{
if (pageState == null)
{
// When this is a new page, select the first item automatically unless logical page
// navigation is being used (see the logical page navigation #region below.)
if (!this.UsingLogicalPageNavigation() && this.itemsViewSource.View != null)
{
this.itemsViewSource.View.MoveCurrentToFirst();
}
}
else
{
// Restore the previously saved state associated with this page
if (pageState.ContainsKey("SelectedItem") && this.itemsViewSource.View != null)
{
this.itemsViewSource.View.MoveCurrentTo(pageState["SelectedItem"]);
}
}
var item = ArticleDataSource.GetItem((int)navigationParameter);
if (item != null)
{
this.DefaultViewModel["Group"] = item.Group;
this.DefaultViewModel["Items"] = item.Group.Items;
if (this.itemsViewSource.View != null) this.itemsViewSource.View.MoveCurrentTo(item); // remove?
// Register this page as a share source.
this.dataTransferManager = DataTransferManager.GetForCurrentView();
this.dataTransferManager.DataRequested += new TypedEventHandler<DataTransferManager, DataRequestedEventArgs>(this.OnDataRequested);
}
}
If I navigate from the OverviewPage to the DetailsPage the selected item (A) is shown.
I select an other item (from the list) and the correct details (B) are shown.
If I navigate from the DetailsPage to the GalleryPage the images of the correct item (B) are shown.
If I now navigate back (to the DetailsPage) not the last selected item (B) but the item I selected (A) to enter DetailsPage is shown.
I am aware of the fact that I changed the order (proposed by the template) and I added if (this.itemsViewSource.View != null) this.itemsViewSource.View.MoveCurrentTo(item); that I'd probably better remove.
I think that the problem (described in step 4) is, that this.itemsViewSource.View is null and therefore (logically) this.itemsViewSource.View.MoveCurrentTo(pageState["SelectedItem"]) doesn't get executed. Unfortunately, I was unable to find out why or if this is the bug.
Any help or link to a tutorial (which could solve my problem) are really much appreciated! thanks.
The point is to override the navigationParameter as needed by the previous page state. The item is then loaded and selected. Try using
// Override the navigationParameter if a page state is set:
if (pageState != null && pageState.ContainsKey("SelectedItem"))
{
navigationParameter = pageState["SelectedItem"];
}
var item = ArticleDataSource.GetItem((int)navigationParameter);
if (item != null)
{
DefaultViewModel["Group"] = item.Group;
DefaultViewModel["Items"] = item.Group.Items;
if (itemsViewSource.View != null)
{
itemsViewSource.View.MoveCurrentTo(item);
}
else
{
// A serious error happened here..
}
}
else
{
// Oooops, an item disappeared..
}