Table of Contents

Filter Expressions

A rule's FilterText is the condition that decides, per item, whether the match values or the no-match values are written. It is parsed by the shared TTMSFNCFilterBuilder engine — the same engine the TMS FNC Dataset uses for row filtering — so the syntax is consistent across the product. Write expressions by hand when you need operators, grouping, or runtime values that the one-line ShowWhere/AddRuleEquals helpers do not express. This guide covers the expression syntax, how field names map to item properties, forcing a field's data type, and substituting live values through {Placeholder} text sources.

Filter Rules Manager editor with a configured filter expression Filter Rules Manager editor with a configured filter expression

Expression syntax

FilterText reads like a SQL WHERE clause:

  • Field references are wrapped in square brackets — [Status], [Order Date]. The brackets are the column delimiter, so they are required even for simple names (a bare or double-quoted name is not recognised as a field and silently matches nothing).
  • String literals use single quotes: 'Open'. LIKE accepts the % wildcard: [Name] LIKE 'A%'.
  • Operators mirror the TTMSFNCFilterBuilderExpressionOperator set: =, <>, >, <, >=, <=, and LIKE, plus empty/not-empty tests.
  • Grouping combines conditions with AND and OR and parentheses: ([Price] >= 100 AND [Stock] > 0) OR [Featured] = 1.
  • Negation uses NOT, either on a single condition or on a parenthesised group: NOT [Status] = 'Closed', NOT ([Price] > 100 OR [Stock] = 0).

An empty FilterText matches every item unconditionally — useful when the rule exists only to push inactive values or to be filled in later.

Negating a condition or a group

NOT binds to whatever follows it. Placed before a field reference it inverts that one condition and is stored on the expression's IsInverted; placed before a parenthesised group it inverts the group's combined result and is stored on the group's IsInverted. Both survive a round-trip: regenerating FilterText writes the NOT back in the same position.

Expression Matches
NOT [Status] = 'Closed' Items whose status is not Closed.
NOT ([Status] = 'Closed') The same, expressed as a one-condition group.
NOT ([Price] > 100 OR [Stock] = 0) Items that are neither over 100 nor out of stock — the group is evaluated first, then negated.
[Featured] = 1 AND NOT ([Status] = 'Closed') A negated group combined with another condition.

Setting IsInverted in code has the same effect as writing the NOT, so a group built with AddANDGroup / AddORGroup can be negated without touching the text. Clearing or re-parsing a group resets its IsInverted, since the flag belongs to the parsed structure.

uses
  TMS.TMSFNCFilterRulesManager, TMS.TMSFNCFilterBuilder, TMS.TMSFNCValue;

{ Inside a method of your form: }
var
  Rule: TTMSFNCFilterRule;
  Builder: TTMSFNCFilterBuilder;
  Grp: TTMSFNCFilterBuilderGroup;
begin
  // NOT before a field reference inverts that one condition.
  Rule := FilterRulesManager1.AddRule('Not closed', List1, 'Items[*]',
    'NOT [Status] = ''Closed''');

  // NOT before a parenthesised group inverts the group's combined result:
  // neither expensive nor out of stock.
  Rule := FilterRulesManager1.AddRule('In easy reach', List1, 'Items[*]',
    'NOT ([Price] > 100 OR [Stock] = 0)');

  // A negated group combined with another condition.
  Rule := FilterRulesManager1.AddRule('Featured and open', List1, 'Items[*]',
    '[Featured] = 1 AND NOT ([Status] = ''Closed'')');

  // The same negation built through the object API rather than as text.
  Builder := TTMSFNCFilterBuilder.Create;
  try
    Builder.AddDataColumn('Price', fdtNumber);
    Builder.AddDataColumn('Stock', fdtNumber);

    Grp := Builder.Filter.AddORGroup;
    Grp.IsInverted := True;
    Grp.AddExpression('Price', feoLargerThan, FNCValueFromInteger(100));
    Grp.AddExpression('Stock', feoEqual, FNCValueFromInteger(0));

    // Reading FilterText writes the NOT back in the same position:
    // NOT ([Price] > 100 OR [Stock] = 0)
    Rule.FilterText := Builder.FilterText;
  finally
    Builder.Free;
  end;
end;

Expression format

The syntax above is the default Delphi DataSet format, which mirrors a dataset filter: bracketed field names, single-quoted strings, LIKE with %/_ wildcards, and AND/OR keywords. The shared TTMSFNCFilterBuilder also understands an OData format — useful when you need to exchange filter expressions with a web API or backend that speaks OData v4. Field names are bare (no brackets), string functions replace wildcards, and the logical operators are lowercase keywords:

