I'm new to C# and XAML, and I'm trying to learn how to work with data context and bindings in a MVVM design pattern. I've written a very simple program. The program allows the user to create a new person and displays the person's details in the MainWindow. To create a new person, the user selects Add new person from the MainWindow's menu. A window (PersonView) pops up where the user can enter the name and age for the person. When the user clicks Ok on the PersonView window an event fires that copies the Person's data over to a property in the MainWindow. Once data is copied over the PersonView window closes.
The problem is that the Person's details do not update in the main window. Using debugger, I can see that my PropertyChanged event triggers, and that the eventhandler in the MainWindow runs. I've set the DataContext (in c#) for the MainWindow equal to the Property in the MainWindow that contains the data I want to display. I've set the bindings for the Name and Age TextBlocks in XAML in MainWindow.
I've read this and this, but I still can't get it to work. What am I missing?
Code:
Person.cs:
{
public class Person
{
#region Constructors
public Person()
{
Name = "";
Age = 0;
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
#endregion
#region Properties
public string Name { get; set; }
public int Age { get; set; }
#endregion
#region Methods
#endregion
}
}
PersonViewModel.cs
using System.ComponentModel;
namespace MVVM_Basics.ViewModel
{
public class PersonViewModel : INotifyPropertyChanged
{
#region Constructors
public PersonViewModel()
{
_person.Name = "";
_person.Age = 0;
}
public PersonViewModel(string name, int age)
{
_person.Name = name;
_person.Age = age;
}
#endregion
#region Properties
Person _person = new Person();
public Person Person
{ get { return _person; }
set
{
_person = value;
OnPropertyChanged(new PropertyChangedEventArgs("Person"));
}
}
#region Events and EventHandlers
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged; //Associate event handler with event
handler?.Invoke(this, e); //invoke the delegate
}
#endregion
#endregion
#region Methods
#endregion
}
}
PersonView.xaml.cs
using System;
using System.Windows;
namespace MVVM_Basics.View
{
/// <summary>
/// Interaction logic for PersonView.xaml
/// </summary>
public partial class PersonView : Window
{
#region Constructors
public PersonView()
{
InitializeComponent();
}
#endregion
#region Properties
public PersonViewModel pvm { get; set; }
#endregion
#region Events and EventHandlers
public event EventHandler NewPersonCreated;
protected void OnNewPersonCreated(EventArgs e)
{
EventHandler handler = NewPersonCreated; //Link handler to event
handler?.Invoke(this, e); //Invoke handler
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
pvm = new PersonViewModel(this.NameTextBox.Text, Convert.ToInt32(this.AgeTextBox.Text));
OnNewPersonCreated(new EventArgs()); //Trigger the event
this.Close();
}
private void CancelEventHandler(object sender, RoutedEventArgs e)
{
//When the cancel button is clicked...
this.Close();
}
#endregion
#region Methods
#endregion
}
}
PersonView.xaml
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:MVVM_Basics.View"
mc:Ignorable="d"
Title="PersonView" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Name" Margin="30, 30"/>
<TextBox x:Name="NameTextBox" Grid.Row="0" Grid.Column="1" Margin="30, 30"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Age" Margin="30, 30"/>
<TextBox x:Name="AgeTextBox" Grid.Row="1" Grid.Column="1" Margin="30, 30"/>
<Button x:Name="OkButton" Content="Ok" Grid.Row="2" Grid.Column="0" Click="OkButton_Click" Margin="30, 30"/>
<Button x:Name="CancelButton" Content="Cancel" Grid.Row="2" Grid.Column="1" Click="CancelEventHandler" Margin="30, 30"/>
</Grid>
</Window>
MainWindow.xaml.cs
using MVVM_Basics.ViewModel;
using System;
using System.Windows;
namespace MVVM_Basics
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
#region Constructors
public MainWindow()
{
InitializeComponent();
pvm.Person.Name = "Name from constructor";
this.DataContext = pvm; //Set Window's data context
}
#endregion
#region Properties
public PersonViewModel pvm { get; set; } = new PersonViewModel();
#endregion
#region Events and EventHandlers
private void AddNewPerson(object sender, RoutedEventArgs e)
{
//When user clicks on File>New person...
PersonView pv = new PersonView();
pv.NewPersonCreated += Pv_NewPersonCreated; //Subscribe event handler to event
pv.Show();
}
private void Pv_NewPersonCreated(object sender, EventArgs e)
{
//This delegate is called when PersonView creates a new PersonViewModel object
PersonView pv = (PersonView)sender;
pvm = pv.pvm; //Copy the PersonViewModel object from PersonView to MainWindow
//NameTextBlock.Text = pvm.Person.Name; //This works, but I want to use bindings, not this hack
pv.NewPersonCreated -= Pv_NewPersonCreated; //Unsubscribe event handler from event, going to close PersonView shortly
}
#endregion
#region Methods
#endregion
}
}
MainWindow.xaml
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:MVVM_Basics" mc:Ignorable="d" Title="MainWindow" Height="450" Width="400" > <Grid> <Grid.RowDefinitions> <RowDefinition Height="25"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Menu Grid.Row="0"> <MenuItem Header="_File"> <MenuItem Header="Add _new person" Click="AddNewPerson"/> </MenuItem> </Menu> <Grid Grid.Row="1" Grid.Column="1" > <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--Subgrid Row 0--> <TextBlock Text="Name:" Grid.Row="0" Grid.Column="0"/> <TextBlock x:Name="NameTextBlock" Text="{Binding Person.Name}" Grid.Row="0" Grid.Column="1" MinWidth="50" /> <!--Subgrid Row 1--> <TextBlock Text="Age:" Grid.Row="1" Grid.Column="0"/> <TextBlock x:Name="AgeTextBlock" Text="{Binding Person.Age}" Grid.Row="1" Grid.Column="1"/> </Grid> </Grid> </Window>