我有一个包含 7 个控件的表单。两个控件是数据感知的,一个 TDBGrid 和一个 TDBNavigator。另外三个不支持数据,一个 TJvCalendar2 和两个 TjvDateEdits。最后两个控件是一个 TDataSource 和一个 TTzDbf 作为数据源的数据集。
在我的一生中,我无法弄清楚如何使用 JvCalendar 或任何一个 JvDateEdits 上的日期更新当前数据库记录,而不会引发导致程序崩溃的灾难性竞争条件。
在表单的 OnActivate 方法中,我将数据库当前定位的记录中的数据复制到表单变量中。然后我调用两种方法,一种更新 JvCalendar,另一种更新两个 JvDateEdit。
这两种方法保存并设置它们各自控件的 OnChange 处理程序为零,设置其控件的日期,恢复控件的原始 OnChange 处理程序,然后退出。
为了跟踪数据集何时被移动,我保存并替换了 dataSet 的 AfterScroll 和 BeforeScroll 事件。当 dbGrid 中的当前行通过鼠标单击或 dbGrid 中的光标移动或 dbNavigator 中的记录更改而更改时,这些处理程序在 BeforeScroll 或检索期间从表单变量更新数据库的记录,设置表单变量并然后更新 JvCalendar 和 JvDateEdits。
在 BeforeScroll 事件期间保存、更新数据库记录会导致重新读取记录、更新控件,然后重写数据库记录。所有这些都会导致循环、堆栈空间耗尽和崩溃。
请问我对事件处理程序和数据感知控件的理解和实现缺少什么?
完整的示例代码如下:
------------------------------------ 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