我正在尝试使用WPF和MVVM协议(protocol)构建应用程序。该应用程序具有一个窗口和多个 View (UserControls)。其中一个 View 还具有 subview ,以显示不同的数据。

我的问题
我不明白如何将数据绑定(bind)到 subview 。我试图了解如何绑定(bind)数据,但是只能将数据向下绑定(bind)一层。

代码

这是一些代码。我希望它可以使我的问题更容易理解。

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

在这里,我创建了引擎对象,并将其传递给要进一步绑定(bind) View 层次结构的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>

在这里,我试图绑定(bind){Binding Engine.BarCode},它可以工作并为我提供带有正确数据的字符串。但是<ContentPresenter Content="{Binding CurrentView}"/>不会显示我在ViewModel中为workspaceView设置的当前 View 。

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

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

  • 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#文件中,它将显示当前 View (InjectorView)。现在唯一的问题是,当我尝试在InjectorView {Binding Engine.BarCode}中绑定(bind)某些数据时,它将不会显示与以前相同的字符串(我想它不再是该对象的相同实例了?)

    我想念什么?我将MVVM和wpf解释完全错误吗?

    (由于该产品,我不得不删除一些代码(例如 namespace ))

    最佳答案

    您肯定误会了不少。
    首先,_currentView = new InjectorView();永远不会出现在ViewModel中。 ViewModels不应该包含任何视觉类(您自己的View类和UI类)的任何引用。您应该实例化该 View 的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 Designer不会渲染数据绑定(bind)的东西,因为在设计时没有DataContext。该行告诉设计者您要创建InjectorViewModel实例来模拟此行为-但这纯粹是为设计者设计的。当您的应用程序运行时,该d:DataContext行的绝对对您的应用程序没有的影响。
    目前,您的InjectorView没有ViewModel(没有DataContext)。如果您遵循我在此答案前面部分中的建议,那么您现在将拥有一个DataContext
    编辑(根据OP的评论)
    上面的方法称为 View First Approach 。使用这种方法,您可以在XAML中定义一个View实例。然后,使用DataContext附加相应的ViewModel。
    对于您的情况,应该使用 ViewModel First Approach 。您定义在ViewModel中使用哪些组件。
    WorkspaceViewModel:
    private ViewModelBase _myCurrentView;
    public ViewModelBase MyCurrentView
    {
        get { return _currentView; }
        set
        {
            if (value != _myCurrentView)
            {
                _myCurrentView = value;
                RaisePropertyChanged(); // You need to implement INotifyPropertyChanged interface
            }
        }
    }
    
    工作区 View :
    <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 View 中,您将使用ContentControl托管此 subview 。此ContentControl需要绑定(bind)到MyCurrentView类型的ViewModelBase属性。 DataTemplate会告诉WPF,如果内容为InjectorViewModel类型,则为我实例化InjectorView对象,因为InjectorViewModel只是非可视数据对象-可渲染的等效项是InjectorView。您需要为每个期望的ViewModel类创建一个DataTemplate
    使用这种方法时要注意两件事(首先是ViewModel)。首先,所有可以动态加载的ViewModel必须是ViewModelBase的子类。 ViewModelBase可以是抽象类或接口(interface),您可以随意更改为所需的名称。最重要的是,它必须是所有可能的ViewModel的通用类/接口(interface)。
    其次,您无需在 View 中实例化另一个ViewModel。您也不需要设置DataContext。使用DataTemplate将自动设置 View 的DataContext
    编辑2
    有很多方法可以将数据从主ViewModel传递到子ViewModel。一种方法是让存储库(数据库)保存数据。
    如果您不想拥有存储库,则可以使用单例作为存储库进行仿真,也可以让主ViewModel保留所有数据,让所有其他ViewModel保留主ViewModel实例的引用。但老实说,通过View-First Approach实例化子ViewModel时,这很难实现,因为ViewModel是通过View实例化的,这不会调用传入主ViewModel的构造函数。
    解决此问题的另一种方法是使用静态内部单例实例创建单例ViewModel。这使ViewModels可以访问彼此的数据。要使ViewModels单例,您需要更改“ View 优先”方法定义DataContext的方式。
    例如,使用我之前为View-First InjectorView提供的示例:
    <View:InjectorView>
        <View:InjectorView.DataContext>
            <Binding Source="{x:Static ViewModel:InjectorViewModel.Instance}" />
        <View:InjectorView.DataContext>
    </View:InjectorView>
    
    您无法从View实例化ViewModel,因为必须为单例实现创建构造函数private。相反,您将提供对静态实例的绑定(bind)。
    总的来说,我将只使用一个singleton类来充当存储库。对我来说,实现起来似乎很容易。

    关于c# - 在WPF(MVVM)中将数据绑定(bind)到 subview ,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/38652918/

    10-11 17:09