【问题标题】:Binding data to child views in WPF (MVVM)将数据绑定到 WPF (MVVM) 中的子视图
【发布时间】:2016-07-29 07:27:30
【问题描述】:

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

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

代码

这里是一些代码。我希望它能让我更容易理解我的问题。

App.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},它可以工作并为我提供一个包含正确数据的字符串。但是&lt;ContentPresenter Content="{Binding CurrentView}"/&gt; 不会显示我在 ViewModel 中为 workspaceView 设置的当前视图。

WorkspaceViewModel.cs

    public WorkspaceViewModel()
    {
        _currentView = new InjectorView();

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

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 的解释是否完全错误?

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

【问题讨论】:

    标签: c# .net wpf mvvm data-binding


    【解决方案1】:

    你肯定误会了很多。

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

    接下来,如果 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 设计器不会呈现数据绑定的东西,因为在设计时没有DataContext。该行告诉设计人员您要创建InjectorViewModel 的实例来模拟此行为 - 但这纯粹是针对设计人员的。当您的应用程序运行时,d:DataContext绝对对您的应用程序没有影响。

    目前,您的InjectorView 没有ViewModel(没有DataContext)。如果您按照我在此答案前面部分中的建议进行操作,那么您现在将拥有DataContext

    编辑(基于 OP 的评论)

    上面的方法称为View First Approach。使用这种方法,您可以在 XAML 中定义一个 View 实例。然后使用 DataContext 附加相应的 ViewModel。

    对于您的情况,您应该使用 ViewModel First Approach。您可以定义要在 ViewModel 中使用的组件。

    工作区视图模型:

    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

    使用这种方法时需要注意两点(首先是 ViewModel)。首先,所有可以动态加载的ViewModel必须是ViewModelBase的子类。 ViewModelBase 可以是抽象类,也可以是接口,您可以随意更改为任何您想要的名称。最重要的是它必须是所有可能的 ViewModel 的通用类/接口。

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

    编辑 2

    有很多方法可以将数据从主 ViewModel 传递到子 ViewModel。一种方法是让存储库(数据库)保存数据。

    如果您不想拥有存储库,您可以使用单例模拟器作为存储库,或者让主 ViewModel 保存所有数据,并让所有其他 ViewModel 保存对主 ViewModel 实例的引用。但老实说,当您的子 ViewModel 由 View-First Approach 实例化时,这更难实现,因为 ViewModel 是由 View 实例化的,它不会调用传入主 ViewModel 的构造函数。

    解决这个问题的另一种方法是创建带有静态内部单例实例的单例视图模型。这允许 ViewModel 访问彼此的数据。要使 ViewModels 单例,您需要更改 View-First Approach 定义 DataContext 的方式。

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

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

    您不能从 View 实例化 ViewModel,因为您必须为单例实现创建构造函数 private。您将改为提供与静态实例的绑定。

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

    【讨论】:

    • 谢谢。这澄清了'd:DataContext =“{d:DesignInstance ViewModels:InjectorViewModel}'为什么当我删除这一行时它没有改变任何东西:)关于将视图与视图模型分开(对于currentView)我可以理解。我忘了说该视图将更改为另一个视图,并不总是 InjectorView。我该如何处理?
    • 非常感谢@Jai 抽出宝贵时间。我会尝试实施这个策略,看看效果如何。
    • 感谢您很好地实施了您的建议。我正在努力解决如何将数据从顶视图(MainWindow)绑定到 InjectorView。你有任何指导方针如何处理吗?
    • @robin 再次更新。
    猜你喜欢
    • 2011-10-07
    • 1970-01-01
    • 1970-01-01
    • 2019-02-05
    • 2017-06-05
    • 2018-10-25
    • 1970-01-01
    • 2011-02-06
    • 2011-03-11
    相关资源
    最近更新 更多