Filter Builder
TTMSFNCFilterBuilder builds and evaluates complex multi-rule filter expressions. It is the shared filtering engine used by TTMSFNCDataGrid, TTMSFNCDataSetFilterDialog, and other FNC components.
Key class: TTMSFNCFilterBuilder
Tip
Before using TTMSFNCFilterBuilder directly, prefer the built-in filtering UI of controls that integrate it (e.g. TTMSFNCDataGrid's column filter). Direct use is for programmatic filter evaluation or sharing a single filter across multiple components.
Quick Start
Create a local instance or use the global TMSFNCFilterBuilder:
uses
TMS.TMSFNCFilterBuilder;
var
fb: TTMSFNCFilterBuilder;
begin
fb := TTMSFNCFilterBuilder.Create;
try
fb.ParseExpression('[Name] LIKE ' + QuotedStr('C%'));
finally
fb.Free;
end;
// Or use the global instance:
TMSFNCFilterBuilder.ParseExpression('[Name] LIKE ' + QuotedStr('C%'));
end;
Parsing Filter Text
ParseExpression converts a filter string into structured Groups and Expressions. It returns True on success, or fires parse-error events for malformed input.
Default format (fftDelphiDataSet):
[Name] LIKE 'K*' AND [Status] LIKE 'Abroad') OR ([Name] LIKE 'S%' AND [Progress] > 50) OR [Available] = True
Universal expression format (fftUniversalFilterExpressions):
[Name] = "K*" & [Status] = "Abroad") | ([Name] = "S*" & [Progress] > 50) | [Available] = True
Building Filter Expressions Programmatically
uses
TMS.TMSFNCFilterBuilder, TMS.TMSFNCValue;
var
g1, g2: TTMSFNCFilterBuilderGroup;
begin
TMSFNCFilterBuilder.ClearFilter;
TMSFNCFilterBuilder.Filter.GroupOperator := fgoOR;
g1 := TMSFNCFilterBuilder.Filter.AddANDGroup;
g1.AddExpression('Name', feoStartsWith, 'K');
g1.AddExpression('Status', feoEqual, 'Abroad');
g2 := TMSFNCFilterBuilder.Filter.AddANDGroup;
g2.AddExpression('Name', feoStartsWith, 'S');
g2.AddExpression('Progress', feoLargerThan, 50);
TMSFNCFilterBuilder.Filter.AddExpression('Available', feoEqual, True);
ShowMessage(TMSFNCFilterBuilder.FilterText);
// ([Name] LIKE 'K%' AND [Status] = Abroad) OR ([Name] LIKE 'S%' AND [Progress] > 50) OR [Available] = True
end;
Validating Data Against a Filter
Validation compares a row of values against the parsed filter. Declare the data
type of every column before the first call: without it the builder infers a type
from whatever value it receives, so the same row can compare differently
depending on how it was supplied. The values in a row are matched to the columns
by position, in DataColumns order.
uses
TMS.TMSFNCFilterBuilder;
{ Inside your form's OnCreate: }
begin
TMSFNCFilterBuilder.ClearFilter;
TMSFNCFilterBuilder.DataColumns.Clear;
// Declare the column types up front. Without them the builder infers a type
// from the incoming value, which makes numeric and boolean comparisons
// depend on how the caller happens to pass the value.
TMSFNCFilterBuilder.AddDataColumn('Name', fdtText);
TMSFNCFilterBuilder.AddDataColumn('Status', fdtText);
TMSFNCFilterBuilder.AddDataColumn('Progress', fdtNumber);
TMSFNCFilterBuilder.AddDataColumn('Available', fdtBoolean);
// ParseExpression returns False (and fires the OnParseError* events) when the
// text is malformed, so never validate against an unchecked filter.
if not TMSFNCFilterBuilder.ParseExpression(
'[Status] LIKE ' + QuotedStr('Abroad') + ' AND [Progress] > 50') then
raise Exception.Create('Invalid filter expression');
// The value order of every row passed to ValidateFilterRow or
// ValidateFilterArray must match the DataColumns order declared above.
end;
Single Row
TMSFNCFilterBuilder.DataColumn['Name'].DataType := fdtText;
TMSFNCFilterBuilder.DataColumn['Status'].DataType := fdtText;
TMSFNCFilterBuilder.DataColumn['Progress'].DataType := fdtNumber;
TMSFNCFilterBuilder.DataColumn['Available'].DataType := fdtBoolean;
InputRow := ['Sarah', 'Abroad', 60, False];
if TMSFNCFilterBuilder.ValidateFilterRow(InputRow) then
ShowMessage('Matches Filter');
Multiple Rows
// Data organized as columns (default, AByRow = False):
InputArray := [
['Sarah', 'Sarah', 'Kurt', 'Alex', 'Alex'],
['Abroad', 'Abroad', 'Abroad', 'Abroad', 'Abroad'],
[60, 40, 40, 40, 40],
[False, False, False, False, True]
];
OutputArray := TMSFNCFilterBuilder.ValidateFilterArray(InputArray, False);
// Data organized as rows (AByRow = True):
InputArray := [
['Sarah', 'Abroad', 60, False],
['Kurt', 'Abroad', 40, False],
['Alex', 'Abroad', 40, True]
];
OutputArray := TMSFNCFilterBuilder.ValidateFilterArray(InputArray, True);
// [True, False, True, False, True]
Properties
| Property | Description |
|---|---|
DataColumn['Name'] |
Returns (or creates) a column by name |
DataColumnDisplayName['Name'] |
User-friendly display name for a column |
DataColumnType['Name'] |
Data type of the column |
ParseFormat |
Format settings for parsing filter text |
FilterText |
Raw filter expression text |
DisplayFilterText |
FilterText with column display names substituted |
Filter |
Root TTMSFNCFilterBuilderGroup of the filter structure |
DataColumns |
Collection of all defined columns |
FormatType |
Output format type (default: fftDelphiDataSet) |
Methods
| Method | Description |
|---|---|
ParseExpression(AFilterText) |
Parse text into groups/expressions |
ClearFilter |
Reset all groups and expressions |
ValidateFilterRow(AInputData) |
Validate one row |
ValidateFilterArray(AInputArray, AByRow) |
Validate multiple rows |
AddDataColumn(AName, ADataType, ADisplayName) |
Add a column definition |
AddDataColumnsFromFilter |
Create the missing column definitions for every column referenced by the current filter |
GetColumnByName(AColumnName) / GetColumnByDisplayName(AColumnDisplayName) |
Look up an existing column definition |
isExpressionValid(AExpression) |
Check one expression string without parsing it into the filter |
DeleteGroup(AGroup) / DeleteExpression(AExpression) |
Remove a group or expression |
BeginUpdate / EndUpdate |
Suspend and resume change notifications while building a filter |
Events
| Event | Description |
|---|---|
OnParseError |
General parse error |
OnParseErrorParenthesis |
Mismatched parentheses |
OnParseErrorOperatorMismatch |
Operator mismatch within a group |
OnParseErrorOperatorPosition |
Operator in wrong position |
OnParseErrorInvalidExpression |
Invalid expression syntax |
OnFilterTextParsed |
Full filter text successfully parsed |
OnExpressionTextParsed |
Single expression successfully parsed |
OnValidateFilter / OnValidateGroup / OnValidateExpression |
Custom validation hooks |
OnGetFilterText / OnGetGroupText / OnGetExpressionText |
Customize text generation |
OnExpressionAdded |
A new expression was added |
Wire the parse-error events whenever filter text can come from a user: a failed
parse leaves the previous filter in place, so without a handler a typo silently
keeps filtering on the old expression. OnValidateExpression and its group and
filter counterparts are the hook for comparisons the built-in operators cannot
express - set AExitValidation there to keep the default evaluation from
overwriting your result.
uses
TMS.TMSFNCFilterBuilder, TMS.TMSFNCValue;
procedure TForm1.FormCreate(Sender: TObject);
begin
TMSFNCFilterBuilder.OnParseError := HandleParseError;
TMSFNCFilterBuilder.OnFilterTextParsed := HandleFilterTextParsed;
TMSFNCFilterBuilder.OnExpressionAdded := HandleExpressionAdded;
TMSFNCFilterBuilder.OnValidateExpression := HandleValidateExpression;
end;
procedure TForm1.HandleParseError(Sender: TObject; AFilterText: string);
begin
// Fires for text the parser cannot turn into groups and expressions. The more
// specific OnParseErrorParenthesis, OnParseErrorOperatorPosition,
// OnParseErrorOperatorMismatch and OnParseErrorInvalidExpression events also
// carry the character position of the problem.
ShowMessage('Could not parse: ' + AFilterText);
end;
procedure TForm1.HandleFilterTextParsed(Sender: TObject;
AFilter: TTMSFNCFilterBuilderGroup; AText: string);
begin
// AFilter is the root group AText was parsed into.
Memo1.Lines.Add(Format('%s -> %d expression(s), %d group(s)',
[AText, AFilter.Expressions.Count, AFilter.Groups.Count]));
end;
procedure TForm1.HandleExpressionAdded(Sender: TObject;
AExpression: TTMSFNCFilterBuilderExpression; AText: string);
begin
// Fires for every expression added, whether parsed from text or added in
// code. Use it to apply a house rule to new expressions.
if AExpression.GetValueDataType = fdtText then
AExpression.CaseSensitive := True;
end;
procedure TForm1.HandleValidateExpression(Sender: TObject;
AExpression: TTMSFNCFilterBuilderExpression; AInputValue: TTMSFNCValue;
var AValidationResult: Boolean; var AExitValidation: Boolean);
begin
// Override the built-in comparison for one column: an empty Status counts as
// a match. Setting AExitValidation stops the default evaluation from
// overwriting AValidationResult afterwards.
if (AExpression.GetColumnName = 'Status') and ValueIsEmpty(AInputValue) then
begin
AValidationResult := True;
AExitValidation := True;
end;
end;
TTMSFNCFilterBuilderColumn
| Property | Description |
|---|---|
Name |
Internal column name used in filter expressions |
DisplayName |
User-visible name shown in the UI |
DataType |
fdtAutomatic, fdtText, fdtBoolean, fdtDateTime, fdtDate, fdtTime, fdtFloat, fdtNumber, fdtOther |
A column definition is what ties a name inside an expression to a value type and
a caption. AddDataColumn creates one; the indexed DataColumn,
DataColumnType and DataColumnDisplayName properties reach an existing one by
name (and create it on demand).
uses
TMS.TMSFNCFilterBuilder;
var
LColumn: TTMSFNCFilterBuilderColumn;
begin
TMSFNCFilterBuilder.ClearFilter;
TMSFNCFilterBuilder.DataColumns.Clear;
// Name is what expressions refer to, DataType decides how values compare,
// DisplayName is what an end user reads.
LColumn := TMSFNCFilterBuilder.AddDataColumn('Progress', fdtNumber, 'Completion');
// The same column stays reachable by name through the indexed properties.
TMSFNCFilterBuilder.DataColumnDisplayName['Progress'] := 'Completion (%)';
TMSFNCFilterBuilder.DataColumnType['Progress'] := fdtFloat;
TMSFNCFilterBuilder.Filter.AddExpression(LColumn, feoLargerThan, 50);
// FilterText writes Name, DisplayFilterText writes DisplayName.
ShowMessage(TMSFNCFilterBuilder.FilterText); // [Progress] > 50
ShowMessage(TMSFNCFilterBuilder.DisplayFilterText); // [Completion (%)] > 50
end;
TTMSFNCFilterBuilderGroup
Groups combine expressions using AND or OR. The Filter property of TTMSFNCFilterBuilder is the root group.
| Property | Description |
|---|---|
GroupOperator |
fgoAND or fgoOR |
IsInverted |
Negate the group's combined result (NOT (...)) |
Groups |
Nested subgroups |
Expressions |
Filter expressions in this group |
FilterText |
Textual representation of this group |
Nesting is how precedence is expressed: a sub-group is emitted as a bracketed
term, so mixing AND and OR in one filter means adding a sub-group rather
than relying on operator precedence.
uses
TMS.TMSFNCFilterBuilder;
var
LGroup: TTMSFNCFilterBuilderGroup;
begin
TMSFNCFilterBuilder.ClearFilter;
// The root group combines its own expressions with its sub-groups.
TMSFNCFilterBuilder.Filter.GroupOperator := fgoAND;
TMSFNCFilterBuilder.Filter.AddExpression('Status', feoEqual, 'Abroad');
// A sub-group brackets an alternative: (Progress > 50 OR Available = True)
LGroup := TMSFNCFilterBuilder.Filter.AddORGroup;
LGroup.AddExpression('Progress', feoLargerThan, 50);
LGroup.AddExpression('Available', feoEqual, True);
// IsInverted puts a NOT in front of the whole sub-group instead of in front
// of each expression, so the group's combined result is negated.
LGroup.IsInverted := True;
ShowMessage(TMSFNCFilterBuilder.Filter.FilterText);
ShowMessage(Format('%d expression(s), %d sub-group(s)',
[TMSFNCFilterBuilder.Filter.Expressions.Count,
TMSFNCFilterBuilder.Filter.Groups.Count]));
end;
TTMSFNCFilterBuilderExpression
| Property | Description |
|---|---|
Column |
The column being tested |
ExpressionOperator |
feoStartsWith, feoEndsWith, feoContains, feoEqual, feoNotEqual, feoEmpty, feoNotEmpty, feoLargerThan, feoSmallerThan, feoLargerThanOrEqual, feoSmallerThanOrEqual |
Value |
The comparison value |
Expression |
Read-only text of this single expression |
IsInverted |
Invert the condition (e.g. "not contains") |
CaseSensitive |
Case-sensitive text comparison (default: False) |
An expression stays editable after it is added, which is what a filter editor UI
binds to. ValidateExpression evaluates a single value against one expression
without walking the rest of the filter, so it is the cheap way to preview the
effect of a change.
uses
TMS.TMSFNCFilterBuilder;
var
LExpression: TTMSFNCFilterBuilderExpression;
begin
TMSFNCFilterBuilder.ClearFilter;
TMSFNCFilterBuilder.DataColumns.Clear;
TMSFNCFilterBuilder.AddDataColumn('Name', fdtText, 'Employee');
LExpression := TMSFNCFilterBuilder.Filter.AddExpression('Name', feoContains, 'ar');
// A text comparison is case insensitive by default.
LExpression.CaseSensitive := True;
// Every part of an expression can be retargeted after it was added.
LExpression.Column := TMSFNCFilterBuilder.GetColumnByName('Name');
LExpression.ExpressionOperator := feoStartsWith;
LExpression.Value := 'Sar';
// IsInverted negates this one expression ("does not start with").
LExpression.IsInverted := False;
ShowMessage(LExpression.Expression); // [Name] LIKE 'Sar%'
ShowMessage(LExpression.GetColumnDisplayName); // Employee
// ValidateExpression evaluates a single value against this expression only,
// without walking the rest of the filter.
if LExpression.ValidateExpression('Sarah') then
ShowMessage('Sarah matches');
end;
Parse Format Reference
| Property | Delphi DataSet | Universal Expression |
|---|---|---|
StringDelimiter |
' |
" |
AndOperator |
AND |
& |
OrOperator |
OR |
\| |
EqualStrOperator |
LIKE '#VALUE' |
= "#VALUE" |
ContainsOperator |
LIKE '%#VALUE%' |
= "*#VALUE*" |
StartsWithOperator |
LIKE '#VALUE%' |
= "#VALUE*" |
MultiCharWildCard |
% |
* |
Set FormatType to fftDelphiDataSet (default), fftUniversalFilterExpressions, fftOData, or fftCustom, or configure ParseFormat properties individually.