3

Here is my code (it searches a WPF ListView for all matches and then selects all of them):

            public bool FindAll(LogFilter filter, bool matchCase)
            {
                lastLogFilter = filter;
                lastMatchCase = matchCase;
                MatchSearcher quickSearchSearcher = new MatchSearcher(filter, !matchCase);
                bool foundOnce = false;
                Stopwatch watch = new Stopwatch();
                watch.Start();
                var query = from x in listView.Items.Cast<LogRecord>() where quickSearchSearcher.IsMatch(x, false) select x;
                watch.Stop();
                Console.WriteLine("Elapsed milliseconds to search: {0}.", watch.ElapsedMilliseconds);
                if (query.Count() > 0)
                {
                    foundOnce = true;
                    listView.SelectedItems.Clear();
                    watch.Restart();
                    foreach (LogRecord record in query)
                    {
                        listView.SelectedItems.Add(record);
                    }
                    watch.Stop();
                    Console.WriteLine("Elapsed milliseconds to select: {0}.", watch.ElapsedMilliseconds);
                    listView.ScrollIntoView(query.First());
                }
                return foundOnce;
            }

Here are the results with 10,000 ListView items:

Elapsed milliseconds to search: 0.
Elapsed milliseconds to select: 36385.

So, clearly my problem is with the loop:

foreach (LogRecord record in query)
{
    listView.SelectedItems.Add(record);
}

I feel like there must be a better way to add to the selected items list, or at least block data template updates (or something like that) on the list until all selected items have been set. Is there any way to get better performance when trying to select multiple items programmatically in a WPF ListView?

Alexandru
  • 12,264
  • 17
  • 113
  • 208
  • 1
    alright let me try to explain it better: your code is wrong. You need to delete it all and start all over. Use DataBinding as opposed to a procedural approach in WPF. It improves performance and makes your code cleaner and much easier to maintain – Federico Berasategui Feb 21 '14 at 17:28
  • @HighCore I'm sorry man, but I don't think you understood the problem...you definitely don't see the bigger picture here because I haven't described it but I think you'd be a fool to go about it from the binding collection...case and point: How would you get the collection to contain pre-selected elements after you data bind? LOL... – Alexandru Feb 21 '14 at 17:34
  • the problem at hand is that you are probably breaking UI virtualization by using the wrong approach. You will never get WPF to work this way. See [**MSDN**](http://msdn.microsoft.com/en-us/library/cc716879(v=vs.110).aspx): *Unfortunately, you can disable UI virtualization for these controls without realizing it. The following is a list of conditions that disable UI virtualization.* - *...Item containers are added directly to the ItemsControl. For example, if an application explicitly adds ListBoxItem objects to a ListBox, the ListBox does not virtualize the ListBoxItem objects.* – Federico Berasategui Feb 21 '14 at 17:37
  • @HighCore I'm not convinced this is the case. How can I tell if UI virtualization is being broken? – Alexandru Feb 21 '14 at 18:01
  • @HighCore Check Clemens' answer. I think you owe me an apology, and it also seems you've become the son. Clearly, adding to SelectedItems is NOT breaking virtualization...its just that, a simple O(1) operation is becoming an O(n) operation, as I suspected. Clemens' answer showed us how to bring it back to an O(1) operation... – Alexandru Feb 21 '14 at 18:09
  • 1
    @HighCore Even if OP would programatically add items, that wouldn't automatically mean that he adds ListViewItems. You can easily add data items (apparently of type `LogRecord` here) by calling `Items.Add(new LogRecord(...))`. – Clemens Feb 21 '14 at 18:20
  • @HighCore Hey listen, next time don't be so rude and cruel. I wish you had kept your original post up top so people could see how mean and closed-minded you are. If I wanted to switch controls, I would just rebind a different object to my collection...but oh, wait, I don't need to, that's right...I decided to only use the ListView after running some performance tests on it. – Alexandru Feb 21 '14 at 18:21
  • @Clemens that's right – Federico Berasategui Feb 21 '14 at 18:22
  • @Alexandru I came here to help and you gave a rejective response. It's not my fault that you do not know how to use WPF properly. My point still stands you should manage this at the ViewModel level and not in code behind. – Federico Berasategui Feb 21 '14 at 18:23
  • @HighCore Although I don't approve of the comment war going on here, I agree that this kind of functionality should be in the view model. WPF is designed to work in an MVVM environment, and this isn't adhering to that. – Logarr Feb 21 '14 at 18:26
  • "The one who follows the crowd will usually go no further than the crowd. Those who walk alone are likely to find themselves in places no one has ever been before." - Albert Einstein. – Alexandru Feb 21 '14 at 18:42
  • @Alexandru *We can't solve problems by using the same kind of thinking we used when we created them."* - Albert Einstein. WPF was created to replace winforms. Don't use a winforms approach in WPF. I do apologize if I offended you. It wasn't my intention. I get very passionate about things easily. – Federico Berasategui Feb 21 '14 at 18:45
  • @HighCore I'm sorry for the way I reacted as well. I get very passionate about my work and my code. – Alexandru Feb 21 '14 at 18:51
  • 1
    haha that was a good comments read. Everyone's passionate. Wonder why I'm not so passionate :P – Viv Feb 21 '14 at 19:14
  • @Viv We deleted a lot of the good stuff, though. :D – Alexandru Feb 21 '14 at 19:22

3 Answers3

6

Instead of adding selected items one by one to the SelectedItems property, you may call the SetSelectedItems method. Unfortunately the method is protected, so you have to create a derived ListBox that makes it publicly available:

public class MyListView : ListView
{
    public void SelectItems(IEnumerable items)
    {
        SetSelectedItems(items);
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Thanks, this dropped the time down to under a second. :) – Alexandru Feb 21 '14 at 18:05
  • SetSelectedItems() is also very slow for many thousands of elements. Any ideas? – Lumo Jan 08 '19 at 16:01
  • 1
    While significantly faster than adding items individually, it still gets slow for large lists. I can see it calling `IList.Contains()` in the call stack, so it's probably still O(n^2). The MVVM answer doesn't work correctly with virtualization, and my attempts to hack around the issues haven't been successful, so programmatic selection of many items in a large list may simply not be practical with WPF ListView. (FWIW, you can also use reflection to call `SetSelectedItems` if you don't want to create a subclass just for this.) – fadden May 30 '19 at 00:38
3

Alright. You already accepted an answer to this question, But I wanted to show a different approach anyways:

enter image description here

XAML:

<Window x:Class="WpfApplication1.ListViewSearch"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ListViewSearch" Height="300" Width="300">
    <DockPanel>
        <DockPanel DockPanel.Dock="Left" Margin="2">
            <Button DockPanel.Dock="Bottom" Content="Find All" Margin="2" Click="FindAll_Click"/>

            <ListBox ItemsSource="{Binding Filters}"
                     SelectedItem="{Binding SelectedFilter}"
                     DisplayMemberPath="DisplayName"/>
        </DockPanel>

        <ListView ItemsSource="{Binding Items}">
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding FirstName}" Header="First Name"/>
                    <GridViewColumn DisplayMemberBinding="{Binding LastName}" Header="Last Name"/>
                </GridView>
            </ListView.View>

            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
                </Style>
            </ListView.ItemContainerStyle>
        </ListView>

    </DockPanel>
