ObservableProperty attribute
The ObservableProperty
type is an attribute that allows generating observable properties from annotated fields. Its purpose is to greatly reduce the amount of boilerplate that is needed to define observable properties.
Note
In order to work, annotated fields need to be in a partial class with the necessary INotifyPropertyChanged
infrastructure. If the type is nested, all types in the declaration syntax tree must also be annotated as partial. Not doing so will result in a compile errors, as the generator will not be able to generate a different partial declaration of that type with the requested observable property.
Platform APIs:
ObservableProperty
,NotifyPropertyChangedFor
,NotifyCanExecuteChangedFor
,NotifyDataErrorInfo
,NotifyPropertyChangedRecipients
,ICommand
,IRelayCommand
, ObservableValidator,PropertyChangedMessage<T>
,IMessenger
How it works
The ObservableProperty
attribute can be used to annotate a field in a partial type, like so:
[ObservableProperty]
private string? name;
And it will generate an observable property like this:
public string? Name
{
get => name;
set => SetProperty(ref name, value);
}
It will also do so with an optimized implementation, so the end result will be even faster.
Note
The name of the generated property will be created based on the field name. The generator assumes the field is named either lowerCamel
, _lowerCamel
or m_lowerCamel
, and it will transform that to be UpperCamel
to follow proper .NET naming conventions. The resulting property will always have public accessors, but the field can be declared with any visibility (private
is recommended).
Running code upon changes
The generated code is actually a bit more complex than this, and the reason for that is that it also exposes some methods you can implement to hook into the notification logic, and run additional logic when the property is about to be updated and right after it is updated, if needed. That is, the generated code is actually similar to this:
public string? Name
{
get => name;
set
{
if (!EqualityComparer<string?>.Default.Equals(name, value))
{
string? oldValue = name;
OnNameChanging(value);
OnNameChanging(oldValue, value);
OnPropertyChanging();
name = value;
OnNameChanged(value);
OnNameChanged(oldValue, value);
OnPropertyChanged();
}
}
}
partial void OnNameChanging(string? value);
partial void OnNameChanged(string? value);
partial void OnNameChanging(string? oldValue, string? newValue);
partial void OnNameChanged(string? oldValue, string? newValue);
This allows you to implement any of those methods to inject additional code. The first two are useful whenever you want to run some logic that only needs to reference the new value that the property has been set to. The other two are useful whenever you have some more complex logic that also has to update some state on both the old and new value being set.
For instance, here is an example of how the first two overloads can be used:
[ObservableProperty]
private string? name;
partial void OnNameChanging(string? value)
{
Console.WriteLine($"Name is about to change to {value}");
}
partial void OnNameChanged(string? value)
{
Console.WriteLine($"Name has changed to {value}");
}
And here is an example of how the other two overloads can be used:
[ObservableProperty]
private ChildViewModel? selectedItem;
partial void OnSelectedItemChanging(ChildViewModel? oldValue, ChildViewModel? newValue)
{
if (oldValue is not null)
{
oldValue.IsSelected = true;
}
if (newValue is not null)
{
newValue.IsSelected = true;
}
}
You're free to only implement any number of methods among the ones that are available, or none of them. If they are not implemented (or if only one is), the entire call(s) will just be removed by the compiler, so there will be no performance hit at all for cases where this additional functionality is not required.
Note
The generated methods are partial methods with no implementation, meaning that if you choose to implement them, you cannot specify an explicit accessibility for them. That is, implementations of these methods should also be declared as just partial
methods, and they will always implicitly have private accessibility. Trying to add an explicit accessibility (eg. adding public
or private
) will result in an error, as that is not allowed in C#.
Notifying dependent properties
Imagine you had a FullName
property you wanted to raise a notification for whenever Name
changes. You can do that by using the NotifyPropertyChangedFor
attribute, like so:
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
private string? name;
This will result in a generated property equivalent to this:
public string? Name
{
get => name;
set
{
if (SetProperty(ref name, value))
{
OnPropertyChanged("FullName");
}
}
}
Notifying dependent commands
Imagine you had a command whose execution state was dependent on the value of this property. That is, whenever the property changed, the execution state of the command should be invalidated and computed again. In other words, ICommand.CanExecuteChanged
should be raised again. You can achieve this by using the NotifyCanExecuteChangedFor
attribute:
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(MyCommand))]
private string? name;
This will result in a generated property equivalent to this:
public string? Name
{
get => name;
set
{
if (SetProperty(ref name, value))
{
MyCommand.NotifyCanExecuteChanged();
}
}
}
In order for this to work, the target command has to be some IRelayCommand
property.
Requesting property validation
If the property is declared in a type that inherits from ObservableValidator, it is also possible to annotate it with any validation attributes and then request the generated setter to trigger validation for that property. This can be achieved with the NotifyDataErrorInfo
attribute:
[ObservableProperty]
[NotifyDataErrorInfo]
[Required]
[MinLength(2)] // Any other validation attributes too...
private string? name;
This will result in the following property being generated:
public string? Name
{
get => name;
set
{
if (SetProperty(ref name, value))
{
ValidateProperty(value, "Value2");
}
}
}
That generated ValidateProperty
call will then validate the property and update the state of the ObservableValidator
object, so that UI components can react to it and display any validation errors appropriately.
Note
By design, only field attributes that inherit from ValidationAttribute
will be forwarded to the generated property. This is done specifically to support data validation scenarios. All other field attributes will be ignored, so it is not currently possible to add additional custom attributes on a field and have them also be applied to the generated property. If that is required (eg. to control serialization), consider using a traditional manual property instead.
Sending notification messages
If the property is declared in a type that inherits from ObservableRecipient
, you can use the NotifyPropertyChangedRecipients
attribute to instruct the generator to also insert code to send a property changed message for the property change. This will allow registered recipients to dynamically react to the change. That is, consider this code:
[ObservableProperty]
[NotifyPropertyChangedRecipients]
private string? name;
This will result in the following property being generated:
public string? Name
{
get => name;
set
{
string? oldValue = name;
if (SetProperty(ref name, value))
{
Broadcast(oldValue, value);
}
}
}
That generated Broadcast
call will then send a new PropertyChangedMessage<T>
using the IMessenger
instance in use in the current viewmodel, to all registered subscribers.
Adding custom attributes
In some cases, it might be useful to also have some custom attributes over the generated properties. To achieve that, you can simply use the [property: ]
target in attribute lists over annotated fields, and the MVVM Toolkit will automatically forward those attributes to the generated properties.
For instance, consider a field like this:
[ObservableProperty]
[property: JsonRequired]
[property: JsonPropertyName("name")]
private string? username;
This will generate a Username
property, with those two [JsonRequired]
and [JsonPropertyName("name")]
attributes over it. You can use as many attribute lists targeting the property as you want, and all of them will be forwarded to the generated properties.
Examples
- Check out the sample app (for multiple UI frameworks) to see the MVVM Toolkit in action.
- You can also find more examples in the unit tests.
MVVM Toolkit