Entity validation - Invalid value accepted in TAureliusDataset

I have defined some validations on an entity TTest:

  **{$RTTI EXPLICIT METHODS([vcPrivate..vcPublished])}**
  [Entity]
  [Table('Test')]
  [Id('FID', TIdGenerator.IdentityOrSequence)]
  TTest = class
  private
    [Column('ID', [TColumnProp.Required, TColumnProp.NoInsert, TColumnProp.NoUpdate])]
    FID: Integer;

    [Column('Year', [])]
    FYear: Nullable<Integer>;

    [Column('Description', [], 50)]
    **[MinLength(4)]**
    FDescription: Nullable<string>;
  public
    **[OnValidate]**
**    function CheckYear: IValidationResult;**
    property Id: Integer read FID write FID;
    property Year: Nullable<Integer> read FYear write FYear;
    property Description: Nullable<string> read FDescription write FDescription;
  end;

function TTest.CheckYear: IValidationResult;
begin
  result := TValidationResult.Create;
  if Year.IsNull then
    Exit;
  if Year <> 2024 then
    result.Errors.Add(TValidationError.Create('Year must be 2024!'));
end;

When editing through a TAureliusDataset, bound to a DBGrid (or DevExpress TcxGrid for that matter), an exception is correctly triggered if I enter an invalid value for Year or Description. So far so good...

But when I cancel the record edit, the invalid value is stored in the entity and shown in the grid! The database value is NOT updated (i.e. a refresh on the entity restores the old value). Please have a look at the attached demo app (ValidationTest.zip (92.1 KB)).

Is there something I am missing or is this a bug in the TAureliusDataset (I find it hard to believe nobody ever noticed this before)?

Also, it appears the compiler directive ({$RTTI EXPLICIT METHODS([vcPrivate..vcPublished])}) has no effect. If I comment it out, all still works. I did notice that the CheckYear function must be Public. Is that correct?

1 Like

RTTI for public methods is generated by default, so it's not needed for public methods.

That's the reason for the {$RTTI} directive. If you want the method to be protected, you must add the directive. But you are adding it in the wrong place, you must add it at the top of the unit, before the interface keyword.

I cannot reproduce the issue. What are the exact steps?
I tried to:

  1. type "2023" and "asdf" in fields "Year" and "Description", respectively.
  2. Click the post button in the db navigator. The validation error is raised.
  3. Click the cancel button in the db navigator. The record being edited is gone, correctly.

All clear regarding the {$RTTI} directive. I now have it before the interface statement and made the validation function protected. No further issues there.

It appears you tried to reproduce the issue when inserting a new record? That indeed works as expected. To reproduce the issue, please try the following:

  1. Insert a valid record (2024 and 'Long description')
  2. Post the new record
  3. Edit the record and change to an invalid value (e.g. 2023)
  4. Try to post the record (triggers exception, correctly)
  5. Cancel the record change (navigator)

You'll see that when you cancel, the record is no longer in edit mode but the invalid value (2023) remains.

  1. Clicking Refresh in the navigator has no effect, telling me the entity object now holds the invalid value
  2. Click the [Refresh entity] button does revert the value to its previous (valid) value, telling me the database is not updated (despite the dataset being linked to an object manager (in FormCreate).

Looking forward to your findings!

I have attempted some debugging (started tracing from some dummy code in OnBeforeCancel) and stepped into the following:

unit Aurelius.Bind.BaseDataset;
......
procedure TBaseAureliusDataset.InternalCancel;
var
  Obj: TObject;
begin
  inherited;
  if FErrorOnPost and FRefreshObjectOnCancel and (State = dsEdit) then
  begin
    Obj := GetBufferRecInfo(GetActiveRecBuf)^.Obj;
    if Obj <> nil then
      DoObjectRefresh(Obj);
  end;
end;

I noticed that FRefreshObjectOnCancel is false so the if statement (which appears to "reset" the object) is skipped.

I also noticed that TBaseAureliusDataset has the following property definition:

property RefreshObjectOnCancel: Boolean read FRefreshObjectOnCancel write FRefreshObjectOnCancel default False;

Adding adsTest.RefreshObjectOnCancel := true indeed solves the issue! Why is this property False by default. As the default introduces this unexpected behavior, I think it should be documented and perhaps even be published for the TAureliusDataset. What do you think?

Yes, you must set RefreshObjectOnCancel to True. The data is never saved to the database, it's just the object in memory that remains with the properties updated. This property is False just for backward compatibility reasons, as it was introduced at a later point.

Well I could include RefreshObjectOnCancel := true on any form using a TAureliusDataset or derive a TMyAureliusDataset as a work-around, I suppose. However, do you not agree that this is actually a design flaw? After all, it may not corrupt the database (with an invalid value) but it leaves the object in an invalid state, confusing the end-user.

Needing to include a statement to ensure correct behavior is not the best design, is it?

Agree that it should be True by default, but since it was introduced later, we didn't want to break existing applications. Backward compatibility is avoid introducing breaking changes is also something we consider a good design.