CommunityToolkit / CommunityToolkit/dotnet

Add properties for validation state and error

Open
#488 3 comments 0 reactions 0 assignees View on GitHub
feature request :mailbox_with_mail: mvvm-toolkit :toolbox: needs author feedback :memo:
Dominant language
C#
Stars
3.8k
Forks
400
PR merge metrics
No merged PRs in 30d

Description

### Overview

I tried to add validations for my sign in and sign forms based on ObservableValidator.

I found that there are many lines of code can be generated by a generator.

Here is [a snapshot of my code](https://github.com/tuyen-vuduc/chick-and-paddy-dotnet-maui/blob/main/src/ChickAndPaddy/Features/Auth/Models/ForgotPasswordFormModel.cs) to have validation in my form

```csharp
namespace ChickAndPaddy;

public abstract class BaseFormModel : ObservableValidator
{
protected virtual string[] ValidatableAndSupportPropertyNames => new string[0];

public virtual bool IsValid()
{
ValidateAllProperties();

foreach (var propertyName in ValidatableAndSupportPropertyNames)
{
OnPropertyChanged(propertyName);
}

return !HasErrors;
}
}

public partial class ForgotPasswordFormModel : BaseFormModel
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PhoneNumberValid), nameof(PhoneNumberInvalidMessage))]
[Required(ErrorMessage = "Your phone number is required to recover your password.")]
[Phone(ErrorMessage = "You have enter an invalid phone number.")]
[NotifyDataErrorInfo]
string phoneNumber;

public bool PhoneNumberValid => GetErrors(nameof(PhoneNumber)).Any() == false;
public string PhoneNumberInvalidMessage => GetErrors(nameof(PhoneNumber)).FirstOrDefault()?.ErrorMessage;

protected override string[] ValidatableAndSupportPropertyNames => new[]
{
nameof(PhoneNumber),
nameof(PhoneNumberValid),
nameof(PhoneNumberInvalidMessage),
};
}
````

**Another version**
```csharp
public partial class ForgotPasswordFormModel : BaseFormModel
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PhoneNumberErrors))]
[Required(ErrorMessage = "Your phone number is required to recover your password.")]
[Phone(ErrorMessage = "You have enter an invalid phone number.")]
[NotifyDataErrorInfo]
string phoneNumber;

public string PhoneNumberErrors => GetErrors(nameof(PhoneNumber));

protected override string[] ValidatableAndSupportPropertyNames => new[]
{
nameof(PhoneNumber),
nameof(PhoneNumberValid),
nameof(PhoneNumberInvalidMessage),
};
}
```

### API breakdown

- Add `IsValid` to `ObservableValidator` class
- To validate all properties and
- To notify validatable and support properties (so is the UI reflected)

- Changes to the generated observable property
- We should make a call to `SetProperty` instead of assigning backing field directly if the base class is `ObservableObject`
- We should call to `SetProperty` version of `ObservableValidator` if the owning class inherits from that operation with param `validate` assigned to true if there is any validation attributes

**Current generated code**
```csharp
///
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.ComponentModel.DataAnnotations.RequiredAttribute(ErrorMessage = "Please enter your phone number")]
[global::System.ComponentModel.DataAnnotations.PhoneAttribute(ErrorMessage = "Please enter a valid phone number")]
public string UserName
{
get => userName;
set
{
if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(userName, value))
{
OnUserNameChanging(value);
OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.UserName);
userName = value;
OnUserNameChanged(value);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserName);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameValid);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameInvalidMessage);
}
}
}
```

**Base class is ObjectValidator**
```csharp
///
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.ComponentModel.DataAnnotations.RequiredAttribute(ErrorMessage = "Please enter your phone number")]
[global::System.ComponentModel.DataAnnotations.PhoneAttribute(ErrorMessage = "Please enter a valid phone number")]
public string UserName
{
get => userName;
set
{
if (SetProperty(ref userName, value, global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.UserName))
{ OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameValid);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameInvalidMessage);
}
}
}
```

**Base class is ObservableObject**
```csharp
///
[global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.0.0.0")]
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.ComponentModel.DataAnnotations.RequiredAttribute(ErrorMessage = "Please enter your phone number")]
[global::System.ComponentModel.DataAnnotations.PhoneAttribute(ErrorMessage = "Please enter a valid phone number")]
public string UserName
{
get => userName;
set
{
var validate = true; // check by generator to know if there are any validation attributes attached to the field
if (SetProperty(ref userName, value, validate, global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.UserName))
{ OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameValid);
OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.UserNameInvalidMessage);
}
}
}
```

### Usage example

```
// Call this method to validate and notify validatable and support properties
validator.IsValid();
```

### Breaking change?

No

### Alternatives

Define properties manually

```csharp
namespace ChickAndPaddy;

public abstract class BaseFormModel : ObservableValidator
{
protected virtual string[] ValidatableAndSupportPropertyNames => new string[0];

public virtual bool IsValid()
{
ValidateAllProperties();

foreach (var propertyName in ValidatableAndSupportPropertyNames)
{
OnPropertyChanged(propertyName);
}

return !HasErrors;
}
}

public partial class ForgotPasswordFormModel : BaseFormModel
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PhoneNumberValid), nameof(PhoneNumberInvalidMessage))]
[Required(ErrorMessage = "Your phone number is required to recover your password.")]
[Phone(ErrorMessage = "You have enter an invalid phone number.")]
[NotifyDataErrorInfo]
string phoneNumber;

public bool PhoneNumberValid => GetErrors(nameof(PhoneNumber)).Any() == false;
public string PhoneNumberInvalidMessage => GetErrors(nameof(PhoneNumber)).FirstOrDefault()?.ErrorMessage;

protected override string[] ValidatableAndSupportPropertyNames => new[]
{
nameof(PhoneNumber),
nameof(PhoneNumberValid),
nameof(PhoneNumberInvalidMessage),
};
}
````

**Another version**
```csharp
public partial class ForgotPasswordFormModel : BaseFormModel
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PhoneNumberErrors))]
[Required(ErrorMessage = "Your phone number is required to recover your password.")]
[Phone(ErrorMessage = "You have enter an invalid phone number.")]
[NotifyDataErrorInfo]
string phoneNumber;

public string PhoneNumberErrors => GetErrors(nameof(PhoneNumber));

protected override string[] ValidatableAndSupportPropertyNames => new[]
{
nameof(PhoneNumber),
nameof(PhoneNumberValid),
nameof(PhoneNumberInvalidMessage),
};
}
```

### Additional context

_No response_

### Help us help you

Yes, I'd like to be assigned to work on this item

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.