Does TClientDataSet.ApplyUpdates(0) require to execute CheckBrowseMode/Post before? - delphi

I have TIBQuery-TDataSetProvider-TClientDataSet chain in Delphi 2009 (Firebird 3.0) and I execute MyClientDataSet.ApplyUpdates(0). Am I required to call CheckBrowseMode or Post on this CDS before calling ApplyUpdates(0). I am almost sure that I am required to call Post/CheckBrowseMode and I think that no-posted updates will not be applied to the IBQuery, I have no documentation for/against such thinking but it is logical to think so. But sometimes I can observe that MyClientDataSet is in [dsInsert, dsEdit] state before ApplyUpdates(0) and the new values are still posted and saved in query. But there are evidence and reason against that as well. So - I am confused.
Of course, I don't use CachedUpdated for TIBQuery (because there is CDS) and this is not the question about commiting of transactions, I strongly control them and that issue is excluded.
I made test: I put raise Exception in the BeforePost event of MyClientDataSet and MadException gives trace:
TMyDM.MyClientDataSetBeforePost
TDataSet.DoBeforePost
TDataSet.Post
TCustomClientDataSet.Post
TDataSet.CheckBrowseMode
TCustomClientDataSet.ApplyUpdates
So, there is empirical evidence, that CheckBroseMode is called automatically, but is it by accident (e.g. due to some special configuration of DataSetProvider or ClientDataSet) or is it rule that I can find in the documentation?

or is it rule that I can find in the documentation?
There is a simpler rule, which should be self-evident, I would have thought: If the CDS's state is dsEdit or dsInsert, one might think of it as being in a change-pending state - changes have made (e.g. in a DB-aware control) but not yet been written back to the CDS's record buffer (or buffers if it has nested data). Otoh, the point of calling ApplyUpdates is to write changes in the record buffer back to the source dataset via the DSP. It would seem elementary therefore that one should not attempt to call ApplyUpdates while the CDS is in dsEdit/dsInsert. In any case, CheckBrowse mode is called by TCustomClientDataSet.ApplyUpdates:
function TCustomClientDataSet.ApplyUpdates(MaxErrors: Integer): Integer;
var
RootDataset: TCustomClientDataset;
begin
CheckBrowseMode;
RootDataset := Self;
while RootDataset.FParentDataSet <> nil do
RootDataset := RootDataset.FParentDataset;
with RootDataset do
if ChangeCount = 0 then
Result := 0 else
Reconcile(DoApplyUpdates(Delta, MaxErrors, Result));
end;
It is worth noting that if a dataset's state is dsEdit or dsInsert, CheckBrowseMode does one of three things:
Nothing, except calling CheckActive and then DataEvent(deCheckBrowseMode, 0),
if the dataset's state is not dsEdit, dsInsert or dsSetKey
Calls Post if the current record had been modified
Calls Cancel otherwise.
procedure TDataSet.CheckBrowseMode;
begin
CheckActive;
DataEvent(deCheckBrowseMode, 0);
case State of
dsEdit, dsInsert:
begin
UpdateRecord;
if Modified then Post else Cancel;
end;
dsSetKey:
Post;
end;
end;

Related

Why fires the AfterScroll event of a nested (detail) dataset more than once when its master dataset scrolled to another record? How to deal with it?

