web-dev-qa-db-fra.com

Faites défiler WPF Listview jusqu'à une ligne spécifique

WPF, application semblable à un navigateur.
J'ai une page contenant un ListView. Après avoir appelé une PageFunction, j’ajoute une ligne à ListView et je souhaite faire défiler la nouvelle ligne:

  ListViewItem item = ItemContainerGenerator.ContainerFromIndex(index) as ListViewItem;
  if (item != null)
    ScrollIntoView(item);

Cela marche. Tant que la nouvelle ligne est visible, la ligne obtient le focus comme il se doit.

Le problème, c'est que les choses ne fonctionnent pas quand la ligne n'est pas visible.
Si la ligne n'est pas visible, il n'y a pas de ListViewItem pour la ligne générée. ItemContainerGenerator.ContainerFromIndex renvoie null.

Mais sans l'article, comment puis-je faire défiler la ligne dans la vue? Est-il possible de faire défiler jusqu'à la dernière ligne (ou n'importe où) sans avoir besoin d'un ListViewItem?

22
Sam

Je pense que le problème ici est que ListViewItem n'est pas encore créé si la ligne n'est pas visible. WPF crée le visible sur demande.

Donc dans ce cas, vous obtenez probablement null pour l'article, et vous? (Selon ton commentaire tu le fais)

J'ai trouvé un lien sur les forums MSDN qui suggère d'accéder directement à Scrollviewer afin de faire défiler. Pour moi, la solution présentée ressemble beaucoup à un hack, mais vous pouvez décider vous-même.

Voici l'extrait de code provenant du lien ci-dessus :

VirtualizingStackPanel vsp =  
  (VirtualizingStackPanel)typeof(ItemsControl).InvokeMember("_itemsHost",
   BindingFlags.Instance | BindingFlags.GetField | BindingFlags.NonPublic, null, 
   _listView, null);

double scrollHeight = vsp.ScrollOwner.ScrollableHeight;

// itemIndex_ is index of the item which we want to show in the middle of the view
double offset = scrollHeight * itemIndex_ / _listView.Items.Count;

vsp.SetVerticalOffset(offset);
10
EFrank

Quelqu'un m'a dit un moyen encore meilleur de faire défiler jusqu'à une ligne spécifique, ce qui est facile et fonctionne comme un charme.
En bref:

public void ScrollToLastItem()
{
  lv.SelectedItem = lv.Items.GetItemAt(rows.Count - 1);
  lv.ScrollIntoView(lv.SelectedItem);
  ListViewItem item = lv.ItemContainerGenerator.ContainerFromItem(lv.SelectedItem) as ListViewItem;
  item.Focus();
}

La version longue dans Forums MSDN :

39
Sam

J'ai apporté quelques changements à la réponse de Sam. Notez que je voulais faire défiler jusqu'à la dernière ligne. Malheureusement, certains aspects de ListView ne contenaient que la dernière ligne (même quand il y avait par exemple 100 lignes au-dessus), c'est pourquoi j'ai corrigé cela:

    public void ScrollToLastItem()
    {
        if (_mainViewModel.DisplayedList.Count > 0)
        {
            var listView = myListView;
            listView.SelectedItem = listView.Items.GetItemAt(_mainViewModel.DisplayedList.Count - 1);
            listView.ScrollIntoView(listView.Items[0]);
            listView.ScrollIntoView(listView.SelectedItem);
            //item.Focus();
        }
    }

À votre santé

Une solution de contournement à cela consiste à modifier le ItemsPanel de ListView. Le panneau par défaut est VirtualizingStackPanel, qui crée uniquement le ListBoxItem lorsqu’il devient visible. Si vous n'avez pas trop d'éléments dans votre liste, cela ne devrait pas être un problème.

<ListView>
   ...
   <ListView.ItemsPanel>
      <ItemsPanelTemplate>
         <StackPanel/>
      </ItemsPanelTemplate>
   </ListView.ItemsPanel>
</ListView>
3
decasteljau

Essaye ça

private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            ScrollViewer scrollViewer = GetScrollViewer(lstVw) as ScrollViewer;
            scrollViewer.ScrollToHorizontalOffset(dataRowToFocus.RowIndex);
            if (dataRowToFocus.RowIndex < 2)
                lstVw.ScrollIntoView((Entity)lstVw.Items[0]);
            else
                lstVw.ScrollIntoView(e.AddedItems[0]);
        } 

 public static DependencyObject GetScrollViewer(DependencyObject o)
        {
            if (o is ScrollViewer)
            { return o; }

            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++)
            {
                var child = VisualTreeHelper.GetChild(o, i);

                var result = GetScrollViewer(child);
                if (result == null)
                {
                    continue;
                }
                else
                {
                    return result;
                }
            }
            return null;
        } 

private void Focus()
{
 lstVw.SelectedIndex = dataRowToFocus.RowIndex;
 lstVw.SelectedItem = (Entity)dataRowToFocus.Row;

 ListViewItem lvi = (ListViewItem)lstVw.ItemContainerGenerator.ContainerFromItem(lstVw.SelectedItem);
ContentPresenter contentPresenter = FindVisualChild<ContentPresenter>(lvi);
contentPresenter.Focus();
contentPresenter.BringIntoView();

}
2
Murali.S

Merci pour ce dernier conseil, Sam. J'avais un dialogue qui s'ouvrait, ce qui signifiait que ma grille perdait le focus à chaque fermeture du dialogue. J'utilise ceci:

if(currentRow >= 0 && currentRow < lstGrid.Items.Count) {
    lstGrid.SelectedIndex = currentRow;
    lstGrid.ScrollIntoView(lstGrid.SelectedItem);
    if(shouldFocusGrid) {
        ListViewItem item = lstGrid.ItemContainerGenerator.ContainerFromItem(lstGrid.SelectedItem) as ListViewItem;
        item.Focus();
    }
} else if(shouldFocusGrid) {
    lstGrid.Focus();
}
2
Echilon

