40

We want to set the SelectedItem of a ListBox programmatically and want that item to then have focus so the arrow keys work relative to that selected item. Seems simple enough.

The problem however is if the ListBox already has keyboard focus when setting SelectedItem programmatically, while it does properly update the IsSelected property on the ListBoxItem, it doesn't set keyboard focus to it, and thus, the arrow keys move relative to the previously-focused item in the list and not the newly-selected item as one would expect.

This is very confusing to the user as it makes the selection appear to jump around when using the keyboard as it snaps back to where it was before the programmatic selection took place.

Note: As I said, this only happens if you programmatically set the SelectedItem property on a ListBox that already has keyboard focus itself. If it doesn't (or if it does but you leave, then come right back), when the keyboard focus returns to the ListBox, the correct item will now have the keyboard focus as expected.

Here's some sample code showing this problem. To demo this, run the code, use the mouse to select 'Seven' in the list (thus putting the focus on the ListBox), then click the 'Test' button to programmatically select the fourth item. Finally, tap the 'Alt' key on your keyboard to reveal the focus rect. You will see it's still on 'Seven', not 'Four' as you may expect, and because of that, if you use the up and down arrows, they are relative row 'Seven', not 'Four' as well, further confusing the user since what they are visually seeing and what is actually focused are not in sync.

Important: Note that I have Focusable set to false on the button. If I didn't, when you clicked it, it would gain focus and the ListBox would lose it, masking the issue because again, when a ListBox regains focus, it properly focuses the selected ListBoxItem. The issue is when a ListBox already has focus and you programmatically select a ListBoxItem.

XAML file:

<Window x:Class="Test.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="525" Height="350" WindowStartupLocation="CenterScreen"
    Title="MainWindow" x:Name="Root">

    <DockPanel>

        <Button Content="Test"
            DockPanel.Dock="Bottom"
            HorizontalAlignment="Left"
            Focusable="False"
            Click="Button_Click" />

        <ListBox x:Name="MainListBox" />

    </DockPanel>

</Window>

Code-behind:

using System.Collections.ObjectModel;
using System.Windows;

namespace Test
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            MainListBox.ItemsSource = new string[]{
                "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight"
            };

        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            MainListBox.SelectedItem = MainListBox.Items[3];
        }

    }

}

Note: Some have suggested to use IsSynchronizedWithCurrentItem, but that property synchronizes the SelectedItem of the ListBox with the Current property of the associated view. It isn't related to focus as this problem still exists.

Our work-around is to temporarily set the focus somewhere else, then set the selected item, then set the focus back to the ListBox but this has the undesireable effect of us having to make our ViewModel aware of the ListBox itself, then perform logic depending on whether or not it has the focus, etc. (i.e. you wouldn't want to simply say 'Focus elsewhere then come back here, if 'here' didn't have the focus already as you'd steal it from somewhere else.) Plus, you can't simply handle this through declarative bindings. Needless to say this is ugly.

Then again, 'ugly' ships, so there's that.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286

5 Answers5

63

It's a couple lines of code. If you didn't want it in code-behind, I sure it could be packaged in a attached behaviour.

private void Button_Click(object sender, RoutedEventArgs e)
{
    MainListBox.SelectedItem = MainListBox.Items[3];
    MainListBox.UpdateLayout(); // Pre-generates item containers 

    var listBoxItem = (ListBoxItem) MainListBox
        .ItemContainerGenerator
        .ContainerFromItem(MainListBox.SelectedItem);

    listBoxItem.Focus();
}
Rob
  • 5,223
  • 5
  • 41
  • 62
jeff
  • 3,269
  • 3
  • 28
  • 45
  • 4
    ContainerFromItem will return Null if no container has yet been generated for it, which is the case of a virtualized list and the item is off-screen. Plus if you attempt to set the value from a binding it breaks down as you don't have access to the ListBox, nor should you. (Continued below...) – Mark A. Donohoe May 05 '12 at 17:48
  • Even an attached behavior poses issues as you wouldn't want the control to blindly set keyboard focus to the ListBoxItem unless a) the control already had focus (easy to test), and b) the change came from the code-behind (not easy to test without a subclass.) Otherwise you could inadvertently steal the focus from another control (case a) or mess up the focus in the current one (case b) when in multi-select mode and unselecting a row, which normally should retain the keyboard focus, but instead would be set to the new SelectedItem, if any, causing odd behaviors. – Mark A. Donohoe May 05 '12 at 17:52
  • Actually, on second thought, I think I have the work-around for case 'B' above. You do create the attached behavior like you said, but you don't set it directly in XAML. Instead you create a property on your ViewModel and bind the behavior to that. That way you don't need to know (or care) who's listening. Then, just before you set the selected item from code behind, you enable the behavior, select the item, then disable the behavior again. This addresses 'B' above. (You'd still need 'A' of course.) Voting yours as the answer since, although not complete, did lead me down this path. – Mark A. Donohoe May 05 '12 at 18:02
  • 5
    I've edited to `UpdateLayout()` before calling `ContainerFromItem()`, which should pre-generate item containers and avoid unexpected null results. – Rob Jan 16 '14 at 21:45
  • If you use listBox.ScrollIntoView(listBox.SelectedItem) than ListboxItem.Focus() will be better and no need to use ContainerFromItem(); – Richard Aguirre Oct 29 '16 at 04:34
  • @RichardAguirre, the ScrollIntoView is definitely promising, but how are you supposed to get the ListBoxItem without using ContainerFromItem? That's where you lost me. – Mark A. Donohoe Jan 06 '17 at 04:53
