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.
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'.LIKEaccepts the%wildcard:[Name] LIKE 'A%'. - Operators mirror the
TTMSFNCFilterBuilderExpressionOperatorset:=,<>,>,<,>=,<=, andLIKE, plus empty/not-empty tests. - Grouping combines conditions with
ANDandORand 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 bareNameis not recognised as a column and the expression silently matches nothing. - The field must be readable on the item. A bracketed name in
FilterTextwith 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-callApplyfor the new value to take effect. - Pin the type when auto-detection guesses wrong. A code like
007read as a number loses its leading zeros — forcefdtTextviaFieldTypes. - 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:ParseExpressionandisExpressionValidreturnFalse, 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
- Rules and actions — what happens once an item matches
- Upsert helpers — operator shortcuts that build the expression for you
TTMSFNCFilterBuilderTTMSFNCFilterRuleFieldTypesTTMSFNCFilterRulePlaceholders