Awesome
PropertyChanged.SourceGenerator
Implementing INotifyPropertyChanged
/ INotifyPropertyChanging
is annoying. PropertyChanged.SourceGenerator hooks into your compilation process to generate the boilerplate for you, automatically.
PropertyChanged.SourceGenerator works well if you're using an MVVM framework or going without, and supports various time-saving features such as:
- Automatically notifying dependent properties.
- Calling hooks when particular properties change.
- Keeping track of whether any properties have changed.
Table of Contents
- Installation
- Quick Start
- Versioning
- Defining your ViewModel
- Defining Properties
- Property Dependencies
- Property Changed Hooks
- Change Tracking with
[IsChanged]
- Configuration
- Contributing
- Comparison to PropertyChanged.Fody
Installation
PropertyChanged.SourceGenerator is available on NuGet. You'll need to be running Visual Studio 2019 16.9 or higher, or by building using the .NET SDK 5.0.200 or higher (your project doesn't have to target .NET 5, you just need to be building using a newish version of the .NET SDK).
These dependencies may change in future minor versions, see Versioning.
If you're using Visual Studio 17.2.6 with WPF and you get lots of "... already contains a definition for ..." error messages, see this bug and workaround.
Quick Start
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[Notify] private string _lastName;
public string FullName => $"Dr. {LastName}";
}
Make sure your ViewModel is partial
, and define the backing fields for your properties, decorated with [Notify]
. When you build your project, PropertyChanged.SourceGenerator will create a partial class which looks something like:
partial class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string LastName
{
get => _lastName;
set
{
if (!EqualityComparer<string>.Default.Equals(_lastName, value))
{
_lastName = value;
OnPropertyChanged(EventArgsCache.LastName);
OnPropertyChanged(EventArgsCache.FullName);
}
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
PropertyChanged?.Invoke(args);
}
}
What happened there?
- PropertyChanged.SourceGenerator spotted that you defined a partial class and at least one property was decorated with
[Notify]
, so it got involved and generated another part to the partial class. - It noticed that you hadn't implemented
INotifyPropertyChanged
, so it implemented it for you (it's also fine if you implement it yourself). - For each field decorated with
[Notify]
, it generated property with the same name (but with the leading_
removed and the first letter capitalised) which used that field as its backing field. That property implementedINotifyPropertyChanged
. - It noticed that
FullName
depended onLastName
, so raised thePropertyChanged
event forFullName
wheneverLastName
changed.
Note: It's really important that you don't refer to the backing fields after you've defined them: let PropertyChanged.SourceGenerator generate the corresponding properties, and then always use those propertues.
Versioning
Source Generators are a relatively new technology, and they're being improved all the time. Unfortunately, in order for source generators to take advantage of improvements, they must target a newer version of Visual Studio / the .NET SDK.
If/when PropertyChanged.SourceGenerator is updated to depend on a new version Visual Studio / the .NET SDK, this will be signified by a minor version bump: the minor version number will be incremented. Changes which mean you have to change existing code to keep PropertyChanged.SourceGenerator working will be signified by a major version bump.
Version Number | Min Visual Studio Version | Min .NET SDK Version |
---|---|---|
1.0.x | 2019 16.9 | 5.0.200 |
1.1.x | 2022 17.3 | 6.0.304 |
Defining your ViewModel
INotifyPropertyChanged
When you define a ViewModel which makes use of PropertyChanged.SourceGenerator, that ViewModel must be partial
. If it isn't, you'll get a warning.
Your ViewModel can implement INotifyPropertyChanged
, or not, or it can implement parts of it (such as implementing the interface but not defining the PropertyChanged
event), or it can extend from a base class which implements INotifyPropertyChanged
: PropertyChanged.SourceGenerator will figure it out and fill in the gaps.
If you've got a ViewModel
base class which implements INotifyPropertyChanged
(perhaps as part of an MVVM framework), PropertyChanged.SourceGenerator will try and find a suitable method to call in order to raise the PropertyChanged
event. It will look for a method called OnPropertyChanged
, RaisePropertyChanged
, NotifyOfPropertyChange
, or NotifyPropertyChanged
, which covers all of the major MVVM frameworks (although this is configurable, see Configuration), with one of the following signatures:
void OnPropertyChanged(PropertyChangedEventArgs args)
void OnPropertyChanged(string propertyName)
void OnPropertyChanged(PropertyChangedEventArgs args, object oldValue, object newValue)
void OnPropertyChanged(string propertyName, object oldValue, object newValue)
If it can't find a suitable method, you'll get a warning and it won't run on that particular ViewModel.
INotifyPropertyChanging
Working with INotifyPropertyChanging
is similar, with the caveat that your class or one of its base classes must implement INotifyPropertyChanging
(not everone wants to implement this interface, so it's opt-in). You don't need to implement all of the interface members: PropertyChanged.SourceGenerator will step in and fill in the gaps.
As with INotifyPropertyChanged
, PropertyChanged.SourceGenerator will try and find a suitable method to call in order to raise the PropertyChanging
event. It will look for a method called OnPropertyChanging
, RaisePropertyChanging
, NotifyOfPropertyChanging
, or NotifyPropertyChanging
(again this is configurable, see Configuration), with one of the following signatures:
void OnPropertyChanging(PropertyChangingEventArgs args)
void OnPropertyChanging(string propertyName)
void OnPropertyChanging(PropertyChangingEventArgs args, object oldValue)
void OnPropertyChanging(string propertyName, object oldValue)
Defining Properties
To get PropertyChanged.SourceGenerator to generate a property which implements INotifyPropertyChanged
, you must define the backing field for that property, and decorate it with [Notify]
.
(This is an annoying effect of how Source Generators work. If you'd like a better way, please vote for this issue on partial properties).
If you write:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel : INotifyPropertyChanged
{
[Notify] private int _foo;
}
PropertyChanged.SourceGenerator will generate something like:
partial class MyViewModel
{
public int Foo
{
get => _foo,
set
{
if (!EqualityComparer<int>.Default.Equals(_foo, value))
{
_foo = value;
OnPropertyChanged(EventArgsCache.Foo);
}
}
}
// PropertyChanged event, OnPropertyChanged method, etc.
}
Property Names
The name of the generated property is derived from the name of the backing field, by:
- Removing a
_
prefix, if one exists - Changing the first letter to upper-case
This can be customised, see Configuration.
If you want to manually specify the name of a particular property, you can pass a string to [Notify]
:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel : INotifyPropertyChanged
{
[Notify("FOO")] private int _foo;
}
PropertyChanged.SourceGenerator will generate a property called FOO
.
Property Accessibility
By default, all generated properties have public getters and public setters.
This isn't always what you want, so it's possible to override this by passing Getter.XXX
and Setter.XXX
to [Notify]
.
using PropertyChanged.SourceGenerator;
public partial class MyViewModel : INotifyPropertyChanged
{
[Notify(Setter.Private)]
private int _foo;
[Notify(Getter.PrivateProtected, Setter.Protected)]
private string _bar;
}
This generates:
partial class MyViewModel
{
public int Foo
{
get => _foo
private set { /* ... */ }
}
protected string Bar
{
private protected get => _bar,
set { /* ... */ }
}
}
Property Doc Comments
Any XML doc comments applied to your field will be copied to the generated property. Note that any such comments must appear before the [Notify]
attribute.
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
/// <summary>
/// The Foo property
/// </summary>
[Notify] private int _foo;
}
Generates:
partial class MyViewModel
{
/// <summary>
/// The Foo property
/// </summary>
public int Foo
{
// ...
}
}
Attributes on Generated Properties
Sometimes you need the to place attributes onto the generated property, e.g. to control validation or serialization. You can do this by passing a string containing this attribute to the [PropertyAttribute]
attribute, e.g.:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[PropertyAttribute("[System.Xml.Serialization.XmlIgnore]")]
[Notify] private int _foo;
}
The string that you pass to [PropertAttribute]
will be pasted into the generated file verbatim. It's important to note that the generated file doesn't have any using
statements, so you need to fully-qualify all types. For example:
[PropertyAttribute("[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]")]
rather than just:
[PropertyAttribute("[EditorBrowsable(EditorBrowsabeState.Never)]")]
Virtual Properties
If you want the generated property to be virtual
, use:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel : INotifyPropertyChanged
{
[Notify(IsVirtual = true]
private int _foo;
}
Generates:
partial class MyViewModel
{
public virtual int Foo
{
// ...
}
}
Property Dependencies
Sometimes, you have properties which depend on other properties, for example:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[Notify] private string _firstName;
[Notify] private string _lastName;
public string FullName => $"{FirstName} {LastName}";
}
Whenever FirstName
or LastName
is updated, you want to raise a PropertyChanged event for FullName
, so that the UI also updates the value of FullName
which is displayed.
Automatic Dependencies
If a property has a getter which accesses a generated property on the same type, then PropertyChanged.SourceGenerator will automatically raise a PropertyChanged event every time the property it depends on changes.
For example, if you write:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[Notify] private string _lastName;
public string FullName => $"Dr. {LastName}";
}
PropertyChanged.SourceGenerator will notice that the getter for FullName
accesses the generated LastName
property, and so it will add code to the LastName
setter to raise a PropertyChanged event for FullName
whenever LastName
is set:
partial class MyViewModel : INotifyPropertyChanged
{
public string LastName
{
get => _lastName;
set
{
if (!EqualityComparer<string>.Default.Equals(_lastName, value))
{
_lastName = value;
OnPropertyChanged(EventArgsCache.LastName);
OnPropertyChanged(EventArgsCache.FullName); // <-- Here
}
}
}
}
If the property being depended on is not being generated by PropertyChanged.SourceGenerator, or is defined in a base class, then PropertyChanged.SourceGenerator handles this by overriding the OnPropertyChanged
method. For example:
public partial class Base
{
[Notify] private string _firstName;
}
public partial class Derived : Base
{
[Notify] private string _middleName;
public string LastName { get; }
public string FullName => $"{FirstName} {MiddleName} {LastName}";
}
This will generate something like the following OnPropertyChanged
method for Derived
:
partial class Derived
{
protected override void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
{
base.OnPropertyChanged(eventArgs);
switch (eventArgs.PropertyName)
{
case "FirstName":
case "LastName":
OnPropertyChanged(EventArgsCache.FullName);
break;
}
}
}
Note that this cannot work for getters which access properties on other types, or on other instances of the current type. Also note that your property getter must reference the generated property and not its backing field (i.e. LastName
, not _lastName
above).
Automatic dependency resolution does not happen if the property is decorated with [DependsOn(...)]
. Therefore, to disable automatic dependency resolution for a single property, you can decorate it with an empty [DependsOn]
. To disable automatic dependency resolution across your entire project, set propertychanged.auto_notify = false
in your .editorconfig, see Configuration.
Manual Dependencies with [DependsOn]
If automatic dependencies aren't working for you, you can also specify dependencies manually using the [DependsOn]
attribute. [DependsOn]
takes the names of one or more generated properties, and means that a PropertyChanged event will be raised if any of those properties are set.
For example:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[Notify] private string _firstName;
[Notify] private string _lastName;
[DependsOn(nameof(FirstName), nameof(LastName))]
public string FullName { get; set; }
}
The generated setters for FirstName
and LastName
will raise a PropertyChanged event for FullName
.
As with automatic dependencies, [DependsOn]
can refer to properties in the current class or base classes. It can also refer to properties which don't actually exist, which means you can refer to properties on derived classes as well.
Manual Dependencies with [AlsoNotify]
[AlsoNotify]
is the opposite of [DependsOn]
: you place it on a backing field which also has [Notify]
, and PropertyChanged.SourceGenerator will insert code to raise a PropertyChanged for each named property whenever the generated property is set.
The named property or properties don't have to exist (although you'll get a warning if they don't), and you can raise PropertyChanged events for properties in base classes.
If you're naming a property which is generated by PropertyChanged.SourceGenerator, make sure you use the name of the generated property, and not the backing field.
For example:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[Notify, AlsoNotify(nameof(FullName))] private string _firstName;
[Notify, AlsoNotify(nameof(FullName))] private string _lastName;
public string FullName { get; set; }
}
Property Changed Hooks
Hooks are a way for you to be told when a generated property is changed, without needing to subscribe to a type's own PropertyChanged event.
Type Hooks with OnAnyPropertyChanged
/ OnAnyPropertyChanging
INotifyPropertyChanged
The easiest way to be notified when any generated property has changed is to specify an OnAnyPropertyChanged
method. This is called from the generated OnPropertyChanged
method.
This method can have the following signatures, and any accessibility:
void OnAnyPropertyChanged(string propertyName);
void OnAnyPropertyChanged(string propertyName, object oldValue, object newValue);
In order for PropertyChanged.SourceGenerator to be able to supply values for oldValue
and newValue
, the OnPropertyChanged
method in your base class must have a signature which also has these parameters.
Note that the oldValue
might be null
, if the property is being raised because of a property dependency.
INotifyPropertyChanging
To be notified before a generated property changes, you can specify an OnAnyPropertyChanging
method.
This method can have the following signatures, and any accessibility:
void OnAnyPropertyChanged(string propertyName);
void OnAnyPropertyChanged(string propertyName, object oldValue);
In order for PropertyChanged.SourceGenerator to be able to supply values for oldValue
, the OnPropertyChanging
method in your base class must have a signature which also has this parameter.
Note that the oldValue
might be null
, if the property is being raised because of a property dependency.
Property Hooks with On{PropertyName}Changed
/ On{PropertyName}Changing
INotifyPropertyChanged
Let's say you have a generated property called FirstName
. If you define a method called OnFirstNameChanged
in the same class, that method will be called every time FirstName
changes.
This method can have two signatures:
On{PropertyName}Changed()
.On{PropertyName}Changed(T oldValue, T newValue)
whereT
is the type of the property calledPropertyName
.
For example:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[Notify] private string _firstName;
[Notify] private string _lastName;
private void OnFirstNameChanged(string oldValue, string newValue)
{
// ...
}
private void OnLastNameChanged()
{
// ...
}
}
Note that oldValue
might have a value of default(T)
, if the property is being raised because of a property dependency.
INotifyPropertyChanging
Using the example above, if you define a method called OnFirstNameChanging
in the same class, that method will be called just before FirstName
changes.
This method can have two signatures:
On{PropertyName}Changing()
.On{PropertyName}Changing(T oldValue)
whereT
is the type of the property calledPropertyName
.
Note that oldValue
might have a value of default(T)
, if the property is being raised because of a property dependency.
Change tracking with [IsChanged]
Sometimes you need to keep track of whether any properties on a type have been set.
If you define a bool
property and decorate it with [IsChanged]
, then that property will be set to true
whenever any generate properties are set. It's then up to you to set it back to false
when appropriate.
For example:
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[IsChanged] public bool IsChanged { get; private set; }
[Notify] private string _firstName;
}
var vm = new MyViewModel();
Assert.False(vm.IsChanged);
vm.FirstName = "Harry";
Assert.True(vm.IsChanged);
That bool IsChanged
property can also be generated by PropertyChanged.SourceGenerator, if you want a PropertyChanged event to be raised when it changed;
using PropertyChanged.SourceGenerator;
public partial class MyViewModel
{
[Notify, IsChanged] private bool _isChanged;
}
Configuration
Various aspects of PropertyChanged.SourceGenerator's behaviour can be configured through a .editorconfig
file.
If you have one already, great! If not simply add a file called .editorconfig
in the folder which contains your .csproj
file (if you want those settings to apply to a single project), or next to your .sln
file (to apply them to all projects in the solution). There are various other ways to combine settings from different .editorconfig
files, see the MSDN documentation.
All of PropertyChanged.SourceGenerator's settings must be in a [*.cs]
section.
Generated Property Names
There are a few settings which control how PropertyChanged.SourceGenerator turns the name of your backing field into the name of the property it generates.
[*.cs]
# A string to add to the beginning of any generated property name
# Default: ''
propertychanged.add_prefix =
# A semicolon-delimeted list of values to remove from the beginning of any generated property name, if present
# Default: '_'
propertychanged.remove_prefixes = _
# A string to add to the end of any generated property name
# Default: ''
propertychanged.add_suffix =
# A semicolon-delimeted list of values to remove from the end of any generated property name
# Default: ''
propertychanged.remove_suffixes =
# How the first letter of the generated property name should be capitalised
# Valid values: none, upper_case, lower_Case
# Default: upper_case
propertychanged.first_letter_capitalization = upper_case
OnPropertyChanged
/ OnPropertyChanging
Method Name
When PropertyChanged.SourceGenerator runs, it looks for a suitable pre-existing method which can be used to raise the PropertyChanged and PropertyChanging event. If none is found, it will generate a suitable method itself, if it can.
The names of the pre-existing methods which it searches for, and the name of the method which it will generate, can be configured.
[*.cs]
# A ';' separated list of method names to search for when finding a method to raise the
# PropertyChanged event. If none is found, the first name listed here is used to generate one.
# Default: OnPropertyChanged;RaisePropertyChanged;NotifyOfPropertyChange;NotifyPropertyChanged
propertychanged.onpropertychanged_method_name = OnPropertyChanged;RaisePropertyChanged;NotifyOfPropertyChange;NotifyPropertyChanged
# A ';' separated list of method names to search for when finding a method to raise the
# PropertyChanging event. If none is found, the first name listed here is used to generate one.
# Default: OnPropertyChanging;RaisePropertyChanging;NotifyOfPropertyChanging;NotifyPropertyChanging
propertychanged.onpropertychanging_method_name = OnPropertyChanging;RaisePropertyChanging;NotifyOfPropertyChanging;NotifyPropertyChanging
Automatic Dependencies
To disable automatic property dependency resolution across your whole project, set:
[*.cs]
# Whether to enable automatic property dependency resolution
# Valid values: true, false
# Default: true
propertychanged.auto_notify = false
Contributing
It's great that you want to get involved, thank you! Please open a discussion before doing any serious amount of work, so we can agree an approach before you get started.
Open a feature branch based on develop
(not master
), and make sure that you submit any Pull Requests to the develop
branch.
Comparison to PropertyChanged.Fody
PropertyChanged.SourceGenerator has the same goals as PropertyChanged.Fody. Here are some of the differences:
- PropertyChanged.Fody is able to rewrite your code, which PropertyChanged.SourceGenerator can only add to it (due to the design of Source Generators). This means that PropertyChanged.Fody is able to insert event-raising code directly into your property setters, whereas PropertyChanged.SourceGenerator needs to generate the whole property itself.
- PropertyChanged.Fody supports some functionality which PropertyChanged.SourceGenerator does not, such as global interception. Please let me know if you need a bit of functionality which PropertyChanged.SourceGenerator doesn't yet support.
- PropertyChanged.SourceGenerator supports some functionality which PropertyChanged.Fody does not, such as letting you define
On{PropertyName}Changed
methods which accept the old and new values of the property. - PropertyChanged.Fody uses a variety of methods to locate a suitable method to compare a property's old and new value; PropertyChanged.SourceGenerator just uses
EqualityComparer<T>.Default
. - I don't expect you to pay to use PropertyChanged.SourceGenerator, and will never close an issue or refuse a contribution because you're not giving me money.