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:
| Level | Where | When applied |
|---|---|---|
| Model | Rules: at the top of a Models/*.yaml file | Always, whenever a record of that model is created, edited or deleted. |
| Model field | Rules: under a field in Models/*.yaml | Always, unless a view-table-field rule of the same type overrides it. |
| View table | Rules: under a MainTable / detail table in a Views/*.yaml file | Only when editing through that view, in addition to (and before) the model rules. |
| View table field | Rules: under a field in a view | Only 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:
# 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 selectionNotes from the example above:
- Several rules can be stacked on the same field; they fire in declaration order.
ForceUpperCaseis 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
Referencefield (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.
| Method | Called when | Typical use |
|---|---|---|
NewRecord | A new record is created, before it is shown | Set computed default values (declarative DefaultValue is already applied). |
EditRecord | An existing record is opened for editing | Block editing of locked records; set transient values. |
AfterShowEditWindow | After the edit form has been built | Show/hide or enable editors based on the record. |
DuplicateRecord | A record is being duplicated | Reset or adjust values that must not be copied. |
BeforeFieldChange | Before a field value changes | Disallow the change (ADoIt := False), or rewrite ANewValue. |
AfterFieldChange | After a field value changed | Look up related data, recalculate dependent fields, validate the single value. |
AfterRefreshReferenceField | After a reference field is refreshed | Update editors that depend on the referenced record. |
BeforeAdd | Server-side, before INSERT | Cross-field validation specific to new records. |
BeforeUpdate | Server-side, before UPDATE | Cross-field validation specific to updates. |
BeforeAddOrUpdate | Convenience: called by both BeforeAdd and BeforeUpdate | The most common place for "before save" validation and calculation. |
BeforeDelete | Server-side, before DELETE | Block deletion (referential checks, status checks); cascade deletes. |
AfterAdd / AfterUpdate | After a successful INSERT / UPDATE | Side effects within the same transaction (insert detail rows, send mail, etc.). |
AfterAddOrUpdate | Convenience: called by both AfterAdd and AfterUpdate | Post-save logic common to insert and update. |
AfterDelete | After a successful DELETE | Post-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:
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:
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(orTK).GetClassIdstrips aTKprefix if present, otherwise a singleT. SoTNominativoCheckis referenced asNominativoCheck:in YAML, andTKEnforceRangeasEnforceRange:. - Register the class in the unit
initializationand unregister it infinalization. - The unit must be reachable from the project — add it (directly or transitively) to the
usesclause ofUseKitto.pas, so itsinitializationsection 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:
Cognome: String(100) not null
Rules:
CalcDescrizione: {Cognome} {Nome} - {Campagna_Descrizione}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(...):
Id: String(32) not null primary key
Rules:
GenerateNewId:
CharSize: 5procedure 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;FindFieldreturnsnilif the field is not in the current view.AField.ParentRecord— from insideAfterFieldChange, 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.MasterRecordandARecord.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):
| Rule | Effect |
|---|---|
ForceUpperCase | Converts the input to upper case while typing. |
ForceLowerCase | Converts the input to lower case while typing. |
ForceCamelCaps | Capitalizes the first letter of each word. |
MinValue / MaxValue | Sets the min / max attribute on numeric inputs. |
MaxLength | Limits the number of characters. |
Server-side:
| Rule | Parameters | Effect |
|---|---|---|
EnforceRange | From, To | Ensures the To field is greater than or equal to the From field. |
Fields:
Code: String(20)
Rules:
ForceUpperCase:
MaxLength: 20
Rules:
EnforceRange:
From: StartDate
To: EndDateCatalogue 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 onAfterAdd, blocking edits of confirmed records inEditRecord/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
- Models — where field-level and model-level
Rules:nodes are declared. - How to validate data before storing — a focused recipe with the
BeforeAddpattern and message formatting. - Form — partial date / time values during typing — how
AfterFieldChangeinteracts with incomplete dates. - DB access from Delphi — running SQL from inside a rule.
