I have a form with 7 controls. Two controls are data aware, a TDBGrid and a TDBNavigator. Three others are not data-aware, a TJvCalendar2 and two TjvDateEdits. The last two controls are a TDataSource and a TTzDbf as the dataSource’s dataset.

I cannot, for the life of me, figure out how to update the current database record with the dates on the JvCalendar or either of the JvDateEdits without precipitating a catastrophic race condition that crashes the program.

In the form’s OnActivate method I copy data from the record that the database is currently positioned into a form variable. I then call two methods, one to update the JvCalendar and the other to update the two JvDateEdits.

These two methods save and then set to nil their respective control’s OnChange handlers, set their control’s date(s), restore the control’s original OnChange handlers and then exit.

To track when the dataset is being moved, I save and substitute the dataSet’s AfterScroll and BeforeScroll events. When the current row in the dbGrid is changed, either by a mouse click or cursor movement in the dbGrid or by a record change in the dbNavigator, these handlers update the database’s record from the form variable during BeforeScroll or retrieve, set the form variable and then update the JvCalendar and JvDateEdits.

Saving, updating the database’s record during the BeforeScroll event causes a reread of the record, an updating of the controls and then a rewrite of the database’s record. All of which leads to a loop, exhaustion of stack space and a crash.

What am I missing from my understanding and implementation of event handlers and data-aware controls, please?

Full example code follows:

----------------------- RaceCondition.dpr ----------------------

/// <summary>
///   An application to demonstrate one programmer's incomplete 
understanding
///   of data control's event system
/// </summary>
program RaceConditionDpr;

uses
  /// <summary>
  ///   Forms, forms and more forms
  /// </summary>
  Forms,
  /// <summary>
  ///   The application's main form with controls to try to plead for help
  ///   at understanding data control's interactions
  /// </summary>
  RaceConditionFrm in 'RaceConditionFrm.pas' {Form5};

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm5, Form5);
  Application.Run;
end.

----------------------- RaceConditionFrm.pas ----------------------

/// <summary>
///   Unit containing the application, RaceConditionDpr's main form.Uses
///   several third party controls:
///   <list type="number">
///     <item>
///       JEDI's TJvMonthCalendar2
///     </item>
///     <item>
///       JEDI's TJvDateEdit
///     </item>
///     <item>
///       Topaz' TTzDbf dataset. This might be able to be substituted by
///       another dataset type and still demonstrate the race condition
///       problem that this application is intended to convey.
///     </item>
///   </list>
///   Uses several third party libraries:
///   <list type="number">
///     <item>
///       TurboPower's SysTools for routines in its StDate and StDateSt
///       units
///     </item>
///   </list>
/// </summary>
/// <remarks>
///   Has 7 controls on a single form
///   <list type="bullet">
///     <item>
///       Two controls are data aware, a TDBGrid and a TDBNavigator.
///     </item>
///     <item>
///       Three others are not data aware, a TJvCalendar2 and two
///       TjvDateEdits.
///     </item>
///     <item>
///       The last two controls are a TDataSource and a TTzDbf as the
///       dataSource’s dataset.
///     </item>
///   </list>
/// </remarks>
unit RaceConditionFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, DB, tzprimds, ucommon, utzcds, utzfds, StdCtrls, Mask, JvExMask,
  JvToolEdit, JvExControls, JvCalendar, ExtCtrls, DBCtrls, Grids, DBGrids;

{$ifdef WIN32}
 {$A-}  {byte alignment}
{$else}
 {$ifdef LINUX}
  {$A-}  {byte alignment}
 {$endif}
{$endif}

type
  /// <summary>
  ///   Defines the type used to hold a dBase date in 'yyyymmdd' form. The
  ///   actual .dbf holds the date in this 'yyyymmdd' form but
  ///   retrieval/storage methods may insert date separators between the three
  ///   portions of the date, ie: 'mm/dd/yyyy' if the date locality has been
  ///   set to American.
  /// </summary>
  Tstring10 = string[10];     { for Date fields }
  /// <summary>
  ///   Record structure reflecting the field structure present in the .dbf.
  /// </summary>
  TDATES_Record = Record
     /// <summary>
     ///   Can be populated with the status of the .dbf record as on disk
     /// </summary>
     /// <value>
     ///   True if the record has been marked as deleted; False if not deleted
     /// </value>
     Deleted         : Boolean;
     /// <summary>
     ///   Field with the first date of the date span stored in the .dbf
     /// </summary>
     _DATEFIRST      : Tstring10;     { Date field }
     /// <summary>
     ///   Field with the last date of the date span stored in the .dbf
     /// </summary>
     _DATELAST       : Tstring10;     { Date field }
  end;

/// <summary>
///   Application's main form
/// </summary>
/// <remarks>
///   Has 7 controls.
///   <list type="bullet">
///     <item>
///       Two controls are data aware, a TDBGrid and a TDBNavigator.
///     </item>
///     <item>
///       Three others are not data aware, a TJvCalendar2 and two
///       TjvDateEdits.
///     </item>
///     <item>
///       The last two controls are a TDataSource and a TTzDbf as the
///       dataSource’s dataset.
///     </item>
///   </list>
/// </remarks>
  TForm5 = class(TForm)
    /// <summary>
    ///   dataaware control to display a grid of the database's records' data <br /><br />
    ///   Linked to DataSource DataSource1 <br />
    /// </summary>
    DBGrid1: TDBGrid;
    /// <summary>
    ///   <para>
    ///     dataaware control to ease user re-positioning of the database's
    ///     record pointer
    ///   </para>
    ///   <para>
    ///     Linked to DataSource DataSource1
    ///   </para>
    /// </summary>
    DBNavigator1: TDBNavigator;
    /// <summary>
    ///   <para>
    ///     Cool calendar control that can be configured to display more than
    ///     one month at a time. Will also display a time span in days and
    ///     this across multiple months.
    ///   </para>
    ///   <para>
    ///     Thanks JEDI
    ///   </para>
    /// </summary>
    JvMonthCalendar21: TJvMonthCalendar2;
    /// <summary>
    ///   <para>
    ///     An edit control that drops down a calendar to permit selecting a
    ///     date in a nice natural way. Selects the date that will become the
    ///     DateFirst date.
    ///   </para>
    ///   <para>
    ///     Thanks, again, JEDI
    ///   </para>
    /// </summary>
    JvDateEditDateFirst: TJvDateEdit;
    /// <summary>
    ///   <para>
    ///     An edit control that drops down a calendar to permit selecting a
    ///     date in a nice natural way. Selects the date that will become the
    ///     DateLast date.
    ///   </para>
    ///   <para>
    ///     Thanks, again, JEDI
    ///   </para>
    /// </summary>
    JvDateEditDateLast: TJvDateEdit;
    /// <summary>
    ///   <para>
    ///     the DataSource for the application.
    ///   </para>
    ///   <para>
    ///     Linked to DataSet TzDbf1
    ///   </para>
    /// </summary>
    DataSource1: TDataSource;
    /// <summary>
    ///   <para>
    ///     the DataSet for the application.
    ///   </para>
    ///   <para>
    ///     Linked to DataSource DataSource1
    ///   </para>
    /// </summary>
    TzDbf1: TTzDbf;
    /// <summary>
    ///   When the form gains focus, updates the non-data aware controls with
    ///   the contents of the current database record
    /// </summary>
    procedure FormActivate(Sender: TObject);
    /// <summary>
    ///   <para>
    ///     OnChange event handler called after the DateEdit1 control has
    ///     been changed, either by user interaction or by having its date
    ///     programmatically set.
    ///   </para>
    ///   <para>
    ///     With the control possibly having been edited by the user, it then
    ///     calls UpdateJvMontCalendar to update the calendar too.
    ///   </para>
    /// </summary>
    procedure JvDateEditDateFirstChange(Sender: TObject);
    /// <summary>
    ///   <para>
    ///     OnChange event handler called after the DateEdit2 control has
    ///     been changed, either by user interaction or by having its date
    ///     programmatically set.
    ///   </para>
    ///   <para>
    ///     With the control possibly having been edited by the user, it then
    ///     calls UpdateJvMontCalendar to update the calendar too.
    ///   </para>
    /// </summary>
    procedure JvDateEditDateLastChange(Sender: TObject);
    /// <summary>
    ///   <para>
    ///     OnChange event handler called after the Calendar control has been
    ///     changed, either by user interaction or by having its StartDate
    ///     and/or EndDate programmatically set.
    ///   </para>
    ///   <para>
    ///     With the control possibly having been edited by the user, it then
    ///     calls UpdateJvDateEdits to update the two DateEdit controls too.
    ///   </para>
    /// </summary>
    /// <param name="StartDate">
    ///   The first, earliest date on the calendar control
    /// </param>
    /// <param name="EndDate">
    ///   The second, later date on the calendar control. May be the same date
    ///   as the StartDate if the user has not selected different dates by
    ///   shift-clicking on a second date. The two dates will have been sorted
    ///   to supply the handler with the two different dates in ascending
    ///   order.
    /// </param>
    procedure JvMonthCalendar21SelChange(Sender: TObject; StartDate,
      EndDate: TDateTime);
    /// <summary>
    ///   <para>
    ///     OnAfterScroll event handler for the DataSet.
    ///   </para>
    ///   <para>
    ///     Called once the dataset has settled on what has become the
    ///     current record.
    ///   </para>
    ///   <para>
    ///     Causes the data in the FDates instance variable to be read, from
    ///     the database from its current record
    ///   </para>
    /// </summary>
    procedure TzDbf1AfterScroll(DataSet: TDataSet);
    /// <summary>
    ///   <para>
    ///     OnBeforeScroll event handler for the DataSet. <br /><br />Called
    ///     before the dataset leaves the current record to begin a move to
    ///     another.
    ///   </para>
    ///   <para>
    ///     Causes the data in the FDates instance variable to be written,
    ///     posted, to the database <br />
    ///   </para>
    /// </summary>
    procedure TzDbf1BeforeScroll(DataSet: TDataSet);
  private
    { Private declarations }
    /// <summary>
    ///   <para>
    ///     Instance variable to serve as the holder of values read from the
    ///     .dbf and input by the user by interaction with the form.
    ///   </para>
    ///   <para>
    ///     To be written to the .dbf to replace the field values on the
    ///     current record when the dataset is about to be repositioned.
    ///   </para>
    ///   <para>
    ///     To be populated by the field values on what comes to be the
    ///     current record after the dataset has been repositioned to what is
    ///     now the current record. Will have its field values modified when
    ///     the user interacts with the controls on the form.
    ///   </para>
    /// </summary>
    FDates : TDATES_Record;
    /// <summary>
    ///   Called to update the two date edit controls.
    ///   <list type="bullet">
    ///     <item>
    ///       Updates the DateEdit1 control with the DateFirst value in the
    ///       FDates record
    ///     </item>
    ///     <item>
    ///       Updates the DateEdit2 control with the DateLast value in the
    ///       FDates record <br />
    ///     </item>
    ///   </list>
    /// </summary>
    procedure UpdateJvDateEdits;
    /// <summary>
    ///   Called to update the calendar control.
    ///   <list type="bullet">
    ///     <item>
    ///       Updates the DateFirst property with the DateFirst value in
    ///       the FDates record
    ///     </item>
    ///     <item>
    ///       Updates the DateLast property with the DateLast value in the
    ///       FDates record <br />
    ///     </item>
    ///   </list>
    /// </summary>
    procedure UpdateJvMonthCalendar;
    /// <summary>
    ///   <para>
    ///     Update the .dbf wth the values modified by user interaction with
    ///     the form's controls, that is from instance variable FDates.
    ///   </para>
    ///   <para>
    ///     Writes FDates values to the current database record.
    ///   </para>
    /// </summary>
    procedure UpdateDbf;
    /// <summary>
    ///   Utility method to convert a Topaz style date string into a TDateTime
    ///   equivalent
    /// </summary>
    /// <param name="aTopazDate">
    ///   Date as string in 'yyyymmdd' format
    /// </param>
    /// <returns>
    ///   the equivalent date as a TDateTime
    /// </returns>
    function TopazToDate( const aTopazDate : Tstring10 ): TDateTime;
    /// <summary>
    ///   Utility method to convert a TDateTime into the equivalent Topaz style
    ///   date string in 'yyyymmdd' format
    /// </summary>
    /// <param name="aDate">
    ///   Date as TDateTime in format <br />
    /// </param>
    /// <returns>
    ///   the equivalent date as a string in 'yyyymmdd' format
    /// </returns>
    function DateToTopaz( aDate : TDateTime ): Tstring10;
  public
    { Public declarations }
  end;

var
  /// <summary>
  ///   Instance variable holding the form
  /// </summary>
  Form5: TForm5;

implementation

{$R *.dfm}

uses
  StDate,
  StDateSt;

const
  /// <summary>
  ///   constant for use in converting Topaz string dates to and from TDateTime
  /// </summary>
  zYYYYdMMdDDmask = 'yyyy.mm.dd';
//  zyyyymmddMask = 'yyyymmdd';

procedure TForm5.FormActivate(Sender: TObject);
begin
  FDates._DATEFIRST := TzDbf1.GetDField( 'DateFirst' );
  FDates._DATELAST := TzDbf1.GetDField( 'DateLast' );
  UpdateJvDateEdits;
  UpdateJvMonthCalendar;
end;

procedure TForm5.TzDbf1AfterScroll(DataSet: TDataSet);
begin
  UpdateJvDateEdits;
  UpdateJvMonthCalendar;
end;

procedure TForm5.TzDbf1BeforeScroll(DataSet: TDataSet);
begin
  UpdateDbf;
end;

procedure TForm5.UpdateDbf;
begin
//  TzDbf1.DisableControls;
  repeat
    asm nop end;
  until (TzDbf1.RLock);
  TzDbf1.SetDField( 'DateFirst', FDates._DATEFIRST );
  TzDbf1.SetDField( 'DateLast',  FDates._DATELAST );
  TzDbf1.ReplaceRec;
  TzDbf1.UnLock;
