Desplazar la vista de lista de WPF a una línea específica
listview scroll (11)
WPF, aplicación de navegador.
Tengo una página que contiene un ListView. Después de llamar a PageFunction, agrego una línea a ListView y quiero desplazar la nueva línea a la vista:
ListViewItem item = ItemContainerGenerator.ContainerFromIndex(index) as ListViewItem;
if (item != null)
ScrollIntoView(item);
Esto funciona. Mientras la nueva línea esté a la vista, la línea se enfoca como debería.
El problema es que las cosas no funcionan cuando la línea no está visible.
Si la línea no es visible, no hay ListViewItem para la línea generada, por lo que ItemContainerGenerator.ContainerFromIndex devuelve null.
Pero sin el artículo, ¿cómo desplazo la línea a la vista? ¿Hay alguna manera de desplazarse a la última línea (o en cualquier lugar) sin necesidad de un ListViewItem?
Acabo de tener el mismo problema con ItemContainerGenerator.ContainerFromItem () y ItemContainerGenerator.ContainerFromIndex () devolviendo null para los elementos que claramente existían en el cuadro de lista. Decasteljau tenía razón, pero tuve que investigar un poco para entender exactamente a qué se refería. Aquí está el desglose para salvar al próximo chico / gal un poco de trabajo de campo.
Para resumir, ListBoxItems se destruyen si no están a la vista. En consecuencia, ContainerFromItem () y ContainerFromIndex () devuelven nulo, ya que ListBoxItems no existe. Esto es aparentemente una función de ahorro de memoria / rendimiento detallada aquí: http://blogs.msdn.com/b/oren/archive/2010/11/08/wp7-silverlight-perf-demo-1-virtualizingstackpanel-vs-stackpanel-as-a-listbox-itemspanel.aspx
El código <ListBox.ItemsPanel>
vacío es lo que desactiva la virtualización. Código de muestra que me solucionó el problema:
Plantilla de datos:
<phone:PhoneApplicationPage.Resources>
<DataTemplate x:Key="StoryViewModelTemplate">
<StackPanel>
<your datatemplated stuff here/>
</StackPanel>
</DataTemplate>
</phone:PhoneApplicationPage.Resources>
Cuerpo principal:
<Grid x:Name="ContentPanel">
<ListBox Name="lbResults" ItemsSource="{Binding SearchResults}" ItemTemplate="{StaticResource StoryViewModelTemplate}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel>
</StackPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
Alguien me dijo una forma aún mejor de desplazarse a una línea específica, que es fácil y funciona como el encanto.
En breve:
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 versión más larga en los foros de MSDN :
Creo que el problema aquí es que ListViewItem no se ha creado aún si la línea no está visible. WPF crea Visible a pedido.
Entonces, en este caso, probablemente obtengas null
el artículo, ¿verdad? (De acuerdo con tu comentario, lo haces)
He encontrado un enlace en los foros de MSDN que sugieren acceder al Scrollviewer directamente para desplazarse. Para mí, la solución presentada allí se parece mucho a un truco, pero puedes decidir por ti mismo.
Aquí está el fragmento de código del enlace de arriba :
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);
En mi proyecto, necesito mostrar la línea de índice seleccionada de la vista de lista al usuario, así que asigné el elemento seleccionado al control ListView. Este código desplazará la barra de desplazamiento y mostrará el elemento seleccionado.
BooleanListView.ScrollIntoView (BooleanListView.SelectedItem);
O
var listView = BooleanListView; listView.SelectedItem = listView.Items.GetItemAt (BooleanListView.SelectedIndex); listView.ScrollIntoView (listView.Items [0]); listView.ScrollIntoView (listView.SelectedItem);
Gracias por el último consejo, Sam. Tuve un diálogo que se abrió, es decir, mi cuadrícula perdió el enfoque cada vez que se cerró el diálogo. Yo uso esto:
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();
}
Hice algunos cambios a la respuesta de Sam. Tenga en cuenta que quería desplazarme a la última línea. Desafortunadamente, los elementos de ListView solo mostraban la última línea (incluso cuando había, por ejemplo, 100 líneas arriba), así que así es como lo arreglé:
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();
}
}
Aclamaciones
Para superar el problema de la virtualización pero aún utilizar ScrollIntoView
y no hackear en las entrañas de ListView, también podría usar sus objetos de ViewModel para determinar qué se selecciona. Suponiendo que tiene objetos ViewModel en su lista que tienen una propiedad IsSelected
. Debería vincular los elementos a ListView en XAML de esta manera:
<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>
Luego, el método de código subyacente puede desplazarse al primer elemento seleccionado con esto:
var firstSelected = PersonsListView.Items
.OfType<TreeViewItemViewModel>().FirstOrDefault(x => x.IsSelected);
if (firstSelected != null)
CoObjectsListView.ScrollIntoView(firstSelected);
Esto también funciona si el elemento seleccionado está fuera de la vista. En mi experimento, la propiedad PersonsListView.SelectedItem
era null
, pero, por supuesto, su propiedad ViewModel IsSelected
siempre está ahí. Asegúrese de llamar a este método después de que se haya completado todo el enlace y la carga (con el DispatcherPriority
correcto).
Usando el patrón ViewCommand , su código de ViewModel podría verse así:
PersonVMs.ForEach(vm => vm.IsSelected = false);
PersonVMs.Add(newPersonVM);
newPersonVM.IsSelected = true;
ViewCommandManager.InvokeLoaded("ScrollToSelectedPerson");
Prueba esto
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();
}
Si solo desea mostrar y enfocar el último elemento después de crear un nuevo elemento de datos, este método es quizás mejor. Comparar con ScrollIntoView, ScrollTo End de ScrollViewer es en mis pruebas más confiable. En algunas pruebas, el método ScrollIntoView de ListView como el anterior falló y no sé por qué. Pero usar ScrollViewer para desplazarse hasta el último puede 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();
}
Una solución para esto es cambiar el Panel de elementos de ListView. El panel predeterminado es VirtualizingStackPanel que solo crea ListBoxItem la primera vez que se vuelven visibles. Si no tiene demasiados elementos en su lista, no debería ser un problema.
<ListView>
...
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
no estoy seguro de si este es el camino a seguir, pero esto actualmente funciona para mí usando WPF, MVVM Light y .NET 3.5
Agregué el evento SelectionChanged para ListBox llamado " lbPossibleError_SelectionChanged "
luego detrás de este evento " lbPossibleError_SelectionChanged ", aquí está el código
funciona como debería para mí.