问题
I have a treeview bound to an object tree. When I remove an object from the object tree, it is removed correctly from the tree view, but the treeview's default behaviour is to jump the selecteditem up to the deleted item's parent node. How can I change this so it jumps to the next item instead?
EDIT:
I updated my code with Aviad's suggestion. Here is my code..
public class ModifiedTreeView : TreeView
{
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldStartingIndex - 1 > 0)
{
ModifiedTreeViewItem item =
this.ItemContainerGenerator.ContainerFromIndex(
e.OldStartingIndex - 2) as ModifiedTreeViewItem;
item.IsSelected = true;
}
}
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ModifiedTreeViewItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ModifiedTreeViewItem;
}
}
public class ModifiedTreeViewItem : TreeViewItem
{
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action == NotifyCollectionChangedAction.Remove)
{
if (e.OldStartingIndex > 0)
{
ModifiedTreeViewItem item =
this.ItemContainerGenerator.ContainerFromIndex(
e.OldStartingIndex - 1) as ModifiedTreeViewItem;
item.IsSelected = true;
}
}
}
protected override DependencyObject GetContainerForItemOverride()
{
return new ModifiedTreeViewItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is ModifiedTreeViewItem;
}
}
The code above does not work unless I debug it, or in some way slow down the OnItemsChanged method. For example, if I put a thread.sleep(500) at the bottom of the OnItemsChanged method, it works, otherwise it does not. Any idea what I'm doing wrong? This is really strange.
回答1:
The behavior you mention is controlled by a virtual method in the Selector
class called OnItemsChanged
(reference: Selector.OnItemsChanged Method) - In order to modify it, you should derive from TreeView
and override that function. You might use reflector to base your implementation on the existing implementation, although it's pretty straightforward.
Here's the code for the treeview override TreeView.OnItemsChanged
extracted using reflector:
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
if ((this.SelectedItem == null) || this.IsSelectedContainerHookedUp)
{
break;
}
this.SelectFirstItem();
return;
case NotifyCollectionChangedAction.Replace:
{
object selectedItem = this.SelectedItem;
if ((selectedItem == null) || !selectedItem.Equals(e.OldItems[0]))
{
break;
}
this.ChangeSelection(selectedItem, this._selectedContainer, false);
return;
}
default:
throw new NotSupportedException(SR.Get("UnexpectedCollectionChangeAction", new object[] { e.Action }));
}
}
Alternatively, you might hook into the collection NotifyCollectionChanged
event from one of your code-behind classes and explicitly change the current selection before the event reaches the TreeView
(I'm not sure of this solution though because I am not sure of the order in which event delegates are called - the TreeView
might get to process the event before you do - but it might work).
回答2:
Original answer
In my original answer I guessed that you may be encountering a bug in WPF and gave a generic workaround for this kind of situation, which was to replace item.IsSelected = true;
with:
Disptacher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
{
item.IsSelected = true;
}));
I explained that the reason this kind of workaround does the trick 90% of the time is that it delays the selection until almost all current operations have finished processing.
When I actually tried the code you posted in your other question I discovered that it was indeed a bug in WPF but found a more direct and reliable workaround. I'll explain how I diagnosed the problem and then describe the workaround.
Diagnosis
I added a SelectedItemChanged handler with a breakpoint in it, and looked at the stack trace. This made it obvious where the problem lies. Here are selected portions of the stack trace:
...
System.Windows.Controls.TreeView.ChangeSelection
...
System.Windows.Controls.TreeViewItem.OnGotFocus
...
System.Windows.Input.FocusManager.SetFocusedElement
System.Windows.Input.KeyboardNavigation.UpdateFocusedElement
System.Windows.FrameworkElement.OnGotKeyboardFocus
System.Windows.Input.KeyboardFocusChangedEventArgs.InvokeEventHandler
...
System.Windows.Input.InputManager.ProcessStagingArea
System.Windows.Input.InputManager.ProcessInput
System.Windows.Input.KeyboardDevice.ChangeFocus
System.Windows.Input.KeyboardDevice.TryChangeFocus
System.Windows.Input.KeyboardDevice.Focus
System.Windows.Input.KeyboardDevice.ReevaluateFocusCallback
...
As you can see, KeyboardDevice
has a ReevaluateFocusCallback
private or internal method which changes the focus to the parent of the deleted TreeViewItem
. This causes a GotFocus
event which causes the parent item to be selected. This all happens in the background after your event handler returns.
Solution
Normally in this case I would tell you to just manually .Focus()
the TreeViewItem
you are selecting. That is difficult here because in a TreeView
there is no easy way to get from an arbitrary data item to the corresponding container (there are separate ItemContainerGenerators
at each level).
So I think your best solution is to force the focus to the parent node (just where you don't want it to end up), then set IsSelected in the child's data. That way the input manager will never decide it needs to move the focus on its own: It will find the focus already set to a valid IInputElement
.
Here is some code to do that:
if(child != null)
{
SomeObject parent = child.Parent;
// Find the currently focused element in the TreeView's focus scope
DependencyObject focused =
FocusManager.GetFocusedElement(
FocusManager.GetFocusScope(tv)) as DependencyObject;
// Scan up the VisualTree to find the TreeViewItem for the parent
var parentContainer = (
from element in GetVisualAncestorsOfType<FrameworkElement>(focused)
where (element is TreeViewItem && element.DataContext == parent)
|| element is TreeView
select element
).FirstOrDefault();
parent.Children.Remove(child);
if(parent.Children.Count > 0)
{
// Before selecting child, first focus parent's container
if(parentContainer!=null) parentContainer.Focus();
parent.Children[0].IsSelected = true;
}
}
This also requires this helper method:
private IEnumerable<T> GetVisualAncestorsOfType<T>(DependencyObject obj) where T:DependencyObject
{
for(; obj!=null; obj = VisualTreeHelper.GetParent(obj))
if(obj is T)
yield return (T)obj;
}
This should be more reliable than using Dispatcher.BeginInvoke
because it will work around this particular problem without making any assumptions about input queue ordering, Dispatcher priorities, and so forth.
回答3:
This works for me (thanks to investigations provided above)
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action == NotifyCollectionChangedAction.Remove)
{
Focus();
}
}
回答4:
According to the answer provided by @Kirill I think the correct answer to this specific question would be the following code added to a class derived from TreeView.
protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove && SelectedItem != null)
{
var index = Items.IndexOf(SelectedItem);
if (index + 1 < Items.Count)
{
var item = Items.GetItemAt(index + 1) as TreeViewItem;
if (item != null)
{
item.IsSelected = true;
}
}
}
}
回答5:
Based on the answers above, here's the solution that worked for me (it has fixed various other problems as well, such as the loss of focus after selecting an item via model etc.)
Note the OnSelected override (scroll all the way down) which actually did the trick.
This was compiled in VS2015 for Net 3.5.
using System.Windows;
using System.Windows.Controls;
using System.Collections.Specialized;
namespace WPF
{
public partial class TreeViewEx : TreeView
{
#region Overrides
protected override DependencyObject GetContainerForItemOverride()
{
return new TreeViewItemEx();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is TreeViewItemEx;
}
#endregion
}
public partial class TreeViewItemEx : TreeViewItem
{
#region Overrides
protected override DependencyObject GetContainerForItemOverride()
{
return new TreeViewItemEx();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is TreeViewItemEx;
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
if (HasItems)
{
int newIndex = e.OldStartingIndex;
if (newIndex >= Items.Count)
newIndex = Items.Count - 1;
TreeViewItemEx item = ItemContainerGenerator.ContainerFromIndex(newIndex) as TreeViewItemEx;
item.IsSelected = true;
}
else
base.OnItemsChanged(e);
break;
default:
base.OnItemsChanged(e);
break;
}
}
protected override void OnSelected(RoutedEventArgs e)
{
base.OnSelected(e);
Focus();
}
#endregion
}
}
来源:https://stackoverflow.com/questions/2030678/wpf-treeview-itemselected-moves-incorrectly-when-deleting-an-item