//  TzDbf1.EnableControls;
end;

procedure TForm5.UpdateJvDateEdits;
var
  EventSaved : TNotifyEvent;
begin
  EventSaved := JvDateEditDateFirst.OnChange;
  JvDateEditDateFirst.OnChange := nil;
  JvDateEditDateFirst.Date := TopazToDate( FDates._DATEFIRST );
  JvDateEditDateFirst.OnChange := EventSaved;

  EventSaved := JvDateEditDateLast.OnChange;
  JvDateEditDateLast.OnChange := nil;
  JvDateEditDateLast.Date := TopazToDate( FDates._DATELAST );
  JvDateEditDateLast.OnChange := EventSaved;
end;

procedure TForm5.UpdateJvMonthCalendar;
var
  EventSaved : TJvMonthCalSelEvent;
begin
  EventSaved := JvMonthCalendar21.OnSelChange;
  JvMonthCalendar21.OnSelChange := nil;

  JvMonthCalendar21.DateFirst := TopazToDate( FDates._DATEFIRST );
  JvMonthCalendar21.DateLast := TopazToDate( FDates._DATELAST );

  JvMonthCalendar21.OnSelChange := EventSaved;
end;

procedure TForm5.JvDateEditDateFirstChange(Sender: TObject);
begin
  FDates._DATEFIRST := DateToTopaz( JvDateEditDateFirst.Date );

  UpdateJvMonthCalendar;
end;

procedure TForm5.JvDateEditDateLastChange(Sender: TObject);
begin
  FDates._DATELAST := DateToTopaz( JvDateEditDateLast.Date );

  UpdateJvMonthCalendar;
end;

procedure TForm5.JvMonthCalendar21SelChange(Sender: TObject; StartDate,
  EndDate: TDateTime);
begin
  FDates._DATEFIRST := DateToTopaz( StartDate );
  FDates._DATELAST := DateToTopaz( EndDate );

  UpdateJvDateEdits;
end;

function TForm5.TopazToDate( const aTopazDate : Tstring10 ): TDateTime;
var
  anStDate : StDate.TStDate;
begin
  anStDate := stdatest.DateStringToStDate( zYYYYdMMdDDmask, aTopazDate, 2000 );
  Result := StDate.StDateToDateTime( anStDate );
end;

function TForm5.DateToTopaz(aDate: TDateTime): Tstring10;
var
  anStDate : StDate.TStDate;
