In WPF apps, should the implementation of INotifyPropertyChanged only be in the ViewModel?

Rod Falanga 561 Reputation points
2020-10-01T00:54:39.513+00:00

I'm assisting in writing a WPF app. One thing I've done only in the ViewModel (VM) to date, is define properties associated with the data from the model, so that I can use MVVM Light's RaisePropertyChanged() method in the setter of the property. That's worked well, especially with getting notifications of when a value has changed by the user so something else in the usercontorl/window/page could be updated. However, we have a tendency to write windows, pages and usercontrols that only use one table at a time, so writing the additional properties in the VM was a bit tedious, but not too bad.

However, I've now got to work with eight tables in one usercontrol. That's a lot of additional, boilerplate code to write in the VM, which I'd resigned myself to do, but really thought it would be great if someone else has fixed this. Surely, I thought, someone must have addressed this issue. That brought me to a NuGet package named PropertyChanged.Fody. This really looks like it addresses my needs.

But the more I thought about PropertyChanged.Fody, the more I realized that it is used with partial classes that are generated by Entity Framework from the database, using EF's Class First from Existing Database. So, a partial class, corresponding to whatever generated model partial class, handles the INotifyPropertyChanged (INPC) event. It was at this point that I began to question whether or not I should continue with PropertyChanged.Fody. First, there's the question, according to the GitHub repo, of becoming a patron on OpenCollective or having a TideLift subscription. (I'm not familiar with either at this point, but if money's involved, I strongly doubt my employer would be interested.) But more important, where should an implementation of INPC occur? I've always been under the impression it should be only in the VM. To have it in the model class, even if it's in another file that's part of the partial class, seems wrong. And when I looked at Stack Overflow for guidance on this issue, I came across this post. In that post people passionately defend put INPC only in the VM, whereas others said yes, put it also in the views.

So, should INPC only be put into VMs? Or is it acceptable to put INPC into model classes as well?

Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,679 questions
0 comments No comments
{count} votes

Accepted answer
  1. Peter Fleischer (former MVP) 19,231 Reputation points
    2020-10-01T04:41:31.873+00:00

    Hi,
    INotifyPropertyChanged can be used wherever changes are to be reported to the user of an object. This can be in the ViewModel to update the UI. This can also be necessary in data objects if the collection does not forward changes in properties of a data object, but this is necessary, e.g. to update the UI. INotifyPropertyChanged can also be used to report changes in values ​​of the properties of a data object to the business logic so that it can react to the changes.

    Example for forwarding NotifyPropertyChanged in data objects to UI:

    Imports System.Collections.ObjectModel
    Imports System.Collections.Specialized
    Imports System.ComponentModel
    
    ''' <summary>
    ''' Implements the "ItemPropertyChanged" Event for a ObservableCollection
    ''' </summary>
    ''' <typeparam name="T"></typeparam>
    ''' <seealso cref="System.Collections.ObjectModel.ObservableCollection(Of T)" />
    Public NotInheritable Class TrulyObservableCollection(Of T As INotifyPropertyChanged)
      Inherits ObservableCollection(Of T)
      Implements ICollectionItemPropertyChanged(Of T)
    
      ''' <summary>
      ''' Initializes a new instance of the <see cref="TrulyObservableCollection(Of T)"/> class.
      ''' </summary>
      Public Sub New()
        AddHandler CollectionChanged, AddressOf FullObservableCollectionCollectionChanged
      End Sub
    
      ''' <summary>
      ''' Initializes a new instance of the <see cref="TrulyObservableCollection(Of T)"/> class.
      ''' </summary>
      ''' <param name="pItems">The p items.</param>
      Public Sub New(pItems As IEnumerable(Of T))
        MyClass.New
        For Each itm In pItems
          Me.Add(itm)
        Next
      End Sub
    
      Public Event ItemChanged As EventHandler(Of ItemChangedEventArgs(Of T)) Implements ICollectionItemPropertyChanged(Of T).ItemChanged
    
      ''' <summary>
      ''' Fulls the observable collection collection changed.
      ''' </summary>
      ''' <param name="sender">The sender.</param>
      ''' <param name="e">The <see cref="NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
      Private Sub FullObservableCollectionCollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
        If e.NewItems IsNot Nothing Then
          For Each itm In e.NewItems
            AddHandler CType(itm, INotifyPropertyChanged).PropertyChanged, AddressOf ItemPropertyChanged
          Next
        End If
        If e.OldItems IsNot Nothing Then
          For Each itm In e.OldItems
            RemoveHandler CType(itm, INotifyPropertyChanged).PropertyChanged, AddressOf ItemPropertyChanged
          Next
        End If
      End Sub
    
      ''' <summary>
      ''' Items the property changed.
      ''' </summary>
      ''' <param name="sender">The sender.</param>
      ''' <param name="e">The <see cref="PropertyChangedEventArgs"/> instance containing the event data.</param>
      Private Sub ItemPropertyChanged(sender As Object, e As PropertyChangedEventArgs)
        Dim args As New CollectionItemPropertyChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf(CType(sender, T)), e.PropertyName)
        OnCollectionChanged(args)
      End Sub
    
    End Class
    
    Friend Interface ICollectionItemPropertyChanged(Of T)
      Event ItemChanged As EventHandler(Of ItemChangedEventArgs(Of T))
    End Interface
    
    Public Class ItemChangedEventArgs(Of T)
      Public ReadOnly Property ChangedItem As T
      Public ReadOnly Property PropertyName As String
    
      Public Sub New(item As T, propertyName As String)
        Me.ChangedItem = item
        Me.PropertyName = propertyName
      End Sub
    End Class
    
    Public Class CollectionItemPropertyChangedEventArgs
      Inherits NotifyCollectionChangedEventArgs
    
      Public Sub New(action As NotifyCollectionChangedAction, newItem As Object, oldItem As Object, index As Integer, itemPropertyName As String)
        MyBase.New(action, newItem, oldItem, index)
        If itemPropertyName Is Nothing Then Throw New ArgumentNullException(NameOf(itemPropertyName))
        PropertyName = itemPropertyName
      End Sub
    
      ''' <summary>
      ''' Gets the name of the collection item's property that changed.
      ''' </summary>
      ''' <returns>The name of the collection item's property that changed.</returns>
      Public Overridable ReadOnly Property PropertyName As String
    End Class
    
    'Using this Event
    
    'Public Class Demo
    
    '  Private WithEvents c As New TrulyObservableCollection(Of Demo2)
    
    '  Private Sub c_CollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs) Handles c.CollectionChanged
    '    Dim e1 = TryCast(e, CollectionItemPropertyChangedEventArgs)
    '    If e1 Is Nothing Then Exit Sub
    '    If e1.PropertyName = "SomeProperty" Then
    '      ' deal with item property change
    '    End If
    '  End Sub
    'End Class
    
    'Public Class Demo2
    '  Implements INotifyPropertyChanged
    
    '  Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    
    'End Class
    

    Or another implementation in C#:NET

      public sealed class TrulyObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
      {
        public TrulyObservableCollection() => CollectionChanged += FullObservableCollectionCollectionChanged;
        public TrulyObservableCollection(IEnumerable<T> pItems) : this()
        {
          foreach (var item in pItems) this.Add(item);
        }
        private void FullObservableCollectionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
          if (e.NewItems != null) foreach (Object item in e.NewItems) ((INotifyPropertyChanged)item).PropertyChanged += ItemPropertyChanged;
          if (e.OldItems != null) foreach (Object item in e.OldItems) ((INotifyPropertyChanged)item).PropertyChanged -= ItemPropertyChanged;
        }
        private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e) => OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, sender, sender, IndexOf((T)sender)));
      }
    
    1 person found this answer helpful.
    0 comments No comments

