Некоторая путаница в привязке данных MVVM в интерфейсе с несколькими вкладками - PullRequest
0 голосов
/ 04 октября 2018

Я опытный разработчик, но относительный новичок в мире WPF и MVVM.Я читал различные учебные пособия и примеры следования шаблону MVVM.Я работаю над преобразованием существующего приложения форм MDI Windows (система управления студентами / классами) в WPF.Мой основной дизайн для меню (древовидного представления), закрепленного в левой части главного окна, с элементом управления вкладками, который будет содержать различные представления (студент, класс, учитель, биллинг и т. Д.), Которые требуются пользователю.В качестве подтверждения концепции (и чтобы получить представление о WPF) у меня есть следующее:

Простая модель, 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);
    }
}

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, свойства отображения по-прежнему равны нулю;несмотря на то, что StudentView, по-видимому, содержит данные.

После многих недоумений я обнаружил, что если я не установлю контекст данных в xaml StudentView, то привязка будет работать правильно, и тестовое приложение будет работать, как и ожидалось.Но тогда это не значит, что дизайнер xaml не проверяет привязки свойств экрана, даже если во время выполнения путь разрешен?

В любом случае, у меня осталось несколько вопросов.Как и почему то, что я сделал, работает?По сути, это идет вразрез со всем, что я читал и видел на MVVM.Кроме того, при попытке применить это приложение к инфраструктуре MVVM (например, MVVM Light) представления явно определяются с контекстом данных, установленным в xaml (например: DataContext="{Binding Path=Student, Source={StaticResource Locator}}).Что имеет еще меньше смысла ...

Как я уже сказал, то, что у меня есть, работает, но я не совсем понимаю, почему, и поэтому сомневаюсь, что я сделал что-то не так.В результате я не хочу идти дальше в серьезном развитии от страха перед необходимостью доработки позже (закопавшись в яму).

1 Ответ

0 голосов
/ 04 октября 2018

Дочерние элементы управления автоматически наследуют DataContext от своего родителя.Таким образом, если DataContext не указан в UserControl, то каждый экземпляр использует экземпляр StudentViewModel, содержащийся в коллекции WorkSpaces.С другой стороны, при указании контекста данных в XAML UserControl каждый экземпляр представления привязывается к одному и тому же экземпляру ViewModel.Вот почему изменение данных в одном представлении приводит к изменениям во всех других представлениях.Все представления ссылаются на один и тот же объект.Я надеюсь, что это понятно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...