Table of Contents

Filter validation rules

Validate the value of a filter against business rules, independently of the structural filter configuration (FilterRequired, FilterOperators, FilterVisible, ...) already available on a view property or an entity view parameter.

Overview

FilterValidationRules is a list of validation functions declared on IViewModelFilterableAttribute — the interface shared by view properties and entity view parameters, so both benefit from the same mechanism without any extra API.

Each rule receives an IFilterValidationContext — the condition to validate (Condition) and the whole filter currently being edited (Filter) — and returns:

  • null when the value is valid, or
  • a validation message (severity + text) otherwise.

Rules are registered once, in the Initialized event, and re-evaluate automatically whenever the filter's value or operator changes. When several rules are registered on the same attribute, they run in list order and the first one that reports a problem wins — the list order acts as the rule's priority.

Important

Never read a view's Datafilter inside a validation rule — it is always the last-committed filter, stale while the user is still editing. Use ctx.Filter instead, which is always the filter currently being edited (the in-progress draft when one exists, the committed filter otherwise).

Basic example

The following example validates a date range on two properties, using GetFirstValueAsDate() so the rule also works with relative date filters (e.g. "today + 2 days"), and a numeric threshold on an entity view parameter:

public Task OnInitialized(IUIRuleArguments args)
{
    Properties.StartDate.FilterValidationRules.Add(ctx =>
    {
        var start = ctx.Condition.GetFirstValueAsDate();
        var end = ctx.Filter?.Find("EndDate")?.Condition?.GetFirstValueAsDate();

        if (start.HasValue && end.HasValue && start > end)
        {
            return new UIValidationMessage(
                ValidationRuleSeverityLevel.Error,
                UIResources.Bookings.StartDateAfterEndDateError);
        }

        return null;
    }, FilterValidationRuleScope.SimpleFilterOnly);

    EntityViewParameters.MinimumDurationDays.Filterable = true;
    EntityViewParameters.MinimumDurationDays.FilterValidationRules.Add(ctx =>
    {
        var minimumDays = (int?)ctx.Condition.FirstValue;

        if (minimumDays.HasValue && minimumDays <= 0)
        {
            return new UIValidationMessage(
                ValidationRuleSeverityLevel.Error,
                UIResources.Bookings.MinimumDurationNotPositiveError);
        }

        return null;
    });

    return Task.CompletedTask;
}
Important

