简体   繁体   English

关于多选项卡界面中的MVVM数据绑定的一些困惑

[英]Some confusion about MVVM data binding in a multi-tabbed interface

I am an experienced developer, but relative newcomer to the world of WPF and MVVM. 我是一位经验丰富的开发人员,但相对来说是WPF和MVVM领域的新手。 I've been reading up on various tutorials and examples of following the MVVM pattern. 我一直在阅读有关遵循MVVM模式的各种教程和示例。 I am working on converting an existing MDI Windows forms (a student/class management system) application into WPF. 我正在将现有的MDI Windows表单(学生/班级管理系统)应用程序转换为WPF。 My basic design is for a menu (tree view) docked on the left side of the main window with a tab control that would contain the different views (student, class, teacher, billing etc) that the user requires. 我的基本设计是将菜单(树视图)停靠在​​主窗口的左侧,并带有一个选项卡控件,其中将包含用户所需的不同视图(学生,班级,教师,账单等)。 As proof of concept (and to get my head around WPF) I have the following: 作为概念验证(并开始涉足WPF),我有以下几点:

A simple model, Student 一个简单的模型,学生

public class Student
{
    public DateTime BirthDate { get; set; }
    public string Forename { get; set; }
    public int Id { get; set; }
    public string Surname { get; set; }

    public override string ToString()
    {
        return String.Format("{0}, {1}", Surname, Forename);
    }
}

The StudentViewModel StudentViewModel

public class StudentViewModel : WorkspaceViewModel
{
    private Student student;

    public override string DisplayName
    {
        get
        {
            return String.Format("{0} {1}", student.Forename, student.Surname);
        }
    }
    public string Forename
    {
        get
        {
            return student.Forename;
        }
        set
        {
            student.Forename = value;
            RaisePropertyChanged();
            RaisePropertyChanged("DisplayName");
        }
    }
    public int Id
    {
        get
        {
            return student.Id;
        }
        set
        {
            student.Id = value;
            RaisePropertyChanged();
        }
    }
    public string Surname
    {
        get
        {
            return student.Surname;
        }
        set
        {
            student.Surname = value;
            RaisePropertyChanged();
            RaisePropertyChanged("DisplayName");
        }
    }

    public StudentViewModel()
    {
        this.student = new Student();
    }

    public StudentViewModel(Student student)
    {
        this.student = student;
    }
}

The view model inherits WorkspaceViewModel, an abstract class 视图模型继承了WorkspaceViewModel,这是一个抽象类

public abstract class WorkspaceViewModel : ViewModelBase
{
    public RelayCommand CloseCommand { get; set; }

    public event EventHandler OnClose;

    public WorkspaceViewModel()
    {
        CloseCommand = new RelayCommand(Close);
    }

    private void Close()
    {
        OnClose?.Invoke(this, EventArgs.Empty);
    }
}

This in turn inherits ViewModelBase, where I implement INotifyPropertyChanged. 反过来,这继承了ViewModelBase,我在其中实现了INotifyPropertyChanged。 The RelayCommand class is a standard implementation of the ICommand interface. RelayCommand类是ICommand接口的标准实现。

The MainWindowViewModel holds a collection of Workspaces MainWindowViewModel包含工作区的集合

public class MainViewModel : WorkspaceViewModel
{
    private WorkspaceViewModel workspace;
    private ObservableCollection<WorkspaceViewModel> workspaces;

    public WorkspaceViewModel Workspace
    {
        get
        {
            return workspace;
        }
        set
        {
            workspace = value;
            RaisePropertyChanged();
        }
    }
    public ObservableCollection<WorkspaceViewModel> Workspaces
    {
        get
        {
            return workspaces;
        }
        set
        {
            workspaces = value;
            RaisePropertyChanged();
        }
    }

    public RelayCommand NewTabCommand { get; set; }

    public MainViewModel()
    {
        Workspaces = new ObservableCollection<WorkspaceViewModel>();
        Workspaces.CollectionChanged += Workspaces_CollectionChanged;
        NewTabCommand = new RelayCommand(NewTab);
    }

    private void NewTab()
    {
        Student student = new Student();
        StudentViewModel workspace = new StudentViewModel(student);
        Workspaces.Add(workspace);

        Workspace = workspace;
    }

    private void Workspaces_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null && e.NewItems.Count != 0)
        {
            foreach (WorkspaceViewModel workspace in e.NewItems)
            {
                workspace.OnClose += Workspace_OnClose; ;
            }
        }

        if (e.OldItems != null && e.OldItems.Count != 0)
        {
            foreach (WorkspaceViewModel workspace in e.OldItems)
            {
                workspace.OnClose -= Workspace_OnClose;
            }
        }
    }

    private void Workspace_OnClose(object sender, EventArgs e)
    {
        var workspace = (WorkspaceViewModel)sender;
        Workspaces.Remove(workspace);
    }
}

The StudentView xaml StudentView xaml

<UserControl x:Class="MvvmTest.View.StudentView"
         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:MvvmTest.View"
         xmlns:vm="clr-namespace:MvvmTest.ViewModel"
         mc:Ignorable="d">
<UserControl.DataContext>
    <vm:StudentViewModel/>
</UserControl.DataContext>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="100"/>
    </Grid.ColumnDefinitions>
    <TextBlock Grid.Column="0" Grid.Row="0" Text="ID:"/>
    <TextBlock Grid.Column="0" Grid.Row="1" Text="Forename:"/>
    <TextBlock Grid.Column="0" Grid.Row="2" Text="Surname:"/>
    <TextBlock Grid.Column="0" Grid.Row="3" Text="Date of Birth:"/>
    <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Id, Mode=TwoWay}"/>
    <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Forename, Mode=TwoWay}"/>
    <TextBox Grid.Column="1" Grid.Row="2" Text="{Binding Surname, Mode=TwoWay}"/>
    <DatePicker Grid.Column="1" Grid.Row="3" SelectedDate="{Binding BirthDate, Mode=TwoWay}"/>
