Delphi: Access Nested Dataset master information when applying update - delphi

Can I access the parent dataset information (like MyField.NewValue) in the BeforeUpdateRecord event of a provider when applying the updates to the nested dataset?
Reason:
When I apply updates to a CDS that has a nested detail, the master PK is generated by the underlying query (TIBCQuery) and propagated to the master CDS.
But the new key is not visible in the BeforeUpdateRecord of the detail as the field is updated in the AfterUpdateRecord:
DeltaDS.FieldByName(FieldName).NewValue := SourceDS.FieldByName(FieldName).NewValue)
and the delta is not merged yet.
It looks like the DeltaDS parameter of the BeforeUpdateRecord event contains only information to the nested dataset when the call occurs for the details.
It would be nice if I could do something like:
DeltaDS.ParentDS.FieldByName('FIELDNAME').NewValue.
Edit:
When using nested datasets the BeforeUpdateRecord event is called twice, once for the master and once for the detail (if we have one record of both). When the event is called for the detail, is there a way to access the master information contained in the DeltaDS ?
We can't access the data of the master CDS at that moment as the changes are not already merged. I hope this is not adding more confusion.

You can use the provider's Resolver to look up the corresponding TUpdateTree:
function FindDeltaUpdateTree(Tree: TUpdateTree; DeltaDS: TCustomClientDataSet): TUpdateTree;
var
I: Integer;
begin
Result := nil;
if Tree.Delta = DeltaDS then
Result := Tree
else
for I := 0 to Tree.DetailCount - 1 do
begin
Result := FindDeltaUpdateTree(Tree.Details[I], DeltaDS);
if Assigned(Result) then
Break;
end;
end;
You can use this in your OnBeforeUpdate handler:
var
Tree, ParentTree: TUpdateTree;
begin
if SourceDS = MyDetailDataSet then
begin
Tree := FindDeltaUpdateTree(TDataSetProvider(Sender).Resolver.UpdateTree, DeltaDS);
if Assigned(Tree) then
begin
ParentTree := Tree.Parent;
// here you can use ParentTree.Source (the dataset) and ParentTree.Delta (the delta)
end;
end;
end;

Related

Delphi: How to access clientdataset via delta

