When working with WPF, it is easier to do data first via
Data Binding (WPF)[
^]. This way the view and data are separated giving a window into the data. The benefit is that you're separating the data, and you can then choose how that data is seen by the user.
John is partially correct, the
MVVM Design Pattern[
^] will better help separate the data from the view. This solution will use MVVM, however there is a far more efficient WPF technique built into the .Net Framework to do what you want.
When working with Collections of data, and filtering, sorting, etc is used on in-memory collections, the best WPF method is to use the
CollectionViewSource[
^] and bind to the resulting view into you data.
Below is a sample of how to use the
CollectionViewSource
with the
DataGrid
and a
ComboBox
to select the filter type.
First, we need to implement wrappers for the
INotifyPropertyChanged Interface[
^] used to communicating changes in DataBinding.
public abstract class ObservableBase : INotifyPropertyChanged
{
public void Set<TValue>(ref TValue field, TValue newValue, [CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<TValue>.Default.Equals(field, default(TValue)) && field.Equals(newValue)) return;
field = newValue;
RaisePropertyChanged(propertyName);
}
public void RaisePropertyChanged (string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public event PropertyChangedEventHandler PropertyChanged;
}
public abstract class ViewModelBase : ObservableBase
{
public bool IsInDesignMode
=> (bool) DesignerProperties.IsInDesignModeProperty
.GetMetadata(typeof(DependencyObject))
.DefaultValue;
}
Next we need a class to hold instance data for each item in our collection:
public class PersonModel : ObservableBase
{
private string name;
public string Name
{
get => name;
set => Set(ref name, value);
}
private decimal paid;
public decimal Paid
{
get => paid;
set => Set(ref paid, value);
}
private decimal owing;
public decimal Owing
{
get => owing;
set => Set(ref owing, value);
}
}
Now we can create our ViewModel used to bind the data to the view:
class MainViewModel : ViewModelBase
{
public MainViewModel() => Mock();
private Random rand = new Random();
public ObservableCollection<PersonModel> People { get; }
= new ObservableCollection<PersonModel>();
private CollectionViewSource CSV { get; set; }
public ICollectionView PeopleView { get; private set; }
public List<string> PaymentStates { get; }
= new List<string> {"All", "Not Paid", "Partially Paid", "Paid in Full"};
private string selectedState;
public string SelectedState
{
get => selectedState;
set
{
Set(ref selectedState, value);
RefreshFilter();
}
}
private void Mock()
{
for (int i = 0; i < 500; i++)
{
var owed = rand.Next(100, 500);
var paidState = rand.Next(1, 100);
People.Add(new PersonModel
{
Name = $"Person {i}",
Owing = owed,
Paid = paidState > 0 && paidState < 33
? 0
: paidState > 32 && paidState < 66
? rand.Next(5, owed / 10) * 10
: owed
});
}
selectedState = PaymentStates[0];
CSV = new CollectionViewSource {Source = People};
PeopleView = CSV.View;
RefreshFilter();
}
private void RefreshFilter()
{
Func<PersonModel, bool> filterType;
switch (PaymentStates.IndexOf(selectedState))
{
case 1:
filterType = x => x.Paid == 0;
break;
case 2:
filterType = x => x.Paid > 0 && x.Paid < x.Owing;
break;
case 3:
filterType = x => x.Paid >= x.Owing;
break;
default:
filterType = _ => true;
break;
}
PeopleView.Filter = item => filterType(item as PersonModel);
PeopleView.Refresh();
}
}
Now we can finally implement our View:
<Window x:Class="FilteredDataGrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FilteredDataGrid"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="Raw Data"/>
<DataGrid Grid.Row="1"
ItemsSource="{Binding People}"/>
<StackPanel Grid.Column="1"
Orientation="Horizontal">
<Label Content="Filter By:"/>
<ComboBox ItemsSource="{Binding PaymentStates}"
SelectedItem="{Binding SelectedState}"
Width="100" Margin="10 0 0 0"/>
</StackPanel>
<DataGrid Grid.Row="1" Grid.Column="1"
x:Name="FilterResults"
ItemsSource="{Binding PeopleView}"/>
</Grid>
</Window>
Changing the selection in the
Combobox
will change the filter applied and the results
DataGrid
will relect the applied filter.
Note: in the
RefreshFilter()
of the
MainViewModel
I'm using local functions to predefine the filter and not putting the complete filter in the
PeopleView.Filter
. Like any looping code, put as little inside the loop as possible to maximise the performance.
There should be enough information here to apply to your own project to implement the type of filtering that you want to achieve.