Go to content

Bulletcode.NET

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 empty
  • StringLength — specifies the maximum, and optionally minimum, length of a string
  • Range — specifies the minimum and maximum value for numeric properties
  • Compare — the value of a property must be equal to the value of another property
  • RegularExpression — the string must match the given regular expression
  • EmailAddress — 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 with https://, http:// or ftp:// scheme
  • CreditCard — 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 value
  • RequiredIf — the value is required if another property evaluates to true, otherwise it can be null or empty
  • OnlyIf — the value is only allowed if another property evaluates to true, otherwise it must be null

The following validation attributes are defined in Bulletcode.Web.Core:

  • Mandatory — the value of a Boolean property must be true
  • AllowedExtensions — the uploaded file must have one of the specified extensions
  • MaxFileSize — 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 and DataType.Time can be used to disambiguate the format DateTime values, which affects the mode of the date picker component and the client-side validation — by default, DataType.DateTime is assumed
  • DataType.Password can be used with string properties to use a password input component
  • DataType.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 the ErrorMessage 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 the OtherProperty in case of Compare and GreaterThan 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 validation DataType 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 field
  • label — 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 field
  • input — the HTML input element associated with the field
  • other( 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 when true and shown when false
  • valid — the current validation state of the field (true means valid, false means invalid, null means not yet validated)
  • name — the name of the field
  • input — the HTML input element associated with the field
  • addEventListener( 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.