1 additional answer

Sort by: Most helpful
  1. Emon Haque 3,176 Reputation points
    2020-10-02T03:56:51.873+00:00

    I mostly have written apps for fun, demonstration and entertainment and by no means I'm an expert. To me there're 3 scenarios: 1) you don't inherit it in model at all, 2) you inherit and call OnPropertyChanged in one or more properties in the model and 3) you inherit but don't call OnPropertyChanged in any property in the model. In the first case I've a model like this:

    [Serializable]
    

    public class Plot
    {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

        public bool IstValid()
        {
            return !MainVM.PlotBusy &&
                Id > 0 &&
                !string.IsNullOrWhiteSpace(Name) &&
                !string.IsNullOrWhiteSpace(Description) &&
                MainVM.plots.FirstOrDefault(x => x.Name.ToLower() == Name.Trim().ToLower()) == null;
        }
    

    }

    In add/edit ViewModel I create instance of Plots this way:

    Plot newPlot;
    public Plot NewPlot { get => newPlot; set { newPlot = value; OnPropertyChanged(); } }
    

    add it in the collection and I edit a deep copy of a Plot and replace that with the existing one in collection so no need of INPC in model. In second case:

    [Serializable]
    

    public class Space : Notifiable
    {
    public int Id { get; set; }
    public int PlotId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

        bool isVacant;
        public bool IsVacant { get => isVacant; set { isVacant = value; OnPropertyChanged(); } }
    

    public bool IsValid()
    {
    return !MainVM.SpaceBusy &&
    Id > 0 &&
    PlotId > 0 &&
    !string.IsNullOrWhiteSpace(Name) &&
    !string.IsNullOrWhiteSpace(Description) &&
    MainVM.spaces.Where(x => x.PlotId == (MainVM.Plots.CurrentItem as Plot).Id)
    .FirstOrDefault(x => x.Name.ToLower() == Name.Trim().ToLower()) == null;
    }
    }

    I've to inherit and call OnPropertyChanged in IsVacant property because that is used in the LiveFiltering of my ICollectionView. In third case:

    [Serializable]
    

    public class Lease : Notifiable
    {
    public int Id { get; set; }
    public int PlotId { get; set; }
    public int SpaceId { get; set; }
    public int TenantId { get; set; }
    public DateTime Date { get; set; }
    public string Business { get; set; }
    public ObservableCollection<Receivable> FixedReceivables { get; set; }

        public Lease() => App.Current.Dispatcher.Invoke(() => FixedReceivables = new ObservableCollection<Receivable>());
    

    public bool IsValid()
    {
    return !MainVM.LeaseBusy &&
    Id > 0 &&
    PlotId > 0 &&
    SpaceId > 0 &&
    TenantId > 0 &&
    !string.IsNullOrWhiteSpace(Business) &&
    FixedReceivables.Count > 0;
    }
    }

    It inherits INPC but doesnt callOnPropertyChangedanywhere in model. In someDataTemplateI've reference ofPlotId,SpaceIdandTenantIdand it retrives Name of Plot/Space viaIValueConverterso when the Name property of Plot/Space changes I've to callOnPropertyChangedon the relevantLeaseobjects in the collection this waymatchedLease.OnPropertyChanged("PlotId/SpaceId/TenantId")` to update the UI

    Let me know if you're aware of any more scenarios.

    EDIT

    As I'm updating the codebase of one of my Projects, I've noticed an issue in the first case. My Plot collection is bound to several controls in different views and there are filters applied to several other collections based on the Current/Selected Plot. As soon as I replace an existing Plot with edited plot, CurrentChanged fires and all filters are reapplied. So to fix that problem I've implemented INPC there and now instead of replacing object in the collection, I update the properties of current/selected.

    1 person found this answer helpful.
    0 comments No comments