将数据绑定到 WPF (MVVM) 中的子视图

本文关键字:视图 MVVM 数据绑定 WPF | 更新日期: 2023-09-27 17:59:13

我正在尝试使用 WPF 和 MVVM 协议构建一个应用程序。应用程序有一个窗口和多个视图(用户控件(。其中一个视图内部还具有子视图以显示不同的数据。

我的问题我不明白如何将数据绑定到子视图。我试图了解数据是如何绑定的,但只能将数据绑定到一级。

法典

下面是一些代码。我希望它能更容易理解我的问题。

应用xaml.cs

public partial class App : Application
{
    private Engine _engine;
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        MainWindow window = new MainWindow();
        _engine = new Engine("test");
        var viewModel = new MainWindowViewModel(_engine);
        EventHandler handler = null;
        handler = delegate
        {
            viewModel.RequestClose -= handler;
            window.Close();
        };
        viewModel.RequestClose += handler;
        window.DataContext = viewModel;
        window.Show();
    }
}

在这里,我创建了引擎对象并将其传递给 MainWindowViewModel,我想进一步绑定视图层次结构。

MainWindow.xaml

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:"
    xmlns:ViewModels="clr-namespace:ViewModels" 
    xmlns:View="clr-namespace:Views"
    x:Class="MainWindow" 
    Title="title" Height="800" Width="1200"
    WindowStartupLocation="CenterScreen"   Icon="Resources/Images/logo.png"
>
<DockPanel Margin="0" Background="#FF4F4F4F" LastChildFill="True">
    <Menu DockPanel.Dock="Top" Height="20">
        <MenuItem Header="File">
            <MenuItem Header="Exit"/>
        </MenuItem>
    </Menu>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" MinWidth="200" MaxWidth="200"/>
            <ColumnDefinition Width="5*"/>
        </Grid.ColumnDefinitions>
        <View:TabView Grid.Column="0"/>
        <View:WorkspaceView Grid.Column="1"/>
    </Grid>
</DockPanel>

WorkspaceView.xaml

<UserControl x:Class="Views.WorkspaceView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:Views"
         xmlns:ViewModels="clr-namespace:ViewModels" 
         mc:Ignorable="d" 
         d:DesignHeight="750"
         d:DesignWidth="1000"             
         d:DataContext="{d:DesignInstance ViewModels:WorkspaceViewModel}"
         >
<Grid>
    <Label x:Name="label" Height="750" VerticalAlignment="Top" FontSize="60" Foreground="White" Content="{Binding Engine.BarCode}"/>
    <Grid Margin="40">
        <Grid.Background>
            <ImageBrush ImageSource="/;component/Resources/Images/logo.png" Stretch="Uniform"/>
        </Grid.Background>
        <ContentPresenter Content="{Binding CurrentView}"/>
    </Grid>
    <DockPanel>
        <!--ContentPresenter Content="{Binding CurrentView}"/-->
    </DockPanel>
</Grid>

在这里,我尝试绑定有效的{Binding Engine.BarCode}并给我一个包含正确数据的字符串。但是<ContentPresenter Content="{Binding CurrentView}"/>不会显示我在工作区视图的视图模型中设置的当前视图。

工作区视图模型.cs

    public WorkspaceViewModel()
    {
        _currentView = new InjectorView();
    }        
    public UserControl CurrentView
    {
        get { return _currentView; }
    }
  • 更新:_currentView = new InjectorView();仅用于测试,当前视图应根据另一个视图中按下的按钮而变化。

WorkspaceView.xaml.cs

    public WorkspaceView()
    {
        InitializeComponent();
        this.DataContext = new WorkspaceViewModel();
    }

InjectorView.xaml

<UserControl x:Class="Views.InjectorView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:Views"
         xmlns:ViewModels="clr-namespace:ViewModels" 
         mc:Ignorable="d" 
         d:DesignHeight="750"
         d:DesignWidth="1000"
         d:DataContext="{d:DesignInstance ViewModels:InjectorViewModel}"
         >
<Grid Background="#FFAEAEAE">
    <Label x:Name="label1" Content="{Binding Engine.BarCode}"/>
</Grid>
</UserControl>

但是,如果我从 WorkspaceView.xaml 中删除d:DataContext="{d:DesignInstance ViewModels:WorkspaceViewModel}"并将this.DataContext = new WorkspaceViewModel();添加到 xaml 代码的 c# 文件中,它将显示当前视图 (InjectorView(。现在唯一的问题是当我尝试在 InjectorView 中绑定一些数据时{Binding Engine.BarCode}它不会显示与以前相同的字符串(我想它不再是对象的相同实例了??

我错过了什么?我是否完全错误地解释了 MVVM 和 wpf?

(

由于产品的原因,我不得不删除一些代码(例如命名空间(

将数据绑定到 WPF (MVVM) 中的子视图

你肯定误解了很多。

首先,_currentView = new InjectorView();永远不应该出现在ViewModel中。ViewModels 不应包含任何可视类(您自己的 View 类和 UI 类(的任何引用。您应该实例化该视图的视图模型。

接下来,如果该CurrentView始终是InjectorView的实例(这意味着它不能是其他实例(,那么您可以简单地执行以下操作:

<View:InjectorView>
    <View:InjectorView.DataContext>
        <ViewModel:InjectorViewModel />
    <View:InjectorView.DataContext>
</View:InjectorView>

这与您在MainWindow.xaml中为WorkspaceView所做的非常相似

现在进入下一个大问题。 d:DataContext="{d:DesignInstance ViewModels:InjectorViewModel}"用作设计时DataContext。这意味着,通常没有这个,Visual Studios Designer将不会渲染数据绑定的东西,因为在设计时没有DataContext。 该行告诉设计器,您要创建一个InjectorViewModel实例来模拟此行为 - 但这纯粹是针对设计器的。当应用程序运行时,该d:DataContext对您的应用程序绝对没有影响。

目前,您的InjectorView没有ViewModel(没有DataContext(。如果你按照我在这个答案的前面部分的建议,那么你现在就会有一个DataContext

编辑(根据OP的评论(

上述方法称为"查看优先方法"。使用此方法,可以在 XAML 中定义视图实例。然后,使用 DataContext 附加相应的视图模型。

对于您的情况,您应该使用视图模型优先方法。您可以定义要在视图模型中使用的组件。

工作区视图模型:

private ViewModelBase _myCurrentView;
public ViewModelBase MyCurrentView
{
    get { return _currentView; }
    set
    {
        if (value != _myCurrentView)
        {
            _myCurrentView = value;
            RaisePropertyChanged(); // You need to implement INotifyPropertyChanged interface
        }
    }
}

工作区视图:

<ContentControl Content="{Binding MyCurrentView}">
    <ContentControl.Resources>
        <DataTemplate DataType="{x:Type ViewModels:InjectorViewModel}">
            <local:InjectorView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type ViewModels:MySecondViewModel}">
            <local:MySecondView />
        </DataTemplate>
        .....
        ....
    </ContentControl.Resources>
</ContentControl>

使用此方法,当您需要加载InjectorView时,只需实例化其 ViewModel InjectorViewModel,并将其分配给MyCurrentView属性。

WorkspaceView视图中,您将使用ContentControl来托管此子视图。此ContentControl需要绑定到MyCurrentView属性,该属性属于ViewModelBase类型。DataTemplate将告诉 WPF,如果内容是InjectorViewModel类型,则为我实例化一个InjectorView对象,因为InjectorViewModel只是一个非可视数据对象 - 可呈现的等效项是 InjectorView 。您需要为所需的每个可能的 ViewModel 类创建一个DataTemplate

使用此方法时需要注意的两件事(视图模型优先(。首先,所有可以动态加载的视图模型都必须是ViewModelBase的子类。 ViewModelBase可以是抽象类或接口,您可以自由更改为所需的任何名称。最重要的是,它必须是所有可能的 ViewModel 的公共类/接口。

其次,您不需要在视图中实例化另一个视图模型。您也不需要设置 DataContext .使用 DataTemplate 将自动设置视图的DataContext

编辑 2

有许多

方法可以将数据从主视图模型传递到子视图模型。一种方法是让存储库(数据库(保存数据。

如果不希望使用存储库,

则可以使用单一实例到仿真器作为存储库,或者让主 ViewModel 保存所有数据,并让所有其他 ViewModel 保存主 ViewModel 实例的引用。但老实说,当你的子视图模型由视图优先方法实例化时,这更难实现,因为视图模型是由视图实例化的,视图不会调用在主视图模型中传递的构造函数。

解决此问题的另一种方法是使用静态内部单例实例创建单例视图模型。这允许视图模型访问彼此的数据。若要使 ViewModels 成为单例,您需要更改视图优先方法定义 DataContext 的方式。

例如,使用我之前为 View-First InjectorView提供的相同示例:

<View:InjectorView>
    <View:InjectorView.DataContext>
        <Binding Source="{x:Static ViewModel:InjectorViewModel.Instance}" />
    <View:InjectorView.DataContext>
</View:InjectorView>

不能从视图中实例化视图模型,因为必须使构造函数private用于单一实例实现。相反,您将提供对静态实例的绑定。

总的来说,我只会使用一个单例类来充当存储库。对我来说似乎很容易实现。