</Window>

Code Behind:

public partial class ListViewSearch : Window
{
    private ViewModel ViewModel;

    public ListViewSearch()
    {
        InitializeComponent();

        DataContext = ViewModel = new ViewModel();
    }

    private void FindAll_Click(object sender, RoutedEventArgs e)
    {
        ViewModel.Filter();
    }
}

ViewModel:

public class ViewModel
{
    public ViewModel()
    {
        Items = new ObservableCollection<DataItem>(RandomDataSource.GetRandomData());
        Filters = new ObservableCollection<DataFilter>();

        Filters.Add(new DataFilter()
        {
            DisplayName = "First Name starting with A",
            FilterExpression = x => x.FirstName.ToLower().StartsWith("a")
        });

        Filters.Add(new DataFilter()
        {
            DisplayName = "Last Name starting with E",
            FilterExpression = x => x.LastName.ToLower().StartsWith("e")
        });
    }

    public ObservableCollection<DataItem> Items { get; private set; }

    public DataFilter SelectedFilter { get; set; }

    public ObservableCollection<DataFilter> Filters { get; private set; }

    public void Filter()
    {
        if (SelectedFilter == null)
            return;

        foreach (var item in Items)
            item.IsSelected = SelectedFilter.FilterExpression(item);
    }
}

Data Item:

