5
votes

In a WPF application using MVVM I query a database to get an ObservableCollection of clients, create an ICollectionView and apply a filter function.

On my usercontrol I bind the text used for the filter to a textbox and the ICollectionView to a Listbox.

The ICollectionView initally contains 1104 clients (just ClientID and ClientName).

Retrieving the data from the database is very quick. However, the listbox takes about 4 seconds to populate.

As I enter text into the filter, if the number of clients to return is low then the listbox redraws relatively quickly. However, if I clear out the textbox then it's another 4 seconds to redraw.

Am I missing something, or is my code not very well written.

Thanks for any advice/help.

View:

<UserControl x:Class="ClientReports.Module.SchemeSelection.Views.Clients"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:ClientReports.Module.SchemeSelection.Views"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="True" >
    <Grid>
        <StackPanel>
            <TextBox  materialDesign:HintAssist.Hint="Client Search"
                      Style="{StaticResource MaterialDesignFloatingHintTextBox}"
                      Text="{Binding Search, UpdateSourceTrigger=PropertyChanged}"/>
            <ListBox ItemsSource="{Binding ClientsFiltered}" DisplayMemberPath="ClientName" />
        </StackPanel>
    </Grid>
</UserControl>

ViewModel:

using ClientReports.Common.Infrastructure.Models;
using ClientReports.Common.Infrastructure.Services;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows.Data;

namespace ClientReports.Module.SchemeSelection.ViewModels
{
    public class ClientsViewModel : BindableBase
    {
        private IClientService clientService;

        public ClientsViewModel(){ }

        public ClientsViewModel(IClientService clientService)
        {
            this.clientService = clientService;
            Clients = new ObservableCollection<Client>();
            GetClients().ContinueWith(x => { });
        }

        public ObservableCollection<Client> Clients { get; }

        public ICollectionView ClientsFiltered { get; set; }

        private string clientFilter;

        public string Search
        {
            get => clientFilter;
            set
            {
                clientFilter = value;
                ClientsFiltered.Refresh();
                RaisePropertyChanged("ClientsFiltered");
            }
        }

        private bool Filter(Client client)
        {
            return Search == null
                || client.ClientName.IndexOf(Search, StringComparison.OrdinalIgnoreCase) != -1;
        }


        private async Task GetClients()
        {
            var clients = await clientService.GetAllAsync();
            foreach (var client in clients)
            {
                Clients.Add(client);
            }
            ClientsFiltered = CollectionViewSource.GetDefaultView(Clients);
            ClientsFiltered.Filter = new Predicate<object>(c => Filter(c as Client));
        }
    }
}
1
I stumbled across this line in your constructor: GetClients().ContinueWith(x => { });. Just don't do that. Read this instead: msdn.microsoft.com/en-us/magazine/dn605875.aspxChristianMurschall
Thanks @ChristianMurschall. I've taken that on board and rewritten the code based on the article.richarp
@ChristianMurschall That's not helpful. No explanation to back up your statement, and the linked article makes zero references to ContinueWith.DonBoitnott
Hi @richarp since you're using CollectionView and filtering you might be interested in "When you set the Filter, SortDescriptions, or GroupDescriptions property; a refresh occurs. You do not have to call the Refresh method immediately after you set one of those properties. For information about how to delay automatic refresh, see DeferRefresh. " Source: [link] docs.microsoft.com/en-us/dotnet/api/… [/link]pjrki
@DonBoitnott I agree that my comment is lacking explanation. But since the async operation in the constructor is not the OPs problem I thought the link might be 'hindfull' enough. BTW, his code is explained in the linked article in Figure 3 isn't it?ChristianMurschall

1 Answers

4
votes

ListBox likely takes 4 seconds to populate because virtualization is not enabled, so WPF has to create 1104 ListBoxItems (and recreate them when the filter is cleared). By default virtualization is enabled for the ListBox but you can occasionally disable it without even realizing it. In your sample, your ListBox is located in the vertical StackPanel, and it is likely the reason for this behavior. You can try to rewrite the XAML in the following way:

<UserControl x:Class="ClientReports.Module.SchemeSelection.Views.Clients"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:ClientReports.Module.SchemeSelection.Views"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="True" >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

            <TextBox  materialDesign:HintAssist.Hint="Client Search"
                      Style="{StaticResource MaterialDesignFloatingHintTextBox}"
                      Text="{Binding Search, UpdateSourceTrigger=PropertyChanged}"
                      Grid.Row="0"/>
            <ListBox  ItemsSource="{Binding ClientsFiltered}"  
                      DisplayMemberPath="ClientName"
                      Grid.Row="1" />            
    </Grid>
</UserControl>

If it does not help you can try to set the fixed height for your ListBox and check it again.

If it does not help either, please check Microsoft documentation on virtualization for other possible reasons: https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/optimizing-performance-controls