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

时间:2018-10-04 08:47:45

标签: c# wpf mvvm

我是一位经验丰富的开发人员,但是相对而言WPF和MVVM还是一个新手。我一直在阅读有关遵循MVVM模式的各种教程和示例。我正在将现有的MDI Windows表单(学生/班级管理系统)应用程序转换为WPF。我的基本设计是将菜单(树视图)停靠在​​主窗口的左侧,并带有一个选项卡控件,其中将包含用户所需的不同视图(学生,班级,教师,账单等)。作为概念验证(并开始涉足WPF),我有以下几点:

一个简单的模型,学生

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

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

视图模型继承了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);
    }
}

这又继承了ViewModelBase,我在其中实现了INotifyPropertyChanged。 RelayCommand类是ICommand接口的标准实现。

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

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>

StudentViewModel和StudentView通过App.xaml中的资源字典链接

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

最后是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>

当我单击“新学生”按钮时,将创建一个新的学生工作区,并将其添加到“工作区”集合中并显示在TabControl中。一切似乎都很好。但是,当我在视图上输入数据时,我注意到选项卡标题未更新。第一个迹象表明一切都无法正常进行...

然后,当我第二次单击“新学生”时。创建了另一个工作空间,但是该工作空间重复了在第一个工作空间中输入的值。此外,在编辑第二个标签时,第一个标签也会更新。

在NewTab方法中放置一个断点表明,尽管Workspaces集合保存了StudentViewModels,但显示属性仍然为null。即使StudentView似乎可以保存数据。

经过一番困惑后,我发现如果我没有在StudentView xaml上设置数据上下文,则绑定将正常运行,并且测试应用程序将按预期运行。但这是否不意味着xaml设计器并没有真正验证显示属性绑定,即使在运行时路径已解析?

无论如何,我现在只剩下几个问题。我完成工作的方式和原因是什么?从本质上讲,它似乎与我在MVVM上阅读和看到的所有内容都不符。此外,当尝试将此应用程序应用于MVVM框架(例如MVVM Light)时,将使用在xaml中设置的数据上下文(例如:DataContext="{Binding Path=Student, Source={StaticResource Locator}})明确定义视图。这更没有意义...

正如我所说,我的工作确实可以,但是我并不真正理解为什么,因此人们越来越怀疑我做错了什么。因此,由于担心以后不得不重做(我陷入了困境),我不愿继续进行严肃的发展。

1 个答案:

答案 0 :(得分:0)

子控件自动从其父级继承DataContext。因此,如果在UserControl中未指定DataContext,则每个实例都使用WorkSpaces集合中包含的StudentViewModel实例。另一方面,在UserControl XAML中指定数据上下文时,视图的每个实例都绑定到相同的ViewModel实例。这就是为什么更改一个视图上的数据会导致所有其他视图上的更改的原因。所有视图都引用同一对象。我希望这很清楚。