Клавиши со стрелками не работают после программной установки ListView.SelectedItem - PullRequest
10 голосов
/ 09 сентября 2011

У меня есть элемент управления 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. Первая стрелка вверх или вниз после обновления вызывает выбор первого элемента (нового) списка независимо от того, какой элемент был выбран предыдущей логикой. Все дальнейшие клавиши со стрелками работают как положено.

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

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


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

Ответы [ 9 ]

15 голосов
/ 09 сентября 2011

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

Но сам SelectedItem не является элементом UIE.Это элемент того, что вы отображаете в ListView, часто это пользовательский тип.Поэтому вы не можете позвонить this.listView1.SelectedItem.Focus().Это не сработает.Вам нужно получить UIElement (или элемент управления), который отображает этот конкретный элемент.Темный угол интерфейса WPF называется ItemContainerGenerator , который предположительно позволяет получить элемент управления, отображающий определенный элемент в ListView.

Примерно так:

this.listView1.SelectedItem = thing;
// *** WILL NOT WORK!
((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();

Но есть и вторая проблема - она ​​не работает сразу после установки SelectedItem.ItemContainerGenerator.ContainerFromItem () всегда возвращает null.В других местах гуглпространства люди сообщали, что он возвращает значение null с установленным GroupStyle.Но это проявилось со мной, без группировки.

ItemContainerGenerator.ContainerFromItem() возвращает ноль для всех объектов, отображаемых в списке.Также 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.

4 голосов
/ 01 сентября 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();
        }
    }
}

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

2 голосов
/ 14 октября 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 обрабатывать нажатие клавиши

0 голосов
/ 01 августа 2016

Решение Cheeso работает на меня. Предотвратите исключение null, просто установив для этого timer.tick, чтобы вы оставили исходную процедуру.

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

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

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

0 голосов
/ 20 ноября 2014

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

Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
    var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(existing) as ListBoxItem;
    lbi.Focus();
}));
0 голосов
/ 12 мая 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" ..../>
0 голосов
/ 28 февраля 2014

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

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 голосов
/ 17 января 2013

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

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

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

((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();
0 голосов
/ 09 сентября 2011

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

...