This project is read-only.

WPF ListBox not so good at deselection?

Jul 29, 2011 at 10:02 PM
Edited Jul 29, 2011 at 10:16 PM

I'm seeing a strange phenomenon in my program and I wonder if you have ever heard of it. You may recall that I have a program that maintains two kinds of objects, named (for the sake of argument) "Apples" and "Bananas". In my program I have a ListBox for each type of object. Now I'm adding a new feature in which apples are linked 1:1 to bananas, and when you select an Apple, the corresponding Banana is selected automatically.

I implemented this with a special "SmartSelect" property which is bound to the ListBox:

<ListBox.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="IsSelected" Value="{Binding SmartSelect}"/>

When the SmartSelect setter is called, it sets the selection state of not just this object, but the linked object also. This works fine in WinForms, and it works in WPF too as long as the listbox doesn't have a scroll bar. However, if not all objects fit on the screen, something odd happens. See, normally if I click a Banana when another Banana is was selected earlier, then the first Banana is deselected. But suppose I click an Apple while the corresponding Banana is off-screen. The Banana is selected fine; however, if I click another Banana while the first Banana is still off-screen, then the first Banana is not deselected as it should be; instead it stays selected.

The ListBox seems to have a memory of which objects are selected, and when you click an item, the ListBox only deselects other items that it "knows about". Because the Banana was selected without the knowledge of the ListBox, ListBox does not become aware that it is selected unless it is scrolled onto the screen. Therefore, selection behavior malfunctions if the currently selected object is off-screen.

I am not sure if UpdateControls has anything to do with the problem. Do you know how selection logic works in WPF, Mike? I'm not sure what I should do to fix this.

Aug 2, 2011 at 1:26 PM

With a multi-select list box, WPF should be setting IsSelected to false on all of the selected items when you select another one without holding Ctrl. I'm not sure why it would have anything to do with the item being on screen. If this were a virtual list box, then maybe.

But to solve the problem the Update Controls way, I would change the way that SmartSelect works. Instead of setting multiple properties in the setter, set only what the user intends. Do all of your aggregation in the getter.

For example, since apples and bananas are linked 1:1, choose only one of them to represent selection. Say you choose bananas. When you set SmartSelect on an apple, it should only set the selection property on the banana. Apple wouldn't have a selection property. In the SmartSelect getter, both apples and bananas observe the selection property of the banana.

When you reduce the number of Independents in your model, you eliminate cases in which things can be out of sync.

I don't know if this will help in this particular case, but that's what I would try.

Aug 2, 2011 at 6:32 PM
Edited Aug 2, 2011 at 9:45 PM

Well, they aren't totally linked 1:1. Some Apples and some Bananas are loners, not linked to anything. Plus, it actually is possible to select an apple without selecting the banana it is linked to (or vice versa) in a special popup window GUI mode ... I suppose there's no need to explain the details. 

Since I don't actually know what's going wrong, it's possible that what you suggest might help, but it seems unlikely because the problem seems to be that ListBox is not aware that an object becomes selected if that object is off-screen. I expect that would remain true whether it is a Dependent or Independent sentry announcing that the object has become selected.

Actually, I think I might solve this problem by having two different kinds of selected objects, "really" selected and merely "visually" selected. My UI shows a big map which is more important than the listboxes... I could visually select the object and the linked object on the map, but not bother to select the linked object in the listbox, thus bypassing the bug (albeit losing a feature in the process). But if the user clicks an Apple and then an unrelated Banana, I'll just have to figure out how to ensure that only one pair of objects is visually selected, not two pairs.... multiple selection is allowed btw, which makes this trickier because I can't just govern everything by a single "selected object" property.

It's too bad I don't have a clue how to debug problems in WPF.

Update: I used your idea of "aggregation in the getter" (but also aggregation in the setter, in that if SmartSelect is set to false, the linked object is deselected also; otherwise SmartSelect would not actually become false when set to false). My idea, meanwhile, doesn't quite solve the problem because the user can select any object on the map, and it will be selected in the corresponding ListBox, but if it's selected off-screen in the ListBox, then it won't be deselected when the user clicks something else in the ListBox. Oops. Hmm.... we'll see if the customer notices :O

I could solve this by completely replacing the flawed selection logic of ListBox... if I knew how. Or when selecting anything on the map, I could scroll it into view in the ListBox, hopefully forcing the ListBox to come to its senses.

Aug 17, 2011 at 7:02 PM
Edited Aug 17, 2011 at 7:02 PM

Now I have a new problem--it looks related to the first problem, but sort of "in reverse".

