Scroll WPF ListView a linha específica
Pergunta
WPF, Browserlike aplicativo.
Eu tenho uma página que contém um ListView. Depois de chamar um PageFunction adicionar uma linha para o ListView e deseja rolar a nova linha à vista:
ListViewItem item = ItemContainerGenerator.ContainerFromIndex(index) as ListViewItem;
if (item != null)
ScrollIntoView(item);
Isso funciona. Enquanto a nova linha está em vista a linha recebe o foco como deveria.
O problema é que as coisas não funcionam quando a linha não é visível.
Se a linha não é visível, não há ListViewItem para a linha gerada, de modo ItemContainerGenerator.ContainerFromIndex retorna null.
Mas sem o item, como faço para rolar a linha em vista? Existe alguma maneira para ir até a última linha (ou em qualquer lugar) sem precisar de um ListViewItem?
Solução
Eu acho que o problema aqui é que o ListViewItem não é criado ainda se a linha não é visível. WPF cria a Visible sob demanda.
Portanto, neste caso, você provavelmente terá null
para o item, não é?
(De acordo com o seu comentário, você faz)
Eu encontrei um link em fóruns do MSDN que sugerem acessando o ScrollViewer diretamente , a fim de rolagem. Para mim, a solução apresentada não se parece muito com um hack, mas você pode decidir por si mesmo.
Aqui está o trecho de código a partir do link acima :
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);
Outras dicas
Alguém me disse uma maneira ainda melhor para se deslocar para uma linha específica, que é fácil e funciona como charme.
Em suma:
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();
}
A versão mais longa em Fórum MSDN :
Eu fiz algumas mudanças para a resposta de Sam. Note que eu queria para ir até a última linha. Infelizmente, a ListView sometiems apenas exibida a última linha (mesmo quando havia por exemplo 100 linhas acima dela), então isso é como eu fixo que:
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();
}
}
Felicidades
Uma solução para isso é mudar os ItemsPanel do ListView. O painel padrão é o VirtualizingStackPanel que só cria a ListBoxItem a primeira vez que eles se tornam visíveis. Se você não tem muitos itens em sua lista, ele não deve ser um problema.
<ListView>
...
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
Obrigado por essa última ponta Sam. Eu tinha uma janela que abriu, o que significa minha grade perdeu o foco cada vez que o diálogo fechado. Eu uso este:
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();
}
Tente este
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();
}
Se você quiser apenas para mostrar e focar o último item após criar um novo item de dados, este método é talvez melhor. Compare com ScrollIntoView, ScrollToEnd de ScrollViewer está em meus testes mais confiável. Em alguns testes, usando o método ScrollIntoView de ListView como acima falhou e eu não sei a razão. Mas usando ScrollViewer para se deslocar para último pode funcionar.
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();
}
Eu apenas tive o mesmo problema com ItemContainerGenerator.ContainerFromItem () e ItemContainerGenerator.ContainerFromIndex () retornando nulo para itens que claramente existiam na caixa de listagem. Decasteljau estava certo, mas eu tinha que fazer alguma escavação para descobrir exatamente o que ele queria dizer. Aqui é a quebra para salvar o próximo cara / gal algum trabalho braçal.
Para encurtar a história, ListBoxItems são destruídos se não forem dentro da visão. Consequentemente ContainerFromItem () e ContainerFromIndex () return null desde que não existem as ListBoxItems. Este é, aparentemente, um recurso de economia de memória / desempenho detalhado aqui: http://blogs.msdn.com/b/oren/archive/2010/11/08/wp7-silverlight-perf-demo -1-VirtualizingStackPanel-vs-StackPanel-como-um-listagem-itemspanel.aspx
O código <ListBox.ItemsPanel>
vazio é o que desliga a virtualização. código de exemplo que corrigiu o problema para mim:
modelo de dados:
<phone:PhoneApplicationPage.Resources>
<DataTemplate x:Key="StoryViewModelTemplate">
<StackPanel>
<your datatemplated stuff here/>
</StackPanel>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
Corpo principal:
<Grid x:Name="ContentPanel">
<ListBox Name="lbResults" ItemsSource="{Binding SearchResults}" ItemTemplate="{StaticResource StoryViewModelTemplate}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel>
</StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
Para superar o problema de virtualização, mas ainda usar ScrollIntoView
e não de hackers ao redor nas entranhas do ListView, você também pode usar o seu ViewModel objetos para determinar o que está selecionado. Supondo que você tenha objetos ViewModel em sua lista que apresentam uma propriedade IsSelected
. Você iria ligar os itens para o ListView em XAML como este:
<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>
Em seguida, o código-behind método pode se deslocar para o primeiro item selecionado com esta:
var firstSelected = PersonsListView.Items
.OfType<TreeViewItemViewModel>().FirstOrDefault(x => x.IsSelected);
if (firstSelected != null)
CoObjectsListView.ScrollIntoView(firstSelected);
Isso também funciona se o item selecionado é bem fora de vista. Na minha experiência, a propriedade PersonsListView.SelectedItem
foi null
, mas é claro que sua propriedade IsSelected
ViewModel está sempre lá. Certifique-se de chamar esse método, afinal de ligação e carregamento foi concluído (com a DispatcherPriority
direita).
Usando o padrão ViewCommand , seu código ViewModel poderia ser assim:
PersonVMs.ForEach(vm => vm.IsSelected = false);
PersonVMs.Add(newPersonVM);
newPersonVM.IsSelected = true;
ViewCommandManager.InvokeLoaded("ScrollToSelectedPerson");
No meu projeto eu preciso para exibir a linha de índice selecionado a partir do listview para o usuário assim que eu atribuído o item selecionado para o controle ListView. Este código irá rolar a barra de rolagem e exibir o item selecionado.
BooleanListView.ScrollIntoView (BooleanListView.SelectedItem);
ou
var listView = BooleanListView; listView.SelectedItem = listView.Items.GetItemAt (BooleanListView.SelectedIndex); listView.ScrollIntoView (listView.Items [0]); listView.ScrollIntoView (listView.SelectedItem);
não tenho certeza se este é o caminho a percorrer, mas esta atualmente trabalha para mim usando WPF, MVVM Luz, e .NET 3.5
Eu adicionei o evento SelectionChanged para ListBox chamado " lbPossibleError_SelectionChanged "
, em seguida, por trás disso " lbPossibleError_SelectionChanged " evento, aqui está o código
funciona como deveria para mim.