Skip to content

Business Rules

A rule in Kittox is a business rule, or constraint, applied during data entry. Rules are the heart of any non-trivial application: they are where the business logic lives, declaratively attached to the data model rather than scattered across the user interface.

A rule can:

  • validate data and block the operation with an error message (e.g. "Codice Fiscale already exists");
  • compute / set field values (defaults, descriptions, totals, cascaded values from a referenced record);
  • react to a field change (look up related data, recalculate dependent fields, show/hide editors);
  • enforce cross-field or cross-record integrity before writing to the database.

Because rules are declared in the model (and optionally refined in the view), the same business logic is enforced consistently everywhere that model is edited — every List, every Form, every master-detail, regardless of which controller triggered the operation. This is the Kittox way of applying Don't Repeat Yourself to business logic.

How rules are attached to models

A rule is attached by adding a Rules: subnode to the object you want to constrain, and listing the rule by name (with optional parameters) underneath it. You can attach rules at four levels:

LevelWhereWhen applied
ModelRules: at the top of a Models/*.yaml fileAlways, whenever a record of that model is created, edited or deleted.
Model fieldRules: under a field in Models/*.yamlAlways, unless a view-table-field rule of the same type overrides it.
View tableRules: under a MainTable / detail table in a Views/*.yaml fileOnly when editing through that view, in addition to (and before) the model rules.
View table fieldRules: under a field in a viewOnly when editing through that view; overrides the model-field rule of the same type.

The override mechanism follows convention over configuration: a field-level rule defined in the model is the default, and a view can replace it for a specific screen (for example force upper case in the model, but a different behaviour in one special form).

Here is the model-level attachment, taken from the real Iscrizione model of the KittoSCM sample application:

yaml
# Models/Iscrizione.yaml
ModelName: Iscrizione
PhysicalName: ISCRIZIONI
Rules:
  IscrizioneCheck:          # <-- model-level rule, always applied
Fields:
  CodiceFiscale: String(16) not null
    PhysicalName: CODFISC
    Rules:                  # <-- field-level rules, applied in order
      ForceUpperCase:               # predefined, client-side
      CheckFormatoCodiceFiscale:    # custom, server-side format check
      CheckSumCodiceFiscale:        # custom, checksum validation
      ImpostaDatiAnagraficiDaCF:    # custom, fills other fields from the code
  Cognome: String(100) not null
    Rules:
      CalcDescrizione: {Cognome} {Nome} - {Campagna_Descrizione}   # parameterized rule (value)
  Nominativo: Reference(Nominativo)
    Rules:
      IscrizioneSetNominativo:      # reacts to the reference selection

Notes from the example above:

  • Several rules can be stacked on the same field; they fire in declaration order.
  • ForceUpperCase is a predefined rule, the others are custom rules written in Delphi for this application.
  • CalcDescrizione: {Cognome} {Nome} - {Campagna_Descrizione} passes a value to the rule (a template expanded with the record's fields). Rules can also take named sub-parameters (see Rule parameters).
  • Rules on a Reference field (IscrizioneSetNominativo) typically run when the user picks a value in the lookup and cascade data into other fields.

The rule lifecycle

Every rule is a Delphi class descending from TKRuleImpl (unit Kitto.Rules). You implement a rule by overriding one or more of the methods that Kittox calls at well-defined moments of the editing lifecycle. The default implementation of every method does nothing, so you only override what you need.

MethodCalled whenTypical use
NewRecordA new record is created, before it is shownSet computed default values (declarative DefaultValue is already applied).
EditRecordAn existing record is opened for editingBlock editing of locked records; set transient values.
AfterShowEditWindowAfter the edit form has been builtShow/hide or enable editors based on the record.
DuplicateRecordA record is being duplicatedReset or adjust values that must not be copied.
BeforeFieldChangeBefore a field value changesDisallow the change (ADoIt := False), or rewrite ANewValue.
AfterFieldChangeAfter a field value changedLook up related data, recalculate dependent fields, validate the single value.
AfterRefreshReferenceFieldAfter a reference field is refreshedUpdate editors that depend on the referenced record.
BeforeAddServer-side, before INSERTCross-field validation specific to new records.
BeforeUpdateServer-side, before UPDATECross-field validation specific to updates.
BeforeAddOrUpdateConvenience: called by both BeforeAdd and BeforeUpdateThe most common place for "before save" validation and calculation.
BeforeDeleteServer-side, before DELETEBlock deletion (referential checks, status checks); cascade deletes.
AfterAdd / AfterUpdateAfter a successful INSERT / UPDATESide effects within the same transaction (insert detail rows, send mail, etc.).
AfterAddOrUpdateConvenience: called by both AfterAdd and AfterUpdatePost-save logic common to insert and update.
AfterDeleteAfter a successful DELETEPost-delete side effects.

Before vs After, single record vs both

BeforeAdd, BeforeUpdate and BeforeDelete run inside the database transaction, before the SQL statement. BeforeAddOrUpdate is just a convenience hook invoked by both BeforeAdd and BeforeUpdate — override it when the logic is identical for insert and update. The same pattern applies to AfterAddOrUpdate. All After* methods can still raise an error to roll the transaction back.

To abort an operation and show a message to the user, call RaiseError:

pascal
procedure TNominativoCheck.BeforeAddOrUpdate(const ARecord: TKRecord);
begin
  inherited;
  if EsisteNominativoByCodiceFiscale(
       ARecord.FieldByName('CodiceFiscale').AsString,
       ARecord.FieldByName('Id').AsString) then
    RaiseError(_('Codice Fiscale already exists - use the existing record'));

  // a rule can also *set* values, not just validate:
  ARecord.FieldByName('Descrizione').AsString :=
    ARecord.FieldByName('Cognome').AsString + ' ' +
    ARecord.FieldByName('Nome').AsString;
end;

RaiseError raises an EKValidationError; the operation is aborted and the message is shown to the user. Always call inherited at the top of an overridden method.

Build messages with Field.AsString

Field.AsString returns a user-friendly, locale-formatted string for every data type (dates and times use the application FormatSettings). Prefer it over reading the raw Value Variant when composing error messages, so users see 01/07/2026 instead of a raw float. See How to validate data before storing for details.

Writing a custom rule

A custom rule is a class registered with the rule registry. The pattern is always the same:

pascal
unit RulesNominativo;

interface

uses
  Kitto.Rules, Kitto.Store, Kitto.Metadata.DataView;

type
  TNominativoCheck = class(TKRuleImpl)
  strict protected
    procedure BeforeAddOrUpdate(const ARecord: TKRecord); override;
  end;

implementation

uses
  EF.Localization, Kitto.Config, DBUtils;

procedure TNominativoCheck.BeforeAddOrUpdate(const ARecord: TKRecord);
begin
  inherited;
  // ... business logic ...
end;

initialization
  TKRuleImplRegistry.Instance.RegisterClass(
    TNominativoCheck.GetClassId, TNominativoCheck);

finalization
  TKRuleImplRegistry.Instance.UnregisterClass(
    TNominativoCheck.GetClassId);

end.

Key points:

  • The YAML name is the class name without its leading T (or TK). GetClassId strips a TK prefix if present, otherwise a single T. So TNominativoCheck is referenced as NominativoCheck: in YAML, and TKEnforceRange as EnforceRange:.
  • Register the class in the unit initialization and unregister it in finalization.
  • The unit must be reachable from the project — add it (directly or transitively) to the uses clause of UseKitto.pas, so its initialization section runs and the rule is registered. The application must be recompiled for a new or changed rule to take effect (unlike YAML changes, which only need a server restart).

Rule parameters

A rule can read parameters from its own YAML node. There are two flavours:

A single inline value — accessed through Rule.Value:

yaml
Cognome: String(100) not null
  Rules:
    CalcDescrizione: {Cognome} {Nome} - {Campagna_Descrizione}
pascal
procedure TCalcDescrizione.AfterFieldChange(const AField: TKField;
  const AOldValue, ANewValue: Variant);
var
  LExpression: string;
begin
  inherited;
  LExpression := Rule.Value;                 // the template after the colon
  AField.ParentRecord.ExpandExpression(LExpression);
  AField.ParentRecord.FieldByName('Descrizione').AsString := LExpression;
end;

Named sub-parameters — accessed through Rule.GetString/GetInteger/GetBoolean(...):

yaml
Id: String(32) not null primary key
  Rules:
    GenerateNewId:
      CharSize: 5
pascal
procedure TGenerateNewId.NewRecord(const ARecord: TKRecord);
var
  LCharSize: Integer;
begin
  inherited;
  LCharSize := Rule.GetInteger('CharSize', 0);   // default 0 if omitted
  ARecord.FieldByName('Id').AsString := CalcNewProgress(..., LCharSize);
end;

Use CheckRuleParam('Name') in SetRule to make a parameter mandatory (it raises a clear error if missing). The special ErrorMessage parameter, if present, overrides the rule's default error text; override InternalGetErrorMessage to provide a custom default.

Helpers available inside a rule

TKRuleImpl provides convenient access to the record and to referenced data:

  • ARecord.FieldByName('X') / ARecord.FindField('X') — read and write field values; FindField returns nil if the field is not in the current view.
  • AField.ParentRecord — from inside AfterFieldChange, reach the whole record.
  • GetReferencedModelInstanceValue('RefField', 'FieldName', ARecord) — pull a value from the record pointed to by a reference field (e.g. read the province of the selected municipality).
  • TKConfig.Database.CreateDBQuery / GetSingletonValue(SQL) — run ad-hoc SQL against the model's database. See DB access from Delphi.
  • For master-detail logic, (ARecord as TKViewTableRecord).Store.MasterRecord and ARecord.DetailStores[...] give access to the parent and child stores.

Predefined rules

Kittox ships with a set of ready-to-use rules — you don't need to write any Delphi to use them.

Client-side (rendered as HTML input attributes, enforced live in the browser):

RuleEffect
ForceUpperCaseConverts the input to upper case while typing.
ForceLowerCaseConverts the input to lower case while typing.
ForceCamelCapsCapitalizes the first letter of each word.
MinValue / MaxValueSets the min / max attribute on numeric inputs.
MaxLengthLimits the number of characters.

Server-side:

RuleParametersEffect
EnforceRangeFrom, ToEnsures the To field is greater than or equal to the From field.
yaml
Fields:
  Code: String(20)
    Rules:
      ForceUpperCase:
      MaxLength: 20
Rules:
  EnforceRange:
    From: StartDate
    To: EndDate

Catalogue of real rules (KittoSCM)

The KittoSCM application is a large, real-world Kittox project that makes heavy use of rules. Its Source/Rules*.pas units are an excellent reference. A few representative patterns:

  • Generic, reusable rules (Rules.pas): GenerateNewId, CheckFieldDupValue (no duplicate values in the same table), CalcDescrizione / CalcId (compute a field from a template), GreatherThan / LowerThan / GreatherOrEqualThan / LowerOrEqualThan (compare two fields), PercentualeCheck (0–100), CheckIBAN, CheckFormatoPartitaIVA, CheckPasswordStrength, CalcProvincia / CalcNazione (cascade from a reference).
  • Domain rules (RulesNominativo.pas, RulesIscrizione.pas, …): Italian Codice Fiscale format and checksum validation, anagraphic data lookups, registration workflow (creating instalments, sending e-mails on AfterAdd, blocking edits of confirmed records in EditRecord/BeforeDelete).
  • Field-driven calculations (AfterFieldChange): selecting a Quota recomputes the total, applies discounts, and regenerates the detail instalment rows of the master record.

These show the full spectrum: pure validation, value computation, cascaded lookups via SQL, and master-detail side effects — all attached declaratively through the model's Rules: nodes.

See also

Released under Apache License, Version 2.0.