MisterGray
MisterGray

Reputation: 339

C# WPF MVVM CollectionView Filter - Apply to Sub-ViewModels

I'm building a WPF/MVVM application that displays some lists below each other. My MainViewModel contains in addition to the lists a textbox, whose text content I want to use as a filter for my lists. However, these lists are not in the MainViewModel, but in sub-controls (UserControl2_*).

If the filter property is in the same ViewModel as the ICollectionView, then filtering works (see CollectionViewFilter in ViewModel2.cs), but I don't understand how to apply a filter to multiple Sub-ViewModels.

Is there an MVVM compliant method to pass the filter through to the sub-controls? Or do I need to pass the collections up so that I can access them from the ViewModel, where the filter property is also set?

If there is any more code you want me to upload or adapt, let me know and I will edit my question.

 ___________________      ___________________   
|Search:            |    |Search: FG         |  
|___________________|    |___________________|  
|Collection         |    |Collection         |  
| _________________ |    | _________________ |  
||UserControl1_1   ||    ||UserControl1_1   ||  
|| _______________ ||    || _______________ ||  
|||UserControl2_1 |||    |||UserControl2_1 |||  
|||* ABCDEF       |||    |||* BCDEFG       |||  
|||* BCDEFG       |||    |||* CDEFGH       |||  
|||* CDEFGH       |||    |||_______________|||  
|||_______________|||    ||_________________||  
|| _______________ ||    | _________________ |  
|||UserControl2_2 |||    ||UserControl1_2   ||  
|||* ABCDEF       |||    || _______________ ||  
|||* UVWXYZ       ||| => |||UserControl2_3 |||  
|||_______________|||    |||* BCDEFG       |||  
| _________________ |    |||_______________|||  
||UserControl1_2   ||    || _______________ ||  
|| _______________ ||    |||UserControl2_4 |||  
|||UserControl2_3 |||    |||* CDEFGH       |||  
|||* LMNOPQ       |||    |||_______________|||  
|||* BCDEFG       |||    ||_________________||  
|||* UVWXYZ       |||    |___________________|  
|||_______________|||                           
|| _______________ ||                           
|||UserControl2_4 |||                           
|||* ABCDEF       |||                           
|||* CDEFGH       |||                           
|||_______________|||                           
||_________________||                           
|___________________|                           

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new CollectionViewModel();
    }
}

MainViewModel.xaml

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <TextBox Grid.Row="0" Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
    <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
        <ItemsControl ItemsSource="{Binding ViewModels1}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <local:UserControl1 />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>
</Grid>

CollectionViewModel.cs

public class CollectionViewModel : ObservableObject
{
    public ObservableCollection<ViewModel1> ViewModels1 { get; set; }
    // ...
}

ViewModel1.cs

public class ViewModel1 : ObservableObject
{
    public ObservableCollection<ViewModel2> ViewModels2 { get; set; }
    // ...
}

ViewModel2.cs

public class ViewModel2 : ObservableObject
{
    private readonly ObservableCollection<ViewModel3> ViewModels3 { get; set; }
    public ICollectionView CollectionView3 { get; }

    private string _filter = string.Empty;
    public string Filter
    {
        get => _filter;
        set => SetProperty(ref _filter, value);
    }

    public ViewModel2(Model2 model2)
    {
        model = model2;
    
        ViewModels3 = new ObservableCollection<ViewModel3>();

        CollectionView3 = CollectionViewSource.GetDefaultView(ViewModels3);
        CollectionView3.Filter = CollectionViewFilter;
    }
    
    private bool CollectionViewFilter(object obj)
    {
        if (obj is ViewModel3 viewModel)
        {
            return viewModel.Name.Contains(Filter, StringComparison.InvariantCultureIgnoreCase);
        }
        return true;
    }
    // ...
}

ViewModel3.cs

public class ViewModel3 : ObservableObject
{   
    private Model3 _model;
    public ViewModel3(Model3 model3)
    {
        _model = model3;
    }

