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.
📌 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));
}
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));
}
}
}
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));
}
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);
}
}
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;
}
}
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);
}
}
CustomerFields.cs
namespace PropertyChangedApp1.Models;
public partial class Customer
{
private string _firstName;
private string _lastName;
private DateOnly _birthDate;
}
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;
}
}
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>
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
}
}
}
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>
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();
}
Once the code has been compiled, hover over SentenceCaseRegex and get nice intellisense.
To see the source generated add the following to the project file.
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>GeneratedStuff</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
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.