Skip to main content
Table of Contents

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:

Quick StartPascalC++
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

Building Filter Expressions ProgrammaticallyPascalC++
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.

Preparing a filter before validating dataPascalC++
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

Single RowPascalC++
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

Multiple RowsPascalC++
// 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.

Wiring filter builder eventsPascalC++
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).

Defining filter builder columnsPascalC++
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.

Nesting filter builder groupsPascalC++
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.

Configuring a single filter expressionPascalC++
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:

Build and apply a typed filterPascalC++
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 EndUpdate in a finally block. 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 to ValidateFilterRow.

See Also