如何在 MVVM 中的 UserControl 之间进行通信 - WPF 应用程序

Posted

技术标签:

【中文标题】如何在 MVVM 中的 UserControl 之间进行通信 - WPF 应用程序【英文标题】:How to communicate between UserControl in MVVM - WPF Application 【发布时间】:2020-01-26 17:29:21 【问题描述】:

我想创建一个左侧菜单的应用程序,该菜单将更改右侧的内容。 为此,我有一个带有两个 ContentControl 的 MainWindow(一个将内容 UserControl 'Menu',另一个将内容选定的 UserControl 'Red' 或 'Green'。

问题是右边的Content没有改变……

我进行了一些研究并看到了诸如依赖注入、委托、事件、消息总线、ViewModelLocator 之类的概念...... 但我不知道在这种情况下哪个最合适以及如何实现它。 (我不想使用 MVVMLight 或任何类似的插件)

感谢您的关注。

为此,我使用 MVVM 模式和 DataTemplate 绑定:

App.xaml

<Application.Resources>
    <DataTemplate DataType="x:Type viewModel:MainViewModel">
        <view:MainWindow />
    </DataTemplate>
    <DataTemplate DataType="x:Type viewModelMenu:LeftViewModel">
        <viewMenu:Left />
    </DataTemplate>
    <DataTemplate DataType="x:Type viewModelContent:RedViewModel">
        <viewContent:Red />
    </DataTemplate>
</Application.Resources>

ViewModel.cs

public abstract class ViewModel : INotifyPropertyChanged

    #region Properties

    private ViewModel _mainContent;

    public ViewModel MainContent
    
        get => _mainContent;
        set
        
            _mainContent = value;
            OnPropertyChanged();
            MessageBox.Show(nameof(MainContent));
        
    

    #endregion Properties

    public ViewModel()
    
        InitCommands();
    

    protected abstract void InitCommands();

    #region Factory Method - CreateCommand

    protected ICommand CreateCommand(Action execute, Func<bool> canExecute)
    
        return new RelayCommand(
            execute: execute,
            canExecute: canExecute
            );
    

    protected ICommand CreateCommand<T>(Action<T> execute, Predicate<T> canExecute)
    
        return new RelayCommand<T>(
            execute: execute,
            canExecute: canExecute
            );
    

    #endregion Factory Method - CreateCommand

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
    
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    

MainViewModel.cs

internal class MainViewModel : ViewModel

    private ViewModel _leftMenu;
    public ViewModel LeftMenu
    
        get => _leftMenu;
        set
        
            _leftMenu = value;
            OnPropertyChanged();
        
    

    public MainViewModel()
    
        LeftMenu = new LeftViewModel();
    

    protected override void InitCommands()
    
    

LeftViewModel.cs

internal class LeftViewModel : ViewModel

    public ICommand ChangeContentToRed  get; set; 
    public ICommand ChangeContentToGreen  get; set; 

    protected override void InitCommands()
    
        ChangeContentToRed = new RelayCommand(
            execute: () => MainContent = new RedViewModel(),
            canExecute: () => !(MainContent is RedViewModel)
            );

        ChangeContentToGreen = new RelayCommand(
            execute: () => MainContent = new GreenViewModel(),
            canExecute: () => !(MainContent is GreenViewModel)
            );
    

RedViewModel 和 GreenViewModel 是空的,所以我不显示代码,而是扩展 ViewModel

Window.xaml

 <Window.DataContext>
    <viewModel:MainViewModel />
</Window.DataContext>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="3*" />
    </Grid.ColumnDefinitions>

    <ContentControl Grid.Column="0" Content="Binding Path=LeftMenu" />
    <ContentControl Grid.Column="1" Content="Binding Path=MainContent" />
</Grid>

Left.xaml

<UserControl.DataContext>
    <viewModel:LeftViewModel />
</UserControl.DataContext>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Button
        Grid.Row="0"
        Command="Binding Path=ChangeContentToRed"
        Content="Red" />
    <Button
        Grid.Row="1"
        Command="Binding Path=ChangeContentToGreen"
        Content="Green" />
</Grid>

Red 和 Green 只是两个带有红色和绿色网格的 UserControl

【问题讨论】:

UserControls 从来没有自己的私有视图模型对象。从 UserControls 的 XAML 中删除 DataContext 分配,并将它们绑定到公共 MainViewModel 的属性(您显然已经这样做了)。 【参考方案1】:

当你有像这样的 DataTemplate 时

<DataTemplate DataType="x:Type viewModelMenu:LeftViewModel">
    <viewMenu:Left />
</DataTemplate>

然后将 LeftViewModel 类型的值赋给 ContentControl 的 Content 属性,如

<ContentControl Content="Binding Path=LeftMenu"/>

DataTemplate 被赋值给 ContentControl 的 ContentTemplate,实例化的 DataTemplate 中的元素(即你的 UserControl)继承 ContentControl 的 ControlTemplate 中 ContentPresenter 的 DataContext,然后保存 Content 值。

但是,这仅在您未显式分配 UserControl 的 DataContext 并因此破坏 DataContext 属性的值继承时才有效。

您必须从 UserControl 中删除显式 DataContext 分配,即:

<UserControl.DataContext>
    <viewModel:LeftViewModel />
</UserControl.DataContext>

【讨论】:

感谢您的快速回复。我是否必须删除除 MainViewModel 和 ViewModel 之外的所有 VM(RedVM、GreenVM、LeftVM)。所以一切都指的是MainVM?或不?因为如果我这样做,我必须将属性类型(例如 MainContent)从 ViemModel 更改为 UserControl... 可以吗? 因为如果我只删除 UserControl.DataContext,MainContent 不会更新... 不确定您到底指的是什么,但是如果您在主视图模型中有“子”视图模型,其中一个属性的更改当然应该传达给另一个子视图如果您的应用程序逻辑需要,请查看模型。最好从单个视图模型开始,即从 UserControls 的 XAML 绑定到 MainViewModel 的属性。 如果我将所有视图都引用到 MainViewModel 它可以工作......但我希望每个 UserControl 引用他自己的 VM(例如,我更喜欢将 LeftMenu 的所有特定命令放在LeftMenuViewModel 而不是在 MainViewModel 中)。问题是在这种情况下,MainContent 不会刷新,而是显示已更改的内容。 如前所述,您需要在主视图模型和子视图模型之间进行某种通信。例如,一个 vm 可以订阅另一个 vm 的 PropertyChanged 事件。

以上是关于如何在 MVVM 中的 UserControl 之间进行通信 - WPF 应用程序的主要内容,如果未能解决你的问题,请参考以下文章

关于在MVVM架构下WPF中UserControl的 visibility Binding问题。 UserControl MVVM

如何从作为wpf mvvm模式中的窗口打开的视图模型中关闭用户控件?

WPF usercontrol mvvm

使用 MVVM 在 MainWindow 上绑定 UserControl 视图模型

使用该控件时的 MVVM UserControl 和绑定

如何使用 MVVM 将 UserControl 从插件传递到我的主窗口?