Validation
Bulletcode.NET relies on the standard .NET mechanism of validation based on validation attributes, with some important differences:
- client-side validation in web applications uses modern JavaScript and doesn’t require jQuery
- validation is also supported in desktop applications
- several additional validation attributes are available
- validation errors are easier to translate
Validation attributes
The following built-in .NET validation attributes are supported by Bulletcode.NET:
Required
— the value cannot be null or emptyStringLength
— specifies the maximum, and optionally minimum, length of a stringRange
— specifies the minimum and maximum value for numeric propertiesCompare
— the value of a property must be equal to the value of another propertyRegularExpression
— the string must match the given regular expressionEmailAddress
— the string must be a valid email address (i.e. must contain exactly one@
)Phone
— the string must be a valid phone number (i.e. must contain at least one digit and any number of space and+-.()
characters)Url
— the string must be a valid URL withhttps://
,http://
orftp://
schemeCreditCard
— the string must be a valid credit card (i.e. must contain digits with valid checksum and any number of space and-
characters)
The following validation attributes are defined in Bulletcode.Common and supported in both web and desktop applications:
GreaterThan
— the value of a property must be greater than or equal, or strictly greater than, the value of another property (numeric datetime, date and time values are supported)Step
— the number must be a multiple of the specified valueRequiredIf
— the value is required if another property evaluates totrue
, otherwise it can benull
or emptyOnlyIf
— the value is only allowed if another property evaluates totrue
, otherwise it must benull
The following validation attributes are defined in Bulletcode.Web.Core:
Mandatory
— the value of a Boolean property must betrue
AllowedExtensions
— the uploaded file must have one of the specified extensionsMaxFileSize
— the uploaded file cannot exceed the specified maximum size (in megabytes)
In addition, the DataType
attribute can also be used in web applications with the following data types:
DataType.DateTime
,DataType.Date
andDataType.Time
can be used to disambiguate the formatDateTime
values, which affects the mode of the date picker component and the client-side validation — by default,DataType.DateTime
is assumedDataType.Password
can be used with string properties to use a password input componentDataType.Currency
can be used with numeric properties to use a currency input component (i.e. a numeric input with two decimal places and a currency prefix or suffix)
When you implement a custom validation attribute, use an error message accessor function and the StaticTranslator<T>
to ensure that error messages are correctly translated using the current language:
public class ExampleAttribute : ValidationAttribute
{
public static string ExampleErrorMessage => StaticTranslator<StepAttribute>._( "{0} is invalid." );
public ExampleAttribute()
: base( () => ExampleErrorMessage )
{
}
}
Bulletcode.NET changes the default error messages for built-in .NET validation attributes listed above to ensure that they are more consistent.
Web applications
The default localization mechanism of ASP.NET Core relies on .resx
resource files. Bulletcode.NET uses a different mechanism, based on .po
and .mo
files, as explained in the chapter about Internationalization. This mechanism is automatically registered when the AddCoreMvcServices()
method is called (see the MVC chapter). The AddLocalization()
method from ASP.NET should not be used.
- A custom implementation of
IStringLocalizerFactory
provides translations of display names of model properties and custom error messages specified by theErrorMessage
property. When no translation domains are available for the given type, it falls back to the default, resource-based string localizer. ValidationMessageProvider
replaces the default error messages in built-in validation attributes. Some error messages generated by the model binding messages are also customized.AdapterModelValidatorProvider
fixes the error messages to use the translated display name of theOtherProperty
in case ofCompare
andGreaterThan
attributes, by replacing them with error messages generated by client-side attribute adapters, which are correctly translated.
In addition to translating data annotations and binding errors, Bulletcode.NET also fixes some problems with model binding related to internationalization:
- Decimal and floating-point numbers in the locale-invariant format are supported, which is consistent with how
<input type="number">
form elements work. By default, form values are expected to be formatted in the locale-specific format. - Query string parameters are parsed in the locale-specific format, which makes it possible to use search forms with the
GET
method. By default, query string parameters are expected to use the locale-invariant format.
Sometimes validation rules are different for a model depending on the scenario in which it’s used. For example, entering a password may be required when adding a new user, but optional when editing an existing user. The RequiredIf
and OnlyIf
validation attributes can be used for such conditional validation, along with the BindScenario
attribute described in the MVC chapter.
Client-side validation in ASP.NET Core applications works by appending additional HTML attributes to form input elements, prefixed by data-val
. They are designed to work with the jQuery Validation Plugin, however Bulletcode.NET uses custom JavaScript validators which don’t require jQuery (see below).
Each validation attribute has a corresponding attribute adapter, which generates these HTML attributes, and ASP.NET provides the adapters for the built-in .NET validation attributes.
Bulletcode.NET uses two different mechanisms for client-side validation. In classic server-side views, the HTML attributes are used, and the form handler, which is attached to every form on the page, handles validation by listening to the change
, focusout
and submit
events. When the ClientView mode is used, information about validation rules is serialized as part of the model metadata, and the validation itself is handled directly by input components, such as Input
, Select
and TextArea
.
In order to support client-side validation in ClientView mode, Bulletcode.NET uses a custom IClientValidationProvider
service, which is defined in the Bulletcode.Common library and implemented in Bulletcode.Web.Core. This service creates the validation rules based on validation attributes.
In order to implement custom validation attributes which support client-side validation, inherit the abstract ClientValidationAttribute
class and implement the IsValid()
and GetClientValidationRule()
methods, and — if necessary — both versions of the FormatErrorMessage()
method, for example:
public class ExampleAttribute : ClientValidationAttribute
{
public static string ExampleErrorMessage
=> StaticTranslator<StepAttribute>._( "{0} is invalid with param {1}." );
public ExampleAttribute( int param )
: base( () => ExampleErrorMessage )
{
Param = param;
}
public int Param { get; set; }
public override string FormatErrorMessage( string name )
{
return string.Format( CultureInfo.CurrentCulture, ErrorMessageString, name, Param );
}
public override string FormatErrorMessage( IStringLocalizer stringLocalizer, string name )
{
return stringLocalizer[ ErrorMessageString, name, Param ];
}
public override ClientValidationRule GetClientValidationRule( Type objectType, PropertyInfo property )
{
return new ClientValidationRule( "example" )
{
{ "param", Param },
};
}
public override bool IsValid( object value )
{
if ( value != null )
{
// check if the value is valid
}
return true;
}
}
The second overload of FormatErrorMessage()
, with an IStringLocalizer
argument, is used when a custom error message is passed to the attribute, to ensure that it’s properly translated.
The information returned by GetClientValidationRule()
will be converted to data-val
HTML attributes in server-side views, or serialized in the model metadata when the ClientView mode is used. Note that the custom client-side validator must be registered as shown in the next section.
In addition to explicit client-side validation rules, which are generated from validation attributes, implicit client-side validations are also generated, based on the types of model properties:
- When a property has a non-nullable type, the required rule is added even if the
Required
validation attribute is not explicitly specified. - When a property has a numeric type, the number validation rule is added. ASP.NET does this only for decimal and floating-point types, but Bulletcode.NET also adds client-side validation rules for integer types (
int
,long
, etc.). - When a property has the
DateTime
type, and the validationDataType
attribute is not specified, the datetime client-side validation rule is added.
Client-side code
The Bulletcode.Web.Front library contains client-side code which handles client-side validations. It provides built-in validators corresponding to all supported validation attributes, and implicit client-side validation rules.
Some of the built-in validator rely on the JavaScript validation API. In order for this to work, the input elements must have the appropriate type, which is ensured automatically by Bulletcode.NET in both classic server-side views and ClientView components. For example, the implicit number
and integer
validators check the badInput
property to detect invalid numbers in <input type="number">
. The email
and url
validator check the typeMismatch
property to detect invalid input in email
and url
inputs, respectively.
The requiredif
validator only works if the condition property is a Boolean property directly associated with a checkbox. If the condition property is calculated based on some expression, a custom validator should be implemented. In order to do that, a custom handler must be registered and attached to the form containing the validated field.
To register a custom form handler, use the registerHandler()
function from the @bc-front
, passing the name of the handler and the handler function, which takes the target form element as an argument. The handler should retrieve the built-in form handler, which is created for every form on the page, using the form()
function. Then it should call the addValidator()
method, which takes the field name, the name of the validator (in this case, requiredif
), the validator function, an optional custom error message (if null
, the default message returned by the validator will be used), and an object containing parameters (in this case, the name of the condition field).
For example, to make the field required only if the condition is false
, i.e. the checkbox is not checked, use the following code:
import { form, getValidator, registerHandler } from '@bc-front';
export function ConditionalValidation( target ) {
const targetForm = form( target );
const required = getValidator( 'required' );
targetForm.addValidator( 'ConditionalField', 'requiredif', requiredifFalse, null,
{ condition: '*.Checkbox' } );
function requiredifFalse( value, { condition }, context ) {
const conditionContext = context.other( condition );
if ( conditionContext != null && conditionContext.value === false )
return required( value, {}, context );
return null;
}
}
registerHandler( 'ConditionalValidation', ConditionalValidation );
The validator takes the value of the field being validated, the parameters passed from addValidator()
or from the data-val
HTML attributes, and a context object containing the following read-only properties and methods:
value
— the value of the fieldlabel
— the field’s label (equivalent to the display name of the model’s property)valid
— the current validation state of the field (true
means valid,false
means invalid,null
means not yet validated)name
— the name of the fieldinput
— the HTML input element associated with the fieldother( otherName )
— returns the context of the form field with the given name (the'*.'
prefix can be used for compatibility with the jQuery Validation Plugin)
The custom validator function shown above calls the built-in required
validator, which can be retrieved using the getValidator()
function.
The validator should return null
if the field is valid, or an error message if it’s invalid. Use the _()
function described in the Internationalization chapter to ensure that the error message is correctly translated, and contains the field’s label, for example:
function customValidator( value, props, context ) {
if ( value != null && value.length > 2 )
return _( '{0} is too long.', context.label );
return null;
}
The default error message returned by the validator will be replaced by the custom error message passed to addValidator()
, if given, or specified by the data-val
HTML attributes, when the validator corresponds to a validation attribute.
The addValidator()
method of the form handler can be used not only to override the validation functions and parameters used by built-in validators, but also to add custom client-side validators. In this case, a custom, unique name of the validator should be specified.
The registerHandler()
function should be called before initialize()
in the entry script of the application (see Assets for more information about creating application scripts).
The handler can be attached to the form using the data-handler
attribute:
<form method="post" data-handler="ConditionalValidation">
<field asp-for="Checkbox" />
<field asp-for="ConditionalField" />
</form>
Custom validation functions can also be registered globally by calling the registerValidator()
functions. This is useful when you implement custom validation attributes with client-side validation, as shown above.
The same built-in validators and custom validators registered using registerValidator()
are also used in ClientView mode, based on the metadata information associated with the model. It is also possible to pass custom validators and replace the validator function and parameters for built-in validators, by passing the customRules
property to the Form
component (see the ClientView chapter for more information about ClientView components).
The customRules
is an object containing an array of rules for each form field. For example:
const customRules = {
'conditionalField': [
[ 'requiredif', requiredifFalse, { condition: 'checkbox' } ],
],
}
return [ Form, { customRules },
[ Field, { for: 'checkbox' } ],
[ Field, { for: 'conditionalField' } ],
];
Each rule is an array containing:
- the name of the validator (only when overriding a rule specified by the model metadata; it should not be specified when adding a custom validator)
- the validation function
- an optional custom error message
- an optional object containing parameters
The parameters of the validation function are identical as in case of classic views, so these function can be reused between server-side and client-side views. The only difference is that the field names, by convention, are converted to camelCase
, and the '*.'
prefix for referencing other fields is not used.
The OnlyIf
validation attribute is typically used for form fields which are only shown when some condition is fulfilled, and hidden otherwise. In classic views, this can be handled by adding the asp-hidden
attribute to the <field>
tag, to ensure that it’s initially hidden when the form is rendered. If the condition property is a Boolean property directly associated with a checkbox, then the built-in form handler automatically shows or hides the field based on the state of the checkbox. If the condition property is calculated based on some expression, a custom handler should be implemented which shows or hides the field when the condition changes.
For example, to make the field visible only if the condition is false
, i.e. the checkbox is not checked, use the following code:
import { form, registerHandler } from '@bc-front';
export function ConditionalFieldHandler( target ) {
const targetForm = form( target );
targetForm.addFieldHandler( 'ConditionalField', 'onlyif', onlyifFalseHandler,
{ condition: '*.Checkbox' } );
function onlyifFalseHandler( field, { condition } ) {
const conditionField = field.other( condition );
if ( conditionField != null )
field.hidden = conditionField.value === true;
}
}
registerHandler( 'ConditionalFieldHandler', ConditionalFieldHandler );
The parameters of the addFieldHandler
are similar to addValidator
, except the error message parameter which is not specified in this case.
The field
parameter which is passed to the handler is has the following properties and methods:
value
— the value of the field (if the field is a fieldset containing checkboxes, the value is an array containing values of input elements which are checked)label
— the field’s label (equivalent to the display name of the model’s property)hidden
— the field is hidden whentrue
and shown whenfalse
valid
— the current validation state of the field (true
means valid,false
means invalid,null
means not yet validated)name
— the name of the fieldinput
— the HTML input element associated with the fieldaddEventListener( type, listener )
— adds and event listener to the field’s input (if the field is a fieldset, the listener is added to all child input elements)focus()
— focuses the field’s input (if the field is a fieldset, focus is set to the first checkbox or selected radio button)other( otherName )
— returns the form field with the given name (the'*.'
prefix can be used for compatibility with the jQuery Validation Plugin)
The value
, hidden
, valid
and error
properties can be modified by the handler.
When the handler calls the field.other()
method, the target field is automatically tracked as its dependency. It means that when the value of the target field is changed, the handler function is automatically called. This way, the field is automatically shown or hidden when the state of the checkbox is changed.
Just like in the previous example, the handler can be attached to the form using the data-handler
attribute. The conditional field is initially hidden when the checkbox is checked using the asp-hidden
attribute:
<form method="post" data-handler="ConditionalFieldHandler">
<field asp-for="Checkbox" />
<field asp-for="ConditionalField" asp-hidden="Model.Checkbox" />
</form>
Field handlers can also be used for other purposes. One example is a read-only field whose value is automatically calculated depending on other fields. The ConditionalValidation.js
handler in the App.Web.Classic demo application, which is included in the Bulletcode.NET sources, contains an example.
The view model contains a read-only property called NumberOfDays
:
[Display( Name = "Number Of Days" )]
public int? NumberOfDays => DateFrom.HasValue && DateTo.HasValue
? (int)( DateTo.Value - DateFrom.Value ).TotalDays : null;
The corresponding read-only field is created using the asp-readonly
attribute:
<field asp-for="NumberOfDays" asp-readonly />
The custom form handler registers a field handler which automatically changes the value of the field when the DateFrom
and DateTo
fields are changed:
demoForm.addFieldHandler( 'NumberOfDays', 'days', calculateNumberOfDays,
{ from: '*.DateFrom', to: '*.DateTo' } );
function calculateNumberOfDays( field, { from, to } ) {
const fromField = field.other( from );
const toField = field.other( to );
if ( fromField != null && toField != null ) {
let result = '';
// calculate the value
field.value = result;
}
}
In ClientView mode, the OnlyIf
validation attribute doesn’t automatically hide the field, but this can be easily achieved using the 'if'
directive:
[ Form,
[ Field, { for: 'checkbox' } ],
[ 'if', model.checkbox,
[ Field, { for: 'visibleConditionally' } ],
],
]
A read-only field can be created using a computed()
value:
const numberOfDays = computed( () => {
const dateFrom = model.dateFrom();
const dateTo = model.dateTo();
// calculate and return the value
} );
return [ Form,
[ Field, { for: 'dateFrom' } ],
[ Field, { for: 'dateTo' } ],
[ Field, { for: 'numberOfDays', value: numberOfDays, readonly: true } ],
];
Desktop applications
WPF supports validation using the INotifyDataErrorInfo
. This interface is implemented by the ObservableValidator
class, which is inherited by BaseViewModel
(see MVVM for more details).
View model properties are automatically validated when the overloaded version of SetProperty()
is called, for example:
public class MyViewModel : BaseViewModel
{
private string _name;
[Required]
[Display( Name = "Name" )]
public string Name
{
get => _name;
set => SetProperty( ref _name, value, true );
}
}
All built-in .NET validation attributes and common validation attributes defined in Bulletcode.Common are supported.
Just like in case of web applications, validation error messages and display names of properties are automatically translated using the Internationalization mechanism implemented by Bulletcode.NET, based on .po
and .mo
files. This is handled automatically by ObservableValidator
.
The simplest way of implementing custom validators in desktop applications is by using the CustomValidation
attribute, and implementing a static method in the view model class, for example:
public class MyViewModel : BaseViewModel
{
private string _name;
[CustomValidation( typeof( MyViewModel ), nameof( ValidateName ) )]
[Display( Name = "Name" )]
public string Name
{
get => _name;
set => SetProperty( ref _name, value, true );
}
public static ValidationResult ValidateName( string name, ValidationContext context )
{
if ( name != null && name.Length > 2 )
{
return new ValidationResult( StaticTranslator<MyViewModel>._( "{0} is too long.",
context.DisplayName ) );
}
return ValidationResult.Success;
}
}
In some cases, the validity of one property depends on the value of another property, for example in case of Compare
and GreaterThan
validation attributes or custom validators. The ValidationDependsOn
attribute can be used to specify that a particular property should be validated whenever the value of another property is changed, for example:
public class MyViewModel : BaseViewModel
{
private DateTime? _dateFrom;
private DateTime? _dateTo;
[Display( Name = "Date From" )]
public DateTime? DateFrom
{
get => _dateFrom;
set => SetProperty( ref _dateFrom, value, true );
}
[GreaterThan( nameof( DateFrom ) )]
[ValidationDependsOn( nameof( DateFrom ) )]
[Display( Name = "Date To" )]
public DateTime? DateTo
{
get => _dateTo;
set => SetProperty( ref _dateTo, value, true );
}
}
The optional IgnoreNull
parameter of the ValidationDependsOn
attribute specifies that the property should only be validated when its value is not null
.
Most WPF controls display a red border when the bound property of the view model has validator errors, but the error message is not automatically shown. A custom style should be defined in order to display error messages; the easiest way to do it is by creating a tooltip. For example:
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip">
<Setter.Value>
<ToolTip
DataContext="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget}"
>
<ItemsControl
ItemsSource="{Binding Path=(Validation.Errors)}"
DisplayMemberPath="ErrorContent"
/>
</ToolTip>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
This creates a tooltip when the Validation.HasError
attached property becomes true
, and displays a list of Validation.Errors
inside the tooltip. Similar styles must be created for other types of controls which support validation, for example ComboBox
.
In desktop applications, view model properties are validated immediately when their value is changed, so a text field is, by default, validated immediately while the user is typing. In web applications, on the other hand, text fields are only validated when the input element loses focus or when the form is submitted.
It is also possible to force validation of all properties of a view model which have at least one validation attribute, for example when a command is invoked by clicking a button. This can be done by calling the ValidateAllProperties()
method. The HasErrors
property can be used to check if the view model is valid.
private void Submit()
{
ValidateAllProperties();
if ( !HasErrors )
{
// TODO: perform an action
}
}
NOTE
The HasErrors
property only detects validation errors which happen at the level of the view model. In order to be able to also detect conversion errors, which can occur when binding a control’s value to a view model property, use the ConversionErrorsBehavior
.
For example, let’s assume that the view model has an integer property which is bound to a text box. A conversion error will occur when the entered value is not a valid integer.
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:ctrl="clr-namespace:Bulletcode.Desktop.Controls;assembly=Bulletcode.Desktop.Wpf"
>
<b:Interaction.Behaviors>
<ctrl:ConversionErrorsBehavior HasErrors="{Binding HasConversionErrors}"/>
</b:Interaction.Behaviors>
<StackPanel>
<TextBox Text="{Binding Value}"/>
</StackPanel>
</UserControl>
The behavior attached to the user control will set the HasConversionErrors
property of the view model to true
if any of the visible controls inside of the user control has any validation errors associated with value conversions. The view model can detect that and prevent the action from being performed.