Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Arrow keys don't work after programmatically setting ListView.SelectedItem

I have a WPF ListView control, ItemsSource is set to an ICollectionView created this way:

var collectionView = 
  System.Windows.Data.CollectionViewSource.GetDefaultView(observableCollection);
this.listView1.ItemsSource = collectionView;

...where observableCollection is an ObservableCollection of a complex type. The ListView is configured to display, for each item, just one string property on the complex type.

The user can refresh the ListView, at which point my logic stores the "key string" for the currently selected item, re-populates the underlying observableCollection. The previous sort and filter is then applied to the collectionView. At this point I'd like to "re select" the item that had been selected before the request to refresh. The items in the observableCollection are new instances, so I compare the respective string properties and then just select one that matches. Like this:

private void SelectThisItem(string value)
{
    foreach (var item in collectionView) // for the ListView in question
    {
        var thing = item as MyComplexType;
        if (thing.StringProperty == value)
        {
            this.listView1.SelectedItem = thing;
            return;
        }
    }
}

This all works. If the 4th item is selected, and the user presses F5, then the list is reconstituted and then the item with the same string property as the previous 4th item gets selected. Sometimes this is the new 4th item, sometimes not, but it provides "least astonishment behavior".

The problem comes when the user subsequently uses arrow keys to navigate through the ListView. The first up or down arrow after a refresh causes the first item in the (new) listview to be selected, regardless of which item had been selected by the previous logic. Any further arrow keys work as expected.

Why is this happening?

This pretty clearly violates the "least astonishment" rule. How can I avoid it?


EDIT
Upon further search, this seems like the same anomaly described by the unanswered
WPF ListView arrow navigation and keystroke problem , except I provide more detail.

like image 526
Cheeso Avatar asked Sep 09 '11 15:09

Cheeso


2 Answers

It looks like this is due to a sort of known but not-well-described problematic behavior with ListView (and maybe some other WPF controls). It requires that an app call Focus() on the particular ListViewItem, after programmatically setting the SelectedItem.

But the SelectedItem itself is not a UIElement. It's an item of whatever you are displaying in the ListView, often a custom type. Therefore you cannot call this.listView1.SelectedItem.Focus(). That's not gonna work. You need to get the UIElement (or Control) that displays that particular item. There's a dark corner of the WPF interface called ItemContainerGenerator, which supposedly lets you get the control that displays a particular item in a ListView.

Something like this:

this.listView1.SelectedItem = thing;
// *** WILL NOT WORK!
((UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(thing)).Focus();

But there's also a second problem with that - it doesn't work right after setting the SelectedItem. ItemContainerGenerator.ContainerFromItem() always seems to return null. Elsewhere in the googlespace people have reported it as returning null with GroupStyle set. But it exhibited this behavior with me, without grouping.

ItemContainerGenerator.ContainerFromItem() is returning null for all objects being displayed in the list. Also ItemContainerGenerator.ContainerFromIndex() returns null for all indicies. What's necessary is to call those things only after the ListView has been rendered (or something).

I tried doing this directly via Dispatcher.BeginInvoke() but that does not work either.

At the suggestion of some other threads, I used Dispatcher.BeginInvoke() from within the StatusChanged event on the ItemContainerGenerator. Yeah, simple huh? (Not)

Here's what the code looks like.

MyComplexType current;

private void SelectThisItem(string value)
{
    foreach (var item in collectionView) // for the ListView in question
    {
        var thing = item as MyComplexType;
        if (thing.StringProperty == value)
        {
            this.listView1.ItemContainerGenerator.StatusChanged += icg_StatusChanged;
            this.listView1.SelectedItem = thing;
            current = thing;
            return;
        }
    }
}


void icg_StatusChanged(object sender, EventArgs e)
{
    if (this.listView1.ItemContainerGenerator.Status
        == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
    {
        this.listView1.ItemContainerGenerator.StatusChanged
            -= icg_StatusChanged;
        Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
                               new Action(()=> {
                                       var uielt = (UIElement)this.listView1.ItemContainerGenerator.ContainerFromItem(current);
                                       uielt.Focus();}));

    }
}

That's some ugly code. But, programmatically setting the SelectedItem this way allows subsequent arrow navigation to work in the ListView.

like image 173
Cheeso Avatar answered Sep 25 '22 00:09

Cheeso


It's possible to focus an item with BeginInvoke after finding it by specifying priority:

Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
    var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromIndex(existing) as ListBoxItem;
    lbi.Focus();
}));
like image 35
anvish Avatar answered Sep 25 '22 00:09

anvish