I have a Delphi program with 3 successive nested (master-details) FireDAC datasets namely: SuperMaster, Master and Details dataset.
In the AfterScroll event of the Details dataset only, new records are inserted into the database.
My program logic needs the AfterScroll event of the Details dataset to fire only once: when any of the parent datasets (SuperMaster or Master) scrolled to another record. The AfterScroll event should not trigger more than once because this will cause my logic to insert wrong records.
The problem is that when scrolling occurs to any of the parent datasets (from the current record to another one, even to the adjacent one), i.e. moving a single record only, the Details dataset AfterScroll event fired more than once and sometimes more than twice!
I do not want to mess with the original FireDac classes by overriding its AfterScroll event to force it to do my logic only once, nor to add a Boolean flag to my logic so that it runs only once.
I tried to put all the tables into one view, but it costs me to change the program logic and make future updates more demanding.
I searched and read many articles and a book ("Delphi in Depth: FIREDAC" by Cary Jensen), but could not find a solution nor know why or how it occurs!
Why does this happen? I want to understand the basics please. Is there any elegant solution?
Short of changing the FireDAC source (which I think would be a thoroughly bad idea - beware of opening Pandora's box) I don't think your goal of avoiding the
use of a Boolean flag is achievable. It should however be trivial to achieve if you do use a Boolean flag.
I also wonder whether you observation that the Detail's AfterScroll event occurs more than once per detail dataset
is correct, if that is what you are saying. If it is, you should edit your q to include a minimal, reproducible example, preferably based on the code below.
TDataSet and its descendants, including all the FireDAC datasets operate to a very tightly designed
state machine which includes the handling of master-detail behaviour that's been tested by all the TDataSet
usage since Delphi was first released. Trying to mess with that would invite
disaster.
If you try the minimal VCL project below, I think you'll find it's not very hard to satisfy yourself that
the Detail's AfterScroll event behaves exactly as you would expect from the coding of TDataSet's and FireDAC's source.
Running the code you will find that the breakpoint in DetailAfterScroll trips 4 times before
the first breakpoint on MasterFirst. The next time the BP trips, observe the call stack via
View | Debug Windows | Call stack; if you look down the cal stack, you'll find that the call to Detail.AfterScroll
was ultimately called via the penultimate line of
procedure TFDMasterDataLink.DataEvent(Event: TDataEvent; Info: NativeInt);
An FDMasterDataLink is automatically created to handle the triggering of Detail data events based on
the operation of the Master. And that, really, is the end of the story. because however much you might
disagree with this behaviour , you can't really do anything about it short of using a Boolean flag
in your own code.
I think it would be wise to verify that DetailAfterScroll is only being called once per dataset that's
on the detail side of the Master. If it's happening more than once, it would be worth checking that
it isn't your own code (or linking together of DataSets) that's causing it.
As you'll see, nearly all of what the example does is defined in the OnCreate event
to avoid having to use the Object Inspector to set up the components, so that is should "just work".
Code:
type
TForm1 = class(TForm)
Master: TFDMemTable;
Detail: TFDMemTable;
DataSource1: TDataSource;
procedure FormCreate(Sender: TObject);
procedure DetailAfterScroll(DataSet: TDataSet);
public
DetailsScrolled : Integer;
end;
[...]
procedure TForm1.DetailAfterScroll(DataSet: TDataSet);
begin
// Place debugger breakpoint on the following line
Inc(DetailsScrolled);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
AField : TField;
begin
AField := TIntegerField.Create(Self);
AField.FieldName := 'MasterID';
AField.DataSet := Master;
Master.CreateDataSet;
AField := TIntegerField.Create(Self);
AField.FieldName := 'DetailID';
AField.DataSet := Detail;
AField := TIntegerField.Create(Self);
AField.FieldName := 'MasterID';
AField.DataSet := Detail;
Detail.CreateDataSet;
DataSource1.DataSet := Master;
Detail.MasterSource := DataSource1;
Detail.MasterFields := 'MasterID';
Detail.IndexFieldNames := 'MasterID;DetailID';
Master.InsertRecord([1]);
Master.InsertRecord([2]);
Detail.InsertRecord([1, 1]);
Detail.InsertRecord([2, 1]);
Detail.InsertRecord([3, 2]);
// Place debugger breakpoint on EACH of the following three lines
Master.First;
Master.Next;
Master.First;
end;

ADODataSet.Open bypasses try catch with `ArgumentOutOfRange` exception, hangs application - Delphi 10.2

I maintain an application that runs as a service in a server environment. It is multithreaded, where each thread does work according to a task queue. This task queue is just a list of strings with "job types" as their values. So while several threads may be running, each thread would be a different job, and each thread internally runs just one task at a time.
I'm experiencing an intermittent issue that happens when calling Open on a TADODataSet. Sometimes, not always, and with no discernible pattern, Data.Win.ADODB will throw an EArgumentOutOfRangeException, bypassing my own attempt to catch any exceptions. This exception hangs the entire thread and prevents future execution from being possible until I completely restart the service.
Being relatively new to the world of Delphi, I've been scratching my head at this issue for quite some time, and have struggled to find any answers. My question is: why is this happening, and how do I stop, catch, or fix it?
Here is a snippet of my offending code. This is the method from which the stack trace originates. It is called from another method in the same unit, where I open a different dataset, loop through its records, and on each record call this function to get some info based on the value passed in.
function TFQFoo.DoSomething(IncNo : Int64): string;
var
ItemList : string;
MySQL: string;
ComponentString: string;
begin
result:='';
if IncNo<=0 then
Exit;
ItemList := '';
MyQuery.Close;
MySQL := 'select ID from tbl ' +
' where val = ' + IntToStr(IncNo) +
' order by col1 DESC, col2, col3';
try
try
MyQuery.CommandText := (MySQL);
MyQuery.Open;
while not (MyQuery.EOF) do
begin
if (ItemList <> '') then
ItemList := ItemList + ',';
ItemList := ItemList +
MyQuery.FieldbyName('ID').asstring;
MyQuery.Next;
end;
except
// exception handling code omitted for brevity -- none of it
// is ever reached, anyway (see below)
end;
finally
MyQuery.Close;
end;
Result := ItemList;
end;
The call stack from the exception indicates that it's occurring at Open. No try..catch block will capture the exception and log it for me -- I have to use EurekaLog in order to see any details. The stack trace methods (too big to post here) look like:
TFQFoo.DoSomething
TDataSet.Open
... internal things
TADOConnection.ExecuteComplete
CheckForAsyncExecute
...
TCustomConnection.GetDataSet
TListHelper.GetItemRange
Thinking possibly my TADODataSet component was somehow getting corrupted / its properties altered at runtime, I added some logging to capture that data for me so I could see if something funky was going on there. I didn't see anything, but here it is in case it's pertinent.
object MyQuery: TADODataSet
AutoCalcFields = False
CacheSize = 15
Connection = FGlobals.RIMSDB
CursorType = ctStatic
LockType = ltReadOnly
CommandText =
'select ID from tbl where val = 202005070074 order by col1 ' +
'DESC, col2, col3'
ParamCheck = False
Parameters = <>
Left = 32
Top = 216
end
For the curious, this is the method actually throwing the exception, from Data.Win.ADODB. Note the except, which I guess hops over my own try..catch block and sends the exception straight to EurekaLog.
procedure CheckForAsyncExecute;
var
I: Integer;
begin
try
if not Assigned(pError) and Assigned(pRecordset) and
((pRecordset.State and adStateOpen) <> 0) then
for I := 0 to DataSetCount - 1 do
if (DataSets[I].Recordset = pRecordset) and (eoAsyncExecute in DataSets[I].ExecuteOptions) then
begin
DataSets[I].OpenCursorComplete;
Break;
end;
except
ApplicationHandleException(Self);
end;
end;
What I have tried:
Many, many iterations of tweaking component properties on the ADODataSet itself
Using the CommandText and Parameters from within the designer, and assigning the parameter prior to execution at runtime
Adding / removing a (NOLOCK) hint to the query itself
Taken the problem to senior members of my team for input
Googling (for hours)
Reading up on Delphi and ADO documentation (not fruitful for this)
Attempted reproduction - I've never been able to get this to occur on any test system I use. This leads me to think it may be environment-related, but I have absolutely no clue how
My question, restated:
How do I stop this from happening? What am I doing wrong? I don't want to just catch the EArgumentOutOfRangeException; I want to learn why it's happening in the first place and prevent it from happening in the future.
I know that sometimes, the query execution will not return results, but the typical CommandText does not return a result set message is never seen nor encountered due to the lower-level code bypassing my own catch statement. Beyond this, I don't know what else to look for.
I've only found one other occurrence of something similar so far, but it relates just to the exception not getting caught: http://www.delphigroups.info/2/d9/410191.html
The call to ApplicationHandleException(Self) in CheckForAsyncExecute() is swallowing exceptions, which is why your except block is not being triggered:
// in Data.Win.ADODB.pas:
procedure CheckForAsyncExecute;
var
I: Integer;
begin
try
...
except
ApplicationHandleException(Self); // <-- a caught exception is NOT re-raised here!
end;
end;
Inside of the Data.Win.ADODB unit, caught exceptions will call the unit's own ApplicationHandleException() function, which then calls System.Classes.ApplicationHandleException if assigned, otherwise it simply exits:
// in Data.Win.ADODB.pas:
procedure ApplicationHandleException(Sender: TObject);
begin
if Assigned(System.Classes.ApplicationHandleException) then
System.Classes.ApplicationHandleException(Sender);
end;
System.Classes.ApplicationHandleException is initialized to nil.
In both a VCL1 and FMX app, the TApplication constructor assigns the TApplication.HandleException() method to System.Classes.ApplicationHandleException, where HandleException() ignores EAbort exceptions, and calls the TApplication.OnException event handler (if assigned), the TApplication.ShowException() method, or the System.SyUtils.ShowException() function, depending on the type of exception being handled.
1: in a VCL TService app, TServiceApplication uses Vcl.Forms.TApplication internally.
TApplication.ShowException() displays the details of the exception to the user in a popup MessageBox and then exits, and System.SysUtils.ShowException() displays the details of the exception to the user in a Console or MessageBox and then exits.
So, at no point does ADO's CheckForAsyncExecute() re-raise a caught exception into user code. And needless to say, displaying a popup MessageBox in a service is not a good idea, as the user will likely not see it so they can dismiss it.
Of course, the best option would be to avoid the EArgumentOutOfRangeException exception from being raised in the first place. But there are other conditions that can also raise exceptions, too.
So, your only option to handle swallowed ADO exceptions yourself, and avoid popup MessageBoxes, is to assign a TApplication.OnException event handler (either directly, or via the TApplicationEvents component).

Delphi DBGrid date format for Firebird timestamp field

I display the content of a Firebird database into a TDBgrid. The database has a 'TIMESTAMP' data type field that I would like to display with date/time format:
'YYYY/MM/DD HH:mm:ss'. (Now its displayed as 'YYMMDD HHmmss')
How to achieve this?
I tried this:
procedure TDataModule1.IBQuery1AfterOpen(DataSet: TDataSet);
begin
TDateTimeField(IBQuery1.FieldByName('timestamp_')).DisplayFormat := 'YYYY/MM/DD HH:mm:ss';
end;
But this causes some side effects at other parts of the program, so its not an alternative. For example at the 'IBQuery1.Open' statement I get the '...timestamp_ not found...' debugger message in the method that I clear the database with.
function TfrmLogger.db_events_clearall: integer;
begin
result := -1;
try
with datamodule1.IBQuery1 do begin
Close;
With SQL do begin
Clear;
Add('DELETE FROM MEVENTS')
end;
if not Prepared then
Prepare;
Open; //Exception here
Close;
Result := 1;
end;
except
on E: Exception do begin
ShowMessage(E.ClassName);
ShowMessage(E.Message);
Datamodule1.IBQuery1.close;
end;
end;
end;
I get the same exception message when trying to open the query for writing into the database.
*EDIT >>
I have modified the database clear as the following:
function TfrmLogger.db_events_clearall: integer;
var
IBQuery: TIBQuery;
IBTransaction: TIBTransaction;
DataSource: TDataSource;
begin
result := -1;
//Implicit local db objects creation
IBQuery := TIBQuery.Create(nil);
IBQuery.Database := datamodule1.IBdbCLEVENTS;
DataSource := TDataSource.Create(nil);
DataSource.DataSet := IBQuery;
IBTransaction := TIBTransaction.Create(nil);
IBTransaction.DefaultDatabase := datamodule1.IBdbCLEVENTS;
IBQuery.Transaction := IBTransaction;
try
with IBQuery do begin
SQL.Text := DELETE FROM MSTEVENTS;
ExecSQL;
IBTransaction.Commit;
result := 1;
end;
except
on E : Exception do
begin
ShowMessage(E.ClassName + ^M^J + E.Message);
IBTransaction.Rollback;
end;
end;
freeandnil(IBQuery);
freeandnil(DataSource);
freeandnil(IBTransaction);
end;
After clearing the database yet i can load the records into the dbgrid, seems like the database has not been updated. After the program restart i can see all the records been deleted.
The whole function TfrmLogger.db_events_clearall seems very dubious.
You do not provide SQL_DELETE_ROW but by the answer this does not seem to be SELECT-request returning the "resultset". So most probably it should NOT be run by ".Open" but instead by ".Execute" or ".ExecSQL" or something like that.
UPD. it was added SQL_DELETE_ROW = 'DELETE FROM MEVENTS'; confirming my prior and further expectations. Almost. The constant name suggests you want to delete ONE ROW, and the query text says you delete ALL ROWS, which is correct I wonder?..
Additionally, since there is no "resultset" - there is nothing to .Close after .Exec.... - but you may check the .RowsAffected if there is such a property in DBX, to see how many rows were actually scheduled to be deleted.
Additionally, no, this function DOES NOT delete rows, it only schedules them to be deleted. When dealing with SQL you do have to invest time and effort into learning about TRANSACTIONS, otherwise you would soon get drown in side-effects.
In particular, here you have to COMMIT the deleting transaction. For that you either have to explicitly create, start and bind to the IBQuery a transaction, or to find out which transaction was implicitly used by IBQuery1 and .Commit; it. And .Rollback it on exceptions.
Yes, boring, and all that. And you may hope for IBX to be smart-enough to do commits for you once in a while. But without isolating data changes by transactions you would be bound to hardly reproducible "side effects" coming from all kinds of "race conditions".
Example
FieldDefs.Clear; // frankly, I do not quite recall if IBX has those, but probably it does.
Fields.Clear; // forget the customizations to the fields, and the fields as well
Open; // Make no Exception here
Close;
Halt; // << insert this line
Result := 1;
Try this, and I bet your table would not get cleared despite the query was "opened" and "closed" without error.
The whole With SQL do begin monster can be replaced with the one-liner SQL.Text := SQL_DELETE_ROW;. Learn what TStrings class is in Delphi - it is used in very many places of Delphi libraries so it would save you much time to know this class services and features.
There is no point to Prepare a one-time query, that you execute and forget. Preparation is done to the queries where you DO NOT CHANGE the SQL.Text but only change PARAMETERS and then re-open the query with THE SAME TEXT but different values.
Okay, sometimes I do use(misuse?) explicit preparation to make sure the library fetches parameters datatypes from the server. But in your example there is neither. Your code however does not use parameters and you do not use many opens with the same neverchanging SQL.text. Thus, it becomes a noise, making longer to type and harder to read.
Try ShowMessage(E.ClassName + ^M^J + E.Message) or just Application.ShowException(E) - no point to make TWO stopping modal windows instead of one.
Datamodule1.IBQuery1.close; - this is actually a place for rolling back the transaction, rather than merely closing queries, which were not open anyway.
Now, the very idea to make TWO (or more?) SQL requests going throw ONE Delphi query object is questionable per se. You make customization to the query, such as fixing DisplayFormat or setting fields' event handlers, then that query is quite worth to be left persistently customized. You may even set DisplayFormat in design-time, why not.
There is little point in jockeying on one single TIBQuery object - have as many as you need. As of now you have to pervasively and accurately reason WHICH text is inside the IBQuery1 in every function of you program.
That again creates the potential for future side effects. Imagine you have some place where you do function1; function2; and later you would decide you need to swap them and do function2; function1;. Can you do it? But what if function2 changes the IBQuery1.SQL.Text and function1 is depending on the prior text? What then?
So, basically, sort your queries. There should be those queries that do live across the function calls, and then they better to have a dedicated query object and not get overused with different queries. And there should be "one time" queries that only are used inside one function and never outside it, like the SQL_DELETE_ROW - those queries you may overuse, if done with care. But still better remake those functions to make their queries as local variables, invisible to no one but themselves.
PS. Seems you've got stuck with IBX library, then I suggest you to take a look at this extension http://www.loginovprojects.ru/download.php?getfilename=uploads/other/ibxfbutils.zip
Among other things it provides for generic insert/delete functions, which would create and delete temporary query objects inside, so you would not have to think about it.
Transactions management is still on you to keep in mind and control.

Delphi. How to Disable/Enable controls without triggering controls events

I have a DataSet (TZQuery), which has several boolean fields, that have TDBCheckBoxes assigned to them.
These CheckBoxes have "OnClick" events assigned to them and they are triggered whenever I change field values (which are assigned to checkboxes).
The problem is that I do not need these events triggerred, during many operations i do with the dataset.
I've tried calling DataSet.DisableControls, but then events are called right after i call DataSet.EnableControls.
So my question is - is there a way to disable triggering Data-aware controls events.
Edit (bigger picture):
If an exception happens while let's say saving data, i have to load the default values (or the values i've had before saving it). Now while loading that data, all these events (TDBCheckBoxes and other data-aware controls) are triggered, which do all sorts of operations which create lag and sometimes even unwanted changes of data, i'm looking for an universal solution of disabling them all for a short period of time.
Building on Guillem's post:
Turn off everything:
Traverse each component on the form with the for-loop, shown below, changing the properties to the desired value.
If you want to later revert back to the original property values, then you must save the original value (as OldEvent is used below.)
Edit: The code below shows the key concept being discussed. If components are being added or deleted at run-time, or if you'd like to use the absolutely least amount of memory, then use a dynamic array, and as Pieter suggests, store pointers to the components rather than indexing to them.
const
MAX_COMPONENTS_ON_PAGE = 100; // arbitrarily larger than what you'd expect. (Use a dynamic array if this worries you.
var
OldEvent: Array[0.. MAX_COMPONENTS_ON_PAGE - 1] of TNotifyEvent; // save original values here
i: Integer;
begin
for i := 0 to ComponentCount - 1 do
begin
if (Components[i] is TCheckBox) then
begin
OldEvent[i] := TCheckBox(Components[i]).OnClick; // remember old state
TCheckBox(Components[i]).OnClick := nil;
end
else if (Components[i] is TEdit) then
begin
OldEvent[i] := TEdit(Components[i]).OnClick; // remember old state
TEdit(Components[i]).OnClick := nil;
end;
end;
Revert to former values
for i := 0 to ComponentCount - 1 do
begin
if (Components[i] is TCheckBox) then
TCheckBox(Components[i]).OnClick := OldEvent[i]
else if (Components[i] is TEdit) then
TEdit(Components[i]).OnClick := OldEvent[i];
end;
There may be a way to fold all of the if-statements into one generic test that answers "Does this component have an OnClickEvent" -- but I don't know what it is.
Hopefully someone will constructively criticize my answer (rather than just down voting it.) But, hopefully what I've shown above will be workable.
One way to do this is following:
var
Event : TNotifyEvent;
begin
Event := myCheckbox.OnClick;
try
myCheckbox.OnClick := nil;
//your code here
finally
myCheckbox.OnClick := Event;
end;
end;
HTH
The internal design of the TCustomCheckBox is that it triggers the Click method every time the Checked property if changed. Be it by actually clicking it or setting it in code. And this is happening here when you call EnableControls because the control gets updated to display the value of the linked field in your dataset.
TButtonControl (which is what TCustomCheckBox inherits from) has the property ClicksDisabled. Use this instead of (or in addition to) the DisableControls/EnableControls call. Unfortunately it is protected and not made public by TCustomCheckBox but you can use a small hack to access it:
type
TButtonControlAccess = class(TButtonControl)
public
property ClicksDisabled;
end;
...
TButtonControlAccess(MyCheckBox1).ClicksDisabled := True;
// do some dataset stuff
TButtonControlAccess(MyCheckBox1).ClicksDisabled := False;
Of course you can put this into a method that checks all components and sets this property if the control inherits from TCustomCheckBox or some other criteria.