</Grid>
</UserControl>

The StudentViewModel and StudentView are linked via a resource dictionary in App.xaml 通过App.xaml中的资源字典链接StudentViewModel和StudentView

    <ResourceDictionary>
        <vm:MainViewModel x:Key="MainViewModel"/>
        <DataTemplate DataType="{x:Type vm:StudentViewModel}">
            <v:StudentView/>
        </DataTemplate>
    </ResourceDictionary>

And finally, the MainWindow view (goal is that this will eventually conform to MVVM in that the MainWindowViewModel will define the menu structure) 最后是MainWindow视图(目标是最终将符合MVVM,因为MainWindowViewModel将定义菜单结构)

<Window x:Class="MvvmTest.MainWindow"
    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:MvvmTest"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:vm="clr-namespace:MvvmTest.ViewModel"
    xmlns:v="clr-namespace:MvvmTest.View"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
    <vm:MainViewModel/>
</Window.DataContext>
<DockPanel>
    <StackPanel DockPanel.Dock="Left" Orientation="Vertical">
        <Button Content="New Student">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <i:InvokeCommandAction Command="{Binding NewTabCommand}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </StackPanel>
    <TabControl ItemsSource="{Binding Workspaces}" SelectedItem="{Binding Workspace}">
        <TabControl.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding DisplayName, Mode=OneWay}"/>
                    <Button>X</Button>
                </StackPanel>
            </DataTemplate>
        </TabControl.ItemTemplate>
        <TabControl.ContentTemplate>
            <DataTemplate>
                <StackPanel>
                    <UserControl Content="{Binding}"/>
                </StackPanel>
            </DataTemplate>
        </TabControl.ContentTemplate>
    </TabControl>
</DockPanel>
</Window>

When I click the 'New student' button a new student workspace is created, added to Workspaces collection and displays in the TabControl. 当我单击“新学生”按钮时,将创建一个新的学生工作区,并将其添加到Workspaces集合中并显示在TabControl中。 All seems well. 一切似乎都很好。 But when I enter data on the view I noticed that the tab header isn't updated. 但是,当我在视图上输入数据时,我注意到选项卡标题未更新。 First sign that all is not working as it should... 第一个迹象表明一切都没有正常进行...

Then when I click 'New student' a second time. 然后,当我第二次单击“新学生”时。 Another workspace is created, but that duplicates the values entered in the first. 创建了另一个工作空间,但是该工作空间重复了在第一个工作空间中输入的值。 Further, when editting the second tab, the first is also updated. 此外,在编辑第二个选项卡时,第一个选项卡也会更新。

Placing a breakpoint into the NewTab method revealed that although the Workspaces collection holds StudentViewModels, the display properties are still null; 在NewTab方法中放置一个断点表明,尽管Workspaces集合保存了StudentViewModels,但显示属性仍然为null。 even though the StudentView appears to hold data. 即使StudentView似乎保留数据。

After much puzzling I discovered that if I do not set the data context on the StudentView xaml then the binding behaves properly and the test app works as expected. 经过一番困惑后,我发现如果我没有在StudentView xaml上设置数据上下文,则绑定将正常运行,并且测试应用程序将按预期运行。 But then doesn't that mean the xaml designer isn't really validating the display property bindings, even though at runtime the path is resolved? 但这是否不意味着xaml设计器并没有真正验证显示属性绑定,即使在运行时已解析路径?

Anyway, I'm now left a few questions. 无论如何,我现在只剩下几个问题。 How and why does what I've done work? 我完成工作的方式和原因是什么? It essentially appears to go against everything I've read and seen on MVVM. 它本质上似乎与我在MVVM上阅读和看到的所有内容都不符。 Furthermore when trying to apply this application to a MVVM framework (eg MVVM Light) the views are explicitly defined with the data context set in the xaml (eg: DataContext="{Binding Path=Student, Source={StaticResource Locator}} ). Which makes even less sense... 此外,当尝试将此应用程序应用于MVVM框架(例如MVVM Light)时,将使用在xaml中设置的数据上下文显式定义视图(例如: DataContext="{Binding Path=Student, Source={StaticResource Locator}} )。这更没有意义...

As I said, what I've got does work, but I'm not really understanding why, and therefore doubt is clawing away that I've done something wrong. 就像我说的,我的工作确实可行,但是我并不能真正理解为什么,因此人们越来越怀疑我做错了什么。 As a result I'm reluctant to proceed further on serious development from fear of having to rework later (having dug myself into a hole). 结果,我由于害怕以后需要重做(不愿陷入困境)而不愿继续进行严肃的发展。

Child controls automatically inherit DataContext from their parent. 子控件会自动从其父控件继承DataContext。 So if no DataContext is specified in the UserControl then each instance uses the instance of StudentViewModel contained in the WorkSpaces Collection. 因此,如果在UserControl中未指定DataContext,则每个实例都使用WorkSpaces集合中包含的StudentViewModel实例。 On the other hand when specifing the datacontext in the UserControl XAML each instance of the view is bound the same ViewModel instance. 另一方面,在UserControl XAML中指定数据上下文时,视图的每个实例都绑定到相同的ViewModel实例。 That is why changing data on one view results in changes on all other views. 这就是为什么更改一个视图上的数据会导致所有其他视图上的更改的原因。 The views are all referencing the same object. 所有视图都引用同一对象。 I hope that is clear. 我希望这很清楚。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM