Gehe zu deutscher Webseite

ViaThinkSoft CodeLib

This article is in:
CodeLibProgramming aidsDelphi

Es wird folgendes Problem behandelt: http://stackoverflow.com/questions/35032857/combining-onvalidate-and-onbeforepost

Als Beispiel haben wir ein Form mit einem DBNavigator and ein paar DBEdits.



Wir möchten folgendes Erzielen:

- Es soll verhindert werden, dass ungültige Daten in die Datenbank gespeichert werden.
- Wenn der Benutzer etwas falsches in ein DBEdit eingibt und zu einem anderen Control wechseln möchte, soll eine Warnung angezeigt werden, aber er soll in der Lage sein, dies zu ignorieren und weiter zuarbeiten. Im idealen Falle wird der Benutzer einmal (!) gewarnt und zum Control zurückgeführt.
- Wenn der Benutzer das Dataset speichern möchte, soll dies verhindert werden, und die Warnung soll erneut angezeigt werden. Im idealen Falle wird der Benutzer zum Control geführt.

Das Hauptproblem ist, dass OnValidate nur ein TFieldNotifyEvent ist und es daher dem Dataset nicht mitteilen kann, ob die Eingabe gültig ist. Somit gibt es keine Möglichkeit um zu prüfen, ob alles OK ist, bevor man abspeichert.

Ich habe folgendes probiert:

Versuch 1: OnBeforePost prüft den Wert wirft eine Exception.

Pro:

- Ungültige Daten können nicht abgespeichert werden.
- Der Benutzer kann mit der Arbeit fortfahren und die Änderungen über den DBNavigator zurücksetzen/abbrechen, wenn er nicht in der Lage ist, das Formular korrekt auszufüllen.

Contra:

- Es wird keine Warnung angezeigt, wenn der Benutzer das DBEdit verlässt.
- Geringe Priorität: Alle validierungen müssen in einer einzigen Prozedur stattfinden. Bessere Objektorientierte Programmierung würde die Validierung direkt in einem Ereignis des TField machen.

Versuch 2: Eine Exception im OnValidate werfen.

Pro:

- Ungültige Daten können nicht abgespeichert werden.

Contra:

- Der Benutzer wird in eine Endlosschleife geraten, wenn er nicht in der Lage ist, etwas gültiges in das DBEdit einzutragen (z.B. wenn er nicht weiß, was er eingeben soll), und er kann nichtmal den "Abbrechen" Button im DBNavigotr klicken. In diesem Fall muss er die Anwendung durch den Task-Manager abbrechen, was ein Worse-Case-Szenario in bezug auf Benutzerfreundlichkeit wäre.

Versuch 3: Zeige eine Warnung (keine Exception) im OnValidate und validiere erneut in OnBeforePost, und werfe dort eine Exception.

Pro:

- Ungültige Daten können nicht abgespeichert werden.
- Eine Warnung ist möglich

Contra:

- Die Prüfung muss zweifach implementiert werden (selbst wenn Sie diese Prüfung in eine Funktion auslagern), und das Risiko ist groß, dass ein Programmierer einen der zwei Prüfungen vergisst. Außerdem wird diese Lösung bei großen Formularen mit vielen Feldern schnell unübersichtlich.
- Wenn der Benutzer den Speichern-Button im DBNavigator klickt, während er im DBEdit ist, erhält er die Nachricht doppelt (eine Warnung beim Verlassen des DBEdit, und eine Exception weil der Post fehlschlägt).
- Für den Fall dass der Wert bereits in der Datenbank falsch ist (z.B. weil eine alte Programmversion die Prüfungen nicht durchgeführt hatte), wird keine Warnung beim verlassen des DBEdits angezeigt, wenn der Benutzer das DBEdit nur verlassen hat, ohne etwas dort zu ändern.

Gibt es einen besseren Weg?

Lösung:

Um einige Probleme zu vermeiden, sollen Werte im OnValidate durchgeführt werden. Wird dort eine Exception geworfen, dann wird der Wert nicht akzeptiert. Allerdings entstehen dadurch neue Probleme.

Problem 1: Dies hat allerdings das Problem, das man in eine Endlosschleife gerät, wenn man keinen gültigen Wert eintragen kann, und auch nicht den "Cancel" Button in DBNavigator klicken kann!

Lösung: Um dies zu verhindern, wird folgende Strategie angewendet:
Im OnValidate des jeweiligen Felds der Tabelle wird folgender Code eingefügt:

Im OnValidate wird folgender Code eingefügt:


            if Sender = nil then
              Exception.Create('Fehlertext ...')
            else
              ShowMessage('Fehlertext ...');

Bei einer Validierung, die aufgrund eines Eingabefehlers stattfand, wird nur gewarnt.
Aber der Wert wird nun trotzdem in den Puffer geschrieben (also Table1XYZ).
Erst beim posten müssen wir nocheinmal genauer prüfen und das endgültige Speichern verhindern.

Daher wird dann folgender Code ausgeführt:


          var
            f: TField;
          begin
            for f in Table1.Fields do
            begin
              if Assigned(f.OnValidate) then
              begin
                f.OnValidate(nil); // "nil" sorgt dafür, dass eine Exception anstelle einer normalen Warnung geworfen wird
              end;
            end;

Problem 2: Im OnValidate können wir zwar ein SetFocus durchführen, um auf das Feld zurückzuspringen, allerdings muss man Edit-OnEnter (wegen grafischer Markierung des Felds) manuell aufrufen, um mit einem Tag verhindern, dass das Edit-OnExit nach dem Edit-OnEnter aufgerufen wird.

Lösung: siehe Code unten.

// -- Problem 1 --

procedure TForm1.Table1Edit1Validate(Sender: TField);
begin
  if IsXYZValid(Table1XYZ.AsString) then
  begin
    SpecialSetFocus(Edit1, Sender=nil);
    if Sender = nil then
      Exception.Create('Fehlertext ...')
    else
      ShowMessage('Fehlertext ...');
  end;
end;

procedure TForm1.Table1BeforePost(DataSet: TDataSet);
var
  f: TField;
begin
  for f in Table1.Fields do
  begin
    if Assigned(f.OnValidate) then
    begin
      f.OnValidate(nil); // "nil" sorgt dafür, dass eine Exception anstelle einer normalen Warnung geworfen wird
    end;
  end;
end;

// -- Problem 2 (kosmetisch) --

procedure TForm1.SpecialSetFocus(x: TWinControl; saveMode: boolean);
begin
  if not x.Focused then
  begin
    x.SetFocus;
    if not saveMode then
    begin
      x.Tag := 1; // Workaround damit EditEnter funktioniert, also nur kosmetisch
      EditEnter(x);
    end;
  end;
end;

procedure TForm1.Edit1Exit(Sender: TObject);
begin
   if TComponent(Sender).Tag = 0 then
   begin
     // Hier der reguläre Code
     TEdit(Sender).Color := clWindowText;
   end;
   TComponent(Sender).Tag := 0;
end;

procedure TForm1.Edit1Enter(Sender: TObject);
begin
   if TComponent(Sender).Tag = 0 then
   begin
     // Hier der reguläre Code
     TEdit(Sender).Color := clRed;
   end;
   TComponent(Sender).Tag := 0;
end;
Daniel Marschall
ViaThinkSoft Co-Founder