Delphi - TClientDataSet: Second call to applyupdates() does not apply the updates - delphi

Again I have a problem with the TClientDataSet.
I guess it's something really simple but I struggle on it for a while now.
Here's some code what shows what I want to do:
procedure TForm1.Button1Click(Sender: TObject);
begin
ClientDataSet1.Insert;
ClientDataSet1.FieldByName('anruf_von').AsDateTime := time;
ClientDataSet1.Post;
ClientDataSet1.ApplyUpdates(0); // without this applyUpdates in button2 works.
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
ClientDataSet1.edit;
ClientDataSet1.FieldByName('anruf_bis').AsDateTime := time;
ClientDataSet1.Post;
showmessage(intToStr(ClientDataSet1.ChangeCount)); // returns 1
if ClientDataSet1.ChangeCount > 0 then
ClientDataSet1.applyUpdates(0);
end;
The code is self explaining I think.
When I press button1, a record is created and after the call to applyUpdates its written to the databse.
When I press button2, I want to make a change to this record and apply the updates to the database - and that doesn't work.
But when I comment out the applyUpdates in button1, the applyUpdates in button2 works correctly.

Try to change Provider.UpdateMode to upWhereKeyOnly, and set key field in Provider.OnUpdateData.
My gues is that insert works always since it is executed as
INSERT INTO ATABLE (anruf_von, anruf_bis) VALUES (...)
But update fails, since WHERE part will match DB stored time with time from clientdataset.
In fact, you will probably try to match two doubles, which is a no-no.
UPDATE ATABLE SET anruf_bis=<Time>
WHERE anruf_von=<WRONG Time, with more precision than stored in db>
When you set UpdateMode to upWhereKeyOnly, generated SQL sholud look like this
UPDATE ATABLE SET anruf_bis=<Time>
WHERE ID=<ID Value>

Related

Delphi - Can you close all of the database tables?

Can you close all database tables except some? Can you then reopen them? I use an absolute database that is similar to BDE. If this is possible, how can I do so many?
Yes, of course you can. You could iterate the Components property of your form/datamodule, use the is operator to check whether each is an instance of your table type and use a cast to call Open or Close on it.
The following closes all TABSDataSet tables on your form except one called Table1.
procedure TForm1.ProcessTables;
var
ATable : TABSDataSet; // used to access a particular TABSDataSet found on the form
i : Integer;
begin
for i := 0 to ComponentCount - 1 do begin
if Components[i] is TABSDataSet then begin
ATable := TABSDataSet(Components[i]);
// Now that you have a reference to a dataset in ATable, you can
// do whatever you like with it. For example
if ATable.Active and (ATable <> Table1) then
ATable.Close;
end;
end;
end;
I've seen from the code you've posted in comments and your answer that you
are obviously having trouble applying my code example to your situation. You
may find the following code easier to use:
procedure ProcessTables(AContainer : TComponent);
var
ATable : TABSTable;
i : Integer;
begin
for i := 0 to AContainer.ComponentCount - 1 do begin
if AContainer.Components[i] is TABSTable then begin
ATable := TABSTable(AContainer.Components[i]);
// Now that you have a reference to a dataset in ACDS, you can
// do whatever you like with it. For example
if ATable.Active then
ATable.Close;
end;
end;
end;
Note that this is a stand-alone procedure, not a procedure of a particular
form or datamodule. Instead, when you use this procedure, you call it passing
whatever form or datamodule contains the TABSTables you want to work with as the
AContainer parameter, like so
if Assigned(DataModule1) then
ProcessTables(DataModule1);
or
if Assigned(Form1) then
ProcessTables(Form1);
However, the downside of doing it this was is that it is trickier to specify which tables, if any, to leave open, because AContainer, being a TComponent, will not have any member tables.
Btw, your task would probably be easier if you could iterate through the tables in a TABSDatabase. However I've looked at its online documentation but can't see an obvious way to do this; I've asked the publishers, ComponentAce, about this but haven't had a reply yet.

I am trying to create a simple chat program by writing to a shared file on my network