Always source the message text from localized UI resources (UIResources.<Module>.<Key>, backed by the module's StringResources metadata) rather than hard-coded literals, so the messages are translatable. UIResources.Bookings.* above is illustrative — replace it with your own module's keys.

Warning

On an entity view parameter, Filterable defaults to false. A rule registered on a parameter left at that default is never evaluated — there is no chip, no builder row and no other UI surface for it to attach to, so the rule silently never runs. Always set Filterable = true first, as in the example above. The same requirement applies to a property explicitly set Filterable = false.

Note

Read a numeric (or otherwise non-date) value with a C# cast — (int?)ctx.Condition.FirstValue — not the as operator. In UI-view code the as operator transpiles to an untyped client value, which breaks the client typecheck; the cast transpiles to a typed value. For dates, prefer GetFirstValueAsDate() (see below).

Cross-filter reads (such as reading EndDate's value from the StartDate rule above) go through the context's own Filterctx.Filter.Find(propertyPath) returns the matching filter, whose Condition exposes the same filter condition shape (FirstValue, GetFirstValueAsDate(), IsEmpty, Values).

GetFirstValueAsDate() resolves the condition's first value regardless of how it was entered: an ISO date/datetime string, a DateTime, or a relative value such as "today + 2 days". Prefer it over reading FirstValue as DateTime? whenever the property accepts relative date filters, since a relative value never arrives as a DateTime directly.

Conditionally required filters

A conditionally required filter is a validation rule too: return an error when ctx.Condition.IsEmpty and the condition under which the filter should be mandatory is met. There is no separate API for this — it is the same mechanism as any other validation rule.

Rules always receive a real, non-null condition — even when the attribute has no active filter, ctx.Condition is an empty FilterCondition (ctx.Condition.IsEmpty == true), never null. This is what makes IsEmpty safe to check unconditionally, and it is what lets a rule require a value the user hasn't provided yet.

The following example makes "Start date" mandatory as soon as an active "Room name" filter is present:

Properties.StartDate.FilterValidationRules.Add(ctx =>
{
    var roomFilter = ctx.Filter?.Find("RoomName");

    if (ctx.Condition.IsEmpty && roomFilter?.Condition?.IsEmpty == false && roomFilter.Active)
    {
        return new UIValidationMessage(
            ValidationRuleSeverityLevel.Error,
            UIResources.Bookings.StartDateRequiredWithRoomFilterError);
    }

    return null;
});

Inactive filters are treated as absent for validation purposes:

  • On the attribute being validated: if its own filter is inactive, the rule receives an empty condition, exactly as if there were no filter at all. A value-checking rule then stops firing — deactivating the chip clears its error and unblocks the search, since the value is no longer applied — while a conditionally-required rule keeps firing, because "no active value" is still "no value".
  • On another attribute read from ctx.Filter (such as roomFilter above): check .Active explicitly before treating its condition as in effect, since ctx.Filter.Find(...) returns the filter whether it is active or not.

Rule scopes and advanced filters

Add has an overload that takes a FilterValidationRuleScope, controlling whether a rule also runs when the user switches the attribute's filter to the advanced filter builder (an arbitrary tree of AND/OR groups instead of a single simple condition):

  • SimpleFilterOnly (the default, used when the two-argument overload is not called) — the rule only runs while the filter is simple. Relational rules — anything that reads another filter's value via ctx.Filter, or a conditionally-required rule that checks ctx.Condition.IsEmpty — are only meaningful against a single condition, so they stay silent in advanced mode rather than firing against a fragment of the tree.
  • AllFilterModes — the rule also runs in advanced mode. Only self-contained rules (that read nothing beyond the condition they receive) should opt in, because in advanced mode the rule is evaluated independently against every occurrence of the attribute in the filter tree, not once for the whole filter. A message on any single occurrence is enough to flag the row, list the message in the recap, and turn the advanced-filter toggle orange or red.

Several behaviors specific to advanced mode are worth calling out:

  • There is no empty-condition synthesis in advanced mode: unlike the simple-mode case where an attribute with no active filter still receives an empty (non-null) condition, advanced mode only evaluates rules against conditions that actually exist as nodes in the tree. A conditionally-required rule (which depends on being called with an empty condition) will simply never see one there — another reason such rules should stay SimpleFilterOnly.
  • Entity view parameters DO appear as editable rows inside the advanced filter builder (only their chip is hidden from the closed filter bar while in advanced mode — the builder popup itself always shows every filterable parameter as a row). Because a parameter condition is never part of the tree and never repeats, scope has no effect on it: a parameter's rules always run against its own single condition, whatever scope was passed to Add.
  • Inside the builder popup, a parameter row is validated with all of its rules as soon as the value changes, even if the parameter was inactive when the popup was opened — applying the builder always reactivates every parameter row it submits, so a stale active == false on the committed parameter says nothing about whether the row will end up active after Apply.
  • The popover's live recap (the summary shown at the bottom of the builder while editing, before Apply) previews every rule — including a SimpleFilterOnly relational one — as long as the working draft stays structurally simple: ctx.Filter is always fresh, so a rule that reads another condition via ctx.Filter.Find(...) sees the in-progress edits, not a stale committed value. FilterValidationRuleScope is about where in the filter tree a rule is meaningful to evaluate, not about live-preview eligibility — the moment the draft becomes genuinely advanced (a real OR/group), a SimpleFilterOnly rule stops running altogether (as described above), and only AllFilterModes rules keep previewing, evaluated per occurrence.
Properties.StartDate.FilterValidationRules.Add(ctx =>
{
    var start = ctx.Condition.GetFirstValueAsDate();

    if (start.HasValue && start < new DateTime(2020, 1, 1))
    {
        return new UIValidationMessage(
            ValidationRuleSeverityLevel.Warning,
            UIResources.Bookings.NoBookingBefore2020Warning);
    }

    return null;
}, FilterValidationRuleScope.AllFilterModes);

Display and search blocking

The filter chip reflects the result of its validation rules:

  • An error colors the chip red, shows the message in a tooltip and in the filter's edit popup, and blocks the search action until the value is corrected.
  • A warning colors the chip orange and shows the message the same way, but never blocks the search.
  • No message means the chip keeps its normal appearance.
Note

Rules work the same way on entity view parameters and on view properties, including conditionally required rules: a parameter with no value is validated against an empty condition (ctx.Condition.IsEmpty == true), so a ctx.Condition.IsEmpty rule fires on a valueless parameter just as it does on a property.

Note

FilterRequired always wins when the requirement is unfulfilled: a validation rule's message (however severe) is not shown on the chip, its tooltip, or the popup while the attribute is still missing its required value. This avoids a non-blocking rule message (Warning/Information) masking the fact that the search is actually blocked by the missing required filter. An Error-severity rule message has the same visual outcome as FilterRequired anyway (red, blocking), so no real validation error is ever hidden by this.

API reference

See also