I have read a number of debates on where to implement INotifyPropertyChanged here on StackOverflow and other blogs but it seems that there are cases where you have to implement it on the Model. Here is my scenario – I am looking for feedback on my conclusion or is my approach wrong.
I am using this implementation of an ObservableDictionary (ObservableDictionary) because I need performant queries using the key.
In this dictionary I place the collection of Model objects.
In my VM, I declare an instance (Books) of the dictionary and in the XAML bind to it.
<tk:DataGrid AutoGenerateColumns="False" Grid.Row="1" ItemsSource="{Binding Mode=TwoWay, Path=Books.Store}" Grid.ColumnSpan="2" Margin="3">
<tk:DataGrid.Columns>
<tk:DataGridTextColumn Binding="{Binding Mode=TwoWay, Path=Value.Name}" MinWidth="100" Header="Name" />
<tk:DataGridTextColumn Binding="{Binding Mode=TwoWay, Path=Value.Details}" MinWidth="300" Header="Details" />
</tk:DataGrid.Columns>
</tk:DataGrid>
If I implement INotifyPropertyChanged on the VM for Books and change the value of a Book name in code, the UI is not updated.
If I implement INotifyPropertyChanged on the VM for Store and change the value of a Book name in code, the UI is not updated.
If I implement INotifyProperyChanged on the Model and change the value a Book name in code, the UI is updated.
The Changed event is not fired in the first case because the Dictionary setter is not called, it's Item (a Book) is.
Am I missing something because if this is the correct interpretation, if I want consistent notifications for my Models regardless of whether they are bound to directly from XAML or via some sort of collection, I would always want the Model to implement INotifyProperyChanged.
Btw, besides the dll reference, I personally do no see INotifyPropertyChanged as a UI function – think it should be defined in a more general .net namespace – my 2 cents.
EDIT STARTS HERE:
We were having such a good semantics debate that I missed the core of my question so here it is posted again but with a very simple MVVM example illustrate my question.
The Models:
public class Book
{
public string Title { get; set; )
public List<Author> Authors { get; set; }
}
public class Author
{
public string Name { get; set; }
}
The Data Provider to generate some dummy data
public class BookProvider
{
public ObservableCollection<Book> GetBooks() {
ObservableCollection<Book> books = new ObservableCollection<Book>();
books.Add(new Book {
Title = "Book1",
Authors = new List<Author> { new Author { Name = "Joe" }, new Author { Name = "Phil" } }
});
books.Add(new Book {
Title = "Book2",
Authors = new List<Author> { new Author { Name = "Jane" }, new Author { Name = "Bob" } }
});
return books;
}
}
The ViewModel
public class BookViewModel : INotifyPropertyChanged
{
private ObservableCollection<Book> books;
public ObservableCollection<Book> Books {
get { return books; }
set {
if (value != books) {
books = value;
NotifyPropertyChanged("Books");
}
}
}
private BookProvider provider;
public BookViewModel() {
provider = new BookProvider();
Books = provider.GetBooks();
}
// For testing the example
public void MakeChange() {
Books[0].Title = "Changed";
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
XAML code behind
Would not normally to it this way – just for simple example
public partial class MainWindow : Window
{
private BookViewModel vm;
public MainWindow() {
InitializeComponent();
vm = new BookViewModel();
this.DataContext = vm;
}
private void Button_Click(object sender, RoutedEventArgs e) {
vm.MakeChange();
}
}
The XAML
<Window x:Class="BookTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="242*" />
<RowDefinition Height="69*" />
</Grid.RowDefinitions>
<ListBox ItemsSource="{Binding Books}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Title}" />
<ListBox ItemsSource="{Binding Authors}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" FontStyle="Italic" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="1" Content="Change" Click="Button_Click" />
</Grid>
As coded above, when I click on the button and change the value in the first Book, the UI does not change.
However, when I move the INotifyPropertyChanged to the Model it works fine (UI updates) because the change is in the Model property setter not Books in the VM:
public class Book : INotifyPropertyChanged
{
private string title;
public string Title {
get { return title; }
set {
if (value != title) {
title = value;
NotifyPropertyChanged("Title");
}
}
}
public List<Author> Authors { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
So back to my original question, how do I accomplish this without implementing INotifyPropertyChanged in the Model?
Thanks.
Best Answer
The thing is that if you were following MVVM, you would have a
BookViewModel
for yourBook
model class. So you would have aINotifyPropertyChanged
implementation on that view model. Exactly for that purpose MVVM exists (but not only).That being said, the
INotifyPropertyChanged
has to be implemented on view model classes, not models.UPDATE: In response to your update and our discussion in comments...
By
BookViewModel
I meant something else. You need to wrap in this view model not the whole collection ofBook
objects but an individualBook
:And your
BookProvider
will returnObservableCollection<BookViewModel>
instead ofObservableCollection<Book>
:As you can see, when you are updating the
Title
property of theBook
you will be doing it through theTitle
property of the corresponding view model that will raise thePropertyChanged
event, which will trigger the UI update.