I am trying to use a timer to check the the timestamp of the file to check if it has been modified, if this is true it must add the line from the text file to a richedit. The problem is that it continually adds the line to the richedit every 1/4 second (timer interval). I have tried different methods but can't get it right.
procedure TForm1.Timer1Timer(Sender: TObject);
Var
Filet : textfile;
filename, readtxt : string;
filedate1, filedate2 : integer;
begin
assignfile(filet, 'S:\share.talk');
filename := 'S:\share.talk';
filedate1 := FileAge(filename);
if filedate1 <> filedate2 then begin
reset(filet);
readln(filet, readtxt);
richedit1.lines.add(readtxt);
closefile(filet);
filedate2 := filedate1;
end;//if
end;
thanks for all help.
In your code
if filedate1 <> filedate2 then begin
reset(filet);
readln(filet, readtxt);
richedit1.lines.add(readtxt);
closefile(filet);
filedate2 := filedate1;
end;
the comparison between filedate and filedate2 assumes that these retain their values between calls to Timer1Timer. They do not, because they are declared local to Timer1Timer and are therefore 'forgotten' between calls because they are stored on the stack.
To get them to retain their values, remove the declaration on them local to Timer1Timer and declare them as fields of TForm1 instead.
Btw, be aware that with this design, you are going to run into other issues, like how to handle concurrent access to the network textfile, etc, but they are not related to the specific point you asked about.
The problem it that you are openning and closing file every time the timer ticks.Open the file on TForm1.FormCreate by a TFileStream with fmOpenReadWrite or fmOpenShareDenyNone parameters, close it on TForm1.FormDestroy, and read it on TForm1.Timer1Timer if the number of read bytes are greater than zero convert the buffer to string and add it to the richedit.
This is because everytime you are resetting the filedate1 and filedate2 when the procedure runs. A better way to implement this function is to return the lastwrite time from the procedure and call the procedure on timer with the lastwrite time that was returned to you. Then you can compare the current time and the last time and do the refresh. On the first loop pass the current time and then keep using the lastread time in all subsequent calls and the result of which will keep updating the lastread time.

Delphi how to save 3 datasources with one save button?

I got a problem with saving all values from 3 datasources into a SMDBGrid with another datasouce.
I got AdressID, ContactpersonID and RelationID.
Those all dont match each others.
The problem is that my SMDBGrid has another datasource then those 3.
I wanna save them with one button.
Tried many ways but can't find a good result.
this is the code i use right now for my Insert button:
procedure TFRelatiebeheer.ToolButton1Click(Sender: TObject);
begin
DRelatiebeheer.ContactpersonID.Insert;
DRelatiebeheer.RelationID.Insert;
DRelatiebeheer.AdressID.Insert;
end;
This is the code i use for my save button right now
if (DRelatiebeheer.ContactpersonID.State in dsEditModes) then
if not (DRelatiebeheer.ContactpersonID.State in [dsInsert]) then
begin
KJSMDBGrid1.RefreshData;
KJPanel4.Visible := True;
end
else
begin
if (DRelatiebeheer.ContactpersonID.State IN dsEditModes) then
DRelatiebeheer.ContactpersonID.Post;
if (DRelatiebeheer.AdressID.State IN dsEditModes) then
DRelatiebeheer.AdressID.Post;
end;
Hope you have a good sight for what I am doing right now, if not please notify.
I got the problem with the datasources that need to be saved on 1 click and then be refreshed in the database and in the Grid.
That means that when I insert a Contactperson there needs to be a AdressID and a RelationID coupled with it.
After that the grid needs to reload all of the data.
Focusing on the given problem
Depending on the intended behavior (should it be possible posting only one or two table(s) or is it necessary to post all tables) the first thing to do would be to ensure that the tables can be posted. You coulds create a function for each table e.g. CanAdressIDBePosted:Boolean to check if required fields are already entered. The condition of the table ContactpersonID would contain additional conditions: needed fields are entered AND CanAdressIDBePosted AND CanRelationIDBePosted. You could create an Action which would be bound on your button with an OnUpdate event which could look like this:
procedure TForm1.PostActionUpdate(Sender: TObject);
begin
TAction(Sender).Enabled := CanAdressIDBePosted and CanContactpersonIDBePosted and CanRelationIDBePosted;
// depending on your requirements (e.g. no need to post RelationID if not entered) it also could be
TAction(Sender).Enabled := CanAdressIDBePosted or CanContactpersonIDBePosted ;
end;
procedure TForm1.PostActionExecute(Sender: TObject);
begin
if CanAdressIDBePosted then AdressID.Post; // ensure ID fields will be generated
if CanRelationIDBePosted then RelationID.Post; // ensure ID fields will be generated
if CanContactpersonIDBePosted then
begin
ContactpersonID.FieldByName('AdressID').value := AdressID.FieldByName('ID').Value;
ContactpersonID.FieldByName('RelationID').value := RelationID.FieldByName('ID').Value;
end;
DateSetBoundToTheGrid.Requery;
// furthor actions you need
end;
Function TForm1.CanAdressIDBePosted:Boolean;
begin
// example implementation
Result := (AdressID.State in [dsEdit,dsInsert]) and (not AdressID.FieldByName('NeededField').IsNull);
end;
Function TForm1.CanContactpersonIDBePosted:Boolean;
begin
// example implementation
Result := (ContactpersonID.State in [dsEdit,dsInsert]) and (not ContactpersonID.FieldByName('NeededField').IsNull)
and CanAdressIDBePosted and CanRelationIDBePosted;
end;
An addidtional Action should be created to cancel if needed:
procedure TForm1.CancelActionExecute(Sender: TObject);
begin
AdressID.Cancel;
RelationID.Cancel;
ContactpersonID.Cancel;
end;
procedure TForm1.CancelActionUpdate(Sender: TObject);
begin
TAction(Sender).Enabled := (AdressID.State in [dsEdit,dsInsert])
or (RelationID.State in [dsEdit,dsInsert])
or (ContactpersonID.State in [dsEdit,dsInsert]);
end;
In general I am not sure if the approach you took ist the best which can be taken, since from the structure given IMHO it should be possible to assign already existing relations and adresses to new generated contactpersons, but that would be another question.
This code just looks somewhat random to me. What SHOULD happen there ?
if (DRelatiebeheer.ContactpersonID.State in dsEditModes) then
// remember this check (1)
if not (DRelatiebeheer.ContactpersonID.State in [dsInsert]) then
// this check better written as "...State = dsInsert"
begin
// why don't you call DRelatiebeheer.ContactpersonID.Post to save chanegs ?
KJSMDBGrid1.RefreshData;
KJPanel4.Visible := True;
end
else
begin
if (DRelatiebeheer.ContactpersonID.State IN dsEditModes) then
// you already checked this above (1), why check again ?
DRelatiebeheer.ContactpersonID.Post;
if (DRelatiebeheer.AdressID.State IN dsEditModes) then
DRelatiebeheer.AdressID.Post;
end;
// so what about DRelatiebeheer.RelationID ?
For what i may deduce, you don't have to make any complex if-ladders, you just have to literally translate your words to Delphi. You want to save three tables and then refresh the grid. Then just do it.
procedure TFRelatiebeheer.SaveButtonClick(Sender: TObject);
begin
DRelatiebeheer.ContactpersonID.Post;
DRelatiebeheer.RelationID.Post;
DRelatiebeheer.AdressID.Post;
DatabaseConnection.CommitTrans;
KJSMDBGrid1.RefreshData;
KJPanel4.Visible := True;
end;
Just like you was told in your other questions.
Delphi save all values from different datasources with 1 save button
Delphi set Panel visible after post
PS. ToolButton1Click - plase, DO rename the buttons. Believe me when you have 10 buttons named Button1, Button2, ...Button10 you would never be sure what each button should do and would mix everything and make all possible program logic errors.