Condition Delphi DataSet OData
Equals (text) [Name] LIKE 'Open' Name eq 'Open'
Starts with [Name] LIKE 'A%' startswith(Name, 'A')
Ends with [Name] LIKE '%z' endswith(Name, 'z')
Contains [Name] LIKE '%A%' contains(Name, 'A')
Greater than [Price] > 100 Price gt 100
Is empty [Status] = '' Status eq null
And / Or AND / OR and / or

Write rule expressions in the Delphi DataSet format for local filtering. Use the OData format to exchange filter strings with a remote API — set FormatType to fftOData on the TTMSFNCFilterBuilder instance the manager uses, then read FilterText to generate a string, or assign it to parse one back into rules. Both directions work, including the function-call operators (contains, startswith, endswith), null tests, not, and and / or grouping; a filter written as OData round-trips through FilterText unchanged.

Field names and item paths

Field names in the expression are resolved as RTTI properties on each item that ItemsPath yields. ItemsPath itself is an RTTI path navigated from Source: Items[*] evaluates every element of an Items collection, Items[1..3|5] selects specific indices, and an empty path targets Source directly. The field name in FilterText must match a readable property on the resolved item.

Overriding a field's data type

The builder normally infers a field's type from its value. When a value would be read as the wrong type — a numeric code stored as text, a date that should sort chronologically — add a TTMSFNCFilterRuleFieldTypes entry to pin it. FieldTypes.Add(name, type) takes a TTMSFNCFilterBuilderDataType such as fdtNumber, fdtDate, fdtBoolean, or fdtText (the default is fdtAutomatic).

uses
  TMS.TMSFNCFilterRulesManager, TMS.TMSFNCFilterBuilder;

{ Inside a method of your form: }
var
  Rule: TTMSFNCFilterRule;
begin
  Rule := FilterRulesManager1.AddRule('Expensive', List1, 'Items[*]', '');

  // Field references use [brackets]; string literals use single quotes and
  // LIKE accepts the % wildcard.
  Rule.FilterText := '[Price] >= 1000 AND [Name] LIKE ''A%''';

  // The builder auto-detects types from the value, but you can force one when
  // the field text would otherwise be read as the wrong type.
  Rule.FieldTypes.Add('Price', fdtNumber);

  Rule.AddVisibilityAction(True);
  FilterRulesManager1.Apply;
end;

Placeholder text sources

To filter against a value that changes at runtime — text typed into a search box, a selected combo item — bind a placeholder through the manager's Placeholders collection (a TTMSFNCFilterRulePlaceholders). Text sources are defined once on the manager and shared across every rule. Write {PlaceholderName} anywhere in a rule's FilterText (or in ItemsPath); at each Apply, every occurrence is replaced with the current value of the bound Source.PropertyName before the expression is evaluated. This keeps the rule fixed while the value it tests against follows the UI.

Combining a placeholder with a typed field

A live search usually combines both features: a placeholder feeds the text part of the filter while a FieldTypes override keeps a numeric condition correct. Re-call Apply whenever the bound control changes:

uses
  TMS.TMSFNCFilterRulesManager, TMS.TMSFNCFilterBuilder;

{ Inside a method of your form, with an edit named SearchEdit: }
var
  Rule: TTMSFNCFilterRule;
begin
  Rule := FilterRulesManager1.AddRule('LiveSearch', List1, 'Items[*]', '');

  // Bind {SearchText} to SearchEdit.Text. Text sources live on the manager and
  // are shared across every rule; the token is substituted at Apply.
  FilterRulesManager1.AddPlaceholder('SearchText', SearchEdit, 'Text');

  // The placeholder appears inside the expression; a typed field keeps the
  // numeric comparison correct.
  Rule.FilterText := '[Name] LIKE ''{SearchText}%'' AND [Stock] > 0';
  Rule.FieldTypes.Add('Stock', fdtNumber);

  Rule.AddVisibilityAction(True);
  FilterRulesManager1.Apply;   // re-call Apply whenever SearchEdit changes
end;

Common pitfalls

  • Brackets for fields, single quotes for strings. [Name] = 'Open' is correct. A field reference must be bracketed: "Name" or a bare Name is not recognised as a column and the expression silently matches nothing.
  • The field must be readable on the item. A bracketed name in FilterText with no matching item property resolves to nothing and never matches.
  • Placeholders are substituted as text. {SearchText} is spliced into the expression literally, so wrap it in quotes and wildcards as needed (LIKE '{SearchText}%'). Re-call Apply for the new value to take effect.
  • Pin the type when auto-detection guesses wrong. A code like 007 read as a number loses its leading zeros — force fdtText via FieldTypes.
  • An operator outside the active format is rejected, not approximated. [Name] contains 'A' is not DataSet syntax (LIKE '%A%' is), and an operator the format does not define makes the whole expression invalid: ParseExpression and isExpressionValid return False, the filter is not applied, and the expression matches nothing. Check the operator against the format table above when a rule stops matching after an edit.

See also