Serge
Serge

Reputation: 199

Data validation problems in Delphi

I have a form with several labeled edits and radio buttons (grouped in radio groups) for the user to enter information, and a Save button to save the entries. The Save button also closes the form. If there are invalid entries (e.g., an alpha character instead of a number) or if some entries are left blank, I want to alert the user to correct the errors, and not close the form until there are no blank edit boxes or unchecked radio groups. I do an error check separately for each edit box and radio group by means of raising a messageDlg alerting the user to the mistake. However, if the user does not correct a mistake and tries to save the form, I want to keep the form open (not be able to close it) and alert the user to the mistake. He should be able to close the form only after all the entries are valid and there are no unchecked radio groups.

Based on some replies to my earlier message, I have written the following code. The first one is an example of data entry validation for one edit box (there are several such) and the second is the code for the OnClick event of my Save button.

procedure TfrmAnalysisOptions.lbleConstraintsMaxChange(Sender: TObject);
var
  I: integer;
  Val, ValidEntry: string;
  Chr: char;
  RangeMin, RangeMax: Double;
  const Allowed = ['0'..'9', '.'];
begin

  Val := lbleConstraintsMax.Text;

      //initialize values    
  ValidEntry := '';
  ConstraintsMaxChange := '';

  //value can contain only numerals, and "."
    for I := 1 to Length(Val) do
     begin
       Chr := Val[I];
       if not (Chr in Allowed) then
       begin
     MessageDlgPos('The value entered for the max value of the ' +
               'constraint must contain only a numeral, a decimal ' +
               'point or a negative sign.',
            mtError, [mbOK], 0, 300, 300);
     Exit;

       end
       else ValidEntry := 'OK'; //validity check for this part

     end;

     //max value cannot be zero or less than the min value
    if not TryStrToFloat(Val, RangeMax) then Exit
    else if RangeMax = 0 then
    begin
       MessageDlg('Max value cannot be zero.', mtError, [mbOK], 0);
       Exit;
    end
    else if not TryStrToFloat(lbleConstraintsMin.Text, RangeMin) then Exit
    else if RangeMax < RangeMin then
      begin
    MessageDlgPos('Max value cannot be less than Min value.',
           mtError, [mbOK], 0, 300, 300);
    Exit;
      end

    else if (RangeMax < 0) then
      begin
    MessageDlgPos('A constraint cannot be negative.',
              mtError, [mbOK], 0, 300, 300);
    Exit;
      end

    //final validity check
    else if ValidEntry = 'OK' then ConstraintsMaxChange := 'OK'
    else MessageDlgPos('There was an unexpected problem with the ' +
               'value entered in the max constraints box.',
            mtError, [mbOK], 0, 300, 300);

end;

And this is the code for the OnClick event of the Save button. As you can see I am checking for the validity of several entries - and only if all of them are valid (that is have an 'OK' as the value of the corresponding variable), I allow the form to be closed. it is raising the messageDlg correctly when there is an invalid entry. However, it is continuing to raise the messageDlg even after the error is corrected and the Save button is pressed.

