以编程方式设置ListView.SelectedItem后,箭头键不起作用

时间:2011-09-09 15:12:44

标签: wpf listview observablecollection icollectionview

我有一个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,但我提供了更多细节。

9 个答案:

答案 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获得的灵感和解决方案的第一篇文章。快乐的编码!