2011-09-09 3 views
10

Ho un controllo ListView WPF, ItemsSource è impostato su un ICollectionView creato in questo modo:tasti freccia non funzionano dopo l'impostazione di programmazione ListView.SelectedItem

var collectionView = 
    System.Windows.Data.CollectionViewSource.GetDefaultView(observableCollection); 
this.listView1.ItemsSource = collectionView; 

... dove ObservableCollection è un ObservableCollection di un complesso genere. ListView è configurato per visualizzare, per ciascun elemento, una sola proprietà stringa sul tipo complesso.

L'utente può aggiornare ListView, a quel punto la mia logica memorizza la "stringa chiave" per l'elemento attualmente selezionato, ri-popola la ObservableCollection sottostante. L'ordinamento e il filtro precedenti vengono quindi applicati a collectionView. A questo punto mi piacerebbe "selezionare" l'elemento che era stato selezionato prima della richiesta da aggiornare. Gli oggetti in ObservableCollection sono nuove istanze, quindi confronto le rispettive proprietà della stringa e poi ne seleziono una che corrisponde. Come questo:

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; 
     } 
    } 
} 

Tutto questo funziona. Se viene selezionato il quarto elemento e l'utente preme F5, l'elenco viene ricostituito e quindi viene selezionato l'elemento con la stessa proprietà di stringa del quarto elemento precedente. A volte questo è il nuovo 4 ° elemento, a volte no, ma fornisce "least astonishment behavior".

Il problema si verifica quando l'utente successivamente utilizza i tasti di direzione per navigare all'interno di ListView. La prima freccia su o giù dopo un aggiornamento fa sì che il primo elemento nella (nuova) listview sia selezionato, indipendentemente da quale elemento sia stato selezionato dalla logica precedente. Eventuali altri tasti freccia funzionano come previsto.

Perché sta succedendo?

Questo chiaramente viola la regola del "meno stupore". Come posso evitarlo?


EDIT
Su ulteriore ricerca, questa sembra la stessa anomalia descritto dalla risposta
WPF ListView arrow navigation and keystroke problem, tranne che fornire maggiori dettagli.

risposta

15

Sembra che questo sia dovuto a a sort of known but not-well-described problematic behavior with ListView (e forse ad alcuni altri controlli WPF). Richiede che un'app chiami lo Focus() sul particolare ListViewItem, dopo aver impostato a livello di codice l'oggetto SelectedItem.

Ma lo stesso SelectedItem non è un UIElement. È un elemento di qualsiasi cosa tu stia visualizzando in ListView, spesso un tipo personalizzato. Pertanto non è possibile chiamare this.listView1.SelectedItem.Focus(). Non funzionerà. È necessario ottenere UIElement (o Control) che visualizza quell'elemento particolare. C'è un angolo oscuro dell'interfaccia WPF chiamato ItemContainerGenerator, che presumibilmente ti consente di ottenere il controllo che visualizza un particolare elemento in un ListView.

Qualcosa di simile a questo:

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

Ma c'è anche un secondo problema con quello - non funziona a destra dopo aver impostato la SelectedItem. ItemContainerGenerator.ContainerFromItem() sembra sempre restituire null. Altrove nel googlespace le persone hanno segnalato che restituiscono null con il set di GroupStyle. Ma ha esibito questo comportamento con me, senza raggruppamento.

ItemContainerGenerator.ContainerFromItem() restituisce null per tutti gli oggetti visualizzati nell'elenco. Anche ItemContainerGenerator.ContainerFromIndex() restituisce null per tutti gli indici. Ciò che è necessario è chiamare quelle cose solo dopo che il ListView è stato reso (o qualcosa).

Ho provato a farlo direttamente tramite Dispatcher.BeginInvoke() ma anche questo non funziona.

Su suggerimento di alcuni altri thread, ho utilizzato Dispatcher.BeginInvoke() dall'interno dell'evento StatusChanged sullo ItemContainerGenerator. Sì, semplice eh? (Non)

Ecco come appare il codice.

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();})); 

    } 
} 