On the TDatasetProvider.OnBeforeUpdateRecord, how do I
access the source or originating clientdataset of the
sent DeltaDS parameter?
procedure TdmLoanPayment.dpLoanPaymentBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
var
sourceCDS: TClientDataset;
begin
sourceCDS := DeltaDS.???;
...
end;
I need to access some properties from the corresponding clientdataset. Setup is TSQLDataset/TDatasetProvider/TClientDataset.
Edit:
The cause of all this hassle is, I wanted to derive a component from TDatasetProvider and assign a default OnBeforeUpdateRecord.
I think SourceDS is what are looking for.
The Sender parameter identifies the provider that is applying updates.
The SourceDS parameter is the dataset from which the data originated.
If there is no source dataset, this value is nil (Delphi) or NULL
(C++). The source dataset may not be active when the event occurs, so
set its Active property to true before trying to access its data.
The DeltaDS parameter is a client dataset containing all the updates
that are being applied. The current record represents the update that
is about to be applied.
The UpdateKind parameter indicates whether this update is the
modification of an existing record (ukModify), a new record to insert
(ukInsert), or an existing record to delete (ukDelete).
The Applied parameter controls what happens after exiting the event
handler. If the event handler sets Applied to true, the provider
ignores the update: it neither tries to apply it, nor does it log an
error indicating that the update was not applied. If the event handler
leaves Applied as false, the provider tries to apply the update after
the event handler exits.
for example:
procedure TdmLoanPayment.dpLoanPaymentBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
ShowMessage(TClientDataSet(SourceDS).Name); // get source name
...
end;
Edit
or
procedure TdmLoanPayment.dpLoanPaymentBeforeUpdateRecord(Sender: TObject;
SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind;
var Applied: Boolean);
begin
if SourceDS.Name = 'Name1'then
...do something ...
if SourceDS.Name = 'Name2'then
...do something ...
end;
If you trace out of the call to your DataSetProvider1BeforeUpdateRecord, you'll
see that the dataset passed as the SourceDS parameter is the Source dataset of
the UpdateTree, and that is, AFAICS, the dataset that the DataSet property of
the Provider is set to. Of course, this is not the CDS from which the Delta
has been derived (in my test case it's actually a TAdoQuery).
Looking at the source code in Provider.Pas, I can't immediately see a
way to find the identity of the Delta's source CDS. I don't think that is particularly surprising because the Provider's operation is invoked by a CDS and not vice versa, and all the data the Provider needs from the CDS is its Delta.
On the other hand, it's a pretty fair bet that the BeforeUpdateRecord event has
been triggered by the most recent, still-pending, call to ApplyUpdates on one of your CDSs, so if
you make a note of that in their BeforeApplyUpdates event(s), that will probably
tell you what you want to know. I'd expect that to work for a single-level update, but it might be more tricky if the UpdateTree is operating on nested CDSs.
If your CDSs all have individual Providers, but the providers share a BeforeUpdateRecord event, you could identify the CDS for a given provider using code like this:
function TCDSForm.FindCDSForProvider(DataSetProvider: TDataSetProvider):
TClientDataSet;
var
i : Integer;
begin
Result := Nil;
for i := 0 to ComponentCount - 1 do begin
if Components[i] is TClientDataSet then
if TClientDataSet(Components[i]).ProviderName = DataSetProvider.Name then begin
Result := TClientDataSet(Components[i]);
Exit;
end;
end;
end;

Refresh Nested DataSet with poFetchDetailsOnDemand

Is there a way to refresh only the Detail DataSet without reloading all master dataset?
this is what I've tried so far:
DM.ClientDataSet2.Refresh;
DM.ClientDataSet2.RefreshRecord;
I have also tried:
DM.ClientDataSet1.Refresh;
But the method above refreshes the entire Master dataset, not just the current record.
Now, the following code seems to do anything:
DM.ClientDataSet1.RefreshRecord;
Is there a workaround or a proper way to do what I want? (maybe an interposer...)
Additional Info:
ClientDataSet1 = Master Dataset
ClientDataSet2 = Detail DataSet , is the following: *
object ClientDataSet2: TClientDataSet
Aggregates = <>
DataSetField = ClientDataSet1ADOQuery2
FetchOnDemand = False
.....
end
Provider properties:
object DataSetProvider1: TDataSetProvider
DataSet = ADOQuery1
Options = [poFetchDetailsOnDemand]
UpdateMode = upWhereKeyOnly
Left = 24
Top = 104
end
Googling finds numerous articles that say that it isn't possible at all with nested ClientDataSets without closing and re-opening the master CDS, which the OP doesn't want to do in this case. However ...
The short answer to the q is yes, in the reasonably simple case I've tested, and it's quite straightforward, if a bit long-winded; getting the necessary steps right took a while to figure out.
The code is below and includes comments explaining how it works and a few potential problems and how it avoids or works around them. I have only tested it with TAdoQueries feeding the CDSs' Provider.
When I started looking into all this, it soon became apparent that with the usual master
+ detail set-up, although Providers + CDSs are happy to refresh the master data from the server, they simply will not refresh the detail records once they've been read from the server for the first time since the cdsMaster was opened. This may be by design of course.
I don't think I need to post a DFM to go with the code. I simply have AdoQueries set up in the usual master-detail way (with the detail query having the master's PK as a parameter), a DataSetProvider pointed at the master AdoQuery, a master CDS pointed at the provider, and a detail cDS pointed at the DataSetField of the cdsMaster. To experiment and see what's going on, there are DBGrids and DBNavigators for each of these datasets.
In brief, the way the code below works is to temporarily filter the AdoQuery master and the CDS masterdown to the current row and then force a refresh of their data and the dtail data for the current master row. Doing it this way, unlike any other I tried, results in the detail rows nested in the cdsMaster's DataSet field getting refreshed.
Btw, the other blind alleys I tried included with and without poFetchDetailsOnDemand set to true, ditto cdsMaster.FetchDetailsOnDemand. Evidently "FetchDetailsOnDemand" doesn't mean ReFetchDetailsOnDemand!
I ran into a problem or two getting my "solution" working, the stickiest one being described in this SO question:
Refreshing a ClientDataSet nested in a DataSetField
I've verified that this works correctly with a Sql Server 2000(!) back-end, including picking up row data changes fired at the server from ISqlW. I've also verified, using Sql Server's Profiler, that the network traffic in a refresh only involves the single master row and its details.
Delphi 7 + Win7 64-bit, btw.
procedure TForm1.cdsMasterRowRefresh(MasterPK : Integer);
begin
// The following operations will cause the cursor on the cdsMaster to scroll
// so we need to check and set a flag to avoid re-entrancy
if DoingRefresh then Exit;
DoingRefresh := True;
try
// Filter the cdsMaster down to the single row which is to be refreshed.
cdsMaster.Filter := MasterPKName + ' = ' + IntToStr(MasterPK);
cdsMaster.Filtered := True;
cdsMaster.Refresh;
Inc(cdsMasterRefreshes); // just a counter to assist debugging
// release the filter
cdsMaster.Filtered := False;
// clearing the filter may cause the cdsMaster cursor to move, so ...
cdsMaster.Locate(MasterPKName, MasterPK, []);
finally
DoingRefresh := False;
end;
end;
procedure TForm1.qMasterRowRefresh(MasterPK : Integer);
begin
try
// First, filter the AdoQuery master down to the cdsMaster current row
qMaster.Filter := MasterPKName + ' = ' + IntToStr(MasterPK);
qMaster.Filtered := True;
// At this point Ado is happy to refresh only the current master row from the server
qMaster.Refresh;
// NOTE:
// The reason for the following operations on the qDetail AdoQuery is that I noticed
// during testing situations where this dataset would not be up-to-date at this point
// in the refreshing operations, so we update it manually. The reason I do it manually
// is that simply calling qDetail's Refresh provoked the Ado "Insufficient key column
// information for updating or refreshing" despite its query not involving a join
// and the underlying table having a PK
qDetail.Parameters.ParamByName(MasterPKName).Value := MasterPK;
qDetail.Close;
qDetail.Open;
// With the master and detail rows now re-read from the server, we can update
// the cdsMaster
cdsMasterRowRefresh(MasterPK);
finally
// Now, we can clear the filter
qMaster.Filtered := False;
qMaster.Locate(MasterPKName, MasterPK, []);
// Obviously, if qMaster were filtered in the first place, we'd need to reinstate that later on
end;
end;
procedure TForm1.RefreshcdsMasterAndDetails;
var
MasterPK : Integer;
begin
if cdsMaster.ChangeCount > 0 then
raise Exception.Create(Format('cdsMaster has %d change(s) pending.', [cdsMaster.ChangeCount]));
MasterPK := cdsMaster.FieldByName(MasterPKName).AsInteger;
cdsDetail.DisableControls;
cdsMaster.DisableControls;
qDetail.DisableControls;
qMaster.DisableControls;
try
try
qMasterRowRefresh(MasterPK);
except
// Add exception handling here according to taste
// I haven't encountered any during debugging/testing so:
raise;
end;
finally
qMaster.EnableControls;
qDetail.EnableControls;
cdsMaster.EnableControls;
cdsDetail.EnableControls;
end;
end;
procedure TForm1.cdsMasterAfterScroll(DataSet: TDataSet);
begin
RefreshcdsMasterAndDetails;
end;
procedure TForm1.cdsMasterAfterPost(DataSet: TDataSet);
// NOTE: The reason that this, in addition to cdsMasterAfterScroll, calls RefreshcdsMasterAndDetails is
// because RefreshcdsMasterAndDetails only refreshes the master + detail AdoQueries for the current
// cdsMaster row. Therefore in the case where the current cdsMaster row or its detail(s)
// have been updated, this row needs the refresh treatment before we leave it.
begin
cdsMaster.ApplyUpdates(-1);
RefreshcdsMasterAndDetails;
end;
procedure TForm1.btnRefreshClick(Sender: TObject);
begin
RefreshcdsMasterAndDetails;
end;
procedure TForm1.cdsDetailAfterPost(DataSet: TDataSet);
begin
cdsMaster.ApplyUpdates(-1);
end;

