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;
TTMSFNCFilterBuilder *fb = new TTMSFNCFilterBuilder();
try
{
fb->ParseExpression("[Name] LIKE " + QuotedStr("C%"));
}
__finally
{
delete fb;
}
// Or use the global instance:
TMSFNCFilterBuilder()->ParseExpression("[Name] LIKE " + QuotedStr("C%"));
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;
TMSFNCFilterBuilder()->ClearFilter();
TMSFNCFilterBuilder()->Filter->GroupOperator = TTMSFNCFilterBuilderGroupOperator::fgoOR;
TTMSFNCFilterBuilderGroup *g1 = TMSFNCFilterBuilder()->Filter->AddANDGroup();
g1->AddExpression("Name", TTMSFNCFilterBuilderExpressionOperator::feoStartsWith, TValue::From<String>("K"));
g1->AddExpression("Status", TTMSFNCFilterBuilderExpressionOperator::feoEqual, TValue::From<String>("Abroad"));
TTMSFNCFilterBuilderGroup *g2 = TMSFNCFilterBuilder()->Filter->AddANDGroup();
g2->AddExpression("Name", TTMSFNCFilterBuilderExpressionOperator::feoStartsWith, TValue::From<String>("S"));
g2->AddExpression("Progress", TTMSFNCFilterBuilderExpressionOperator::feoLargerThan, TValue::From<int>(50));
TMSFNCFilterBuilder()->Filter->AddExpression("Available",
TTMSFNCFilterBuilderExpressionOperator::feoEqual, TValue::From<bool>(true));
ShowMessage(TMSFNCFilterBuilder()->FilterText);
// ([Name] LIKE "K%" AND [Status] = Abroad) OR ([Name] LIKE "S%" AND [Progress] > 50) OR [Available] = True
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;
// Inside the OnCreate handler of your form:
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", TTMSFNCFilterBuilderDataType::fdtText);
TMSFNCFilterBuilder()->AddDataColumn("Status", TTMSFNCFilterBuilderDataType::fdtText);
TMSFNCFilterBuilder()->AddDataColumn("Progress", TTMSFNCFilterBuilderDataType::fdtNumber);
TMSFNCFilterBuilder()->AddDataColumn("Available", TTMSFNCFilterBuilderDataType::fdtBoolean);
// ParseExpression returns false (and fires the OnParseError* events) when the
// text is malformed, so never validate against an unchecked filter.
if (!TMSFNCFilterBuilder()->ParseExpression(
"[Status] LIKE " + QuotedStr("Abroad") + " AND [Progress] > 50"))
throw Exception("Invalid filter expression");
// The value order of every row passed to ValidateFilterRow or
// ValidateFilterArray must match the DataColumns order declared above.
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');
TMSFNCFilterBuilder()->DataColumn["Name"]->DataType = TTMSFNCFilterBuilderDataType::fdtText;
TMSFNCFilterBuilder()->DataColumn["Status"]->DataType = TTMSFNCFilterBuilderDataType::fdtText;
TMSFNCFilterBuilder()->DataColumn["Progress"]->DataType = TTMSFNCFilterBuilderDataType::fdtNumber;
TMSFNCFilterBuilder()->DataColumn["Available"]->DataType = TTMSFNCFilterBuilderDataType::fdtBoolean;
TTMSFNCFilterValidateInputRow InputRow;
InputRow.Length = 4;
InputRow[0] = TValue::From<String>("Sarah");
InputRow[1] = TValue::From<String>("Abroad");
InputRow[2] = TValue::From<int>(60);
InputRow[3] = TValue::From<bool>(false);
if (TMSFNCFilterBuilder()->ValidateFilterRow(InputRow))
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]
TTMSFNCFilterValidateInputArray InputArray;
TTMSFNCFilterValidateOutputArray OutputArray;
// Data organized as columns (default, AByRow = false):
InputArray.Length = 4;
InputArray[0].Length = 5;
InputArray[1].Length = 5;
InputArray[2].Length = 5;
InputArray[3].Length = 5;
const char *Names[5] = {"Sarah", "Sarah", "Kurt", "Alex", "Alex"};
const int Progress[5] = {60, 40, 40, 40, 40};
for (int I = 0; I < 5; I++)
{
InputArray[0][I] = TValue::From<String>(Names[I]);
InputArray[1][I] = TValue::From<String>("Abroad");
InputArray[2][I] = TValue::From<int>(Progress[I]);
InputArray[3][I] = TValue::From<bool>(I == 4);
}
OutputArray = TMSFNCFilterBuilder()->ValidateFilterArray(InputArray, false);
// Data organized as rows (AByRow = true):
const char *RowNames[3] = {"Sarah", "Kurt", "Alex"};
const int RowProgress[3] = {60, 40, 40};
InputArray.Length = 3;
for (int I = 0; I < 3; I++)
{
InputArray[I].Length = 4;
InputArray[I][0] = TValue::From<String>(RowNames[I]);
InputArray[I][1] = TValue::From<String>("Abroad");
InputArray[I][2] = TValue::From<int>(RowProgress[I]);
InputArray[I][3] = TValue::From<bool>(I == 2);
}
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;
void __fastcall TForm1::FormCreate(TObject *Sender)
{
TMSFNCFilterBuilder()->OnParseError = HandleParseError;
TMSFNCFilterBuilder()->OnFilterTextParsed = HandleFilterTextParsed;
TMSFNCFilterBuilder()->OnExpressionAdded = HandleExpressionAdded;
TMSFNCFilterBuilder()->OnValidateExpression = HandleValidateExpression;
}
void __fastcall TForm1::HandleParseError(TObject *Sender, String AFilterText)
{
// 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);
}
void __fastcall TForm1::HandleFilterTextParsed(TObject *Sender,
TTMSFNCFilterBuilderGroup *AFilter, String AText)
{
// AFilter is the root group AText was parsed into.
Memo1->Lines->Add(Format("%s -> %d expression(s), %d group(s)",
ARRAYOFCONST((AText, AFilter->Expressions->Count, AFilter->Groups->Count))));
}
void __fastcall TForm1::HandleExpressionAdded(TObject *Sender,
TTMSFNCFilterBuilderExpression *AExpression, String AText)
{
// 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() == TTMSFNCFilterBuilderDataType::fdtText)
AExpression->CaseSensitive = true;
}
void __fastcall TForm1::HandleValidateExpression(TObject *Sender,
TTMSFNCFilterBuilderExpression *AExpression, const TTMSFNCValue &AInputValue,
bool &AValidationResult, bool &AExitValidation)
{
// 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") && ValueIsEmpty(AInputValue))
{
AValidationResult = true;
AExitValidation = true;
}
}
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;
TMSFNCFilterBuilder()->ClearFilter();
TMSFNCFilterBuilder()->DataColumns->Clear();
// Name is what expressions refer to, DataType decides how values compare,
// DisplayName is what an end user reads.
TTMSFNCFilterBuilderColumn *LColumn = TMSFNCFilterBuilder()->AddDataColumn("Progress",
TTMSFNCFilterBuilderDataType::fdtNumber, "Completion");
// The same column stays reachable by name through the indexed properties.
TMSFNCFilterBuilder()->DataColumnDisplayName["Progress"] = "Completion (%)";
TMSFNCFilterBuilder()->DataColumnType["Progress"] = TTMSFNCFilterBuilderDataType::fdtFloat;
TMSFNCFilterBuilder()->Filter->AddExpression(LColumn,
TTMSFNCFilterBuilderExpressionOperator::feoLargerThan, TValue::From<int>(50));
// FilterText writes Name, DisplayFilterText writes DisplayName.
ShowMessage(TMSFNCFilterBuilder()->FilterText); // [Progress] > 50
ShowMessage(TMSFNCFilterBuilder()->DisplayFilterText); // [Completion (%)] > 50
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;
TMSFNCFilterBuilder()->ClearFilter();
// The root group combines its own expressions with its sub-groups.
TMSFNCFilterBuilder()->Filter->GroupOperator = TTMSFNCFilterBuilderGroupOperator::fgoAND;
TMSFNCFilterBuilder()->Filter->AddExpression("Status",
TTMSFNCFilterBuilderExpressionOperator::feoEqual, TValue::From<String>("Abroad"));
// A sub-group brackets an alternative: (Progress > 50 OR Available = True)
TTMSFNCFilterBuilderGroup *LGroup = TMSFNCFilterBuilder()->Filter->AddORGroup();
LGroup->AddExpression("Progress",
TTMSFNCFilterBuilderExpressionOperator::feoLargerThan, TValue::From<int>(50));
LGroup->AddExpression("Available",
TTMSFNCFilterBuilderExpressionOperator::feoEqual, TValue::From<bool>(true));
// IsInverted puts a NOT in front of the whole sub-group instead of in front
// of each expression, so the combined result of the group is negated.
LGroup->IsInverted = true;
ShowMessage(TMSFNCFilterBuilder()->Filter->FilterText);
ShowMessage(Format("%d expression(s), %d sub-group(s)",
ARRAYOFCONST((TMSFNCFilterBuilder()->Filter->Expressions->Count,
TMSFNCFilterBuilder()->Filter->Groups->Count))));
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;
TMSFNCFilterBuilder()->ClearFilter();
TMSFNCFilterBuilder()->DataColumns->Clear();
TMSFNCFilterBuilder()->AddDataColumn("Name", TTMSFNCFilterBuilderDataType::fdtText, "Employee");
TTMSFNCFilterBuilderExpression *LExpression = TMSFNCFilterBuilder()->Filter->AddExpression(
"Name", TTMSFNCFilterBuilderExpressionOperator::feoContains, TValue::From<String>("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 = TTMSFNCFilterBuilderExpressionOperator::feoStartsWith;
LExpression->Value = TValue::From<String>("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(TValue::From<String>("Sarah")))
ShowMessage("Sarah matches");
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.
Combining Columns, Parsing, and Validation
The following example declares stable column types, parses a textual filter, and validates a row in one flow. Batching the setup prevents observers from seeing a half-configured filter:
uses
System.SysUtils,
TMS.TMSFNCFilterBuilder,
TMS.TMSFNCValue;
var
LRow: TArray<TTMSFNCValue>;
begin
TMSFNCFilterBuilder.BeginUpdate;
try
TMSFNCFilterBuilder.ClearFilter;
TMSFNCFilterBuilder.DataColumns.Clear;
TMSFNCFilterBuilder.AddDataColumn('Status', fdtText);
TMSFNCFilterBuilder.AddDataColumn('Progress', fdtNumber);
if not TMSFNCFilterBuilder.ParseExpression(
'[Status] LIKE ' + QuotedStr('Abroad') + ' AND [Progress] > 50') then
raise Exception.Create('Invalid filter expression');
finally
TMSFNCFilterBuilder.EndUpdate;
end;
LRow := ['Abroad', 75];
if TMSFNCFilterBuilder.ValidateFilterRow(LRow) then
ShowMessage('The row matches the filter');
end;
TMSFNCFilterBuilder()->BeginUpdate();
try
{
TMSFNCFilterBuilder()->ClearFilter();
TMSFNCFilterBuilder()->DataColumns->Clear();
TMSFNCFilterBuilder()->AddDataColumn("Status", TTMSFNCFilterBuilderDataType::fdtText);
TMSFNCFilterBuilder()->AddDataColumn("Progress", TTMSFNCFilterBuilderDataType::fdtNumber);
if (!TMSFNCFilterBuilder()->ParseExpression(
"[Status] LIKE " + QuotedStr("Abroad") + " AND [Progress] > 50"))
throw Exception("Invalid filter expression");
}
__finally
{
TMSFNCFilterBuilder()->EndUpdate();
}
DynamicArray<TValue> LRow;
LRow.Length = 2;
LRow[0] = TValue::From<String>("Abroad");
LRow[1] = TValue::From<int>(75);
if (TMSFNCFilterBuilder()->ValidateFilterRow(LRow))
ShowMessage("The row matches the filter");
Common Pitfalls
- Always place
EndUpdatein afinallyblock. If parsing or column setup raises an exception while updates are suspended, later change notifications otherwise remain disabled. - Check the Boolean result of
ParseExpression. A failed parse leaves the previously valid filter available, which can make an invalid user expression appear to work with stale criteria. - Pass row values in the same order as
DataColumns. Names in the parsed expression do not reorder the array supplied toValidateFilterRow.