C# WPF MVVM CollectionView 过滤器 - 应用于子视图模型

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

我正在构建一个 WPF/MVVM 应用程序,它在彼此下方显示一些列表。 除了列表之外,我的 MainViewModel 还包含一个文本框,我想将其文本内容用作列表的过滤器。 但是,这些列表并不在MainViewModel中,而是在子控件(UserControl2_*)中。

如果过滤器 属性 与 ICollectionView 在同一个 ViewModel 中,则过滤有效(请参阅 ViewModel2.cs 中的 CollectionViewFilter),但我不不了解如何将过滤器应用于多个子视图模型。

是否有符合 MVVM 的方法将过滤器传递给子控件? 或者我是否需要向上传递集合以便我可以从 ViewModel 访问它们,其中还设置了过滤器 属性?

如果您希望我上传或改编更多代码,请告诉我,我将编辑我的问题。

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

+++

基于的解决方案:

我扩展了我的 CollectionViewModel.cs 如下:

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

在 ViewModel2 中,缺少 CollectionView 的刷新:

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

+++

解决方案基于(使用信使):

由于我使用的是 Microsoft.Toolkit.MVVM,因此我使用了 IMessenger 界面。为此,我添加了 class FilterTextChangedMessage.cs:

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

我更改了 CollectionViewModel.cs 的 FilterText 属性 如下:

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

我把ViewModel2.cs改成如下:

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

您有时必须 link CollectionViews 和过滤器方法,因此您的 UserControl 需要允许这样做。我可以想到三种方法来做到这一点:

  1. 让您的 UserControl 处理它自己的 CollectionView,并添加一个方法来配置您的过滤器(将 Predicate<object> 作为参数并将其设置为过滤器 属性).

  2. 让您的 UserControl 为您的列表公开一个 returns 和 CollectionView 的方法,并在 MainWindow 中执行所有操作。不太漂亮,但仍然可能还不错,并且当您只想显示没有任何选项的列表时避免创建不必要的东西。

  3. 最“MVVM”的方法是将 DependencyProperty 添加到您的 UserControl,并使用 PropertyChangedCallback 更新对象的过滤。然后您可以绑定到主窗口视图模型的 属性,并根据您的文本字段更新 属性。这是我的一些代码的示例:

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

自从 CollectionViewModel 定义 FilterText 属性 以来,已经通过 ViewModels1 属性 对子视图模型进行了强引用,您可以通过遍历此源集合来设置这些过滤器。

就 MVVM 而言,这非常好。应用程序逻辑保留在它所属的视图模型中。

然而,保持一种视图模型类型对另一种视图模型类型的强引用确实引入了一种耦合,这种耦合往往会使应用程序更难维护。但这是 ViewModels1 属性.

已经带来的另一个问题

此类问题通常通过使用信使或 event aggregator 在不同视图模型之间进行通信来解决。在这种情况下,您将从 CollectionViewModel 发送某种过滤事件并在子视图模型中处理此事件。