Everytime I try to communicate with my database with stored procedures I get this "Cannot perform this operation on a closed dataset"

I'm working on a Delphi project with a MS SQL Server database, I connected the database with ADOConnection, DataSource and ADOProc components from Borland Delphi 7 and I added this code in behind:
procedure TForm1.Button2Click(Sender: TObject);
begin
ADOStoredProc1.ProcedureName := 'sp_Delete_Clen';
ADOStoredProc1.Refresh;
ADOStoredProc1.Parameters.ParamByName('#clenID').Value := Edit6.Text;
ADOStoredProc1.Active := True;
ADOStoredProc1.ExecProc;
end;
The component Edit6 is an editbox that takes the ID of the tuple that should be deleted from the database and ADOStoredProc1 is the stored procedure in the database that takes 1 parametar (the ID you want to delete).
The project runs with no problems, I even got a TADOTable and a DBGrid that load the information from the database, but when I try to delete a tuple from the database using its ID written in the EditBox I get this Error: "Cannot perform this operation on a closed dataset" and the breakpoint of the project is when the application tries to add the value for the 'clenID' parameter.
Where is my mistake and how to fix it?
I think the ADOStoredProc1.Refresh method is not appropriate here. In this case the stored procedure does not return a result set. Could you leave it out? And also the line ADOStoredProc1.Active := True. The connection to the database is open I presume? Could you also check the values of the Parameters collection in the Object Inspector?
I think you want to call ADOStoredProc1.Parameters.Refresh, not ADOStoredProc1.Refresh.
Also, you should only set Active to True if the SQL Server Stored procedure returns a dataset - i.e. the result of a SELECT statement. Setting Active to True is the same as calling Open.
If the stored procedure only returns a result code (RETURN n), then use ExecProc.
In no case should you use both ADOStoredProc1.Active := True; and ADOStoredProc1.ExecProc;
In summary, you probably want something like
procedure TForm1.btnDeleteClick(Sender: TObject);
begin
ADOStoredProc1.ProcedureName := 'sp_Delete_Clen';
ADOStoredProc1.Parameters.Refresh; // gets the parameter list from SQL Server
ADOStoredProc1.Parameters.ParamByName('#clenID').Value := edtID.Text;
ADOStoredProc1.ExecProc;
end;

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