Sunday, May 1, 2011

Selecting first item in WPF ListView and keyboard navigation

I have been playing with WPF ListView recently and I’ve met its following not quite
comfortable behavior – when ListView is focused and I’m trying to navigate inside it using keyboard (in my case it was down arrow) I had to press down key twice to move to the second item and then once for each next item. My goal was to fix that twice pressing (and also enable correct navigation when some other item is selected, not the first one). I thought that it can be archieved by some simple property like SelectedIndex, but despite setting it to necessary value it still didn’t work.

Lets define some simple list view with custom template:
<ListView Name="list">
  <ListView.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding FirstName}"></TextBlock>
        <TextBlock Text="{Binding LastName}" Margin="10,0,0,0"></TextBlock>
      </StackPanel>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

And lets define some simple item for this list view:

class Item
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
}



And add some items on initialization:

Item[] items = new Item[]
{
  new Item{ FirstName = "F1", LastName="L1" },
  new Item{ FirstName = "F2", LastName="L2" },
  new Item{ FirstName = "F3", LastName="L3" }
};

list.ItemsSource = items;
list.Focus();
list.SelectedIndex = 0;


Now we are ready. If we run this application we will get something like this:

image

And if we press down key selected item won’t be changed, like this:

image

If you are very attentative you could notice that focus was initially on the whole control and now it became on single list view item (look at the border around control on the first picture and around item on the second). This gives us first clue – we need to set focus on first item in the ListView instead of whole ListView.

Google provides following solution: list.ItemContainerGenerator.ContainerFromIndex( int ) will give us ListViewItem for our control. But if you try to use this immediately after setting ItemsSource it will return null. Does this method work at all? Yes, it works. However, trick here is to call it at appropriate moment (actual items are generated in the background and are not available immediately after setting ItemsSource). But how to find out which moment is appropriate? Everything becomes easy after attaching to the StatusChanged event of the ItemContainerGenerator and the code looks like this:

list.ItemContainerGenerator.StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged);


and event handler:


private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
  if (list.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
  {
    int index = list.SelectedIndex;

    if (index >= 0)
    {
      ListViewItem item = list.ItemContainerGenerator.ContainerFromIndex(index) as ListViewItem;

      if (item != null)
        item.Focus();
    }
  }
}


After this everything works fine. First item is selected and you can move down to the second item by simply pressing down key.

5 comments:

  1. This bug has been annoying me for ages. Thank you for providing the solution.

    ReplyDelete
  2. Thank you so much for this. It was driving me nuts too!

    ReplyDelete
  3. ditto: Not as simple as you would think. Thanks

    ReplyDelete
  4. Thanks, Cytivrat. This helps me out a lot.

    ReplyDelete