2

Maybe with an attached behavior? Something like

public static DependencyProperty FocusWhenSelectedProperty = DependencyProperty.RegisterAttached(
            "FocusWhenSelected", 
            typeof(bool), 
            typeof(FocusWhenSelectedBehavior), 
            new PropertyMetadata(false, new PropertyChangedCallback(OnFocusWhenSelectedChanged)));

private static void OnFocusWhenSelectedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var i = (ListBoxItem)obj;
        if ((bool)args.NewValue)
           i.Selected += i_Selected;
        else
           i.Selected -= i_Selected;
    }

static void i_Selected(object sender, RoutedEventArgs e)
{
    ((ListBoxItem)sender).Focus();
}

and in xaml

       <Style TargetType="ListBoxItem">
            <Setter Property="local:FocusWhenSelectedBehavior.FocusWhenSelected" Value="True"/>
        </Style>
Dtex
  • 2,593
  • 1
  • 16
  • 17
  • 1
    This was where I was originally heading, but the issue is those items don't exist yet if you have a virtualized list, so that attached behavior hasn't been wired up yet to respond. You still have to somehow first do the ScrollIntoView method. Secondly, if you strictly respond to IsSelected, things can go a little crazy when in multi-select mode, but then again, that could be considered desired behavior. The virtualization (which is on by default) is the real killer. – Mark A. Donohoe May 04 '12 at 14:33
1

You need only use ListBox.SelectedItem and then use ListBox.ScrollIntoView(listBox.SelectedItem)

Example code:

        private void textBox2_TextChanged(object sender, TextChangedEventArgs e)
    {

        var comparision = StringComparison.InvariantCultureIgnoreCase;
        string myString = textBox2.Text;
        List<dynamic> index = listBox.Items.SourceCollection.OfType<dynamic>().Where(x=>x.Nombre.StartsWith(myString,comparision)).ToList();
        if (index.Count > 0) { 
        listBox.SelectedItem= index.First();


            listBox.ScrollIntoView(listBox.SelectedItem);


        }

    }
Richard Aguirre
  • 543
  • 6
  • 14
0

In your XAML you tried this and didn't work?

<ListBox IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Path=YourCollectionView}" SelectedItem="{Binding SelectedItem}"></ListBox>

And the SelectedItem Property:

    private YourObject _SelectedItem;
    public YourObject SelectedItem
    {
        get
        {
            return _SelectedItem;
        }
        set
        {
            if (_SelectedItem == value)
                return;

            _SelectedItem = value;

            OnPropertyChanged("SelectedItem");
        }
    }

Now in your code you can do:

SelectedItem = theItemYouWant;

To me this approach works always.

Dummy01
  • 1,985
  • 1
  • 16
  • 21
  • I'm going to call you on that. Have you tried it? Load your listbox with say, 100 items. Click the 50th item with the mouse. Then add a button on your form that programmatically sets the `SelectedItem` to the 10th item. The selection does change, but a) that selected item doesn't scroll into view, because b) it isn't focused. You can see that by tapping the 'Alt' key and you'll still see the item you clicked still has the keyboard focus. – Mark A. Donohoe May 04 '12 at 14:26
  • By the way... just to be sure, you have to physically select the other row first using the mouse or the keyboard. – Mark A. Donohoe May 04 '12 at 14:46
  • @MarquelV I am probably confused on what you want. I thought you want to set the selected row programaticaly. The selected row is the focused row. You have to use the ScrollIntoView to move there. And yes, my approach doesn't work for multiple selection. – Dummy01 May 05 '12 at 04:55
  • 1
    Your statement 'The selected row is the focused row' is actually incorrect and is the problem that I'm trying to address. Even in your code above, if you follow the steps in my comments, you'll see this. Also note: The current item is the first-selected item, not the focused item. That's what I think you're getting confused over. I've added a code snippet to show this. – Mark A. Donohoe May 05 '12 at 06:40
0

First) You must find selected items in listbox with ListBox.Items.IndexOf().
Second) Now add items with ListBox.SelectedItems.Add().

This is my code :

DataRow[] drWidgetItem = dtItemPrice.Select(widgetItemsFilter);
lbxWidgetItem.SelectedItems.Clear(); foreach(DataRow drvItem in
drWidgetItem)
lbxWidgetItem.SelectedItems.Add(lbxWidgetItem.Items[dtItemPrice.Rows.IndexOf(drvItem)]);

If you want to select an item in ListBox you can use this way :
ListBox.SelectedItem = (Your ListBoxItem);

If you want to select some items in ListBox you must use this way :
ListBox.SelectedItems.Add(Your ListBoxItem);

Amintabar
  • 2,198
  • 1
  • 29
  • 29
  • I'm not 100% sure, but I think you may have misunderstood my question. Selection isn't the issue. Focus is. Your code does select the items, but it doesn't set focus, which is what I'm trying to resolve. – Mark A. Donohoe Aug 29 '13 at 22:31
  • Sorry! So, i think if you focus your control after that your item must focus. (sorry for my bad English) – Amintabar Sep 02 '13 at 05:09
  • But if the control already has focus, you can't re-set it. You'd have to focus something else first. – Mark A. Donohoe Sep 02 '13 at 05:12