Model Notification changed C#

Karen Payne - Aug 17 - - Dev Community

Introduction

Learn how to clean up a class/model that implements INotifyPropertyChanged.

🚦 This not for everyone, some developer may find this too much work. Even if this not for everyone, the secondary lesson is working with partial classes.

Source code

📌 Coded in Microsoft Visual Studio 2022 using NET 8 Core.

Conventual implementation

Usually a developer will place property change notification directly in their classes as shown below.

using System.ComponentModel;

namespace PropertyChangedApp1.ModelsConventional;
public class Customer : INotifyPropertyChanged
{
    private string _firstName;
    private string _lastName;
    private DateOnly _birthDate;

    public string FirstName
    {
        get => _firstName;
        set
        {
            if (value == _firstName) return;
            _firstName = value;
            OnPropertyChanged(nameof(FirstName));
        }
    }

    public string LastName
    {
        get => _lastName;
        set
        {
            if (value == _lastName) return;
            _lastName = value;
            OnPropertyChanged(nameof(LastName));
        }
    }

    public DateOnly BirthDate
    {
        get => _birthDate;
        set
        {
            if (value.Equals(_birthDate)) return;
            _birthDate = value;
            OnPropertyChanged(nameof(BirthDate));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Enter fullscreen mode Exit fullscreen mode

Option 1

Make the class a partial class and create a second class with the same name in the same folder and give it a different name as shown below.

Customer.cs

Contains only properties.

using System.ComponentModel;

namespace PropertyChangedApp1.ModelsConventional;
public partial class Customer : INotifyPropertyChanged
{
    public string FirstName
    {
        get => _firstName;
        set
        {
            if (value == _firstName) return;
            _firstName = value;
            OnPropertyChanged(nameof(FirstName));
        }
    }

    public string LastName
    {
        get => _lastName;
        set
        {
            if (value == _lastName) return;
            _lastName = value;
            OnPropertyChanged(nameof(LastName));
        }
    }

    public DateOnly BirthDate
    {
        get => _birthDate;
        set
        {
            if (value.Equals(_birthDate)) return;
            _birthDate = value;
            OnPropertyChanged(nameof(BirthDate));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

CustomerPropertyChanged.cs

For fields, event for property change and the method for invoking property change.

using System.ComponentModel;

namespace PropertyChangedApp1.ModelsConventional;

public partial class Customer
{
    private string _firstName;
    private string _lastName;
    private DateOnly _birthDate;

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Enter fullscreen mode Exit fullscreen mode

Option 2

Like option 1 in that there are two Customer files, the difference between them is implementation of property change in this option is easier to implement. The get and set are one liners keeping code cleaner.

In the secondary partial class SetField method does the exact same work as in option 1 but cleaner as it's generic.

Customer.cs

using System.ComponentModel;


namespace PropertyChangedApp1.Models;

public partial class Customer : INotifyPropertyChanged
{
    public string FirstName
    {
        get => _firstName;
        set => SetField(ref _firstName, value);
    }

    public string LastName
    {
        get => _lastName;
        set => SetField(ref _lastName, value);
    }

    public DateOnly BirthDate
    {
        get => _birthDate;
        set => SetField(ref _birthDate, value);
    }
}
Enter fullscreen mode Exit fullscreen mode

CustomerPropertyChanged.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace PropertyChangedApp1.Models;
public partial class Customer
{
    private string _firstName;
    private string _lastName;
    private DateOnly _birthDate;

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Option 3

Let's break out fields to a separate file which is syntactically cleaner.

Customer.cs

using System.ComponentModel;


namespace PropertyChangedApp1.Models;

public partial class Customer : INotifyPropertyChanged
{
    public string FirstName
    {
        get => _firstName;
        set => SetField(ref _firstName, value);
    }

    public string LastName
    {
        get => _lastName;
        set => SetField(ref _lastName, value);
    }

    public DateOnly BirthDate
    {
        get => _birthDate;
        set => SetField(ref _birthDate, value);
    }
}
Enter fullscreen mode Exit fullscreen mode

CustomerFields.cs

namespace PropertyChangedApp1.Models;
public partial class Customer
{
    private string _firstName;
    private string _lastName;
    private DateOnly _birthDate;
}
Enter fullscreen mode Exit fullscreen mode

CustomerPropertyChanged.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace PropertyChangedApp1.Models;
public partial class Customer
{

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Fody

Another option which is even clean than above is using Fody NuGet package and PropertyChanged.Fody NuGet package.

Create FodyWeavers.xml in the project root, set Copy to Output Directory to Copy if newer.

<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <PropertyChanged />
</Weavers>
Enter fullscreen mode Exit fullscreen mode

Then in a class/model use : INotifyPropertyChanged.

That is it while there are other options that range from adding events and case insensitive checks.

Sample.

using System.ComponentModel;

namespace Fody1.Classes;

    public partial class Customer : INotifyPropertyChanged
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateOnly BirthDate { get; set; }

        private void OnLastNameChanged()
        {
            if (LastName == "doe") // case insensitive
            {
                // TODO
            }
        }

        private void OnFirstNameChanged()
        {
            // TODO
        }

        private void OnBirthDateChanged()
        {
            if (BirthDate < new DateOnly(2000,1,1))
            {
                // TODO
            }
        }

        protected void OnPropertyChanged(string propertyName, object before, object after)
        {
            if (propertyName == nameof(FirstName) && (string)after == "mary") // case insensitive
            {
                // TODO
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

To get case insensitive checks use Caseless.Fody Nuget package.

FodyWeavers.xml changes too.

<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <PropertyChanged />
  <Caseless/>
</Weavers>
Enter fullscreen mode Exit fullscreen mode

Partial classes

Partial classes have many uses such as presented above.

Another purpose is for .NET regular expression source generators.

Example using regular expressions, GeneratedRegexAttribute.

Sample code to convert the first character of a string to upper case.

using System.Text.RegularExpressions;

namespace PropertyChangedApp1.Classes;
public static partial class Helpers
{
    public static string ProperCased(this string source)
        => SentenceCaseRegex()
            .Replace(source.ToLower(), s => s.Value.ToUpper());
    /// <summary>
    /// Represents a regular expression used for converting the first letter of each sentence
    /// in a string to uppercase.
    /// </summary>
    /// <returns>
    /// A regular expression for sentence casing.
    /// </returns>
    [GeneratedRegex(@"(^[a-z])|\.\s+(.)", RegexOptions.ExplicitCapture)]
    private static partial Regex SentenceCaseRegex();
}
Enter fullscreen mode Exit fullscreen mode

Once the code has been compiled, hover over SentenceCaseRegex and get nice intellisense.

Shows intellisense for SentenceCaseRegex

To see the source generated add the following to the project file.

<PropertyGroup>
   <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
   <CompilerGeneratedFilesOutputPath>GeneratedStuff</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
Enter fullscreen mode Exit fullscreen mode

Build, traverse to the file, select properties and set Build Action to None.

Now examine the generated code.

Summary

This article has presented alternatives to implementing INotifyPropertyChanged from conventional to using a generic method to invoke changes to those listening along with a brief discussion on partial classes.

Regarding the options shown for property change, feel free to experiment with what has been provided.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .