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
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
Obviously, the TCalendarDataLink code could be included in its own unit and used from there, if desired.