Vogliamo impostare lo SelectedItem
di un ListBox
a livello di programmazione e desidera che quell'elemento abbia il focus in modo che i tasti freccia funzionino relativamente a quell'elemento selezionato. Sembra abbastanza semplice.Come si imposta a livello di programmazione l'elemento SelectedItem in un ListBox WPF che ha già lo stato attivo?
Il problema, tuttavia, è se il ListBox
ha già della tastiera quando l'impostazione SelectedItem
programmazione, mentre non aggiorna correttamente la proprietà IsSelected
sulla ListBoxItem
, esso non set tastiera fuoco ad esso, e quindi, i tasti freccia spostati rispetto all'elemento focalizzato in precedenza nell'elenco e non all'elemento appena selezionato come ci si aspetterebbe.
Questo è molto confuso per l'utente in quanto fa apparire la selezione a saltare quando si utilizza la tastiera come si aggancia indietro a dove era prima che la selezione programmatica ha avuto luogo.
Nota: come ho detto, ciò si verifica solo se si imposta la proprietà SelectedItem
su un ListBox
che ha già l'attivazione della tastiera. Se non lo fa (o se lo fa, ma te ne vai, quindi torna indietro), quando lo stato attivo della tastiera ritorna allo ListBox
, l'elemento corretto avrà ora lo stato attivo della tastiera come previsto.
Ecco alcuni esempi di codice che mostrano questo problema. Per dimostrarlo, esegui il codice, usa il mouse per selezionare "Sette" nell'elenco (mettendo così a fuoco lo ListBox
), quindi fai clic sul pulsante "Prova". Infine, tocca il tasto "Alt" sulla tastiera per rivelare il punto focale. Vedrai che è ancora su "Sette" e se usi le frecce su e giù, sono relative a quella riga, non "Quattro" come ci si aspetterebbe un utente.
Nota che ho Focusable
impostato su false
sul pulsante per non derubare la casella di elenco quando lo si preme. Se non avessi questo, lo ListBox
perderebbe la messa a fuoco quando clicchi sul pulsante, e quindi, quando il focus tornerà al ListBox, sarebbe sull'elemento corretto.
file XAML:
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="525" Height="350" WindowStartupLocation="CenterScreen"
Title="MainWindow" x:Name="Root">
<DockPanel>
<Button Content="Test"
DockPanel.Dock="Bottom"
HorizontalAlignment="Left"
Focusable="False"
Click="Button_Click" />
<ListBox x:Name="MainListBox" />
</DockPanel>
</Window>
Codice-behind:
using System.Collections.ObjectModel;
using System.Windows;
namespace Test
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainListBox.ItemsSource = new string[]{
"One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight"
};
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MainListBox.SelectedItem = MainListBox.Items[3];
}
}
}
Nota: alcuni hanno suggerito di usare IsSynchronizedWithCurrentItem
, ma che la proprietà sincronizza il SelectedItem
del ListBox
con l'Current
proprietà del associata vista. Non è correlato al focus poiché questo problema esiste ancora.
nostro work-around è quello di impostare temporaneamente la messa a fuoco da qualche altra parte, quindi impostare l'elemento selezionato, quindi impostare la messa a fuoco di nuovo alla ListBox
, ma questo ha l'effetto undesireable di noi dover fare la nostra ViewModel
consapevoli del ListBox
stesso, quindi esegui la logica a seconda che abbia o meno la messa a fuoco, ecc. (es. non vorresti dire semplicemente 'Concentrati altrove, poi torna qui, se' qui 'non avesse già l'obiettivo come lo avresti rubato da qualche altra parte). Inoltre, non è possibile gestirlo semplicemente attraverso associazioni dichiarative. Inutile dire che questo è brutto.
Poi di nuovo, "brutte" navi, quindi c'è.
ContainerFromItem restituirà Null se non è stato ancora generato alcun contenitore per esso, che è il caso di un elenco virtualizzato e l'elemento è fuori schermo. Inoltre, se si tenta di impostare il valore da un'associazione esso si interrompe non avendo accesso al ListBox, né si dovrebbe. (Continua di seguito ...) – MarqueIV
Anche un comportamento collegato pone problemi in quanto non si vorrebbe che il controllo impostasse ciecamente lo stato attivo della tastiera su ListBoxItem a meno che a) il controllo avesse già lo stato attivo (facile da testare) eb) il cambiamento avvenne dal code-behind (non facile da testare senza sottoclasse.) Altrimenti si potrebbe inavvertitamente rubare la messa a fuoco da un altro controllo (caso a) o rovinare la messa a fuoco in quella corrente (caso b) in modalità multi-select e deselezionando una riga, che normalmente dovrebbe mantenere il focus della tastiera, ma dovrebbe essere impostata sul nuovo SelectedItem, se presente, causando comportamenti strani. – MarqueIV
In realtà, a pensarci bene, penso di avere il rimedio per il caso 'B' sopra. Crea il comportamento allegato come hai detto tu, ma non lo imposta direttamente in XAML. Invece si crea una proprietà sul ViewModel e si lega il comportamento a quello. In questo modo non hai bisogno di sapere (o preoccuparti) chi sta ascoltando. Quindi, appena prima di impostare l'elemento selezionato dal codice sottostante, abiliti il comportamento, seleziona l'elemento, quindi disattiva nuovamente il comportamento. Questo indirizzo 'B' sopra. (Avresti comunque bisogno di "A", ovviamente.) Votare la tua come risposta, anche se non completa, mi ha portato su questa strada. – MarqueIV