Клавиши со стрелками не работают после программной установки 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 стрелка навигации и проблема нажатия клавиш, за исключением того, что я предоставлю более подробную информацию.
9 ответов
Похоже, это связано с неким хорошо известным, но недостаточно хорошо описанным проблемным поведением с ListView (и, возможно, некоторыми другими элементами управления WPF). Это требует, чтобы вызов приложения Focus()
на конкретном ListViewItem после программной установки SelectedItem.
Но сам SelectedItem не является элементом UIElement. Это элемент того, что вы отображаете в 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.
У меня была эта проблема с элементом управления 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();
}
}
}
Надеюсь, это поможет кому-то сэкономить время.
Я нашел несколько иной подход. Я использую привязку данных, чтобы убедиться, что в коде выделен правильный элемент, а затем вместо того, чтобы устанавливать фокус при каждом повторном связывании, я просто добавляю обработчик перед событием в код для навигации с помощью клавиатуры. Как это.
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 обрабатывать нажатие клавиш
Можно сфокусировать предмет с помощью BeginInvoke
после его нахождения, указав приоритет:
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(existing) as ListBoxItem;
lbi.Focus();
}));
Выбор пункта программно не дает ему фокус клавиатуры. Вы должны сделать это прямо... ((Control)listView1.SelectedItem).Focus()
Cheeso, в своем предыдущем ответе вы сказали:
Но есть и вторая проблема - она не работает сразу после установки SelectedItem. ItemContainerGenerator.ContainerFromItem() всегда возвращает null.
Простое решение этого - вообще не устанавливать SelectedItem. Это автоматически произойдет, когда вы сфокусируете элемент. Так что просто вызов следующей строки будет делать:
((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();
После долгих раздумий я не смог заставить его работать в 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" ..../>
Все это кажется немного навязчивым... Я сам переписал логику:
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);
}
}
}
Решение Cheeso работает на меня. Предотвратить null
Исключение, просто установив timer.tick
чтобы сделать это, так что вы оставили свою первоначальную рутину.
var uiel = (UIElement)this.lv1.ItemContainerGenerator
.ContainerFromItem(lv1.Items[ix]);
if (uiel != null) uiel.Focus();
Проблема решена при вызове таймера после RemoveAt/Insert
а также на Window.Loaded
установить фокус и выбрать первый элемент.
Хотел вернуть этот первый пост за то большое вдохновение и решения, которые я получил в SE. Удачного кодирования!