From 9f7fb353430efe86b0848e91dd8e1f2da6798be1 Mon Sep 17 00:00:00 2001 From: Dongle <29563098+dongle-the-gadget@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:09:48 +0700 Subject: [PATCH 1/3] Update observable property generator to use partial properties --- docs/mvvm/generators/ObservableProperty.md | 58 ++++++++++++---------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/docs/mvvm/generators/ObservableProperty.md b/docs/mvvm/generators/ObservableProperty.md index 2125ca852..a02a9d397 100644 --- a/docs/mvvm/generators/ObservableProperty.md +++ b/docs/mvvm/generators/ObservableProperty.md @@ -18,26 +18,31 @@ The [`ObservableProperty`](/dotnet/api/microsoft.toolkit.mvvm.componentmodel.Obs ## How it works -The `ObservableProperty` attribute can be used to annotate a field in a partial type, like so: +The `ObservableProperty` attribute can be used to annotate a partial property in a partial type, like so: ```csharp [ObservableProperty] -private string? name; +public partial string? Name { get; set; } ``` And it will generate an observable property like this: ```csharp -public string? Name +public partial string? Name { - get => name; - set => SetProperty(ref name, value); + get => field; + set => SetProperty(ref field, value); } ``` It will also do so with an optimized implementation, so the end result will be even faster. > [!NOTE] +> Partial properties support in MVVM Toolkit requires the .NET 9 SDK and `LangVersion` to be set to `preview`. If that's not an option, you can use `[ObservableProperty]` on a field, like so: +> ```cs +> [ObservableProperty] +> private string? name; +> ``` > 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 @@ -47,16 +52,16 @@ The generated code is actually a bit more complex than this, and the reason for ```csharp public string? Name { - get => name; + get => field; set { - if (!EqualityComparer.Default.Equals(name, value)) + if (!EqualityComparer.Default.Equals(field, value)) { - string? oldValue = name; + string? oldValue = field; OnNameChanging(value); OnNameChanging(oldValue, value); OnPropertyChanging(); - name = value; + field = value; OnNameChanged(value); OnNameChanged(oldValue, value); OnPropertyChanged(); @@ -77,7 +82,7 @@ For instance, here is an example of how the first two overloads can be used: ```csharp [ObservableProperty] -private string? name; +public partial string> Name { get; set; } partial void OnNameChanging(string? value) { @@ -94,7 +99,7 @@ And here is an example of how the other two overloads can be used: ```csharp [ObservableProperty] -private ChildViewModel? selectedItem; +public partial ChildViewModel? SelectedItem { get; set; } partial void OnSelectedItemChanging(ChildViewModel? oldValue, ChildViewModel? newValue) { @@ -122,7 +127,7 @@ Imagine you had a `FullName` property you wanted to raise a notification for whe ```csharp [ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] -private string? name; +public partial string? Name; ``` This will result in a generated property equivalent to this: @@ -130,10 +135,10 @@ This will result in a generated property equivalent to this: ```csharp public string? Name { - get => name; + get => field; set { - if (SetProperty(ref name, value)) + if (SetProperty(ref field, value)) { OnPropertyChanged("FullName"); } @@ -148,7 +153,7 @@ Imagine you had a command whose execution state was dependent on the value of th ```csharp [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(MyCommand))] -private string? name; +public partial string? Name; ``` This will result in a generated property equivalent to this: @@ -156,10 +161,10 @@ This will result in a generated property equivalent to this: ```csharp public string? Name { - get => name; + get => field; set { - if (SetProperty(ref name, value)) + if (SetProperty(ref field, value)) { MyCommand.NotifyCanExecuteChanged(); } @@ -178,7 +183,7 @@ If the property is declared in a type that inherits from [ObservableValidator](/ [NotifyDataErrorInfo] [Required] [MinLength(2)] // Any other validation attributes too... -private string? name; +public partial string? Name { get; set; } ``` This will result in the following property being generated: @@ -186,10 +191,10 @@ This will result in the following property being generated: ```csharp public string? Name { - get => name; + get => field; set { - if (SetProperty(ref name, value)) + if (SetProperty(ref field, value)) { ValidateProperty(value, "Value2"); } @@ -200,7 +205,7 @@ public string? Name 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`](/dotnet/api/system.componentmodel.dataannotations.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. +> By design, only field attributes that inherit from [`ValidationAttribute`](/dotnet/api/system.componentmodel.dataannotations.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 partial properties instead. ## Sending notification messages @@ -209,7 +214,7 @@ If the property is declared in a type that inherits from [`ObservableRecipient`] ```csharp [ObservableProperty] [NotifyPropertyChangedRecipients] -private string? name; +public partial string? Name { get; set; } ``` This will result in the following property being generated: @@ -217,12 +222,12 @@ This will result in the following property being generated: ```csharp public string? Name { - get => name; + get => field; set { - string? oldValue = name; + string? oldValue = field; - if (SetProperty(ref name, value)) + if (SetProperty(ref field, value)) { Broadcast(oldValue, value); } @@ -234,6 +239,9 @@ That generated `Broadcast` call will then send a new [`PropertyChangedMessage ## Adding custom attributes +> [!NOTE] +> The following section applies only if `[ObservableProperty]` is annotated on a field. If `[ObservableProperty]` is used on a partial property, you can add the attribute on the partial property as you would a normal property. + 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: From b52e49319f640d20337afd17d4f0579f8397934f Mon Sep 17 00:00:00 2001 From: Dongle <29563098+dongle-the-gadget@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:12:26 +0700 Subject: [PATCH 2/3] Fix a typo --- docs/mvvm/generators/ObservableProperty.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mvvm/generators/ObservableProperty.md b/docs/mvvm/generators/ObservableProperty.md index a02a9d397..9dc8d1630 100644 --- a/docs/mvvm/generators/ObservableProperty.md +++ b/docs/mvvm/generators/ObservableProperty.md @@ -82,7 +82,7 @@ For instance, here is an example of how the first two overloads can be used: ```csharp [ObservableProperty] -public partial string> Name { get; set; } +public partial string? Name { get; set; } partial void OnNameChanging(string? value) { From 47cba215dfe79832b83ec965d0f9bace16b0c3b5 Mon Sep 17 00:00:00 2001 From: Dongle <29563098+dongle-the-gadget@users.noreply.github.com> Date: Fri, 6 Jun 2025 10:13:31 +0700 Subject: [PATCH 3/3] Fix some syntax errors --- docs/mvvm/generators/ObservableProperty.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/mvvm/generators/ObservableProperty.md b/docs/mvvm/generators/ObservableProperty.md index 9dc8d1630..d8227528a 100644 --- a/docs/mvvm/generators/ObservableProperty.md +++ b/docs/mvvm/generators/ObservableProperty.md @@ -127,7 +127,7 @@ Imagine you had a `FullName` property you wanted to raise a notification for whe ```csharp [ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] -public partial string? Name; +public partial string? Name { get; set; } ``` This will result in a generated property equivalent to this: @@ -153,7 +153,7 @@ Imagine you had a command whose execution state was dependent on the value of th ```csharp [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(MyCommand))] -public partial string? Name; +public partial string? Name { get; set; } ``` This will result in a generated property equivalent to this: