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);
}
}
CodePudding user response:
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:
Have your UserControl handle its own
CollectionView, and add a method to configure your filter (take aPredicate<object>as parameter and set that to the Filter property).Have your UserControl expose a method which returns a
CollectionViewfor 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.The most "MVVM" way to do it would be to add a
DependencyPropertyto your UserControl, with aPropertyChangedCallbackthat 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);
}
CodePudding user response:
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.
