程序设置ListView.SelectedItem后箭头键无法使用

10

我有一个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;
        }
    }
}

这个都可以正常工作。如果选择了第四项,并且用户按下F5键,则列表将被重组,然后选择具有与之前的第四项相同的字符串属性的项目。有时这是新的第四项,有时不是,但它提供了“最小惊讶行为原则”。

问题出现在用户随后使用箭头键在ListView中导航时。刷新后的第一个向上或向下箭头使得(新的)listview中的第一项被选中,而不管之前逻辑选择了哪个项目。任何进一步的箭头键都按预期工作。

这是为什么?

这显然违反了“最小惊讶”原则。我该如何避免这种情况?


编辑
经过进一步搜索,这似乎是未回答的WPF ListView箭头导航和按键问题所描述的相同异常,只是我提供了更多细节。

9个回答

16
似乎这是由于ListView存在已知但未描述清楚的问题行为(可能还有其他WPF控件)引起的。它要求应用程序在以编程方式设置SelectedItem后,调用Focus()方法来聚焦到特定的ListViewItem上。
但是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。在谷歌空间的其他地方,人们报告说它会在设置了GroupStyle时返回null。但是我没有进行分组,它也表现出这种行为。
ItemContainerGenerator.ContainerFromItem()对于列表中显示的所有对象都返回null。同时,ItemContainerGenerator.ContainerFromIndex()对于所有索引也返回null。必须在ListView被呈现(或者其他什么)之后才能调用这些东西。
我尝试直接通过Dispatcher.BeginInvoke()来实现这一点,但那也不起作用。
在一些其他线程的建议下,我在ItemContainerGenerator的StatusChanged事件中使用了Dispatcher.BeginInvoke()。是的,很简单吧?(不)
以下是代码样例。
MyComplexType current;

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.ItemContainerGenerator.StatusChanged += icg_StatusChanged;
            this.listView1.SelectedItem = thing;
            current = thing;
            return;
        }
    }
}


void icg_StatusChanged(object sender, EventArgs e)
{
    if (this.listView1.ItemContainerGenerator.Status
        == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
    {
        this.listView1.ItemContainerGenerator.StatusChanged
            -= icg_StatusChanged;
        Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
                               new Action(()=> {
                                       var uielt = (UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(current);
                                       uielt.Focus();}));

    }
}

那是一些丑陋的代码。但是,以编程方式设置SelectedItem可以让ListView中后续的箭头导航正常工作。


我曾经遇到过类似的情况,需要使用dispatch.begininvokes来进行“重新聚焦”。别忘了将自己标记为答案! - John Gardner
我打算接受这个答案,但是我认为现在还不行——有一个计时器。 - Cheeso
丑陋,而且不可靠。在短暂的一瞬间,ListBox 获得了焦点(可以看到焦点矩形),然后再次获得了项目的焦点。当在那个时刻按箭头键时,你的选择就会消失。 - ygoe

4

我在使用ListBox控件时遇到了问题(这也是我找到这个SO问题的原因)。在我的情况下,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

我发现一种略有不同的方法。我使用数据绑定来确保在代码中正确地突出显示项目,然后在每次重新绑定时,我不是设置焦点,而是向代码后台添加一个键盘导航的预事件处理程序。像这样。

    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处理按键之前设置了正确的焦点。

1

在指定优先级后,可以使用BeginInvoke聚焦于找到的项目:

Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
    var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(existing) as ListBoxItem;
    lbi.Focus();
}));

0
Cheeso的解决方案对我有效。只需设置一个计时器来执行null异常的预防措施,这样您就可以保留原始程序了。
var uiel = (UIElement)this.lv1.ItemContainerGenerator                        
           .ContainerFromItem(lv1.Items[ix]); 
if (uiel != null) uiel.Focus();

问题已解决,当调用RemoveAt/Insert后,再调用计时器,以及在Window.Loaded中设置焦点并选择第一个项目。
想要回馈这篇首帖,感谢SE给我带来的灵感和解决方案。愉快编码!

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);
                }
            }
}

0

Cheeso,在你的先前的回答中,你说:

但是还有第二个问题 - 在设置SelectedItem之后它不起作用。ItemContainerGenerator.ContainerFromItem()似乎总是返回null。

一个简单的解决方法是根本不设置SelectedItem。当你聚焦元素时,这将自动发生。所以只需调用以下行即可:

((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();

我想我尝试过那个,但它没有起作用。无论如何,现在已经过去很长时间了,而且我已经超越了它。但对于面临类似问题的人可能会有用。 - Cheeso

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" ..../>

0
以编程方式选择一个项目不会使其获得键盘焦点,您需要明确地执行这个操作... ((Control)listView1.SelectedItem).Focus()

1
谢谢。抛出异常的是cast,因为在ListView中显示的Item不是一个Control。我也尝试了this.listView1.Focus(),它不会抛出异常,但我没有看到任何区别。 - Cheeso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接