Writing data to PVirtualNode without setting each field value manually

Lets say I have this node data record:
Type
PPerson = ^TPerson;
TPerson = record
Name: String;
Age: Integer;
SomeBool: Boolean;
end;
To populate my VirtualStringTree, I would do this:
Procedure AddToTree(Person: TPerson);
Var
Node: PVirtualNode;
Data: PPerson;
Begin
Node := VT.AddChild(nil);
Data := VT.GetNodeData(Node);
Data.Name := Person.Name;
Data.Age := Person.Age;
Data.SomeBool := Person.SomeBool;
End;
Procedure TMyForm.MyButtonClick(Sender: TObject);
Var
Person: TPerson;
Begin
Person.Name := 'Jeff';
Person.Age := 16;
Person.SomeBool := False;
AddToTree(Person);
End:
Now, while this works perfectly fine, I would like to simplify it, so whenever I add new fields to the record, I wont have modify the AddToTree method.
So I tried this:
Procedure AddToTree(Person: TPerson);
Begin
VT.AddChild(nil,#Person);
End;
This compiles, but it appears the PVirtualNode did not get the data, because my VT is not displaying anything, and when breaking in the OnGetText event, I see the variables are empty.
What am I doing wrong? :)
Records support the assignment operator:
procedure AddToTree(const Person: TPerson);
var
Node: PVirtualNode;
Data: PPerson;
begin
Node := VT.AddChild(nil);
Data := VT.GetNodeData(Node);
Data^ := Person;
end;
You aren't reading the manual :)
OK, in this case the source is the manual - quote from the AddChild() source:
UserData can be used to set the first 4 bytes of the user data area to an initial value which can be used
in OnInitNode and will also cause to trigger the OnFreeNode event (if <> nil) even if the node is not yet
"officially" initialized.
IOW it isn't meant to be used in the way youre using it / expecting it to work.
BTW why do you copy data around? Why not have
type
PTreeData = ^TTreeData;
TTreeData = record
Data: PPerson;
end;
and allocate records with New() keep them in the tree and then Dispose() when tree is cleared?
The best way of storing data in VTV when using records as data holders is to store only pointers to the records while records themselves are stored separately in a list/array. This also corresponds to a virtual and MVC paradigm when visual component doesn't actually owns data.
IOW, the scheme of adding a record is:
Allocate memory for the record (!) using AllocMem, New, ...
Fill its fields
Add it to the list/array
Add new node to VTV with NodeData = PNewRecord
and the scheme of deleting a record is:
Delete corresponding node from VTV
Finalize record using Finalize (!) thus avoiding memory leaks with
ref-counted fields
Dispose allocated memory using FreeMem, Dispose, ...
Delete item from list/array
Yet another "I found the answer 2 minutes after I asked the question" - how humiliating... :(
Anyways, so - this can be acomplished by using CopyMemory, like this:
Procedure AddToTree(Person: TPerson);
Var
Data: PPerson;
Node: PVirtualNode;
Begin
// add node
Node := VT.AddChild(nil);
// Get data of the node
Data := VT.GetNodeData(Node);
// Copy the Person stuff to the Node's data.
CopyMemory(Data,#Person,SizeOf(Person));
End;

how to show Only relevant information in dbgrid delphi

information:
I have an order form.
With "keuze" and "aantal" it wright a new line. The Orderline gets an OrderID.
But the user may only see the orderline from his OrderID.
How can i make it work that it only shows, for example the OrderID "47" ?
procedure TfmOrder.btInvoerenClick(Sender: TObject);
begin
dm.atOrder.open;
dm.atOrder.Append;
dm.atOrder ['OrderStatus'] := ('Aangemeld');
dm.atOrder ['klantID'] := fminloggen.userid;
dm.atOrder ['OrderDatum'] := Kalender.date;
dm.atOrder ['Opmerkingen'] := leOpmerkingen.text;
dm.atOrder.post;
cbkeuze.Visible := true;
dbRegel.Visible := true;
leAantal.visible := true;
btOpslaan.Visible:= true;
end;
This is the code for making a new Order
procedure TfmOrder.btOpslaanClick(Sender: TObject);
var orderid:string;
begin
dm.atOrderregel.Open;
dm.atDier.open;
dm.atorderregel.Append;
dm.atOrderregel ['AantalDieren'] := leAantal.text;
dm.atOrderregel ['OrderID'] := dm.atOrder ['OrderID'];
dm.atOrderregel ['Diernaam'] := cbKeuze.Text;
dm.atOrderregel.Post;
leaantal.clear;
cbkeuze.ClearSelection;
end;
And this for a new orderline
thanks in advance
I know got a different error using this code:
begin
dm.atorder.Open;
dm.atorder.filter := 'KlantID = ' + (fminloggen.userid);
dm.atorder.filtered := true;
while not dm.atorder.Eof do
begin
cbOrder.Items.Add (dm.atorder['OrderID']);
dm.atOrder.Next;
end;
dm.atOrder.Close;
end;
It gives an error: The arguments are from the wrong type, or doesn't have right reach or are in conflict with each other.
here is userid declared.
var Gevonden: boolean;
userid : string;
begin
dm.atInlog.open;
Gevonden := false;
while (not Gevonden) and (not dm.atInlog.eof) do
begin
if dm.atInlog['email'] = leUser.Text
then
begin
Gevonden := true ;
inlognaam := dm.atInlog['email'];
userid := dm.atInlog['KlantID'];
end
else
dm.atInlog.Next
end;
this is obviously in another form
You can use the Filter property of the data set:
atOrderregel.Filter := 'OrderID = 47';
atOrderregel.Filtered := True;
You can add the grid's columns property statically in the object inspector, showing only the fields you need. If the columns list is empty (default) it is filled with all available fields.
Just add as many columns as you need and link each column to the corresponding field. You can reorder the columns and set the widths and titles individually. There are still some more properties available which are worth to explore.
Im assuming your grid is bound to a datasource component. This datasource is then linked with a TDataset descendant. There are a couple of ways you could acheive the desired filtering of the dataset to display only orderid 47.
Firstly, you could set the Datasets SQL property to contain a (server side) SQL query such as:
SELECT * from table WHERE OrderID = #OrderID
You would also need to create a parameter in the dataset to pass the (changing) value for the required OrderID. So add a new Parameter to the dataset (#OrderID), and then at runtime you can set this parameter value in code, something like:
DataSet.Parameters['#OrderID'].Value := ParameterValue;
Alternatively, you could also FILTER the dataset (client side) to just show the correct data:
Set your SQL property of the dataset to retrive the entire table, something like:
SELECT * FROM table
And then at runtime you could set the Filter property of the dataset to only get OrderID 47:
Dataset.Filter := 'OrderID = '+InttoStr(ParameterValue);
Depending on your needs one method may suit better (performance/memory) wise.
As Najem has commented, there is also a third method - using a Master-Detail dataset relationship. This method works using two datasets, one is the master of the other. When the master table record is changed, the detail dataset is then filtered using the value defined in the Key or MasterFields property of the M-D relatioship.
If you are connected to some datasource you could always create a SQL Query. Something like:
SELECT * FROM YourDBTable WHERE OrderID=47

DataSetProvider - DataSet to ClientDataSet

EDIT: It seems as if the DataSetProvider doesn't have the functionality I need for this project, so I'll be implementing a custom class for loading the data into the ClientDataSet.
I am trying to take data from a TMSQuery which is connected to my DB and populate a ClientDataSet with some of that data using a DataSetProvider.
My problem is that I will need to modify some of this data before it can go into my ClientDataSet. The ClientDataSet has persistent fields that will not match up with the raw DB data. I can't even get a string from the DB into a memo field in ClientDataSet.
The ClientDataSet is a part of my data tier so I will need to conform the data from the DB to the ClientDataSet field by field (well most will be able to go right through, but many will require routing and/or conversion).
Does anyone have experience with this?
You're looking for the TDataSetProvider.BeforeUpdateRecord event. Write an event handler for this event and you can take manual control of how the data is applied back to the data base.
Something like this
procedure TDataModule1.DataSetProvider1BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean);
begin
{ Set applied to tell DataSnap that you have applied this record yourself }
Applied := True;
case UpdateKind of
ukModify:
begin
Table1.Edit;
{ set the values of the fields something like this }
if not VarIsEmpty(DeltaDS.FieldByName('NewValue')) then
Table1['SomeField'] := DeltaDS.FieldByName('SomeField').NewValue;
Table1.Post;
end;
ukInsert:
begin
Table1.Insert;
{ set the values of the fields }
Table1['SomeField'] := DeltaDS['SomeField']
Table1.Post;
end;
ukDelete:
if Table1.Locate('PrimaryKeyField', DeltaDS['PrimaryKeyField'], []) then
Table1.Delete;
end; // case
end;
You can modify the data going to the ClientDataSet by implementing the TDataSetProvider.OnGetData event.
procedure TDataModule1.DataSetProvider1GetData(Sender: TObject; DataSet: TCustomClientDataSet);
begin
DataSet.First;
while not DataSet.Eof do begin
DataSet.Edit;
DataSet['Surname'] := UpperCase(DataSet['Surname']);
DataSet.Post;
DataSet.Next;
end; // while
end;
When applying updates from the ClientDataSet you can use the TDataSetProvider.OnUpdateData event. Like the OnGetData event you are operating on the whole dataset rather than a single record.
procedure TDataModule1.DataSetProvider1UpdateData(Sender: TObject; DataSet: TCustomClientDataSet);
begin
DataSet.First;
while not DataSet.Eof do begin
DataSet.Edit;
DataSet['Surname'] := LowerCase(DataSet['Surname']);
DataSet.Post;
DataSet.Next;
end; // while
end;
This OnUpdateData event is called before the OnBeforeUpdateRecord event. Also the OnGetData and OnUpdateData events operate on the whole dataset while OnBeforeUpdateRecord is called once for each modified record.
If I need a ClientDataSet to have data that doesn't exactly match the database schema, I write a query for the TQuery component that returns the data in the format that I want. I then write my own, separate, Delete, Insert, Refresh, and Update queries for the TQuery component.
Alternatively, you could create a view on the database and use the view in your TQuery component.
If you want a custom ClientDataSet that is independent of the database, what you need is an in-memory dataset. If you don't have an in-memory dataset component, Google for "TClientDataSet as in-memory dataset". What you end up with though, is basically a glorified list view component. Of course, you can hook into the OnUpdateRecord of the in-memory dataset to know when to update your real dataset.

Resources