begin
  anStDate := StDate.DateTimeToStDate( aDate );
  Result := StDateSt.StDateToDateString( zYYYYdMMdDDmask, anStDate, False );
end;

end.

----------------------- RaceConditionFrm.dfm ---------------------

object Form5: TForm5
  Left = 0
  Top = 0
  Caption = 'Form5'
  ClientHeight = 336
  ClientWidth = 628
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnActivate = FormActivate
  PixelsPerInch = 96
  TextHeight = 13
  object DBGrid1: TDBGrid
    Left = 8
    Top = 8
    Width = 320
    Height = 120
    DataSource = DataSource1
    TabOrder = 0
    TitleFont.Charset = DEFAULT_CHARSET
    TitleFont.Color = clWindowText
    TitleFont.Height = -11
    TitleFont.Name = 'Tahoma'
    TitleFont.Style = []
  end
  object DBNavigator1: TDBNavigator
    Left = 8
    Top = 134
    Width = 240
    Height = 25
    DataSource = DataSource1
    TabOrder = 1
  end
  object JvMonthCalendar21: TJvMonthCalendar2
    Left = 168
    Top = 168
    Width = 451
    ParentColor = False
    TabStop = True
    TabOrder = 2
    DateFirst = 43364.000000000000000000
    DateLast = 43364.000000000000000000
    MaxSelCount = 366
    MultiSelect = True
    Today = 43364.458842245370000000
    OnSelChange = JvMonthCalendar21SelChange
  end
  object JvDateEditDateFirst: TJvDateEdit
    Left = 24
    Top = 192
    Width = 121
    Height = 21
    ShowNullDate = False
    StartOfWeek = Sun
    TabOrder = 3
    OnChange = JvDateEditDateFirstChange
  end
  object JvDateEditDateLast: TJvDateEdit
    Left = 24
    Top = 240
    Width = 121
    Height = 21
    ShowNullDate = False
    StartOfWeek = Sun
    TabOrder = 4
    OnChange = JvDateEditDateLastChange
  end
  object DataSource1: TDataSource
    DataSet = TzDbf1
    Left = 408
    Top = 64
  end
  object TzDbf1: TTzDbf
    Active = True
    BeforeScroll = TzDbf1BeforeScroll
    AfterScroll = TzDbf1AfterScroll
    DbfFields.Strings = (
      'datefirst, D, 10, 0'
      'datelast, D, 10, 0')
    DbfFileName = 
      'f:\delphi projects\theo\fillsound in delphi for mdx on 20161109\' +
      'dunit\holidaytracking\race condition\dates.dbf'
    HideDeletedRecs = False
    TableLanguage = tlOem
    ReadOnly = False
    CreateIndex = ciNotFound
    Exclusive = True
    Left = 496
    Top = 64
  end
end
3

There are 3 answers

7
MartynA On

Firstly, a couple of things to note:

  • I'm not sure if you know but there is a db-aware version of the TJvDateEdit, tJvDBDateEdit - http://wiki.delphi-jedi.org/wiki/JVCL_Help:TJvDBDateEdit

  • There is an Embarcadero tutorial about making a db-aware version of TMonthCalendar, which should be readily adaptable to TJvMonthCalender

Secondly, I thought I would include an example of how to make a TMonthCalendar functionally db-aware without having to write a db-aware descendant of it. Doing it this way avoids the need for catching and handling TDataSet and TMonthCalendar event to keep them manually synchronised.

The example below works by creating a TFieldDataLink descendant which can be created within your project and can make a standard TMonthCalendar (or TJvMonthCalendar, with trivial modification) behave as db-aware without having to create a custom TDBMonthCalendar component and install it in the Component Palette. The minor downside of this is that a bit of set-up code is required. This TCalendarDataLink class automtaically handlres all the synchronisation necessary.

