This library helps validating Starcounter view-models annotated with ValidationAttributes
, defined in System.ComponentModel.DataAnnotations
and custom.
- Installation
- Creating
IValidatorBuilder
- Declaring validation rules in the view-model
- Using IValidator
- Integrating with Starcounter.Uniform
- Custom validation rules
- Validating using services
- Validating collections
Table of contents generated with markdown-toc
This package is available on nuget. You can get it there. To install with CLI run:
Install-Package Starcounter.Validation
Requires Starcounter 2.4.0.7243 or later and .NET Framework 4.6.2.
The entry point to this library is IValidatorBuilder
interface and its implementation, ValidatorBuilder
.
If you're using Dependency Injection with Starcounter.Startup, simply add this feature in your Startup
class:
public class Startup: IStartup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddStarcounterValidation();
}
// ...
}
If you're not using DI, you can construct ValidatorBuilder
by hand:
var validatorBuilder = new ValidatorBuilder();
This library makes use of ValidationAttribute
. To use it, you have to add a reference to System.ComponentModel.DataAnnotations
assembly. You can use attributes provided by Microsoft in System.ComponentModel.DataAnnotations namespace or create your own custom attributes. For details on how to create and use them, refer to docs.microsoft.com.
This readme contains a small example of how you can annotate your view-model with validation attributes. Consider the following view-model:
{
"Name$": "",
"Breed": "",
"ValidationErrors": ""
}
To annotate it, you have to repeat its properties in the code-behind:
public partial class DogViewModel: Json, IBound<Dog>
{
[Required]
public string Name { get => Data.Name; set => Data.Name = value; }
[MaxLength(120)]
public string Breed { get => Data.Breed; set => Data.Breed = value; }
}
Data
. However, if also declare them in your code-behind, you have to explicitly bind them, as shown in the example above.
Here, the Name
is required not to be empty, and Breed
can't be longer than 120 characters.
To validate this view-model, first obtain an instance of IValidatorBuilder
. If you're using DI, declare it in your Init
. Otherwise create it manually.
Next, you can call WithViewModelAndAllProperties
to specify the view-model instance you want to validate and use all of its validatable properties, WithResultsPresenter
to specify how to present the validation errors.
public partial class DogViewModel: Json, IBound<Dog>, IInitPageWithDependencies
{
private IValidator _validator;
public void Init(IValidatorBuilder validatorBuilder)
{
_validator = validatorBuilder
.WithViewModelAndAllProperties(this)
.WithResultsPresenter(PresentErrors)
.Build();
}
private void PresentErrors(string propertyName, IEnumerable<string> errors)
{
ValidationErrors +=
$"Property {propertyName} failed validation with following errors: {string.Join(", ", errors)}";
}
}
Instead of calling WithViewModelAndAllProperties(this)
you can specify view-model and properties manually:
_validator = validatorBuilder
.WithViewModel(this)
.AddProperty(nameof(Name)) // or use ValidatorBuilderExtensions.AddProperties
.AddProperty(nameof(Breed))
.WithResultsPresenter(PresentErrors)
.Build();
The IValidator
interface exposes two methods for validation:
bool Validate(string propertyName, object value);
Validates the supplied value, using rules from the specified property. Returns true if validation succeeded. The most common use of this would be in an input handler:
public void Handle(Input.Name input)
{
// this will use ResultsPresenter to display or clear validation errors for Name
_validator.Validate(nameof(Name), input.Value);
}
You can also use the return value to stop further processing if validation fails:
public void Handle(Input.Name input)
{
// this will use ResultsPresenter to display or clear validation errors for Name
if(!_validator.Validate(nameof(Name), input.Value))
{
return;
}
// further processing
}
bool ValidateAll();
Validates current values of all configured properties. Also calls ValidateAll
on all the sub validators (see "Validating collections").
Returns true if all the validation succeeded, false if any of it failed. Note that, unlike Validate, it uses actual values of the properties.
This method is usually called before saving the changes or performing a major action to make sure that the view-model is valid as a whole.
public void Handle(Input.SaveTrigger input)
{
// this will use ResultsPresenter to display or clear validation errors for Name
if(!_validator.ValidateAll())
{
return;
}
AttachedScope.Commit();
}
Most commonly, you would not write your custom error presenter, but rather use <uni-form-item>
, a custom element provided by Uniform for cases like this.
To use it, add Starcounter.Validation.Uniform
as a reference to your project:
Install-Package Starcounter.Validation.Uniform
Next, add it to your view-model:
{
"Name$": "",
"Breed": "",
"ValidationErrors": "",
"FormItemMetadata": {}
}
And finally, use BuildWithFormItemMetadata
instead of WithResultsPresenter
and Build
:
public void Init(IValidatorBuilder validatorBuilder)
{
_validator = validatorBuilder
.WithViewModel(this)
.AddProperty(nameof(Name))
.AddProperty(nameof(Breed))
.BuildWithFormItemMetadata(out var formItemMetadata);
FormItemMetadata = formItemMetadata;
}
Note that you don't have to specify DefaultTemplate.FormItemMetadata.InstanceType
.
When possible, try to reuse existing validation attributes from the System.ComponentModel.DataAnnotations namespace. However, when it's not possible, you can create your own custom validation attributes. When doing this, respect following guidelines:
- Your custom attribute class must derive from
ValidationAttribute
and its name should end withAttribute
. - You can either override
bool IsValid(object)
orValidationResult IsValid(object, ValidationContext)
. - If the value you're validiting is null (or empty), return validation success. If your data shouldn't be null nor empty, apply
[Required]
to your properties. - When returning validation errors you should return
new ValidationResult(FormatErrorMessage(validationContext.DisplayName))
. That way future users of your attribute can customize the error message withErrorMessage
,ErrorMessageResourceName
andErrorMessageResourceType
.
When you want to use services in your custom validation attributes - including database access services like a repository - use validationContext
, as shown below:
public class UniqueUsernameAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) // always return success on null, it's a job for [Required]
{
return ValidationResult.Success;
}
var usersRepository = validationContext.GetRequiredService<IUsersRepository>();
return usersRepository.GetUserByName((string)value) == null
? ValidationResult.Success
: new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
public override bool RequiresValidationContext => true;
}
Suppose you have a view-model, where you allow editing elements of a collection and you want to validate it. To do that, you have to add a validator to every element of the collection.
In this example, we will use <uni-form-item>
integration described in the previous paragraph.
{
"PackName$": "",
"FormItemMetadata": {},
"PackMembers": [{
"Name$": ""
"FormItemMetadata": {}
}]
}
To create validators for the collection, use CreateSubValidatorBuilder
method:
public class Dog
{
public string Name {get; set;}
public string Breed {get; set;}
}
public class Pack
{
public string PackName {get; set;}
public IEnumerable<Dog> GetMembers()
{
// ...
}
}
public partial class PackViewModel : Json, IBound<Pack>
{
private IValidator _validator;
public void Init(IValidatorBuilder validatorBuilder)
{
_validator = validatorBuilder
.WithViewModelAndAllProperties(this)
.BuildWithFormItemMetadata(out var formItemMetadata);
FormItemMetadata = formItemMetadata;
foreach (var dog in Data.GetMembers())
{
AddMemberViewModel(dog);
}
}
public void Handle(Input.AddTrigger trigger)
{
AddMemberViewModel(new Dog());
}
private void AddMemberViewModel(Dog dog)
{
var packMemberViewModel = new PackMemberViewModel();
packMemberViewModel.Init(dog, _validator.CreateSubValidatorBuilder());
PackMembers.Add(packMemberViewModel);
}
[PackViewModel_json.PackMembers]
public partial class PackMemberViewModel : Json, IBound<Dog>
{
private IValidator _validator;
[Required]
public string Name
{
get => Data.Name;
set => Data.Name = value;
}
public void Init(Dog dog, IValidatorBuilder validatorBuilder)
{
Data = dog;
_validator = validatorBuilder
.WithViewModel(this)
.AddProperty(nameof(Name))
.BuildWithFormItemMetadata(out var formItemMetadata);
// it refers here to FormItemMetadata of PackMemberViewModel, not of PackViewModel
FormItemMetadata = formItemMetadata;
}
}
}
If you allow removing elements from the list, you will also need to dispose of their validators when you do that:
public partial class PackViewModel : Json, IBound<Pack>
{
// some members omitted for brevity
private IValidator _validator;
private void AddMemberViewModel(Dog dog)
{
var packMemberViewModel = new PackMemberViewModel();
// pass RemoveMember
packMemberViewModel.Init(dog, RemoveMember, _validator.CreateSubValidatorBuilder());
PackMembers.Add(packMemberViewModel);
}
private void RemoveMember(PackMemberViewModel packMemberViewModel)
{
packMemberViewModel.Data.Delete();
PackMembers.Remove(packMemberViewModel);
// call view-model's Dispose method, which in turn disposes of the validator
packMemberViewModel.Dispose();
}
[PackViewModel_json.PackMembers]
public partial class PackMemberViewModel : Json, IBound<Dog>, IDisposable
{
private IValidator _validator;
private Action<PackMemberViewModel> _removeAction;
public void Init(Dog dog, Action<PackMemberViewModel> removeAction, IValidatorBuilder validatorBuilder)
{
_removeAction = removeAction;
}
public void Dispose()
{
// Dispose will detach this sub-validator from its parent
// and it will no longer respond to its parent ValidateAll
_validator?.Dispose();
}
}
}
The full example is presented below:
public partial class PackViewModel : Json, IBound<Pack>
{
private IValidator _validator;
public void Init(IValidatorBuilder validatorBuilder)
{
_validator = validatorBuilder
.WithViewModelAndAllProperties(this)
.BuildWithFormItemMetadata(out var formItemMetadata);
FormItemMetadata = formItemMetadata;
foreach (var dog in Data.GetMembers())
{
AddMemberViewModel(dog);
}
}
public void Handle(Input.AddTrigger trigger)
{
AddMemberViewModel(new Dog());
}
private void AddMemberViewModel(Dog dog)
{
var packMemberViewModel = new PackMemberViewModel();
packMemberViewModel.Init(dog, RemoveMember, _validator.CreateSubValidatorBuilder());
PackMembers.Add(packMemberViewModel);
}
private void RemoveMember(PackMemberViewModel packMemberViewModel)
{
packMemberViewModel.Data.Delete();
PackMembers.Remove(packMemberViewModel);
packMemberViewModel.Dispose();
}
[PackViewModel_json.PackMembers]
public partial class PackMemberViewModel : Json, IBound<Dog>, IDisposable
{
private IValidator _validator;
private Action<PackMemberViewModel> _removeAction;
[Required]
public string Name
{
get => Data.Name;
set => Data.Name = value;
}
public void Init(Dog dog, Action<PackMemberViewModel> removeAction, IValidatorBuilder validatorBuilder)
{
Data = dog;
_removeAction = removeAction;
_validator = validatorBuilder
.WithViewModel(this)
.AddProperty(nameof(Name))
.BuildWithFormItemMetadata(out var formItemMetadata);
// it refers here to FormItemMetadata of PackMemberViewModel, not of PackViewModel
FormItemMetadata = formItemMetadata;
}
public void Handle(Input.RemoveTrigger trigger)
{
_removeAction(this);
}
public void Dispose()
{
_validator?.Dispose();
}
}
}