Как сохранить синхронизацию элементов управления с учетом данных и без данных друг с другом и с базой данных, с которой они работают?
У меня есть форма с 7 элементами управления. Два элемента управления осведомлены о данных, TDBGrid и TDBNavigator. Три других не осведомлены о данных: TJvCalendar2 и два TjvDateEdits. Последние два элемента управления - это TDataSource и TTzDbf в качестве набора данных источника данных.
Насколько я могу судить, я не могу понять, как обновить текущую запись в базе данных датами в JvCalendar или любом из JvDateEdits, не вызывая катастрофического состояния гонки, которое приводит к сбою программы.
В методе OnActivate формы я копирую данные из записи о том, что база данных в настоящее время расположена в переменной формы. Затем я вызываю два метода, один для обновления JvCalendar, а другой для обновления двух JvDateEdits.
Эти два метода сохраняют и затем устанавливают в ноль обработчики OnChange их соответствующих элементов управления, устанавливают дату (даты) их элементов управления, восстанавливают исходные обработчики OnChange элемента управления и затем завершают работу.
Чтобы отслеживать, когда набор данных перемещается, я сохраняю и заменяю события 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
3 ответа
Во-первых, пару вещей, чтобы отметить:
Я не уверен, что вы знаете, но существует версия TJvDateEdit с поддержкой db, tJvDBDateEdit - http://wiki.delphi-jedi.org/wiki/JVCL_Help:TJvDBDateEdit
Существует учебное пособие по Embarcadero о создании версии TMonthCalendar с поддержкой БД, которая должна быть легко адаптирована к TJvMonthCalender.
Во-вторых, я подумал, что включу пример того, как сделать TMonthCalendar функционально-ориентированным на db, без необходимости писать его потомком с поддержкой db. Это позволяет избежать необходимости перехватывать и обрабатывать события TDataSet и TMonthCalendar для их синхронизации вручную.
Приведенный ниже пример работает с созданием потомка TFieldDataLink, который может быть создан в вашем проекте и может заставить стандартный TMonthCalendar (или TJvMonthCalendar с тривиальной модификацией) вести себя как db-осведомленный, не создавая пользовательский компонент 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 может быть включен в его собственный модуль и использоваться оттуда, если это необходимо.
Я использую BeforePost
методы для чтения значений в элементах управления без учета базы данных и установки значений записи, а также AfterScroll
методы настройки не-db-осведомленных элементов управления.
[Изменить, чтобы показать некоторый основной пример кода] Вся концепция BeforePost
это возможность изменить поля в записи. Это урезанный псевдо-пример того, что я делал. Я использую Win10 DatePicker в этом примере. У моего юнита также есть приватная переменная для рассматриваемой даты, так как мне нужно также конвертировать в еврейский календарь. Я проверяю, изменился ли выбор даты по сравнению с первоначальной датой в AfterScroll
в BeforePost
метод, чтобы затем установить поле в записи.
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;
BeforePost
Метод является отличным местом для проведения всевозможных проверок перед записью изменений в запись в базу данных (например, удаление конечных пробелов из текстовых полей).
В такой ситуации целесообразно использовать глобальный флаг, который вы можете проверить (и избежать ненужной) рекурсии.
var
FImCallingMyself: Boolean;
procedure callsitself;
begin
if FImcallingmyself then
EXIT;
FImcallingmself := True;
try
// do stuff
finally
FImcallingmyself := False;
end;
end;