In a different part of my program I have a ListBox that supports multiple selection. The IsSelected property of each item is bound to IsSelected on the ViewModel, and above the listbox there is a TextBlock to tell the user how many items are selected:

<TextBlock Text="{Binding ListHeading}"/>
<ListBox ItemsSource="{Binding ...}" 
         SelectionMode="Extended" MinHeight="30">
    <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
        </Style>
    </ListBox.ItemContainerStyle>
    ....
</ListBox>
public string ListHeading
{
	get { return string.Format("Items: ({0} selected)", 
		_vehicleVMs.Where(v => v.IsSelected).Count()); }
}

Now, this list has over 1000 items in it. But suppose I click the top item and press Shift+End. In theory this should select all the items, but instead, the ListHeading reports that there are "28 selected"! (given its size, only 11-12 items fit on the ListBox, so I don't know where exactly 28 comes from.) As I scroll up the list, the number of items selected increases one at a time; after scrolling through the whole list, all items are reported as selected. Then if I click a single item in the middle of the list, the number selected should drop to 1, but instead it only drops by 37; then if I scroll through the list again, the number eventually drops to 1.

My interpretation is that the ListBox only maintains complete state information for the items that are on the screen, plus a few items above and below the visible items. The state information includes the binding to IsSelected in the viewmodel. Although the ListBox keeps track of which items it has selected, it doesn't bother to maintain bindings for all items and therefore the viewmodel is not informed.

This is more than a cosmetic problem because the user is asked to select a set of items about which to make a remote query. If I am not aware that an item is selected then I won't include it in the query. Hmmm... assuming it's not an UpdateControls thing, I suppose I could formulate a StackOverflow question about this...

Aug 17, 2011 at 10:46 PM
Edited Aug 17, 2011 at 10:49 PM

All righty, I confirmed this is not an issue with UC, wrote my question and got an answer:

http://stackoverflow.com/questions/7097999/wpf-binding-to-listboxitem-isselected-doesnt-work-for-off-screen-items

The fix is VirtualizingStackPanel.IsVirtualizing="False" (add it as an attribute of ListBox). However, this can make WPF very slow and WPF uses an obscene amount of memory without virtualization: almost 8 KB for each little 4-character plain-text ListBoxItem. I have had a bad feeling about WPF performance for a while now, and this result is even worse than I feared! And my test code was just plain WPF, no UpdateControls (well, for convenience, I actually stuck the test code in my UC PresentationModel, but ForView.Wrap stops at something that implements INotifyPropertyChanged, right?)

Maybe I should look into ListBox events and whether I could hook selection event(s) in order to update IsSelected in the viewmodel by hand. Oh, wait... I'd have to do the reverse communication too: detecting when ItemViewModel.IsSelected changes and somehow informing the ListBox about it. This might end up being quite a pain in the butt.

Aug 21, 2011 at 3:27 PM

There should be a way to do this without turning off virtualization. Here's what I think is going on.

The virtualizing list box probably creates ListBoxItems that fit on screen, and those just off screen. When you are at the top of the list, it creates the 11 ListBoxItems that you can see, and the 3 just below that you can't. Total of 14.

When you hit Shift + End, it selects those 14 ListBoxItems, and then switches the page to the bottom. It therefore loads the last 11 ListBoxItems and the 3 just above it, for a total of 28.

If a ListBoxItem is not created for the view model object in the backing list, it can't set the IsSelected property on the view model. But the ListBox knows that the item should be selected. You can test this by scrolling through the list. As items come into view, they should appear selected.

So I believe that ListBox is keeping a sparse list of selected items. It probably knows the first and last selected index (to quickly support the Shift-style range selection), and a list of exceptions (items in the range that should not be selected, and vice-versa, to support the Ctrl-style one-by-one selection). When it creates a ListBoxItem, it applies that information.

The trick is to get at that sparse list of selected items. I don't know if that's possible. But if you can, you should be able to use that information to get the desired behavior.

As an altenative, if we could figure out how to make Update Controls bind the SelectedItems collection, that might work. But right now, Update Controls only supports read-only collections.

(BTW, yes Update Controls stops binding at INPC, but you still have the overhead of 1000 Independents. You just don't have the Dependent and any linkage that might have occurred.)

Sep 22, 2011 at 6:06 PM
Edited Sep 22, 2011 at 6:10 PM

If you look at the StackOverflow question, I didn't use Update Controls at all (besides, I already know Independents require under 100 bytes... let's see, I only see a single field in that class, _firstDependent) the WPF "engineers" really deserve all the blame for using 8KB per item and for the associated performance problems. Scandalous!

I've ignored this problem for now; I've been working on C++ code lately, and we're shipping a preview of the UC product with the IsVirtualizing="False".