Клавиши со стрелками не работают после программной установки 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-й элемент. Иногда это новый 4-й элемент, иногда - нет, но он обеспечивает "наименее удивительное поведение".

Проблема возникает, когда пользователь впоследствии использует клавиши со стрелками для навигации по ListView. Первая стрелка вверх или вниз после обновления вызывает выбор первого элемента в (новом) представлении списка, независимо от того, какой элемент был выбран предыдущей логикой. Дальнейшие клавиши со стрелками работают должным образом.

Почему это происходит?

Это явно нарушает правило «наименьшего удивления». Как мне этого избежать?


ИЗМЕНИТЬ
При дальнейшем поиске это похоже на ту же аномалию, описанную без ответа
Проблема навигации и нажатия клавиш в WPF ListView, за исключением того, что я предоставлю более подробную информацию.


person Cheeso    schedule 09.09.2011    source источник


Ответы (9)


Похоже, это связано с своего рода известное, но плохо описанное проблемное поведение с ListView (и, возможно, с некоторыми другими элементами управления WPF). Для этого требуется, чтобы приложение вызывало Focus() для конкретного ListViewItem после программной установки SelectedItem.

Но сам 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() возвращает ноль для всех индикаторов. Что необходимо, так это вызывать эти вещи только после того, как ListView был отрисован (или что-то в этом роде).

Я пробовал делать это напрямую через Dispatcher.BeginInvoke(), но это тоже не сработало.

По предложению некоторых других потоков я использовал Dispatcher.BeginInvoke() из события StatusChanged на ItemContainerGenerator. Да просто да? (Нет)

Вот как выглядит код.

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.

person Cheeso    schedule 09.09.2011
comment
у меня были похожие, требующие dispatch.begininvokes, которые позже переориентируются. не забудьте отметить себя как ответ! - person John Gardner; 09.09.2011
comment
Я намерен принять это как ответ, но думаю, что пока не могу - есть таймер. - person Cheeso; 09.09.2011
comment
Уродливо и даже ненадежно. На короткое время ListBox фокусируется (вы можете видеть прямоугольник фокуса), прежде чем элемент снова будет сфокусирован. При нажатии клавиши со стрелкой в ​​этот момент ваш выбор исчезнет. - person ygoe; 13.06.2014

У меня возникла проблема с элементом управления 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();
        }
    }
}

Надеюсь, это поможет кому-то сэкономить время.

person E.Z. Hart    schedule 01.09.2014

Я нашел несколько иной подход. Я использую привязку данных, чтобы убедиться, что в коде выделен правильный элемент, а затем вместо того, чтобы устанавливать фокус на каждую повторную привязку, я просто добавляю обработчик перед событием в код для навигации с клавиатуры. Нравится.

    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 обрабатывать нажатие клавиши.

person Grubsnik    schedule 14.10.2014

Можно сфокусировать элемент с помощью BeginInvoke после его нахождения, указав приоритет:

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

Программный выбор элемента не дает ему фокуса клавиатуры. Вы должны делать это четко ... ((Control)listView1.SelectedItem).Focus()

person Robert Levy    schedule 09.09.2011
comment
Спасибо. что бросает. Элемент, отображаемый в ListView, не является элементом управления, поэтому выполняется приведение. Еще пробовал this.listView1.Focus(), который не бросает, но без разницы что вижу. - person Cheeso; 09.09.2011

Чизо, в своем предыдущем ответе вы сказали:

Но есть и вторая проблема - он не работает сразу после установки SelectedItem. ItemContainerGenerator.ContainerFromItem () всегда, кажется, возвращает значение null.

Простое решение - вообще не устанавливать SelectedItem. Это произойдет автоматически, когда вы сфокусируете элемент. Так что достаточно вызвать следующую строку:

((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();
person Christian Lang    schedule 17.01.2013
comment
Думаю, я пробовал это, и это не сработало. В любом случае, это было давно-давно пора, и я уже прошел. Тем не менее, может быть полезно для тех, кто сталкивается с подобной проблемой. - person Cheeso; 18.01.2013

Все это кажется немного навязчивым ... Я сам переписал логику:

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);
                }
            }
}
person Alexandru    schedule 28.02.2014

После долгой возни я не смог заставить его работать в 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" ..../>
person D.Kempkes    schedule 12.05.2014

Решение Cheeso ДЕЙСТВИТЕЛЬНО работает для меня. Предотвратите null исключение, просто установив timer.tick для этого, чтобы вы оставили исходную процедуру.

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

Проблема решена при вызове таймера после RemoveAt/Insert, а также в Window.Loaded для установки фокуса и выбора первого элемента.

Хотел отдать этот первый пост за много вдохновения и решений, которые я получил в SE. Удачного кодирования!

person DoomProof    schedule 01.08.2016