2

I can't find the way to programmatically set focused item in WPF ListView. I can only find variations of Selected Item | Items | Index | Value, but 'Focused' item is not directly related to 'selected' item - focused item can be not-selected (e.g. when unselecting current item with Ctrl+Click).

To be short - I'd like to get following behvaiour from the below provided code (it fills dummy list view with dummy 8 items and on pressing X tries to focus 2nd item from the end):

Wanted behaviour:

  • using mouse - select 2nd item
  • press X - this focuses 2nd item from the end
  • press 'Down' array on keyboard - this should move current selection to the last item

What happens actually:

  • using mouse - select 2nd item
  • press X - this selects 2nd item from the end, but focus remains on the 2nd item from start
  • press 'Down' array on keyboard - this should move current selection to the last item, but 3rd item is selected instead.

Note: plain Win32 API (which is, of course, completely different thing from WPF) has LVM_SETSELECTIONMARK message for this. I could not find analogue in WPF. Does it exist?

Sample XAML:

<Window x:Class="WpfListviewTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="350" Width="525">
  <ListView x:Name="List1" KeyDown="List1_KeyDown">
    <ListView.View>
      <GridView>
        <GridViewColumn Width="140" Header="Column 1" />
        <GridViewColumn Width="140" Header="Column 2" />
        <GridViewColumn Width="140" Header="Column 3" />
      </GridView>
    </ListView.View>
    <sys:DateTime>1/2/3</sys:DateTime>
    <sys:DateTime>4/5/6</sys:DateTime>
    <sys:DateTime>7/8/9</sys:DateTime>
    <sys:DateTime>10/11/12</sys:DateTime>
    <sys:DateTime>1/2/3</sys:DateTime>
    <sys:DateTime>4/5/6</sys:DateTime>
    <sys:DateTime>7/8/9</sys:DateTime>
    <sys:DateTime>10/11/12</sys:DateTime>
  </ListView>
</Window>

Sample code-behind:

public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();
    }

    private void List1_KeyDown(object sender, KeyEventArgs e) {
        if( e.Key == Key.X ) {
            List1.SelectionMode = SelectionMode.Single;
            List1.SelectedIndex = List1.Items.Count - 2;
        }
    }
}
Xtra Coder
  • 3,389
  • 4
  • 40
  • 59

2 Answers2

2

Thanks to links in Mic's answer I've got more useful information and found workable solution for my case.

Some background information:

  • Unlike other implementations of ListView control (in WinForms or Win32), the WPF's version of ListView does not have something like FocusedItem. It seems that MS decided to focus items in listview using generic interface of UIElement, which every visual ListViewItem is decendant of. This causes my use-case to become much more complicated in WPF.

  • Visual items in WPF listview can be obtained via ListView.ItemContainerGenerator.ContainerFromIndex(index) as ListViewItem and this item has Focus() method which does needed function. However in virtualized listview this does not work for items outside of currently visible area - they are not created yet and method returns null :(

  • That is why, for the first, it is needed to make item to be focused visible. This can be done via ScrollViewer within ListView using its ScrollToVerticalOffset() method (you'll need to know index of the item to scroll to - can be done using ListView.Items.IndexOf()).

  • Next, unortunatelly listview items are not created immediatelly after programmatic scroll is done - that is why actual focusing can be done some time later, when newly visible items are created. I've found appropriate event for this - ListView.LayoutUpdated.

  • Side nodes:

    • there is ListView.ScrollIntoView(object item) method which seems to be a 'native path' to get the same, but it does not work reliably when list contains comparably equal items in different possitions. ListView does not have something like ListView.ScrollIndexIntoView(int index).

    • it seems most of the staff above is available in single method VirtualizingStackPanel.BringIndexIntoView(), but MS decided to make it protected and therefore not accessible from outside ...

Here is working solution

Sample XAML:

<Window x:Class="WpfListviewTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Height="150" Width="525">
  <ListView x:Name="List1" KeyDown="List1_KeyDown" LayoutUpdated="List1_LayoutUpdated">
    <ListView.View>
      <GridView>
        <GridViewColumn Width="140" Header="Column 1" />
        <GridViewColumn Width="140" Header="Column 2" />
        <GridViewColumn Width="140" Header="Column 3" />
      </GridView>
    </ListView.View>
    <sys:DateTime>1/2/3</sys:DateTime>
    <sys:DateTime>4/5/6</sys:DateTime>
    <sys:DateTime>7/8/9</sys:DateTime>
    <sys:DateTime>10/11/12</sys:DateTime>
    <sys:DateTime>1/2/3</sys:DateTime>
    <sys:DateTime>4/5/6</sys:DateTime>
    <sys:DateTime>7/8/9</sys:DateTime>
    <sys:DateTime>10/11/12</sys:DateTime>
  </ListView>
</Window>

Sample code-behind:

public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();
    }

    private int? _indexToFocus;

    private void List1_KeyDown(object sender, KeyEventArgs e) {
        switch( e.Key ) {
            case Key.S:
                FocusItemByIndex(1); break;
            case Key.X:
                FocusItemByIndex(List1.Items.Count - 2); break;
            case Key.Z:
                List1.ScrollIntoView(List1.Items[List1.Items.Count - 2]); break;
        }
    }

    public void FocusItemByIndex(int index) {
        ScrollViewer sv = FindChild<ScrollViewer>(List1);
        double firstVisible = sv.VerticalOffset;
        double lastVisible = firstVisible + sv.ViewportHeight;

        if( index > lastVisible ) {
            double topVisible = index - sv.ViewportHeight + 1;
            sv.ScrollToVerticalOffset(topVisible);
        }
        else if( index < firstVisible ) {
            sv.ScrollToVerticalOffset(index);
        }

        _indexToFocus = index;
    }

    public static T FindChild<T>(DependencyObject parent, string name = null) 
        where T : DependencyObject 
    {
        if( parent == null )
            return null;

        int cChildren = VisualTreeHelper.GetChildrenCount(parent);
        T result = null;

        for( int i = 0; (result == null) && (i < cChildren); i++ ) {
            DependencyObject child = VisualTreeHelper.GetChild(parent, i);
            T tChild = child as T;

            if( tChild != null ) {
                if( name == null ) {
                    result = (T)child;
                }
                else {
                    FrameworkElement feChild = child as FrameworkElement;

                    if( feChild != null && feChild.Name == name )
                        result = (T)child;
                }
            }

            if( result == null )
                result = FindChild<T>(child, name);
        }

        return result;
    }

    private void List1_LayoutUpdated(object sender, EventArgs e) {
        if( _indexToFocus != null ) {
            ItemContainerGenerator lvItems = List1.ItemContainerGenerator;
            ListViewItem lvitemToFocus = lvItems.ContainerFromIndex(_indexToFocus.Value) as ListViewItem;

            if( lvitemToFocus != null ) {
                lvitemToFocus.Focus();
                _indexToFocus = null;
            }
        }
    }  
}
Xtra Coder
  • 3,389
  • 4
  • 40
  • 59
1

Looks like you have to do that through your code-behind, and look / modify the IsFocused property. You can find more information in this blog post.

You can also take a look at this SO post which explains exactly what you need.

Community
  • 1
  • 1
Qortex
  • 7,087
  • 3
  • 42
  • 59