以编程方式设置 ListView.SelectedItem 后箭头键不起作用
我有一个 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 个项目具有相同字符串属性的项目。有时这是新的第四项,有时不是,但它提供了“最少惊讶行为”。
当用户随后使用箭头键在 ListView 中导航时,问题就出现了。刷新后的第一个向上或向下箭头会导致选择(新)列表视图中的第一个项目,无论先前的逻辑选择了哪个项目。任何进一步的箭头键都可以按预期工作。
为什么会发生这种情况?
这显然违反了“最少惊讶”规则。我怎样才能避免它?
编辑
经过进一步搜索,这似乎与未答复者描述的异常相同
WPF ListView 箭头导航和击键问题,除非我提供更多详细信息。
I have a WPF ListView control, ItemsSource is set to an ICollectionView created this way:
var collectionView =
System.Windows.Data.CollectionViewSource.GetDefaultView(observableCollection);
this.listView1.ItemsSource = collectionView;
...where observableCollection is an ObservableCollection of a complex type. The ListView is configured to display, for each item, just one string property on the complex type.
The user can refresh the ListView, at which point my logic stores the "key string" for the currently selected item, re-populates the underlying observableCollection. The previous sort and filter is then applied to the collectionView. At this point I'd like to "re select" the item that had been selected before the request to refresh. The items in the observableCollection are new instances, so I compare the respective string properties and then just select one that matches. Like this:
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;
}
}
}
This all works. If the 4th item is selected, and the user presses F5, then the list is reconstituted and then the item with the same string property as the previous 4th item gets selected. Sometimes this is the new 4th item, sometimes not, but it provides "least astonishment behavior".
The problem comes when the user subsequently uses arrow keys to navigate through the ListView. The first up or down arrow after a refresh causes the first item in the (new) listview to be selected, regardless of which item had been selected by the previous logic. Any further arrow keys work as expected.
Why is this happening?
This pretty clearly violates the "least astonishment" rule. How can I avoid it?
EDIT
Upon further search, this seems like the same anomaly described by the unanswered
WPF ListView arrow navigation and keystroke problem , except I provide more detail.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
看起来这是由于某种ListView(可能还有其他一些 WPF 控件)的已知但未详细描述的问题行为。它要求应用程序在以编程方式设置 SelectedItem 后,对特定 ListViewItem 调用
Focus()
。但 SelectedItem 本身不是 UIElement。它是您在 ListView 中显示的任何内容的项目,通常是自定义类型。因此,您无法调用
this.listView1.SelectedItem.Focus()
。那是行不通的。您需要获取显示该特定项目的 UIElement(或 Control)。 WPF 界面中有一个名为 ItemContainerGenerator 的黑暗角落,据说可以让您将获得在 ListView 中显示特定项目的控件。是这样的:
但是还有第二个问题 - 设置 SelectedItem 后它无法立即工作。 ItemContainerGenerator.ContainerFromItem() 似乎总是返回 null。在 googlespace 的其他地方,人们报告说它返回 null 并设置了 GroupStyle。但它在我身上表现出了这种行为,没有分组。
ItemContainerGenerator.ContainerFromItem()
为列表中显示的所有对象返回 null。此外,ItemContainerGenerator.ContainerFromIndex()
为所有索引返回 null。必要的是仅在 ListView 呈现(或其他内容)后调用这些东西。我尝试直接通过 Dispatcher.BeginInvoke() 执行此操作,但这也不起作用。
根据其他一些线程的建议,我在
ItemContainerGenerator
上的StatusChanged
事件中使用了Dispatcher.BeginInvoke()
。是的,很简单吧? (不是)代码如下所示。
这是一些丑陋的代码。但是,以这种方式以编程方式设置 SelectedItem 允许后续箭头导航在 ListView 中工作。
It looks like this is due to a sort of known but not-well-described problematic behavior with ListView (and maybe some other WPF controls). It requires that an app call
Focus()
on the particular ListViewItem, after programmatically setting the SelectedItem.But the SelectedItem itself is not a UIElement. It's an item of whatever you are displaying in the ListView, often a custom type. Therefore you cannot call
this.listView1.SelectedItem.Focus()
. That's not gonna work. You need to get the UIElement (or Control) that displays that particular item. There's a dark corner of the WPF interface called ItemContainerGenerator, which supposedly lets you get the control that displays a particular item in a ListView.Something like this:
But there's also a second problem with that - it doesn't work right after setting the SelectedItem. ItemContainerGenerator.ContainerFromItem() always seems to return null. Elsewhere in the googlespace people have reported it as returning null with GroupStyle set. But it exhibited this behavior with me, without grouping.
ItemContainerGenerator.ContainerFromItem()
is returning null for all objects being displayed in the list. AlsoItemContainerGenerator.ContainerFromIndex()
returns null for all indicies. What's necessary is to call those things only after the ListView has been rendered (or something).I tried doing this directly via
Dispatcher.BeginInvoke()
but that does not work either.At the suggestion of some other threads, I used
Dispatcher.BeginInvoke()
from within theStatusChanged
event on theItemContainerGenerator
. Yeah, simple huh? (Not)Here's what the code looks like.
That's some ugly code. But, programmatically setting the SelectedItem this way allows subsequent arrow navigation to work in the ListView.
我在使用 ListBox 控件时遇到了这个问题(这就是我最终找到这个问题的原因)。就我而言,SelectedItem 是通过绑定设置的,随后的键盘导航尝试将重置 ListBox 以选择第一个项目。我还通过添加/删除项目(而不是每次绑定到新集合)来同步我的底层 ObservableCollection。
根据接受的答案中给出的信息,我能够使用 ListBox 的以下子类来解决它:
希望这可以帮助某人节省一些时间。
I was having this problem with a ListBox control (which is how I ended up finding this SO question). In my case, the SelectedItem was being set via binding, and subsequent keyboard navigation attempts would reset the ListBox to have the first item selected. I was also synchronizing my underlying ObservableCollection by adding/removing items (not by binding to a new collection each time).
Based on the info given in the accepted answer, I was able to work around it with the following subclass of ListBox:
Hope this helps someone save some time.
我发现了一种有些不同的方法。我使用数据绑定来确保代码中突出显示正确的项目,然后我只需将预事件处理程序添加到后面的代码中以进行键盘导航,而不是在每次重新绑定上设置焦点。像这样。
这只是确保在让 WPF 处理按键之前设置正确的焦点
I found a somewhat different approach. I'm using databinding to make sure the correct item is highlighted in the code, and then instead of setting focus on every rebind, I simply add a pre-event handler to the code behind for keyboard navigation. Like this.
This simply makes sure that the correct focus is set before letting WPF handle the keypress
通过指定优先级找到某个项目后,可以使用
BeginInvoke
将其聚焦:It's possible to focus an item with
BeginInvoke
after finding it by specifying priority:以编程方式选择项目不会为其提供键盘焦点。您必须明确地执行此操作...
((Control)listView1.SelectedItem).Focus()
Selecting an item programmatically does not give it keyboard focus. You have to do that explcitly...
((Control)listView1.SelectedItem).Focus()
Cheeso,在您的之前的回答中您说过:
一个简单的解决方案是根本不设置 SelectedItem。当您聚焦该元素时,这会自动发生。所以只需调用以下行即可:
Cheeso, in your previous answer you said:
An easy solution to that is to not set SelectedItem at all. This will automatically happen when you focus the element. So just calling the following line will do:
这一切似乎有点侵扰......我自己重写了逻辑:
This all seems a little bit intrusive...I went with rewriting the logic myself:
经过一番摆弄后,我无法让它在 MVVM 中工作。
我自己尝试了一下并使用了 DependencyProperty。这对我来说非常有用。
在 XAML 中的用法
After a lot a fiddling around I couldn't get it to work in MVVM.
I gave it a go myself and used a DependencyProperty. This worked great for me.
Usage in XAML
Cheeso 的解决方案对我有用。只需设置一个
timer.tick
即可防止出现null
异常,这样您就可以离开原来的例程。在
RemoveAt/Insert
之后调用计时器以及在Window.Loaded
设置焦点并选择第一个项目时,问题得到解决。我想回馈第一篇文章,因为我在 SE 获得了很多灵感和解决方案。快乐编码!
Cheeso's solution DOES work for me. Prevent the
null
exception by just setting atimer.tick
to do this, so you have left your original routine.Problem solved when calling timer after a
RemoveAt/Insert
, and also atWindow.Loaded
to set focus and select to first item.Wanted to give back this first post for the much inspiration and solutions I got at SE. Happy coding!