Code

  type
    TCalendarDataLink = class(TFieldDataLink)
    private
      FCalendar: TMonthCalendar;
    protected
      property Calendar : TMonthCalendar read FCalendar write FCalendar;
      procedure CalendarClick(Sender : TObject);
      procedure DataChange(Sender : TObject);
      procedure UpdateData(Sender : TObject);
    public
      constructor Create(AOwner : TComponent; ACalendar : TMonthCalendar; ADataSource : TDataSource; const AFieldName : String);
    end;

    TForm1 = class(TForm)
      DBGrid1: TDBGrid;
      CDS1: TClientDataSet;
      DataSource1: TDataSource;
      CDS1ID: TAutoIncField;
      CDS1Value: TStringField;
      Button1: TButton;
      CDS1Name: TStringField;
      DBNavigator1: TDBNavigator;
      cbNormal: TCheckBox;
      CDS1Number: TIntegerField;
      CDS1Date: TDateField;
      MonthCalendar1: TMonthCalendar;
      procedure FormCreate(Sender: TObject);
    private
    protected
      Link : TCalendarDatalink;
    public
    end;

  [...]

  procedure TForm1.FormCreate(Sender: TObject);
  var
    i : Integer;
  begin
    CDS1.CreateDataSet;
    for i := 1 to 200 do begin
      CDS1.Insert;
      CDS1.FieldByName('Value').AsString := 'A' + Chr(Ord('A') + i);
      if Odd(i) then
        CDS1.FieldByName('Value').Clear;
      CDS1.FieldByName('Date').AsDateTime := Now - i;
      CDS1.Post;
    end;
    Link := TCalendarDataLink.Create(Self, MonthCalendar1, DataSource1, 'Date');
    CDS1.First;
  end;

  { TCalendarDataLink }

  procedure TCalendarDataLink.CalendarClick(Sender: TObject);
  var
    ADate : TDateTime;
  begin
    ADate := Calendar.Date;
    Edit;
    Calendar.Date := ADate;
    Field.Text := DateToStr(Calendar.Date);
  end;

  procedure TCalendarDataLink.DataChange(Sender: TObject);
  begin
    inherited;
    if Field <> Nil then
      if Field.IsNull then
        Calendar.Date := Now
      else
        Calendar.Date := Field.AsDateTime;
  end;

  procedure TCalendarDataLink.UpdateData(Sender: TObject);
  begin
    Field.AsDateTime := Calendar.Date;
  end;

Obviously, the TCalendarDataLink code could be included in its own unit and used from there, if desired.

2
JacalarRick On

I use BeforePost methods for reading the values in the non db-aware controls and setting the record’s values, and the AfterScroll methods for setting the non db-aware controls.

[Edit to show some basic example code] The whole concept of BeforePost is to have a chance to change fields in the record. This is a stripped down pseudo example of what I have been doing. I am using the Win10 DatePicker in this example. My unit also has a private variable for the date in question, since I need to convert to the Hebrew calendar, as well. I check to see if the date picker has changed against the original date in AfterScroll in the BeforePost method, to then set the field in the record.

    unit uYarzheit;
...
type
   TYarzheitForm = class(Tform)
   ...
   fdqYz : tTFDQuery;
   ...
   dpCivilDoD : TDatePicker;
   ...
   procedure fdqYzAfterScroll(DataSet : TDataSet);
   procedure fdqYzBeforePost(DataSet : TDataSet);
   ...
   private
     dbCDod  : tdatetime;
    ....

  implementaion
  ...

  procedure TYarzheitForm.fdqYzAfterScroll(DataSet : TDataSet);
  begin
  ....
     dbCDoD := fdqYz.FieldByName('MilestoneDate').AsDateTime;
     dpCivilDoD.date := dbCDoD;
  ...
  end;

  procedure TYarzheitForm.fdqYzBeforePost(DataSet : TDataSet);
  begin
    if dpCivilDoD.Date <> dbCDoD then 
     fdqYz.FieldByName('MilestoneDate').AsDateTime := dpCivilDoD.Date;
  end;
end;

The BeforePost method is a great place to do all sorts of validation prior to writing the changes to the record to the database (e.g., stripping trailing spaces from text fields).

3
Freddie Bell On

In a situation like this, it's wise to use a global flag that you can check for (and avoid unnecessary) recursion.

var
   FImCallingMyself: Boolean;

procedure callsitself;
begin
   if FImcallingmyself then
     EXIT;
   FImcallingmself := True;
   try
     // do stuff
   finally
      FImcallingmyself := False;
   end;
end;