procedure TfrmAnalysisOptions.btnSaveOptionsClick(Sender: TObject);
//save input and output options
begin
    //check if all the option questions have been answered
    if not ((ConstraintsMinChange = 'OK') and //validation of correct entry as it is being entered
    (ConstraintsMinExit = 'OK') and //validation of entry as user is moving to another entry after an incorrect entry
    //several other such 'OK's 
      then
      begin
    MessageDlgPos('There is an invalid entry on the form.  Please ' +
              'correct it.', mtError, [mbOK], 0, 300, 300);
    Exit;
      end


    else if //more messageDlgs if some other conditions are not met 

    else
      begin
      //save input and output options
      end;
     //finally if all the conditions are met, close the form 
    Close;
end;

Upvotes: 1

Views: 3387

Answers (3)

AlexSC
AlexSC

Reputation: 1933

In your place I would approach the problem differently. Since you want to perform your check when the Save button is clicked and report any data entry problems at once, I would run a validation process that would return all the error messages at once. It would allow the user to see and fix all the problems before the next attempt to save.

It would be something like this:

Add a TMemo Component to your form, make it invisible and aligned as alTop. This memo will hold all the error messages, if any, and will show them. A good idea is to put all your editing controls on a panel and align it as alClient.

The OnClick event handler of the Save button should have something like this:

procedure TfrmAnalysisOptions.btnSaveOptionsClick(Sender: TObject);
begin
  ErrorsMemo.Lines.Clear; // clear previous messages
  GetErrorMessages(ErrorsMemo.Lines); // run our error check and retrieve messages
  ErrorsMemo.Visible := ErrorsMemo.Lines.Count>0; // if there are messages, show them
  if not ErrorsMemo.Visible then  // if no errors are showing, close
    Close;
end;

The GetErrorMessage method will hold all the checks and will be something like

procedure TfrmAnalysisOptions.GetErrorMessages(aMessageList: TStrings);
var
  I: Integer;
  Chr: Char;
begin
  // Here you put all the checks and add the error message to aMessageList,
  // for instance:

  // value can contain only numerals, and "."
  for I := 1 to Length(Val) do
  begin
    Chr := Val[I];
    if not (Chr in Allowed) then
      aMessageList.Add('The value entered for the max value of the ' +
                       'constraint must contain only a numeral, a decimal ' +
                       'point or a negative sign.');

 // Other validations follow here...

end;

The main idea here is to separate your checking process from your message presentation and make the form closing process simpler.

Upvotes: 1

Remy Lebeau
Remy Lebeau

Reputation: 596256

Set the button's ModalResult property to mrNone so it does not close the Form automatically, then have the OnClick event validate the entries as needed and set the Form's ModalResult property as needed only if they validate OK, eg:

procedure TfrmAnalysisOptions.btnSaveOptionsClick(Sender: TObject);
begin
  // check the entries as needed ...

  if (there is a validation error) then
  begin
    // do not close this form if there is an invalid entry
    MessageDlgPos('There is an invalid entry.  Please check and correct the entry.', mtError, [mbOK], 0, 300, 300)
    Exit;
  end;

  // save all the entries into different variables ...

  // now close the Form
  ModalResult := mrOk;
end;

Update: based on your updates, try this instead:

type
  TfrmAnalysisOptions = class(TForm)
  private
    //...
    ConstraintsMinOK: Boolean;
    ConstraintsMaxOK: Boolean;
    //...
  end;

procedure TfrmAnalysisOptions.lbleConstraintsMaxChange(Sender: TObject);
var
  I: integer;
  Val: string;
  Chr: char;
  RangeMin, RangeMax: Double;
begin
  //initialize values    
  ConstraintsMaxOK := False;

  Val := Trim(lbleConstraintsMax.Text);

  if not TryStrToFloat(Val, RangeMax) then
  begin
    MessageDlgPos('The value entered for the max value of the ' +
                  'constraint is not a valid numeric value.',
                  mtError, [mbOK], 0, 300, 300);
    Exit;
  end;

  //max value cannot be zero or less than the min value

  if RangeMax < 0 then
  begin
    MessageDlgPos('The value entered for the max value of the ' +
                  'constraint cannot be negative.',
                  mtError, [mbOK], 0, 300, 300);
    Exit;
  end;

  if RangeMax = 0 then
  begin
    MessageDlg('The value entered for the max value of the ' +
               'constraint cannot be zero.',
               mtError, [mbOK], 0);
    Exit;
  end;

  if not TryStrToFloat(lbleConstraintsMin.Text, RangeMin) then Exit;
  if RangeMax < RangeMin then
  begin
    MessageDlgPos('The value entered for the max value of the ' +
                  'constraint cannot be less than Min value.',
                  mtError, [mbOK], 0, 300, 300);
    Exit;
  end;

  //final validity check
  ConstraintsMaxChange := True;
end;

procedure TfrmAnalysisOptions.btnSaveOptionsClick(Sender: TObject);
begin
  if (not ConstraintsMinOK) or
     (not ConstraintsMaxOK)
    //or several other such 'OK's missing
  then
  begin
    MessageDlgPos('There is an invalid entry on the form.  Please ' +
                  'correct it.', mtError, [mbOK], 0, 300, 300);
    Exit;
  end;

  if //more messageDlgs if some other conditions are not met 
    //...
    Exit;
  end;

  //save input and output options...

  //finally if all the conditions are met, close the form 
  Close;
end;

Or better, don't do dynamic validation while the user is still entering data, that can lead to all kind of weird side effects if you are not careful. Wait until the Save button is pressed instead, then validate everything:

function ValidateNumeric(const S, Desc: String; var Value: Double): Boolean;
var
  I: Integer;
begin
  Result := TryStrToFloat(Trim(S), Value);
  if not Result then
  begin
    MessageDlgPos('The value entered for the ' + Desc +
                  ' is not a valid numeric value.',
                  mtError, [mbOK], 0, 300, 300);
    Exit;
  end;
end;

procedure TfrmAnalysisOptions.btnSaveOptionsClick(Sender: TObject);
var
  RangeMin, RangeMax: Double;
begin
  if not ValidateNumeric(lbleConstraintsMin.Text, 'min value of the constraint', RangeMin) then
  begin
    lbleConstraintsMin.SetFocus;
    Exit;
  end;

  if not ValidateNumeric(lbleConstraintsMax.Text, 'max value of the constraint', RangeMax) then
  begin
    lbleConstraintsMax.SetFocus;
    Exit;
  end;

  if RangeMax < 0 then
  begin
    MessageDlgPos('The value entered for the max value of the ' +
                  'constraint cannot be negative.',
                  mtError, [mbOK], 0, 300, 300);
    Exit;
  end;

  if RangeMax = 0 then
  begin
    MessageDlg('The value entered for the max value of the ' +
               'constraint cannot be zero.', mtError, [mbOK], 0);
    Exit;
  end;

  if RangeMax < RangeMin then
  begin
    MessageDlgPos('The value entered for the max value of the ' +
                  'constraint cannot be less than Min value.',
                  mtError, [mbOK], 0, 300, 300);
    Exit;
  end;

  //more validations...
  //more messageDlgs if some other conditions are not met ...

  //save input and output options...

  //finally if all the conditions are met, close the form 
  Close;
end;

Upvotes: 3

pf1957
pf1957

Reputation: 1007

Here is a typical scheme how we treate modal result and validity:

procedure TSomeModalForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  if ModalResult = mrOK then
    begin
      CanClose := CheckValidity;
      if CanClose then
        CanClose := SaveData;
    end
  else if dtsPacket.Modified then
    CanClose := dtsPacket.Cancel
  else if dtsNote.Modified then
    CanClose := dtsNote.Cancel
  else
    CanClose := TRUE;
end;

CheckValidity functions looks like this:

function TSomeModalForm.DoCheckValidity: boolean;
var
  InvCnt: Integer;
begin
  Result := inherited;
  InvCnt := 0;
  //
  HighlightInvalidValue(not edClient.IsEmpty, edClient, InvCnt);
  HighlightInvalidValue(not edBranch.IsEmpty, edBranch, InvCnt);
  HighlightInvalidValue(not edDstClient.IsEmpty, edDstClient, InvCnt);
  HighlightInvalidValue(not edDstBranch.IsEmpty, edDstBranch, InvCnt);
  //
  Result := Result and (InvCnt = 0);
  if not Result then
    ShowInErrBox(1);
end;

The code is based on our proprietary non VCL controls and assumes some validation support in a form ascendant, but a principle should be obvious.

Upvotes: 2

Related Questions