Questo è un brutto codice. Ma, impostando in modo selettivo l'oggetto SelectedItem in questo modo, la successiva navigazione con le frecce funzionerà in ListView.

+0

ho avuto simili, requring dispatch.begininvokes che "riorienterà" più tardi. non dimenticare di segnarti come risposta! –

+0

Intendo accettarlo come risposta, ma penso di non poterlo ancora - c'è un timer. – Cheeso

+0

Brutto, e non è nemmeno affidabile. Per un breve momento, il ListBox è focalizzato (puoi vedere il rettangolo di messa a fuoco), prima che l'oggetto sia nuovamente focalizzato. Quando si preme il tasto freccia in quel momento, la selezione è scomparsa. – ygoe

0

Selezionando un elemento a livello di codice non viene assegnato il fuoco della tastiera. Devi farlo in modo esplicito ... ((Control)listView1.SelectedItem).Focus()

+1

Grazie. che getta. L'elemento visualizzato in ListView non è un controllo, quindi il cast viene lanciato. Ho anche provato 'this.listView1.Focus()', che non lancia, ma non fa differenza che posso vedere. – Cheeso

0

Cheeso, nella vostra previous answer hai detto:

Ma c'è anche un secondo problema con quello - non funziona a destra dopo aver impostato la SelectedItem. ItemContainerGenerator.ContainerFromItem() sembra sempre restituire null.

Una soluzione semplice è non impostare affatto SelectedItem. Questo avverrà automaticamente quando focalizzi l'elemento. Quindi, solo chiamando la seguente riga farà:

((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus(); 
+0

Penso di averlo provato, e non ha funzionato. In ogni caso ora è passato un tempo mooolto e io non ci riesco. Può essere utile per qualcuno che si trova ad affrontare un problema simile. – Cheeso

0

Tutto questo sembra un po 'invadente ... sono andato con riscrivere la logica me stesso:

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

Dopo un sacco di armeggiare intorno Non potevo' t farlo funzionare in MVVM. L'ho provato io stesso e ho usato una proprietà di dipendenza. Questo ha funzionato alla grande per me.

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]); 
    } 

} 

Uso in XAML

<ListBox IsSynchronizedWithCurrentItem="True" extenders:ListBoxExtenders.AutoScrollToCurrentItem="True" ..../> 
3

ho avuto questo problema con un controllo ListBox (che è come ho finito per trovare questa domanda SO). Nel mio caso, l'oggetto SelectedItem veniva impostato tramite associazione e successivi tentativi di navigazione da tastiera avrebbero ripristinato il ListBox in modo da avere il primo elemento selezionato. Stavo anche sincronizzando la mia ObservableCollection sottostante aggiungendo/rimuovendo gli elementi (non legandosi ad una nuova collezione ogni volta).

Sulla base di informazioni fornite nella risposta accettata, sono stato in grado di lavorare intorno ad esso con il seguente sottoclasse di 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(); 
     } 
    } 
} 

Spero che questo aiuti qualcuno risparmiare un po 'di tempo.

1

Ho trovato un approccio un po 'diverso. Sto usando il databinding per assicurarmi che l'elemento corretto sia evidenziato nel codice, e poi invece di focalizzare l'attenzione su ogni rebind, semplicemente aggiungo un gestore pre-evento al codice dietro per la navigazione da tastiera. Come questo.

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; 
    } 

Questo rende semplicemente assicurarsi che la corretta messa a fuoco è impostata prima di lasciare WPF gestire la pressione del tasto

0

E 'possibile mettere a fuoco un oggetto con BeginInvoke dopo averlo trovato per priorità specificando:

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

Cheeso di la soluzione funziona per me. Impedisci l'eccezione null impostando semplicemente un timer.tick, così hai lasciato la tua routine originale.

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

Problema risolto quando si chiama il timer dopo un RemoveAt/Insert, e anche a Window.Loaded impostare lo stato attivo e selezionare per prima voce.

Volevo restituire questo primo post per l'ispirazione e le soluzioni che ho ottenuto in SE. Buona programmazione!