WPF MVVM two-way updates

2020-08-09 04:08发布

问题:

I'm trying to setup a working two-way update by using this example.

These are the relevant code snippets:

XAML:

<Button Click="clkInit">Initialize</Button>
<Button Click="clkStudent">Add student</Button>
<Button Click="clkChangeStudent">Change students</Button>
(...)
<TabControl Name="tabControl1" ItemsSource="{Binding StudentViewModels}" >
   <TabControl.ItemTemplate>
      <DataTemplate>
         <TextBlock Text="{Binding Path=StudentFirstName}" />
      </DataTemplate>
   </TabControl.ItemTemplate>
   <TabControl.ContentTemplate>                
      <DataTemplate>
         <Grid>
            <Label Content="First Name" Name="label1" />
            <TextBox Name="textBoxFirstName" Text="{Binding Path=StudentFirstName}" />
            <Label Content="Last Name" Name="label2" />
            <TextBox Name="textBoxLastName" Text ="{Binding Path=StudentLastName}" />
         </Grid>                    
      </DataTemplate>
   </TabControl.ContentTemplate>
</TabControl>

Main Window:

public partial class MainWindow : Window
{
    internal MainWindowViewModel myMWVM;
    public MainWindow()
    {
       InitializeComponent();
    }

    private void clkInit(object sender, RoutedEventArgs e)
    {
       myMWVM= new MainWindowViewModel();
       DataContext = myMWVM;
    }
    private void clkStudent(object sender, RoutedEventArgs e)
    {
       myMWVM.StudentViewModels.Add(new StudentViewModel());
    }
    // For testing - call a function out of the student class to make changes there
    private void clkChangeStudent(object sender, RoutedEventArgs e)
    {
       for (Int32 i = 0; i < test.StudentViewModels.Count; i++)
       {
           myMWVM.StudentViewModels.ElementAt((int)i).changeStudent();
       }
    }
}

Main view:

class MainWindowViewModel : INotifyPropertyChanged
{
   ObservableCollection<StudentViewModel> _studentViewModels = 
        new ObservableCollection<StudentViewModel>();

   // Collection for WPF.
   public ObservableCollection<StudentViewModel> StudentViewModels
   {
      get { return _studentViewModels; }
   }

   // Constructor. Add two stude
   public MainWindowViewModel()
   {
      _studentViewModels.Add(new StudentViewModel());
      _studentViewModels.Add(new StudentViewModel());
   }

   // Property change.
   public event PropertyChangedEventHandler PropertyChanged;
   private void OnPropertyChanged(string propertyName)
   {
      if (PropertyChanged != null)
      {
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
   }
}

Student view:

class StudentViewModel : INotifyPropertyChanged
{
   Lazy<Student> _model;

   string _studentFirstName;
   public string StudentFirstName
   {
      get { return _studentFirstName; }
      set
      {
         if (_studentFirstName != value)
         {
            _studentFirstName = value;
            _model.Value.StudentFirstName = value;
            OnPropertyChanged("StudentFirstName");
         }
      }
   }

   string _studentLastName;
   public string StudentLastName
   {
      get { return _studentLastName; }
      set
      {
         if (_studentLastName != value)
         {
            _studentLastName = value;
            _model.Value.StudentLastName = value;
            OnPropertyChanged("StudentLastName");
         }
      }
   }

   public void changeStudent()
   {
      _model.Value.changeStudent();
   }


   public StudentViewModel()
   {
      _studentFirstName = "Default";
      _model = new Lazy<Student>(() => new Student());
   }


   public event PropertyChangedEventHandler PropertyChanged;
   private void OnPropertyChanged(string propertyName)
   {
      if (PropertyChanged != null)
      {
         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
   }
}

THE student:

class Student
{
   public string StudentFirstName { get; set; }
   public string StudentLastName { get; set; }

   public Student()
   {
      MessageBox.Show("Student constructor called");
   }
   public Student(string nm)
   {
      StudentLastName = nm;
   }
   public void changeStudent()
   {
      StudentLastName = "McDonald";
   }
}

If you read until here I already thank you :) Still, by calling "clkChangeStudent" I don't see the changes in the textbox. I guess it's because I don't call the set-method of the StudentViewModel. The project I'm working on is a bit complex and a lot of things happen in the class (here Student) itself.

How can I get a textbox update by settings values in the Student-class itself?

回答1:

Your actual code clearly won't notify changes to the interface. The reason is simple. Your method that changes the student name is in the Student model and that model does not implement the INotifyPropertyChanged.

There is 2 solutions to fix this issue depending on one question, does the changeStudent() method has to stick with the object model, that is to say, can your requirements allows you to move the changeStudent() method to the view model?

If yes then, first solution, simply remove the changeStudent method from the model and move it to the view model like this:

class StudentViewModel : INotifyPropertyChanged
{
    ...

    public void changeStudent()
    {
        this.StudentLastName = "McDonald";
    }
}

In the other case, second solution, you have to raise events whenever a model property changes and then get your view model to suscribe to these changes. You can proceed like this in the model:

class Student : INotifyPropertyChanged
{
    ...

    private string studentLastName;

    public string StudentLastName
    {
        get
        {
            return this.studentLastName;
        }

        set
        {
            if(this.studentLastname != value)
            {
                this.studentLastName = value;
                this.OnPropertyChanged("StudentLastName");
            }
        }
    }
}

And for the view model:

class StudentViewModel : INotifyPropertyChanged
{
    ...

    public StudentViewModel(Student model)
    {
        this._model = model;

        this._model.PropertyChanged += (sender, e) =>
        {
            if(e.PropertyName == "StudentLastName")
            {
                this.OnPropertyChanged("StudentLastName");
            }
        };
    }
}

Both solution will work. It is really import that you understand that your code explicitely needs to notifies the interface whenever a value changes.



回答2:

ChangeStudent doesn't call any of the methods that trigger a property notify event in the view model, it alters the underlying model instead. It's these events that trigger the view to update itself.

As an aside you should also look at command binding from the view instead of using click handlers in the code-behind. That way your view doesn't need to know anything about the view model that's attached and can be pure presentation.



回答3:

First you should use commands instead of events.

In your current structure you have to add an

OnPropertyChanged("StudentLastName");

call to your ChangedStudent() Method in StudentViewModel. After that you have to set the UpdateSourceTrigger of the Bindings to PropertyChanged

Text="{Binding Path=StudentFirstName, UpdateSourceTrigger=PropertyChanged}"