Si vous souhaitez simplement afficher et focaliser le dernier élément après la création d'un nouvel élément de données, cette méthode est peut-être meilleure. Comparer à ScrollIntoView, ScrollToEnd of ScrollViewer est dans mes tests plus fiable. Dans certains tests utilisant la méthode ScrollIntoView de ListView comme ci-dessus a échoué et je ne sais pas la raison. Mais utiliser ScrollViewer pour aller au dernier peut fonctionner.

void FocusLastOne(ListView lsv)
{
   ObservableCollection<object> items= sender as ObservableCollection<object>;

   Decorator d = VisualTreeHelper.GetChild(lsv, 0) as Decorator;
   ScrollViewer v = d.Child as ScrollViewer;
   v.ScrollToEnd();

   lsv.SelectedItem = lsv.Items.GetItemAt(items.Count - 1);
   ListViewItem lvi = lsv.ItemContainerGenerator.ContainerFromIndex(items.Count - 1) as ListViewItem;
   lvi.Focus();
}
2
Welson Zhong

Je viens d'avoir le même problème avec ItemContainerGenerator.ContainerFromItem () et ItemContainerGenerator.ContainerFromIndex () retournant null pour les éléments qui existaient clairement dans la liste. Decasteljau avait raison, mais je devais faire des recherches pour comprendre exactement ce qu'il voulait dire. Voici la ventilation pour sauver le gars/gal prochain de legwork.

En résumé, les ListBoxItems sont détruits s’ils ne sont pas visibles. Par conséquent, ContainerFromItem () et ContainerFromIndex () renvoient null, car les ListBoxItems n'existent pas. Ceci est apparemment une fonctionnalité d’économie de mémoire/performance détaillée ici: http://blogs.msdn.com/b/oren/archive/2010/11/08/wp7-silverlight-perf-demo-1-virtualizingstackpanel-vs- stackpanel-as-a-listbox-itemspanel.aspx

Le code <ListBox.ItemsPanel> vide est ce qui désactive la virtualisation. Exemple de code qui a résolu le problème pour moi:

Modèle de données:

<phone:PhoneApplicationPage.Resources>
    <DataTemplate x:Key="StoryViewModelTemplate">
        <StackPanel>
          <your datatemplated stuff here/>
        </StackPanel>
    </DataTemplate>
</phone:PhoneApplicationPage.Resources>

Corps principal:

<Grid x:Name="ContentPanel">
    <ListBox Name="lbResults" ItemsSource="{Binding SearchResults}" ItemTemplate="{StaticResource StoryViewModelTemplate}">
        <ListBox.ItemsPanel>
             <ItemsPanelTemplate>
                 <StackPanel>
                 </StackPanel>
             </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
</Grid>
1
Ray Ackley

Pour résoudre le problème de la virtualisation tout en utilisant toujours ScrollIntoView et en évitant de chercher dans les entrailles de ListView, vous pouvez également utiliser vos objets ViewModel pour déterminer les éléments sélectionnés. En supposant que vous ayez des objets ViewModel dans votre liste qui possèdent une propriété IsSelected. Vous lieriez les éléments au ListView en XAML comme ceci:

<ListView Name="PersonsListView" ItemsSource="{Binding PersonVMs}">
  <ListView.ItemContainerStyle>
    <Style TargetType="{x:Type ListViewItem}">
      <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
    </Style>
  </ListView.ItemContainerStyle>
</ListView>

Ensuite, la méthode code-behind peut faire défiler jusqu'au premier élément sélectionné avec ceci:

var firstSelected = PersonsListView.Items
    .OfType<TreeViewItemViewModel>().FirstOrDefault(x => x.IsSelected);
if (firstSelected != null)
    CoObjectsListView.ScrollIntoView(firstSelected);

Cela fonctionne également si l'élément sélectionné est bien hors de vue. Dans mon expérience, la propriété PersonsListView.SelectedItem était null, mais bien sûr votre propriété ViewModel IsSelected est toujours présente. Assurez-vous d'appeler cette méthode une fois la liaison et le chargement terminés (avec la bonne variable DispatcherPriority).

En utilisant ViewCommand pattern, votre code ViewModel pourrait ressembler à ceci:

PersonVMs.ForEach(vm => vm.IsSelected = false);
PersonVMs.Add(newPersonVM);
newPersonVM.IsSelected = true;
ViewCommandManager.InvokeLoaded("ScrollToSelectedPerson");
0
ygoe

Dans mon projet, je dois afficher la ligne d'index sélectionnée à partir de la vue liste pour que je puisse attribuer l'élément sélectionné au contrôle ListView. Ce code fera défiler la barre de défilement et affichera l'élément sélectionné.

BooleanListView.ScrollIntoView (BooleanListView.SelectedItem);

OR

var listView = BooleanListView; listView.SelectedItem = listView.Items.GetItemAt (BooleanListView.SelectedIndex); listView.ScrollIntoView (listView.Items [0]); listView.ScrollIntoView (listView.SelectedItem);

0
Jeyavel

je ne sais pas si c'est la voie à suivre, mais cela fonctionne actuellement pour moi avec WPF, MVVM Light et .NET 3.5.

J'ai ajouté l'événement SelectionChanged pour ListBox appelé " lbPossibleError_SelectionChanged I added the SelectionChanged event for ListBox

puis derrière cet événement " lbPossibleError_SelectionChanged ", voici le code enter image description here

fonctionne comme il se doit pour moi.

0
Jayson Ragasa