首页 > 解决方案 > 如何保持数据感知和非数据感知控件的混合相互同步以及与它们所作用的数据库同步?

问题描述

我有一个包含 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

标签: delphidata-aware

解决方案


首先,有几点需要注意:

其次,我想我将包含一个示例,说明如何使 TMonthCalendar 在功能上具有 db-aware 功能,而无需编写它的 db-aware 后代。这样做可以避免捕获和处理 TDataSet 和 TMonthCalendar 事件以使它们保持手动同步。

下面的示例通过创建一个 TFieldDataLink 后代来工作,该后代可以在您的项目中创建,并且可以使标准 TMonthCalendar(或 TJvMonthCalendar,只需稍加修改)表现为 db-aware,而无需创建自定义 TDBMonthCalendar 组件并将其安装在组件面板中. 这样做的次要缺点是需要一些设置代码。这个 TCalendarDataLink 类自动处理所有必要的同步。

代码

  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;

显然,如果需要,TCalendarDataLink 代码可以包含在它自己的单元中并从那里使用。


推荐阅读