    public string Name
    {
        get => _model.Name;
        set => SetProperty(_model.Name, value, _model, (model, name) => model.Name = name);
    }
}

+++

Solution based on mm8's answer:

I have extended my CollectionViewModel.cs as follows:

private string filterText = string.Empty;
public string FilterText
{
    get => filterText;
    set
    {
        SetProperty(ref filterText, value);
        foreach(var vm1 in ViewModels1)
        {
            foreach(var vm2 in vm1.ViewModels2)
            {
                vm2.Filter = value;
            }
        }
    }
}

In ViewModel2 the refresh for the CollectionView was missing:

public string Filter
{
    get => _filter;
    set
    {
        SetProperty(ref _filter, value);
        CollectionView3.Refresh();
    }
}

+++

Solution based on mm8's second advice (using a messenger):

Since I am working with Microsoft.Toolkit.MVVM, I used IMessenger interface. For this I added the class FilterTextChangedMessage.cs:

public class FilterTextChangedMessage : ValueChangedMessage<string>
{
    public FilterTextChangedMessage(string value) : base(value)
    {
    }
}

I changed the FilterText property of the CollectionViewModel.cs as follows:

public string FilterText
{
    get => filterText;
    set
    {
        SetProperty(ref filterText, value);
        WeakReferenceMessenger.Default.Send(new FilterTextChangedMessage(value));
    }
}

I changed the ViewModel2.cs as follows:

public class ViewModel2 : ObservableRecipient, IRecipient<FilterTextChangedMessage> {
    public ViewModel2(Model2 model)
    {
        WeakReferenceMessenger.Default.Register<FilterTextChangedMessage>(this, (r, m) => {
            Filter = m.Value as string;
        });
    }
}

Upvotes: 2

Views: 1043

Answers (2)

mm8
mm8

Reputation: 169270

Since the CollectionViewModel, where the FilterText property is defined, already has a strong reference to the child view models via the ViewModels1 property, you could set the filter of these by iterating through this source collection.

This is perfectly fine as far as MVVM is concerned. The application logics stays in the view model where it belongs.

Keeping a strong reference form one view model type to another does however introduce a coupling that tend to make the application harder to maintain. But that's another issue that already comes with the ViewModels1 property.

This kind of issue is typically solved by using a messenger or event aggregator for communicating between different view models. In this case, you would then send some kind of filter event from the CollectionViewModel and handle this event in the child view models.

Upvotes: 2

cboittin
cboittin

Reputation: 391

You'll have to link the CollectionViews and the filter method at some point, so your UserControl needs to allow that. I can think of three ways to do it:

  1. Have your UserControl handle its own CollectionView, and add a method to configure your filter (take a Predicate<object> as parameter and set that to the Filter property).

  2. Have your UserControl expose a method which returns a CollectionView for your list, and do everything in the MainWindow. Less pretty, but still probably alright, and avoids creating unnecessary stuff when you just want to display a list without any option.

  3. The most "MVVM" way to do it would be to add a DependencyProperty to your UserControl, with a PropertyChangedCallback that updates the filtering of objects. Then you can bind to a property of your mainwindow's viewmodel, and update that property based on your text field. Here's an example from some code i have :

public static readonly DependencyProperty BlurRadiusProperty =
    DependencyProperty.Register("BlurRadius", typeof(double), typeof(FastShadow),
    new FrameworkPropertyMetadata(
        0.0,
        FrameworkPropertyMetadataOptions.AffectsRender,
        new PropertyChangedCallback((o, e) =>
        {
            var f = (FastShadow)o;
            if ((double)e.NewValue < 0)
                f.BlurRadius = 0;
            f.CalculateGradientStops();
        })));

public double BlurRadius
{
    get => (double)this.GetValue(BlurRadiusProperty);
    set => this.SetValue(BlurRadiusProperty, value);
}

Upvotes: 0

Related Questions