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;
});
}
}