24

WPF, Browserlike app.
I got one page containing a ListView. After calling a PageFunction I add a line to the ListView, and want to scroll the new line into view:

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

This works. As long as the new line is in view the line gets the focus like it should.

Problem is, things don't work when the line is not visible.
If the line is not visible, there is no ListViewItem for the line generated, so ItemContainerGenerator.ContainerFromIndex returns null.

But without the item, how do I scroll the line into view? Is there any way to scroll to the last line (or anywhere) without needing an ListViewItem?

Sam
  • 28,421
  • 49
  • 167
  • 247
  • I've used both ListBox and DataGrid's ScrollIntoView() and they do not exhibit this problem? It's a silly question, but are you running against 3.5 SP1? Alot of things got fixed there. – Bob King Oct 17 '08 at 13:07
  • Yep, I run against 3.5SP1, and found this not to be a bug. The ListViewItem is virtualized, which is ok, but how do I scroll it into view then? – Sam Oct 17 '08 at 13:21

11 Answers11

42

Someone told me an even better way to scroll to a specific line, which is easy and works like charm.
In short:

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

The longer version in MSDN forums:

Sam
  • 28,421
  • 49
  • 167
  • 247
  • The only problem with this for me is that it callously overrides any selection the user might have made already. Still, should be an easy enough modification to make, so +1 for you, sir, thank you. – metao Jun 01 '10 at 06:43
  • Oh, yes - for me, changing the selection was exactly what I wanted, but you are right. – Sam Jun 01 '10 at 09:28
  • 1
    Sadly this doesn't work if the ListBox has `VirtualizingStackPanel.IsVirtualizing="True"` =[. However, the accepted-answer **does**. If `.IsVirtualizing="False"`, which is default, this answer works fine. – newfurniturey Mar 20 '13 at 18:18
  • Yes. Works like Charm Indeed!. I struggled a lot with my virtualized list view with a database. As I had sorting and searching enabled, I was really struggling to find ways that will not load all items from database. I had to implement logic which would tell me at what index would be my item in ListView after applying filters and at What row actually is the item in database. Now It works :). Nice one! – VinayChoudhary99 May 06 '18 at 15:42
12

I think the problem here is that the ListViewItem is not created yet if the line is not visible. WPF creates the Visible on demand.

So in this case you probably get null for the item, do you? (According to your comment, you do)

I have found a link on MSDN forums that suggest accessing the Scrollviewer directly in order to scroll. To me the solution presented there looks very much like a hack, but you can decide for yourself.

Here is the code snippet from the link above:

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);
akjoshi
  • 15,374
  • 13
  • 103
  • 121
EFrank
  • 1,880
  • 1
  • 25
  • 33
  • Tried the code, it works. Not nice, but an answer. I'd like to clean up my question to reflect the real problem, if you want to include the code (links might change) I'd mark you as answer. – Sam Oct 17 '08 at 13:30
  • Since it works I marked you as answer anyway - but I still think it would be nice if the code is copied here, in case the link changes. – Sam Oct 17 '08 at 13:44
5

I made some changes to Sam's answer. Note that I wanted to scroll to the last line. Unfortunately the ListView sometiems just displayed the last line (even when there were e.g. 100 lines above it), so this is how I fixed that:

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

Cheers

Joseph jun. Melettukunnel
  • 6,267
  • 20
  • 69
  • 90
3

One workaround to this is to change the ItemsPanel of the ListView. The default panel is the VirtualizingStackPanel which only creates the ListBoxItem the first time they become visible. If you don't have too many items in your list, it should not be a problem.

<ListView>
   ...
   <ListView.ItemsPanel>
      <ItemsPanelTemplate>
         <StackPanel/>
      </ItemsPanelTemplate>
   </ListView.ItemsPanel>
</ListView>
decasteljau
  • 7,655
  • 10
  • 41
  • 58
2

If you just want to show and focus the last item after creating a new data item, this method is maybe better. Compare to ScrollIntoView, ScrollToEnd of ScrollViewer is in my tests more reliable. In some tests using ScrollIntoView method of ListView like above failed and i don't know reason. But using ScrollViewer to scroll to last one can work.

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();
}
Willi
  • 187
  • 1
  • 1
  • 12
2

Try this

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

}
SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
Murali.S
  • 21
  • 1
  • This is the perfect answer, as you can keep virtualization and scroll to the specific row, not struggling with the original selection - even for listviews, which may have different item heights, like mine (expandable items). Thanks! – lunatix Oct 19 '13 at 13:56
  • Hi What's is Entity? – luka Jan 07 '19 at 09:30
2

Thanks for that last tip Sam. I had a dialog which opened, meaning my grid lost focus every time the dialog closed. I use this:

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();
}
Echilon
  • 10,064
  • 33
  • 131
  • 217
1

I just had the same issue with ItemContainerGenerator.ContainerFromItem() and ItemContainerGenerator.ContainerFromIndex() returning null for items that clearly existed in the listbox. Decasteljau was right but I had to do some digging to figure out exactly what he meant. Here is the breakdown to save the next guy/gal some legwork.

Long story short, ListBoxItems are destroyed if they are not within view. Consequently ContainerFromItem() and ContainerFromIndex() return null since the ListBoxItems do not exist. This is apparently a memory/performance saving feature detailed here: http://blogs.msdn.com/b/oren/archive/2010/11/08/wp7-silverlight-perf-demo-1-virtualizingstackpanel-vs-stackpanel-as-a-listbox-itemspanel.aspx

The empty <ListBox.ItemsPanel> code is what turns off the virtualization. Sample code that fixed the issue for me:

Data template:

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

Main body:

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

To overcome the virtualisation issue but still use ScrollIntoView and not hacking around in the guts of the ListView, you could also use your ViewModel objects to determine what is selected. Assuming that you have ViewModel objects in your list that feature an IsSelected property. You'd link the items to the ListView in XAML like this:

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

Then, the code-behind method can scroll to the first selected item with this:

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

This also works if the selected item is well out of view. In my experiment, the PersonsListView.SelectedItem property was null, but of course your ViewModel IsSelected property is always there. Be sure to call this method after all binding and loading has completed (with the right DispatcherPriority).

Using the ViewCommand pattern, your ViewModel code could look like this:

PersonVMs.ForEach(vm => vm.IsSelected = false);
PersonVMs.Add(newPersonVM);
newPersonVM.IsSelected = true;
ViewCommandManager.InvokeLoaded("ScrollToSelectedPerson");
ygoe
  • 18,655
  • 23
  • 113
  • 210
0

In my project i need to display the selected index line from the listview to the user so i assigned the selected item to ListView control. This code will scroll the scrollbar and display the selected item.

BooleanListView.ScrollIntoView(BooleanListView.SelectedItem);

OR

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

Jeyavel
  • 2,974
  • 10
  • 38
  • 48
0

not sure if this is the way to go but this currently works for me using WPF, MVVM Light, and .NET 3.5

I added the SelectionChanged event for ListBox called "lbPossibleError_SelectionChanged" I added the SelectionChanged event for ListBox

then behind this "lbPossibleError_SelectionChanged" event, here's the code enter image description here

works as it should for me.

Jayson Ragasa
  • 1,011
  • 4
  • 19
  • 33