我有一个WPF ListView控件,ItemsSource设置为以这种方式创建的ICollectionView:
var collectionView =
System.Windows.Data.CollectionViewSource.GetDefaultView(observableCollection);
this.listView1.ItemsSource = collectionView;
...其中observableCollection是复杂类型的ObservableCollection。 ListView配置为为每个项目显示复杂类型上的一个字符串属性。
用户可以刷新ListView,此时我的逻辑存储当前所选项的“键字符串”,重新填充底层的observableCollection。然后将先前的排序和过滤器应用于collectionView。此时我想“重新选择”在刷新请求之前选择的项目。 observableCollection中的项是新实例,因此我比较各自的字符串属性,然后选择一个匹配的属性。像这样:
private void SelectThisItem(string value)
{
foreach (var item in collectionView) // for the ListView in question
{
var thing = item as MyComplexType;
if (thing.StringProperty == value)
{
this.listView1.SelectedItem = thing;
return;
}
}
}
这一切都有效。如果选择了第4项,并且用户按下F5,则重构列表,然后选择具有与前4项相同的字符串属性的项。有时这是新的第4项,有时不是,但它提供了“least astonishment behavior”。
当用户随后使用箭头键浏览ListView时,问题就出现了。刷新后的第一个向上或向下箭头会导致(新)列表视图中的第一个项目被选中,而不管前一个逻辑选择了哪个项目。任何进一步的箭头键按预期工作。
为什么会这样?
这显然违反了“最不惊讶”的规则。我怎么能避免它?
修改
在进一步搜索时,这看起来像是未答复的相同的异常
WPF ListView arrow navigation and keystroke problem,但我提供了更多细节。
答案 0 :(得分:15)
看起来这是由于a sort of known but not-well-described problematic behavior with ListView(可能还有其他一些WPF控件)。在以编程方式设置SelectedItem之后,它需要在特定ListViewItem上调用Focus()
。
但是SelectedItem本身不是UIElement。它是ListView中显示的任何项目,通常是自定义类型。因此,您无法拨打this.listView1.SelectedItem.Focus()
。那不行。您需要获取显示该特定项目的UIElement(或Control)。 WPF接口的一个黑暗角落叫ItemContainerGenerator,据说可以让你获得在ListView中显示特定项目的控件。
这样的事情:
this.listView1.SelectedItem = thing;
// *** WILL NOT WORK!
((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();
但是还有第二个问题 - 它在设置SelectedItem后无法正常工作。 ItemContainerGenerator.ContainerFromItem()似乎总是返回null。在googlespace的其他地方,人们已经报告它在GroupStyle设置时返回null。但它在没有分组的情况下表现出这种行为。
对于列表中显示的所有对象, ItemContainerGenerator.ContainerFromItem()
返回null。此外,ItemContainerGenerator.ContainerFromIndex()
对所有指标都返回null。只有在ListView被渲染(或其他东西)之后才调用那些东西是必要的。
我尝试直接通过Dispatcher.BeginInvoke()
执行此操作,但这也不起作用。
根据其他一些主题的建议,我在Dispatcher.BeginInvoke()
的{{1}}事件中使用了StatusChanged
。是的,简单吧? (不)
这是代码的样子。
ItemContainerGenerator
这是一些丑陋的代码。但是,以这种方式以编程方式设置SelectedItem允许后续箭头导航在ListView中工作。
答案 1 :(得分:4)
我遇到了ListBox控件的问题(这就是我最终找到这个问题的方法)。在我的例子中,SelectedItem是通过绑定设置的,随后的键盘导航尝试会重置ListBox以选择第一个项目。我还通过添加/删除项目来同步我的基础ObservableCollection(不是每次都绑定到新的集合)。
根据接受的答案中给出的信息,我能够使用ListBox的以下子类来解决它:
internal class KeyboardNavigableListBox : ListBox
{
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
var container = (UIElement) ItemContainerGenerator.ContainerFromItem(SelectedItem);
if(container != null)
{
container.Focus();
}
}
}
希望这可以帮助别人节省一些时间。
答案 2 :(得分:2)
我发现了一种不同的方法。我使用数据绑定来确保在代码中突出显示正确的项目,然后不是将焦点设置在每个重新绑定上,而是简单地将事件前处理程序添加到键盘导航后面的代码中。像这样。
public MainWindow()
{
...
this.ListView.PreviewKeyDown += this.ListView_PreviewKeyDown;
}
private void ListView_PreviewKeyDown(object sender, KeyEventArgs e)
{
UIElement selectedElement = (UIElement)this.ListView.ItemContainerGenerator.ContainerFromItem(this.ListView.SelectedItem);
if (selectedElement != null)
{
selectedElement.Focus();
}
e.Handled = false;
}
这样可以确保在让WPF处理按键之前设置正确的焦点
答案 3 :(得分:0)
以编程方式选择项目不会使其获得键盘焦点。你必须这样做...... ((Control)listView1.SelectedItem).Focus()
答案 4 :(得分:0)
Cheeso,在你的previous answer中说:
但是还有第二个问题 - 它不能正常工作 设置SelectedItem后。 ItemContainerGenerator.ContainerFromItem()似乎总是返回 空。
一个简单的解决方案是根本不设置SelectedItem。当您聚焦元素时,这将自动发生。所以只需调用以下行即可:
((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();
答案 5 :(得分:0)
这一切似乎都有点干扰......我自己重写了逻辑:
public class CustomListView : ListView
{
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
// Override the default, sloppy behavior of key up and down events that are broken in WPF's ListView control.
if (e.Key == Key.Up)
{
e.Handled = true;
if (SelectedItems.Count > 0)
{
int indexToSelect = Items.IndexOf(SelectedItems[0]) - 1;
if (indexToSelect >= 0)
{
SelectedItem = Items[indexToSelect];
ScrollIntoView(SelectedItem);
}
}
}
else if (e.Key == Key.Down)
{
e.Handled = true;
if (SelectedItems.Count > 0)
{
int indexToSelect = Items.IndexOf(SelectedItems[SelectedItems.Count - 1]) + 1;
if (indexToSelect < Items.Count)
{
SelectedItem = Items[indexToSelect];
ScrollIntoView(SelectedItem);
}
}
}
else
{
base.OnPreviewKeyDown(e);
}
}
}
答案 6 :(得分:0)
经过大量的摆弄后,我无法让它在MVVM中运行。 我自己试了一下并使用了DependencyProperty。这对我很有用。
public class ListBoxExtenders : DependencyObject
{
public static readonly DependencyProperty AutoScrollToCurrentItemProperty = DependencyProperty.RegisterAttached("AutoScrollToCurrentItem", typeof(bool), typeof(ListBoxExtenders), new UIPropertyMetadata(default(bool), OnAutoScrollToCurrentItemChanged));
public static bool GetAutoScrollToCurrentItem(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToSelectedItemProperty);
}
public static void SetAutoScrollToCurrentItem(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToSelectedItemProperty, value);
}
public static void OnAutoScrollToCurrentItemChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
{
var listBox = s as ListBox;
if (listBox != null)
{
var listBoxItems = listBox.Items;
if (listBoxItems != null)
{
var newValue = (bool)e.NewValue;
var autoScrollToCurrentItemWorker = new EventHandler((s1, e2) => OnAutoScrollToCurrentItem(listBox, listBox.Items.CurrentPosition));
if (newValue)
listBoxItems.CurrentChanged += autoScrollToCurrentItemWorker;
else
listBoxItems.CurrentChanged -= autoScrollToCurrentItemWorker;
}
}
}
public static void OnAutoScrollToCurrentItem(ListBox listBox, int index)
{
if (listBox != null && listBox.Items != null && listBox.Items.Count > index && index >= 0)
listBox.ScrollIntoView(listBox.Items[index]);
}
}
XAML中的用法
<ListBox IsSynchronizedWithCurrentItem="True" extenders:ListBoxExtenders.AutoScrollToCurrentItem="True" ..../>
答案 7 :(得分:0)
通过指定优先级找到项目后,可以使用BeginInvoke
关注项目:
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(existing) as ListBoxItem;
lbi.Focus();
}));
答案 8 :(得分:0)
Cheeso的解决方案适合我。只需设置null
来执行此操作即可防止出现timer.tick
例外,因此您已离开原始例程。
var uiel = (UIElement)this.lv1.ItemContainerGenerator
.ContainerFromItem(lv1.Items[ix]);
if (uiel != null) uiel.Focus();
在RemoveAt/Insert
之后调用计时器时问题已解决,而Window.Loaded
处设置焦点并选择第一项也是问题。
想要回馈这篇关于我在SE获得的灵感和解决方案的第一篇文章。快乐的编码!