How can I detect if ApplyUpdates will Insert or Update data?

In the AfterPost event handler for a ClientDataSet, I need the information if the ApplyUpdates function for the current record will do an update or an insert.
The AfterPost event will be executed for new and updated records, and I do not want to declare a new Flag variable to indicate if a 'update' or ' insert' operation is in progress.
Example code:
procedure TdmMain.QryTestAfterPost(DataSet: TDataSet);
begin
if IsInserting(QryTest) then
// ShowMessage('Inserting')...
else
// ShowMessage('Updating');
QryTest.ApplyUpdates(-1);
end;
The application will write a log in the AfterPost method, after ApplyUpdate has completed. So this method is the place which is closest to the action, I would prefer a solution which completely can be inserted in this event handler.
How could I implement the IsInserting function, using information in the ClientDataSet instance QryTest?
Edit: I will try ClientDataSet.UpdateStatus which is explained here.
ApplyUpdates doesn't give you that information - since it can be Inserting, updating and deleting.
ApplyUpdates apply the change information stored on Delta array. That change information can, for example, contain any number of changes of different types (insertions, deletions and updatings) and all these will be applied on the same call.
On TDatasetProvider you have the BeforeUpdateRecord event (or something like that, sleep does funny things on memory :-) ). That event is called before each record of Delta is applied to the underlying database/dataset and therefore the place to get such information... But Showmessage will stop the apply process.
EDIT: Now I remembered there's another option: you can assign Delta to another clientdataset Data property and read the dataset UpdateStatus for that record.
Of course, you need to do this before doing applyupdates...
var
cdsAux: TClientDataset;
begin
.
.
<creation of cdsAux>
cdsAUx.Data := cdsUpdated.Delta;
cdsAux.First;
case cdsAux.UpdateStatus of
usModified:
ShowMessage('Modified');
usInserted:
ShowMessage('Inserted');
usDeleted:
ShowMessage('Deleted'); // For this to work you have to modify
// TClientDataset.StatusFilter
end;
<cleanup code>
end;
BeforeUpdateRecord event on TDataSetProvider is defined as:
procedure BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS:
TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
Parameter UpdateKind says what will be done with record: ukModify, ukInsert or ukDelete. You can test it like this:
procedure TSomeRDM.SomeProviderBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
case UpdateKind of
ukInsert :
// Process Insert;
ukModify :
// Process update
ukDelete :
// Process Delete
end;
end;
Note: this event signature is from Delphi 7. I don't know if it changed in later versions of Delphi.
Set the ClientDataSet.StatusFilter to an TUpdateStatus value, and then read ClientDataSet.RecordCount
for example,
ClientDataSet1.StatusFilter := [usDeleted];
ShowMessage(IntToStr(ClientDataSet1.RecordCount));
will return the number of Delete queries that will be executed.
Note two things, however. Setting StatusFilter to usModified always includes both the modified and unmodified records, so you take half of that value (a value of 4 means 2 Update queries will be executed). Also, setting StatusFilter to [] (an empty set) is how you restore to default view (Modified, Unmodified, and Inserted)
Make sure that any unposted changes have been posted before doing this, otherwise unposted changes may not be considered.

Resources