public class DataItem : INotifyPropertyChanged
{
    private bool _isSelected;

    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            _isSelected = value;
            OnPropertyChanged("IsSelected");
        }
    }

    public string LastName { get; set; }

    public string FirstName { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Data Filter:

public class DataFilter
{
    public Func<DataItem, bool> FilterExpression { get; set; }

    public string DisplayName { get; set; }
}

Random Data Source (just a bunch of boilerplate)

public static class RandomDataSource
{
    private static string TestData = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum";
    private static List<string> words;
    private static int maxword;
    private static Random random;

    public static List<DataItem> GetRandomData()
    {
        random = new Random();
        words = TestData.Split(' ').ToList();
        maxword = words.Count - 1;

        return Enumerable.Range(0, 10000)
                         .Select(x => GetRandomItem())
                         .ToList();
    }

    private static DataItem GetRandomItem()
    {
        return new DataItem()
        {
            LastName = words[random.Next(0, maxword)],
            FirstName = words[random.Next(0, maxword)],
        };
    }
}

This approach has the following advantages over a traditional code-behind approach:

  • It decouples the UI and the logic. You operate against your own defined classes instead of dealing with the (sometimes arcane and obscure) WPF object model.
  • Since your code does not actually depend on any specific UI element type, you may change the UI to a "3D rotating pink elephant" and it would still work. It enables much more customizability of the view without compromising any code or logic.
  • It is easily reusable (you can go as far as to create a SearchViewModel<T> and a DataFilter<T> and reuse these on many different entity types.
  • It is unit-testable.
Federico Berasategui
  • 43,562
  • 11
  • 100
  • 154
  • Thanks, that's a good write-up and also nice of you to have done. My UI is actually done programmatically, and that's not by choice. The only thing I would have had to change to do the search through your MVVM design pattern is to have added the IsSelected boolean to my LogRecord class and fire the changed event when setting it, since I've already set my style to bind to the IsSelected property of items and I've already bound the UI to the collection. Other than that, our code actually does look quite similar, and I suppose I could have selected all the items this way instead. – Alexandru Feb 21 '14 at 19:33
  • @Alexandru since I'm an super-freak-obsessed-fanatic of generics, I would have gone as far as to create a `Selectable` (like [this](http://stackoverflow.com/a/14971905/643085)) in order to avoid putting a View-related (IsSelected) property in the data class – Federico Berasategui Feb 21 '14 at 19:41
  • OMG...MVVM to the bone. I will say this about MVVM...there are two types of people in this world. People who like abstraction layers and people who don't. I personally hate them. MVVM is a giant abstraction layer between the UI and the code, which is fine if you like abstraction. But to me, it sometimes feels like I get to see a bigger picture when I have everything handy in a single layer. I don't even think I can give a good example of what I mean, or of the pro's and con's. Maybe someone else will come along who could describe what I'm trying to say a little more empirically. – Alexandru Feb 21 '14 at 19:51
  • I think it comes down to how your brain functions. Too much abstraction gives me headaches when I try to search through code, especially code that I haven't written. – Alexandru Feb 21 '14 at 19:53
  • @Alexandru that's right, I agree `partially` with that, the problem is that the WPF Visual Tree is a huge complex beast and it's horrible when you need to deal with `VisualTreeHelper.Whatever()` just to change a text in a TextBlock buried deep inside a `DataTemplate` inside the `ItemTemplate` of a `ListBox`. See what I mean? MVVM comes to resolve all that into simple properties and `INotifyPropertyChanged`. – Federico Berasategui Feb 21 '14 at 19:54
  • @Alexandru abstractions have helped me create a **lot** of generic, reusable code. I have abstractions over literally everything, and most of my code is based on generic classes. I don't see a problem when debugging that because at runtime you're actually working with concrete implementations of the thing rather than the `abstract` abstractions :P – Federico Berasategui Feb 21 '14 at 19:56
  • It is wrong to bring up the discussion about MVVM here. I like this solution, it is quite fast, I did it the same way in my code, but had this effect: after selecting the items I wasn't able to reset the selection as usual by clicking another item (without CTRL). You have to set the IsSelected Property of all items back to false. – BerndK Dec 31 '14 at 02:06
  • On a list with 540K items, adding all to `SelectedItems` took longer than I had patience for (minutes). Calling `myListView.SelectAll()` took ~700ms. Adding an `IsSelected` binding to my item style, and setting `IsSelected=true` in a loop, took 75 ms. Unfortunately, like @BerndK, I saw strange behavior with the standard multi-select actions, even without code-behind involvement. For example, shift-clicking a large range, scrolling, and single-clicking leaves some off-screen stuff visually selected (and SelectionChanged events fire when you scroll across them!). Disabling recycling didn't help. – fadden May 28 '19 at 21:56
  • Further research shows it's an issue with virtualization. Setting `VirtualizingStackPanel.IsVirtualizing="False"` eliminates the weird behavior, but by definition for this question we're working with large lists. Manually updating the `IsSelected` fields when `SelectionChanged` events arrive works around the problem. Nothing works well with `VirtualizationMode="Recycling"`, e.g. Ctrl+A selects a scattered set of items. Note that setting the `IsSelected` property doesn't cause a `SelectionChanged` event unless the item is on-screen. Test project: https://github.com/fadden/DisasmUiTest – fadden May 29 '19 at 00:14
1

There's a lot of information in comments, so I'm going to summarize:

  1. The way you're supposed to update the selection in a multi-select ListView is by modifying the SelectedItems property. There is no way to get or set by index in WPF. Everything operates by item, which means every selection operation requires finding an item in the list. For a large list, this can be slow.
  2. ListView defines a "bulk change" method, SetSelectedItems, that can be used to select multiple items with one call. It's declared protected, so you either need to sub-class ListView, or call it with reflection. Under the hood it still has to find the items in the list to mark them as selected, so while it's faster, it can still be very slow for large lists.
  3. An alternative approach is to move the IsSelected value into the item itself, and use data binding. This approach is very fast, but falls apart when UI virtualization is enabled. Since very large lists will almost always want to use virtualization, this is a non-starter. I haven't yet found a way around the issues.

Bottom line: there is no way to quickly select a large number of items in a WPF ListView.

Anyone who wants to experiment can use the "selection test" in the DisasmUiTest project as a starting point.

fadden
  • 51,356
  • 5
  • 116
  • 166