0

I have a listview that can be filtered using a textbox:

<TextBox TextChanged="txtFilter_TextChanged" Name="FilterLv"/>

In the view code-behind I do the following:

    CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(this.lv.ItemsSource);
    view.Filter = UserFilter;

    private bool UserFilter(object item)
    {
        if (String.IsNullOrEmpty(FilterLv.Text))
            return true;
        else
        {
            DataModel m = (item as DataModel);
            bool result = (m.Name.IndexOf(Filter.Text, StringComparison.OrdinalIgnoreCase) >= 0 ||
                         //m.Surname.IndexOf(Filter.Text, StringComparison.OrdinalIgnoreCase) >= 0);

            return result;
        }
    }

    private void Filter_TextChanged(object sender, TextChangedEventArgs e)
    {
        CollectionViewSource.GetDefaultView(this.lv.ItemsSource).Refresh();
    }

Now I have placed a label in the view and I would like this label to show the number of items currently displayed in the listview.

How can I do it? I have found things like this but I don't understand at all what is RowViewModelsCollectionView. In this link it is suggested to bind as below:

<Label Content="{Binding ModelView.RowViewModelsCollectionView.Count}"/>

Could anyone explain me or provide a very little and simple example on how to do it?

FINAL UPDATE:

View model:

public class TestViewModel
{
// lv is populated later in code
public ObservableCollection<DataModel> lv = new ObservableCollection<DataModel>();

    public ObservableCollection<DataModel> LV
    {
        get
        {
            return this.lv;
        }

        private set
        {
            this.lv= value;
            OnPropertyChanged("LV");
        }
    }

private CollectionView view;

public TestViewModel()
{
        this.view = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
        view.Filter = UserFilter;
}

private string textFilter;
public string TextFilter
{
        get
        {
            return this.textFilter;
        }

        set
        {
            this.textFilter= value;
            OnPropertyChanged("TextFilter");

            if (String.IsNullOrEmpty(value))
                this.view.Filter = null;
            else
                this.view.Filter = UserFilter;
        }
}

private bool UserFilter(object item)
{
    if (String.IsNullOrEmpty(this.TextFilter))
        return true;
    else
    {
        DataModel m = (item as DataModel);
        bool result = (m.Name.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
                     //m.Surname.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0);

        return result;
    }
}



    /// <summary>
    /// Número de registros en la listview.
    /// </summary>
    public int NumberOfRecords
    {
        get
        {
            return this.view.Count;
        }
    }
}

View (xaml):

 <!-- search textbox - filter -->
 <TextBox TextChanged="txtFilter_TextChanged"
          Text="{Binding TextFilter,  UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">

 <!-- label to show the number of records -->
<Label Content="{Binding NumberOfRecords}"/>

view code-behind (xaml.cs):

    private void txtFilter_TextChanged(object sender, TextChangedEventArgs e)
    {          
        CollectionViewSource.GetDefaultView((DataContext as TestViewModel).LV).Refresh();
    }

It is filtering ok when I type in the search textbox and listview is updated correctly but the number of records is always 0.

What am i doing wrong?

ATTEMPT2: Below another attempt not working. If I attach my listivew to the View declared in model view then no items are shown. If I attach listview to LV in model view then items are shown, and when I filter through my search textbox it filters ok, listview is updated but the number of rows shown in the listview always remains to 0.

Notes:

  • I am using NET 3.5 Visual Studio 2008.
  • I need to set View as writable in model view because I do not set it in view model constructor, instead i set it in LoadData method in view model. LoadData is called from view code-behind constructor.

View Model:

namespace MyTest.Example
{
public Class TestViewModel : INotifyPropertyChanged // Implementations not here to simplify the code here.
{
private ObservableCollection<DataModel> lv;
public ObservableCollection<DataModel> LV
{
     get
     {
         return this.lv;
     }

     private set
     {
         this.lv = value;
         OnPropertyChanged("LV");
     }
}

public CollectionView View { get; set; }

public TestViewModel()
{
     this.LV = new ObservableCollection<DataModel>();
            // this.View = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
            // this.View.Filter = UserFilter;
}

private string textFilter = string.Empty;
public string TextFilter
{
      get
      {
           return this.textFilter ;
      }

      set
      {
           this.textFilter = value;
           OnPropertyChanged("TextFilter");

           this.View.Refresh();
      }
}

private bool UserFilter(object item)
{
    if (String.IsNullOrEmpty(this.TextFilter))
        return true;
    else
    {
        DataModel m = (item as DataModel);
        bool result = (m.Name.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
                     //m.Surname.IndexOf(this.TextFilter, StringComparison.OrdinalIgnoreCase) >= 0);

        return result;
    }
}

public void LoadData()
{
    this.LV = LoadDataFromDB();
    this.View = (CollectionView)CollectionViewSource.GetDefaultView(this.LV);
    this.View.Filter = UserFilter;
}
} // End Class
} // End namespace

View code-behing (xaml.cs):

namespace MyTest.Example
{
  public Class TestView
  {
       public TestView()
       {
            InitializeComponent();
            (DataContext as TestViewModel).LoadData();
       }       
  }
}

View (xaml):

xmlns:vm="clr-namespace:MyTest.Example"

 <!-- search textbox - filter -->
 <TextBox Text="{Binding Path=TextFilter,  UpdateSourceTrigger=PropertyChanged}">

 <!-- label to show the number of records -->
<Label Content="{Binding Path=View.Count}" ContentStringFormat="No. Results: {0}"/>

<ListView Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Path=View}" SelectionMode="Extended" AlternationCount="2">

ATTEMPT 3: Finally I have get it to work. Solution is the same as ATTEMPT2 but making below changes:

I have replaced this:

public CollectionView View { get; set; }

by this one:

    private CollectionView view;
    public CollectionView View {
        get
        {
            return this.view;
        }

        private set
        {
            if (this.view == value)
            {
                return;
            }

            this.view = value;
            OnPropertyChanged("View");
        }
    }

All the rest remains the same as in ATTEMPT2. In view View.Count and assigning View as ItemsSource to my listview now is working all perfectly.

Willy
  • 9,848
  • 22
  • 141
  • 284
  • This would be significantly easier if you were *actually* using the MVVM pattern, as your title says. Just bind your `TextBox` to `CollectionViewSource.Count`. I'd first recommend fitting it to the actual MVVM pattern, or if you don't want to go that route, update the `TextBox` from `Count` on the local `CollectionViewSource`. – Bradley Uffner Jul 27 '17 at 12:54
  • I am using MVVM but I am totally lost. What is collectionViewSource? I am new in WPF. My listview is attached to an observablecollection. – Willy Jul 27 '17 at 13:00
  • The very first line of your posted code uses the default `CollectionView` from `CollectionViewSource`, that's what I'm referring to. – Bradley Uffner Jul 27 '17 at 13:01
  • Sorry, `Count` is a property of `CollectionView` not `CollectionViewSource`. Your `view` variable is `CollectionView` though, so you can get it from there. – Bradley Uffner Jul 27 '17 at 13:05
  • @BradleyUffner but 'view' is in the view code-behind, not in the view model. So how to bind it to the label in the view in xaml? – Willy Jul 27 '17 at 13:08
  • That's the problem, you won't be able to bind it because of where it is declared. In MVVM, this should be declared in XAML, or be part of the viewmodel, exposed as a property. You could easily bind to it in those situations. – Bradley Uffner Jul 27 '17 at 13:09
  • ok, and how to declare it in the viewmodel? If in the viewmodel I have the observable collection, let's say, 'lvItems', that is attached to the listview, how to declare it? in this way?: CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(this.lvItems); – Willy Jul 27 '17 at 13:13
  • I've added an answer with an example from one of the projects I've worked on where I do it from XAML. To do it in the viewmodel, just declare a readonly property of type `ICollectionView`, set it equal to `CollectionViewSource.GetDefaultView(SomeObservableCollection)`, and bind everything to that. – Bradley Uffner Jul 27 '17 at 13:20
  • In your updated example, you are returning a new `CollectionView` every time the property is requested, and it isn't linked to the one created in your original code that actually has the filters applied to it. You should use a backingfield for the property, and set it in the constructor (or the where ever you set the actual `ObservableCollection`. You have to make sure you apply the filters and sorting to *that* instance. If you actually modify the instance that `lv` points to, you need to update the `CollectionView` too. – Bradley Uffner Jul 27 '17 at 13:40
  • As a general rule, you should really only be creating a new `ObservableCollection` once (via constructor or initializer). If you need to change its contents call `Clear` on it, and repopulate it. Don't just replace the variable with a new instance. That will make bindings go crazy, and make a lot of extra work for you keeping everything in sync. – Bradley Uffner Jul 27 '17 at 13:43
  • @BradleyUffner see my last final update, now always using the same instance of view but not working. What am i doing wrong? – Willy Jul 27 '17 at 14:48
  • When I have some time later today, I will create a small, complete, sample application for you. I have a bunch of meetings coming up at the moment. – Bradley Uffner Jul 27 '17 at 14:51
  • First quick thing that I notice though, is that you never raise `PropertyChanged` for your `NumberOfRecords` property. You don't quite have everything set up for it to automatically flow through, so you will have to raise it manually anytime your filter changes. – Bradley Uffner Jul 27 '17 at 14:56
  • @BradleyUffner ok, if you later could indicate me how to change my code in order to it automatically flow through i will highly appreciate you. – Willy Jul 27 '17 at 15:09

3 Answers3

4

You should use

<Label Content="{Binding ModelView.Count}"/>

instead of

<Label Content="{Binding ModelView.RowViewModelsCollectionView.Count}"/>

RowViewModelsCollectionView in the other question is the same as ModelView is in your case.

Edit

Count is a property from the CollectionView

For further information have a look at the MSDN

Edit 2

When you dont want to do it via XAML like in my example you have to implement INotifyPropertyChanged and raise this whenever the bound property is changed because otherwiese the UI won't get the change. In your case: you have to call OnPropertyChanged("NumberOfRecords"); in your filter method. But it would be easier to do it via xaml like i Wrote earlier.

Mighty Badaboom
  • 6,067
  • 5
  • 34
  • 51
  • but what is Count? an integer property in my view model? if so, and what value I assign to count? could you elaborate a little more the answer? – Willy Jul 27 '17 at 12:57
  • 1
    It is a property of the `CollectionView` that comes from `CollectionViewSource`. It really should be directly in XAML, or on the viewmodel, not code-behind. That's what is making it tricky for you. – Bradley Uffner Jul 27 '17 at 12:59
  • @user1624552 like Bradley said it's a property from `CollectionView`. I added this information in my answer – Mighty Badaboom Jul 27 '17 at 13:04
  • I was mistaken, it's on `CollectionView`, not `CollectionViewSource`, but your `view` variable is the correct type. – Bradley Uffner Jul 27 '17 at 13:05
  • @BradleyUffner same here, changed it :) – Mighty Badaboom Jul 27 '17 at 13:05
  • @MightyBadaboom see my final update. also don't work. what am i doing wrong? – Willy Jul 27 '17 at 14:49
1

Here is a fully working example with the CollectionView in the view model, and the filter count automatically flowing to the bound control. It uses my mvvm library for the base ViewModel class to supply INotifyPropertyChanged, but you should easily be able to substitute your own system, I'm not doing anything special with it.

The full source code can be downloaded from here

application screenshot

XAML:

<Window
    x:Class="FilterWithBindableCount.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:FilterWithBindableCount"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="525"
    Height="350"
    d:DataContext="{d:DesignInstance local:MainWindowVm}"
    mc:Ignorable="d">
    <Grid Margin="4">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Label
            Grid.Row="0"
            Grid.Column="0"
            Margin="4">
            Filter:
        </Label>
        <TextBox
            Grid.Row="0"
            Grid.Column="1"
            Margin="4"
            VerticalAlignment="Center"
            Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />
        <TextBlock
            Grid.Row="1"
            Grid.Column="0"
            Grid.ColumnSpan="2"
            Margin="4"
            Text="{Binding Path=PeopleView.Count, StringFormat={}Count: {0}}" />
        <DataGrid
            Grid.Row="3"
            Grid.Column="0"
            Grid.ColumnSpan="2"
            Margin="4"
            CanUserAddRows="False"
            CanUserSortColumns="True"
            ItemsSource="{Binding Path=PeopleView}" />
    </Grid>
</Window>

View models:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using AgentOctal.WpfLib;

namespace FilterWithBindableCount
{
    class MainWindowVm : ViewModel
    {
        public MainWindowVm()
        {    
            People = new ObservableCollection<PersonVm>();
            PeopleView = (CollectionView) CollectionViewSource.GetDefaultView(People);
            PeopleView.Filter = obj =>
            {
                var person = (PersonVm)obj;
                return person.FirstName.ToUpper().Contains(FilterText.ToUpper() ) || person.LastName.ToUpper().Contains(FilterText.ToUpper());
            };

            People.Add(new PersonVm() { FirstName = "Bradley", LastName = "Uffner" });
            People.Add(new PersonVm() { FirstName = "Fred", LastName = "Flintstone" });
            People.Add(new PersonVm() { FirstName = "Arnold", LastName = "Rimmer" });
            People.Add(new PersonVm() { FirstName = "Jean-Luc", LastName = "Picard" });
            People.Add(new PersonVm() { FirstName = "Poppa", LastName = "Smurf" });    
        }
        public ObservableCollection<PersonVm> People { get; }
        public CollectionView PeopleView { get; }

        private string _filterText = "";
        public string FilterText
        {
            get => _filterText;
            set
            {
                if (SetValue(ref _filterText, value))
                {
                    PeopleView.Refresh();
                }
            }
        }    
    }

    class PersonVm:ViewModel
    {
        private string _firstName;
        public string FirstName
        {
            get {return _firstName;}
            set {SetValue(ref _firstName, value);}
        }

        private string _lastName;
        public string LastName
        {
            get {return _lastName;}
            set {SetValue(ref _lastName, value);}
        }
    }
}
Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
  • I have tried to adapt it to my code but no working. see my last update ATTEMPT 2. – Willy Jul 28 '17 at 07:40
  • Seems like I have solved it, see my last update Attempt3. Now I am testing it, and seems it works. If I see that this continues working after done some more tests, I will back here and say it. And I will vote this answer since you have helped my a lot and this example has been a great, simple, readable example. This guide me to make my code working. If otherwise some of my test fails I will back as well to explain the problem appeared if any. – Willy Jul 28 '17 at 08:17
0

This is actually significantly easier when properly following MVVM. The CollectionView is either declared in the XAML, or as a property in the viewmodel. This allows you to bind directly to CollectionView.Count.

Here is an example of how to place the CollectionViewSource in XAML from one of my apps:

<UserControl
    x:Class="ChronoPall.App.TimeEntryList.TimeEntryListView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:app="clr-namespace:ChronoPall.App"
    xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:ChronoPall.App.TimeEntryList"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DataContext="{d:DesignInstance local:TimeEntryListViewVm}"
    d:DesignHeight="300"
    d:DesignWidth="300"
    mc:Ignorable="d">
    <UserControl.Resources>
        <CollectionViewSource x:Key="TimeEntriesSource" Source="{Binding Path=TimeEntries}">
            <CollectionViewSource.SortDescriptions>
                <componentModel:SortDescription Direction="Descending" PropertyName="StartTime.Date" />
                <componentModel:SortDescription Direction="Ascending" PropertyName="StartTime" />
            </CollectionViewSource.SortDescriptions>
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="EntryDate" />
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </UserControl.Resources>
    <Grid IsSharedSizeScope="True">
        <ScrollViewer VerticalScrollBarVisibility="Auto">
            <ItemsControl ItemsSource="{Binding Source={StaticResource TimeEntriesSource}}">
                <ItemsControl.GroupStyle>
                    <GroupStyle>
                        <GroupStyle.HeaderTemplate>
                            <DataTemplate DataType="{x:Type CollectionViewGroup}">
                                <local:TimeEntryListDayGroup />
                            </DataTemplate>
                        </GroupStyle.HeaderTemplate>
                    </GroupStyle>
                </ItemsControl.GroupStyle>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local:TimeEntryListItem />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</UserControl>

It doesn't actually bind to Count, but it could easily do that with:

<TextBlock Text="{Binding Path=Count, Source={StaticResource TimeEntriesSource}}/>

To do it in the viewmodel, you would just create a readonly property of ICollectionView, and set it equal to CollectionViewSource.GetDefaultView(SomeObservableCollection‌​), then bind to that.

Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
  • I do not understand at all all this xaml code.... I do not know how to adapt it to my listivew. Anyway, how could I declare this from code in view model? – Willy Jul 27 '17 at 13:24
  • Ok, `` declares a `CollectionView` as a resource of the `UserControl` in XAML, think of it like a variable named `TimeEntriesSource`. That `CollectionViewSource` is bound an `ObservableCollection` named `TimeEntries` from the viewmodel. `` is an `ItemsControl` that is bound to the `CollectionViewSource` resource in the XAML. Any filtering or grouping applied the the `CollectionViewSource`... – Bradley Uffner Jul 27 '17 at 13:27
  • ...will be reflected in the `ItemsControl`. Any changes to the `ObservableCollection` will flow through the `CollectionViewSource`, and in to the `ItemsControl` also. – Bradley Uffner Jul 27 '17 at 13:29
  • It this example, I am grouping instead of sorting, but it is the exact same process. If you want to filter based off of something dynamically, you can bind the filter properties on the `CollectionViewSource` to something in your viewmodel. – Bradley Uffner Jul 27 '17 at 13:30