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

这一切都有效。如果选择了第 4 项,并且用户按 F5,则重新构建列表,然后选择与前第 4 项具有相同字符串属性的项。有时这是新的第 4 项,有时不是,但它提供了“最小惊讶行为”。

当用户随后使用箭头键在 ListView 中导航时,问题就出现了。刷新后的第一个向上或向下箭头会导致选择(新)列表视图中的第一个项目,无论之前的逻辑选择了哪个项目。任何进一步的箭头键都按预期工作。

为什么会这样?

这很明显违反了“最小惊讶”规则。我怎样才能避免它?


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

4

9 回答 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。在 googlespace 的其他地方,人们报告它返回 null 并设置了 GroupStyle。但它在我身上表现出这种行为,没有分组。

ItemContainerGenerator.ContainerFromItem()正在为列表中显示的所有对象返回 null。还ItemContainerGenerator.ContainerFromIndex()为所有索引返回 null。只有在 ListView 被渲染(或其他东西)之后才调用这些东西。

我尝试直接通过这样做,Dispatcher.BeginInvoke()但这也不起作用。

在其他一些线程的建议下,我Dispatcher.BeginInvoke()在. 是的,很简单吧?(不是)StatusChangedItemContainerGenerator

这是代码的样子。

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 中工作。

于 2011-09-09T16:52:45.537 回答
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();
        }
    }
}

希望这可以帮助某人节省一些时间。

于 2014-09-01T19:06:08.233 回答
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 处理按键之前设置正确的焦点

于 2014-10-14T08:59:26.030 回答
1

BeginInvoke通过指定优先级,可以在找到项目后将其聚焦:

Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
    var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(existing) as ListBoxItem;
    lbi.Focus();
}));
于 2014-11-20T18:55:26.067 回答
0

以编程方式选择项目不会为其提供键盘焦点。你必须明确地这样做......((Control)listView1.SelectedItem).Focus()

于 2011-09-09T15:23:26.677 回答
0

Cheeso,在您之前的回答中,您说:

但还有第二个问题 - 在设置 SelectedItem 后它无法正常工作。ItemContainerGenerator.ContainerFromItem() 似乎总是返回 null。

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

((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();
于 2013-01-17T18:13:19.923 回答
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);
                }
            }
}
于 2014-02-28T18:47:43.800 回答
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" ..../>
于 2014-05-12T12:12:29.823 回答
0

Cheeso 的解决方案对我有用。通过设置 a 来防止null异常timer.tick,这样你就离开了原来的例程。

var uiel = (UIElement)this.lv1.ItemContainerGenerator                        
           .ContainerFromItem(lv1.Items[ix]); 
if (uiel != null) uiel.Focus();

在 a 之后调用计时器时解决了问题RemoveAt/Insert,并且还Window.Loaded设置了焦点并选择了第一项。

想回馈我在 SE 获得的许多灵感和解决方案的第一篇文章。快乐编码!

于 2016-08-01T17:32:00.147 回答