Jump to content


  • Content Count

  • Joined

  • Last visited

  • Days Won


KMarb last won the day on August 13

KMarb had the most liked content!

Community Reputation

1 Neutral

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

  1. I needed to get something working, so I wrote the code below. I want to use FireDac for database access, but I want to pass entire datasets to the datasnap client, and the only way I know how to do that now is to put the data in a TClientdataset and then send to client using CDS.Data. If I could use DBExpress to access the server database it is FAR simpler to pass datasets. If you feel like looking at code (working code, although not sure how efficient), please read below and tell me if there is a better way to do what I'm trying to do. Thank you. cdsFD : TClientDataset; // created with data module create procedure linkCDS; // sub-procedure var i : integer; s, s2, s3 : string; begin if cdsFD.Active then cdsFD.Close; cdsFD.FieldDefs.Clear; cdsFD.FieldDefs.Assign(spFD.FieldDefs); // spFD is the TFDStoredProc with data from server that needs to be sent to client // field definitions from the server might have auto-increment fields, readonly fields and required fields... Turn all those off in the CDS for i := 0 to cdsFD.FieldDefs.Count - 1 do begin if cdsFD.FieldDefs .DataType = ftAutoinc then cdsFD.FieldDefs .DataType := ftInteger; if DB.faReadonly in cdsFD.FieldDefs .Attributes then cdsFD.FieldDefs .Attributes := cdsFD.FieldDefs .Attributes - [DB.faReadonly]; if DB.faRequired in cdsFD.FieldDefs .Attributes then cdsFD.FieldDefs .Attributes := cdsFD.FieldDefs .Attributes - [DB.faRequired]; end; cdsFD.CreateDataSet; cdsFD.Active := true; spFD.First; while not spFD.EOF do begin cdsFD.Insert; cdsFD.copyFields (spFD); cdsFD.Post; spFD.Next; end; end; // start of main procedure begin GetFDConnection; // create new TFDConnection named DBCFD spFD.Connection := DBCFD; spFD.StoredProcName := 'JCMGetAllTimeLookupLists'; spFD.Prepare; spFD.paramByName ('@TSLastRequest').Value := lastRequestTimeStamp; spFD.paramByName ('@ReturnTS').Value := 'N'; spFD.open; // retrieves 19 datasets Result := VarArrayCreate ([0, 19], varVariant); linkCDS; // see above... put FDStoredProc data into a CDS result [0] := cdsFD.Data; for i := 1 to 18 do begin spFD.NextRecordSet; linkCDS; result := cdsFD.Data; // weird... I keep trying to make this result [ i ] :=, but when I hit save (on delphipraxis), the [ i ] keeps disappearing end; spFD.Close; end;
  2. For anyone who reads through this post, I never did figure out why CDSSv.applyUpdates (-1); throws the "invalid parameter" exception The thought I had related to ftDatetime versus ftTimestamp was a false lead. I'm rewriting my server to use FIredac rather than DBExpress to access the server database.
  3. My server needs to send 19 datasets to the android client (datasnap). I nearly have things working using Firedac, but I cannot figure out how to make a Clientdataset use datasets in the TFDStoredProc other than the 1st dataset. I have a SQL (Microsoft) stored procedure, 'JCMGetAllLookupLists' that returns 19 datasets. The TFDStoredProc (spFD) can step through those datasets easily enough using spFD.NextRecordset, but I don't know how to get the dataset in the CDS so I can send CDS.Data back to the cliente. here is the gyst of my code. The code below is in a server function that returns an OleVariant Result := VarArrayCreate ([0, 19], varVariant); // result [19] is not a dataset spFD.StoredProcName := 'JCMGetAllLookupLists'; spFD.Prepare; spFD.paramByName ('@TSLastRequest').Value := lastRequestTimeStamp; spFD.paramByName ('@ReturnTS').Value := 'N'; cdsFD.Open; // cdsFD is linked to a datasetprovider (dspFD) using cdsFD.Providername, and dspFD.Dataset is set to spFD // at this point, cdsFD can access the first dataset in spFD // but, how to get to the other 18 datasets? result [0] := cdsFD.Data; // this works fine // the loop below does not work... cdsFD.Data always has the data from spFD's FIRST dataset... for i := 1 to 18 do begin spFD.NextRecordSet; result := cdsFD.Data; end; I could make 19 different calls to the SQL server to get the 19 datasets (that's what I'm doing now), but it seems much more efficient if the server returns all 19 datasets at once and then my server code pulls out the data for each to send back to the client. Does anyone know how to step through FD datasets using NextRecordset in a way that a linked ClientDataset "sees" each of the FD datasets?
  4. I've only ever used TClientDataset with TSQLQuery, with the needed TDatasetProvider in between (TSQLQuery -> TDatasetProvider -> TClientDataset). I have a project to update an old datasnap server that used DBExpress (TSQLQuery comes from there). I'm wondering if I can drop DBExpress and use FireDac, which I know much better. Does this component linking work or make sense: (TFDStoredProc -> TDatasetProvider -> TClientDataset)? Or better, is there a way to pass an entire dataset using datasnap, but using FD datasets, not DBExpress datasets? With DBX, you use TClientDatasets (CDS) to pass an entire table of data with metadata with these lines: transData := CDS.Data; // on the client side, then a server call passes the transData (array of OleVariants) to a server procedure. CDS is linked to a local SQLite table CDS.Data := transData ; // on the server side, and now the TClientDataset has an entire table. The big picture is to have an efficient way for a client (android datasnap client) to send tables of data to the server, and for the server to efficiently save that data as new rows in existing SQL Server tables. I mention that because I feel there is probably a good way to take CDS.Data (or its FD equivalent) and save all rows to a server table without writing much code, but I wouldn't know where to start my research to do that. Discussion and feedback are really apprecaited. Keith
  5. I tested a simple case and have found that even in very simple example (3 integer fields), I get the "invalid parameter" error. So the problem or concern I have about the ftTimestamp versus ftDatetime may or may not be relevant. Here is my test code: procedure testCDS; var i : integer; s : string; begin resetCDS (CDSSv); // the reset calls clear fields and parameters from the clientdataset and query resetQ (QSv); s := 'select fld1, fld2, fld3 from testTbl where 1=0'; // I have other code that builds this select statement, but allowing the select from database to build the dataset fields in memory has many advantages (works with multiple tables, for exampe) QSv.SQL.Add (s); CDSSv.Open; // open empty dataset, all fields should be available now // attempt to add 3 rows to the table (brand new table, no indexes, just for testing) for i := 1 to 3 do begin CDSSv.Insert; CDSSv.fields [0].Value := i; CDSSv.fields [1].Value := i; CDSSv.fields [2].Value := i; CDSSv.Post; // no error yet... rows are successfully added to the CDS end; try CDSSv.ApplyUpdates (-1); // This is the statement that throws the invalid parameter exception... except on e:exception do begin {$IFDEF DBUG} DMServerContainer.writeDbugS ('testCDS - exception - ' + e.Message); {$ENDIF} end; end; end; Also... if i go a different route and build an SQL Insert statement and then execute it, the rows are saved to the SQL database as expected... IOW, the code below works, so there is something broken with TSQLQuery -> TDatasetProvider -> TClientDataset s := 'insert testTbl (fld1, fld2, fld3) values (1, 1, 1), (2, 2, 2), (3, 3, 3)'; QSv.SQL.Add (s); QSv.ExecSQL; So... I can rewrite my code, but then I need to handle strings, datetimes and blob/photo fields which is more work or not possible with an SQL statement (how to insert a photo into image field using an insert statement?).
  6. The exception reported by CDS.applyUpdates is "Invalid parameter." I do not think the datasnap part of this is relevant... maybe I should not have mentioned that at all, but was trying to be complete. With a TSQLQuery (named QSv), if I run a select statement like select fld1, fld2 from tbl where 1=0, and fld2 is a datetime field in SQL Server, the QSv.fields [1].datatype is not ftDatetime, it is ftTimestamp (16 bits long), and I think that is the source of the failure when applyUpdates tries to add rows back to the database table.
  7. One more thing... I've read that for certain field types, you will get the "invalid parameter" error, particularly with strings and GUID if datasize is not specified... Here's something I'm seeing that might be the problem, but I don't know how to fix. After CDSSv.open (which opens the blank dataset on the server), I loop through all fields to see that fieldtype, datasize, etc are... I have several fields in the underlying SQL table defined as datetime fields (8 bytes). In Delphi, after cdsSv.open, these fields show datatype as ftTimeStamp and datasize as 16 (not ftDateTime and size 8). Why would that happen? And how to fix? To summarize: My SQL is like this: select RegID, myDate from table; myDate in the sQL table is datetime After I open the dataset, the field type of myDate is ftTimestamp, not ftDatetime. That seems like it might cause the invalid parameter error when I apply updates. Thanks for looking through all this.
  8. I added some debugging output and have determined a few things: After CDSSv.open... CDSSv.CanModify is TRUE, cdsSv.ReadOnly is FALSE And I loop through all fields in CDSSv and each field also has CanModify = TRUE and ReadOnly = FALSE So I don't think it is a problem that the dataset or any field is read only. I have searched for why TClientDataSet.ApplyUpdates might generate and exception "Invalid Parameter" but I haven't found anything.
  9. Delphi 11 using DataSnap, although the communication between client and server seems fine, so datasnap I don't think is relevant. Problem is with DBExpress and readonly dataset, I think. I don't know a short way to ask for help on this, but here goes: On my datasnap server I'm using TSQLConnection to access SQL Server. I use TSQLQuery to run a query that gets an empty dataset from the SQL Server. The TSQLQuery is connected to a dataset provider which in turn is connected to a TClientDataset. here is some of my code... this code is called after to datasnap client calls the server method to add 1 or more rows to tables on the server: function copyDataToServer (tblName : string; dataIndex : integer) : integer; var i : integer; s : string; s2 : string; begin result := 0; // tmpCDS is a TClientDataset that has the data to be added to the server... the client passes a clientDataset.Data to the server using a Variant array. tmpCDS.Data := transData [dataIndex]; // build a query to pull empty dataset from server s := ''; for i := 0 to tmpCDS.FieldCount - 1 do if s = '' then s := tmpCDS.Fields .FieldName else s := s + ', ' + tmpCDS.Fields .FieldName; s := 'select ' + s + ' from ' + tblName + 'Transfer where RegID = ' + intToStr (RegID); // QSv is a TSQLQuery QSv.SQL.Add (s); QSv.Prepared := true; // CDSSv is the TClientDataset connected to datasetprovider, which in turn is connected to QSv CDSSv.Open; // this successfully gets an empty dataset for the table in question - after the open, CDSSv.CanModify is true, Readonly is false, recordCount is 0... all as they should be. tmpCDS.First; // this is the dataset from the datasnap client while not tmpCDS.EOF do begin CDSSv.Insert; // not all fields might be present, and order of fields might change... for i := 0 to tmpCDS.FieldCount - 1 do CDSSv.FieldByName (tmpCDS.Fields .FieldName).Value := tmpCDS.Fields .Value; CDSSv.Post; // this works, new row is saved in the clientdataset tmpCDS.Next; end; // at this point 100 +/- rows are in the clientdataset, CDSSv try result := CDSSv.ApplyUpdates (-1); // save recs to the transfer table - THIS BLOWS UP!!! except on e:exception do begin writeDbugS ('result := CDSSave.ApplyUpdates (-1) exception - ' + e.Message;); // exception message is Invalid parameter end; end; Any help is much appreciated... The above code has worked for 5 years. I'm updating things to try to keep up with android versions (for the client side of the datasnap) and in the process needed to update the datasnap server, but I've done something to make the above code stop working and I havn't been able to track it down.
  10. I decided to follow your advice, and to call requestPermissions only from the main UI thread. I've been running my app for several hours today and the odd errors and debugger stepping into assembly code seems to have stopped. So maybe that's why I've been having some problems lately. Thanks for the feedback.
  11. That's not my case... I can download a file regardless of whether i can save in external storage. I can save in private storage, which is fine for the app. I just prefer the shared downloads folder to have better visibility outside the app of the downloaded file.
  12. Thank you for the link. Re: when permissions are requested, it's not that I think I need to request outside the main thread, but the logic makes more sense to me to request the permission within the task that downloads the file. IE, within the task, request permission to save to external storage, then if not granted, save the downloaded file to private storage rather than: request permission to save to external storage start task to download a file. Do you think it's a problem to request permissions outside the main ui?
  13. One other question... If I have started a process using TTask.Run, and then within that thread I RequestPermission, then the permission handler is running in the context of the TTask thread, not the main thread, correct?
  14. awesome! Thank you. Follow up question: I'm testing on an android 11 device, a uleFone Armor X9. The permission handler shows that both read and write external storage have been granted. I'm getting a permission error when I try to download a file to the shared downloads folder: if (Length(AGrantResults) = 2) and (AGrantResults[0] = TPermissionStatus.Granted) and (AGrantResults[1] = TPermissionStatus.Granted) then begin // both are granted... UpdateFileName := extractFNFromURL (UpdateURL); UpdateFullFilename := TPath.Combine(TPath.GetSharedDownloadsPath, UpdateFileName); // I get a permission denied on the following line, not sure if the error is triggered by the fileExists function or request to DeleteFile. if fileExists (UpdateFullFilename) then TFile.Deletefile (UpdateFullFilename); Is this possibly related to android 11 changes with shared storage?
  15. My android app (Delphi 11) wants to download a file to the external storage download folder. I call the line below: PermissionsService.RequestPermissions([FPermissionReadExternalStorage, FPermissionWriteExternalStorage], ExternalStoragePermissionRequestResult, externalStorageRationale); I understand the call to requestPermissions happens asynchronously. When user accepts or rejects the request, the following handler runs: procedure TDMCommon.ExternalStoragePermissionRequestResult(Sender: TObject; const APermissions: TClassicStringDynArray; const AGrantResults: TClassicPermissionStatusDynArray); begin if (Length(AGrantResults) = 2) and (AGrantResults[0] = TPermissionStatus.Granted) and (AGrantResults[1] = TPermissionStatus.Granted) then begin toast ('Downloading file...', shortToast); AsyncDownload.Download(FileURL, LocalFilename, asyncDownloadFinished) then ... I've chopped out code I feel is not relevant. Here are my questions: 1 - The handler is not running in the main thread, correct? 2 - If 1 is correct, then I should not call "toast" directly like it's shown, correct? I should call TThread.Queue(nil, procedure begin toast... end);? 3 - I'm downloading a file in the background using the AsyncDownload code (taken from one of the Delphi samples). It works great when run from main thread, but, if the handler is running code in a secondary thread (not the main thread), is it a bad idea to kickoff another thread to download a file? Could I just initiate a synchronous download here? I'm having some strange behavior and trying to get things right. The strangeness: - running in debug mode, sometimes I end up with a "CPU" window where I'm stepping through assembly code... I'm not sure what can cause that. - I set breakpoints in the finally section of "Try" blocks, but the breakpoints never fire, as if the "finally" blocks are not actually executing. I have calls to log.d in the finally and they do execute, so I think things are fine, but execution does not pause at my breakpoint. Any light on the subject is much appreciated. Keith