dotnet
dotnet copied to clipboard
Add properties for validation state and error
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 to have validation in my form
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
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
toObservableValidator
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 isObservableObject
- We should call to
SetProperty
version ofObservableValidator
if the owning class inherits from that operation with paramvalidate
assigned to true if there is any validation attributes
- We should make a call to
Current generated code
/// <inheritdoc cref="userName"/>
[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<string>.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
/// <inheritdoc cref="userName"/>
[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
/// <inheritdoc cref="userName"/>
[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
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
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
[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.")]
string phoneNumber;
It seems you didn't add [NotifyDataErrorInfo]
, doesn't that solve the issue already?
The generator doesn't generate validation code by default, that's by design, it's opt-in 🙂
@Sergio0694 Thanks for your suggestion.
By using [NotifyDataErrorInfo]
, we don't need partial void On[PropertyName]Changing(string value)
implementation any more.
However, if there is a way to add other properties as well, it will really save time and effort. (I am a bit old guy of web validation where I want to validate inputs individually as well as the form as a whole)
The other point, if we can make use of SetProperty
in ObservableObject or ObservableValidator, we can shorten the generated code.
How do you think?
If you look carefully, my suggestion brings in new methods to do the web form like validation,
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;
}
}
If we can bring them into ObservableValidator
class and generate ValidatableAndSupportPropertyNames
as the subclass, it'll really help.