Delphi ADO : Locate with dataset filter on bug - delphi

I have as simple query, which returns following rows:
Name Value
Peter 1
Peter 2
Peter 3
John 1
John 2
Applying filter:
ADO.Filter := 'Name="John"';
ADO.Filtered := True; // Now, its only 2 rows in dataset
ADO.Locate('Value', 2);
Cursor should point to "John 2", but it points to "Peter 2" (which being filtered out by filter). And locate returns True.
Tried on Delphi 7, Rad studio XE 6. It seems that this error is living there for the last 15 years
Any solution ?

The problem with TCustomADODataSet.Locate is that it's internally using Recordset.Clone and trying to locate a record in the cloned recordset, without setting the filter to the cloned recordset (see ADODB TCustomADODataSet.LocateRecord).
From the Remarks in the docs:
The Filter property of the original Recordset, if any, will not be
applied to the clone. Set the Filter property of the new Recordset
to filter the results. The simplest way to copy any existing Filter
value is to assign it directly, as follows. rsNew.Filter =
rsOriginal.Filter The current record of a newly created clone is set
to the first record.
I have been using my own simple Locate function for filtered ADO DataSets: Basically, storing the current bookmark, moving to the first record and iterating the DataSet until it found a match. If no match found restore the previous bookmark.
Bellow is a really limited implementation that worked for me (a lot of todo tho for a perfect solution):
class function TData.Locate(DataSet: TDataSet; const KeyFields: string;
const KeyValues: Variant; Options: TLocateOptions): Boolean;
{ a very simple Locate function - todo: TLocateOptions & multiple KeyFields/KeyValues }
var
BM: TBookmarkStr;
begin
Result := False;
if DataSet.IsEmpty then Exit;
BM := DataSet.Bookmark;
DataSet.DisableControls;
try
DataSet.First;
while not DataSet.Eof do
begin
if DataSet.FieldByName(KeyFields).Value = KeyValues then
begin
Result := True;
Break;
end;
DataSet.Next;
end;
if not Result then DataSet.Bookmark := BM;
finally
DataSet.EnableControls;
end;
end;
Another option is to patch ADODB.pas TCustomADODataSet.LocateRecord and set the FLookupCursor.Filter to match the current dataset filter. This option is acceptable as long as you patch ADODB.pas as a new copy placed in your project folder.
Yet another option is to use TCustomADODataSet.Recordset.Find method (See also: How to use a RecordSet.Find with TADOQuery?).

Try this:
ADO.Filter := 'Name=' + QuotedStr('John');
ADO.Filtered := True; // Now, its only 2 rows in dataset
ADO.Locate('Value',2,[]);
The third parameter of the Locate function is for locate options, which I've left empty in the example above. You can read up on Locate here
http://docwiki.embarcadero.com/RADStudio/XE7/en/Using_Locate and here http://docwiki.embarcadero.com/Libraries/XE7/en/Data.DB.TDataSet.Locate

Related

TStringGrid show (bcd) in delphi live binding

I connect TFDQuery with TStringGrid in live binding in delphi firemonkey apps.
I tried to use filter in TFDQuery based on Editbox for searching purpose, and it's work just fine.
but, whenever I clear the Editbox, one of my row in TStringGrid would show "(bcd)" as it's value like the pict below.
what am I doing wrong ? how can I fix it ?
Edit :
Im using mySql database with firedac tfdconnection + tfdquery
the datatype of the column is AnsiString & FmtBCS(32,0)
Im using live binding feature in delphi.
my filter code
with MainQuery do begin
Filtered := False;
OnFilterRecord := nil;
Filter := FilterValue;
Filtered := True;
end;
I Insert to the table with TFDConnection.execSQL
the "(BCD)" part always change on the selected Row as the pict below.
EDIT 2:
To Reproduce my error, you can :
add TStringGrid.
Add Editbox.
add tfdconnection
add tfdquery
use live binding from tfdquery to tstringgrid.
add query to tfdquery.sql.text that using SUM() in mysql. Example : "select id, sum(stock) as total_stock from stocks"
activate that tfdquery
add onkeyup event on editbox.
add this code :
FilterValue:= 'garmines_id LIKE ''/' +Edit1.Text+'%'' ESCAPE ''/'' ';
with FDQuery1 do begin
Filtered:= false;
OnFilterRecord := nil;
Filter := FilterValue;
Filtered := True;
end;
run
try to type something on editbox to make sure filter works fine.
clear editbox, then the "(BCD)" is show on the selected row.
I reproduce this error. this is the SS :
Well, I still don't know what exactly causing this problem but I found the work around solution that avoid this problem appears.
you need to set TStringGrid.selected value to -1 before refreshing the TFDQuery. so the code become :
FilterValue:= 'garmines_id LIKE ''/' +Edit1.Text+'%'' ESCAPE ''/'' ';
StringGrid1.selected := -1;
with FDQuery1 do begin
Filtered:= false;
OnFilterRecord := nil;
Filter := FilterValue;
Filtered := True;
end;
I suspect that the cause of this problem is data type that come from mysql sum() method namely FmtBCD(32)
Go to DataMapping Rules (firedac connection)
Mark ignore inherited rules
create 2 new rules
rule1: source: dtBCD / target datatype: dtDouble / all min: 0 / all max: 100
rule2: source: dtfmtbcd / target datatype: dtDouble / all min: 0 / all max: 100
click ok. now the fields will be dtDouble, and are compatible with tgrid

how to copy a ftBlob with firedac arrayDML

I use Delphi Berlin.
I am trying to make a function/procedure to copy data from a FireDac query (connected to a database) to another FireDac query (connected to another database) using ArrayDML. first database is firebird and the other is MSSQL in the first care, but in another case both databases are Firebird.
So far so good and almost all datatype are working correct except ftBlob.
Here is the body of the function:
while not querySource.Eof do begin
paramPosition := -1;
Inc(mIndex);
for i := 0 to querySource.FieldCount - 1 do begin
Inc(paramPosition);
// daca exista o valoare
if querySource.FieldByName(querySource.Fields[i].FieldName).AsVariant <> Null then begin
case querySource.Fields[i].DataType of
ftDateTime, ftDate, ftTime, ftTimeStamp : queryInsert.Params[paramPosition].AsDateTimes[mIndex] := querySource.FieldByName(querySource.Fields[i].FieldName).AsDateTime;
ftFloat, ftCurrency, ftBCD, ftFMTBcd : queryInsert.Params[paramPosition].AsFloats[mIndex] := querySource.FieldByName(querySource.Fields[i].FieldName).AsFloat;
ftSmallint, ftInteger, ftLargeint : queryInsert.Params[paramPosition].AsIntegers[mIndex] := querySource.FieldByName(querySource.Fields[i].FieldName).AsInteger;
ftString : queryInsert.Params[paramPosition].AsStrings[mIndex] := querySource.FieldByName(querySource.Fields[i].FieldName).AsString;
ftBlob, ftMemo, ftGraphic : queryInsert.Params[paramPosition].AsBlobs[mIndex] := querySource.FieldByName(querySource.Fields[i].FieldName).AsVariant;
end;
end;
end;
the blob value is not copy the correct value from the source.
how to use the arrayDML in this case? any workaround?
I won't answer your question but suggest you to use the TFDBatchMove component because you are reinventing wheel here. TFDBatchMove is just for what you want to do, for moving data from one database to another (and not only that).
You simply setup the TFDBatchMoveSQLReader as Reader and Writer, write SQL queries for both and the component will automatically map the fields by matching names. If the queries won't have matching field names, you can fine tune this by the Mappings property. Then you just call Execute.

Error when using parameter in ADOQuery

I have this simple code to check if a record exists in a table, but it always returns a runtime error :
Arguments are of the wrong type, are out of acceptable range, or are
in conflict with one another.
my code is this :
function TDataModuleMain.BarCodeExists(barCode: string): boolean;
begin
if ADOQuerySql.Active then
ADOQuerySql.Close;
ADOQuerySql.SQL.Clear;
ADOQuerySql.SQL.Text := 'select count(1) from Card where BarCode = (:TestBarcode)';
ADOQuerySql.Parameters.ParamByName('TestBarcode').Value := barCode;
ADOQuerySql.Open; // HERE THE RUNTIME ERROR APPEARS
Result := ADOQuerySql.Fields[0].AsInteger = 1;
ADOQuerySql.Close;
ADOQuerySql.Parameters.Clear;
end;
The field BarCode in table Card is of type nvarchar(100)
In debug I see that the parameter is created, and gets populated with the correct value.
Running the query in sql server management studio also works.
I also found this How to pass string parameters to an TADOQuery? and checked my code with the code in the answer but I don't see any problems here.
Also this AdoQuery Error using parameters did not help me.
It will no doubt be something very simple that I have missed but I just dont see it now.
EDIT : things I tried from suggestions in the comments:
.ParamCheck := True (default)
.Parameters.ParamByName('TestBarcode').DataType := ftString
.Parameters.ParamByName('TestBarcode').DataType := ftWideString
None of these worked however.
What did help was using a non-shared AdoQuery for this, and that one did the job without any errors. I am using that now as the solution but I am still looking at the shared AdoQuery out of curiousity what the exact problem is.
EDIT: the source of the problem is found.
I used the function provided by MartinA to examine both the dynamic created query and the shared AdoQuery and I found one difference.
The shared AdoQuery had the this property filled :
ExecuteOption := [eoExecuteNoRecords]
and the dynamic created query does not.
Since this property is not set in designtime I did not see it.
After clearing the property to [] the shared AdoQuery worked again.
I am going to switch to using non shared AdoQuery for this kind of work as been suggested.
Thanks everyone for your assistance.
The following isn't intended to be a complete answer to your q, but to follow up my comment that "all you have to do is to inspect your form's DFM and compare the properties of your original ADoQuery with the unshared one. The answer should lie in the difference(s)" and your reply that the unshared query is created dynamically.
There is no "voodoo" involved in the difference in behaviour between your two ADOQuerys. It's just a question of capturing what the differences actually are.
So, what you need, to debug the problem yourself, is some code to compare the properties of two components, even if one or both of them is created dynamically. Using the following routine on both components will enable you to do exactly that:
function TForm1.ComponentToString(AComponent : TComponent) : String;
var
SS : TStringStream;
MS : TMemoryStream;
Writer : TWriter;
begin
// Note: There may be a more direct way of doing the following, without
// needing the intermediary TMemoryStream, MS
SS := TStringStream.Create('');
MS := TMemoryStream.Create;
Writer := TWriter.Create(MS, 4096);
try
Writer.Root := Self;
Writer.WriteSignature;
Writer.WriteComponent(AComponent);
Writer.FlushBuffer;
MS.Position := 0;
ObjectBinaryToText(MS, SS);
Result := SS.DataString;
finally
Writer.Free;
MS.Free;
SS.Free;
end;
end;
Over to you ...

Inputting data from a dbgrid into a word mail merge

I'm wanting to create a mail marge for a letter inputting different names and address on each. I've used Microsoft example as a base point http://support.microsoft.com/kb/229310 and i've customized it to how i like. But my problem arises when trying to get the data for either selected rows of the dbgrid or just the whole thing. I have no idea how to do it. My first thought was do 1 to the amount of rows, then put some tedit boxes down and put them equal to mailmerged data but that still only does it one at a time. The dbgrid is linked up to a ms outlook.
This is how they fill the data..
// Open the file to insert data
wrdDataDoc := wrdApp.Documents.Open('E:\Temp.doc');
for iCount := 1 to (DBGrid1.DataSource.DataSet.RecordCount) do
wrdDataDoc.Tables.Item(1).Rows.Add;
FillRow(wrdDataDoc, 2, 'Steve', 'DeBroux',
'4567 Main Street', 'Buffalo, NY 98052');
// Fill in the data
FillRow(wrdDataDoc, 3, 'Jan', 'Miksovsky',
'1234 5th Street', 'Charlotte, NC 98765');
FillRow(wrdDataDoc, 4, 'Brian', 'Valentine',
'12348 78th Street Apt. 214', 'Lubbock, TX 25874');
So how would I grab the data from the dbgrid and fill the file with that information?
var
i: Integer;
bm: TBookmark;
begin
DBGrid1.DataSource.DataSet.DisableControls;
try
bm := DBGrid1.DataSource.DataSet.GetBookmark;
try
i := 0;
DBGrid1.DataSource.DataSet.First;
while not DBGrid1.DataSource.DataSet.Eof do begin
Inc(i);
FillRow(wrdDataDoc, i,
DBGrid1.DataSource.DataSet.FieldByName('Name').AsString,
DBGrid1.DataSource.DataSet.FieldByName('Address1').AsString,
..
);
DBGrid1.DataSource.DataSet.Next;
end;
if Assigned(bm) then
DBGrid1.DataSource.DataSet.GotoBookmark(bm);
finally
DBGrid1.DataSource.DataSet.FreeBookmark(bm);
end;
finally
DBGrid1.DataSource.DataSet.EnableControls;
end;
end;
Hmm, this gives me a bit of a clue on how to use Bookmarks to manage selectedrows in DBGrid.
My problem is being able to read through certain fields of selectedRows, such as extract the email addresses (or record ID No) of selected contacts(records) to,perhaps, send email to.
Any further info on using TBookmarkList and TBookmark would be helpful :)
Too Easy... it seems the only way to loop through a TBookmarkLIst is using its Count property,
and using its Item[index] as a TBookmark. Which is then used to dataset.gotBookMark and then access the required fieldByName('Fieldname').
I would have liked...
For bmBookmark in bmlBookmarkList do
but this works...
var bmlGridSelectedRows: TBookmarkList;
bmRecord: TBookmark;
I: Integer;
begin
JvdbUltimGridContacts.DataSource.DataSet.DisableControls;
bmlGridSelectedRows := JvdbUltimGridContacts.SelectedRows;
for I := 0 to bmlGridSelectedRows.Count - 1 do
begin
bmRecord := bmlGridSelectedRows.Items[I];
ABSTableContacts.GotoBookmark(bmRecord);
MessageDlg(ABSTableContacts.FieldByName('DisplayName').AsString,mtInformation,[mbOK],0); //this is just to show that you are accessing the correct record you expect, replace with your own code of course
end;
JvdbUltimGridContacts.DataSource.DataSet.EnableControls;
Nice tip about the Dataset.DisableControls / EnableControls properties,:)
Of course you do not need to declare the variables for TbookmarkList and TBookmark as they can be accessed directly, I am just in the habit of doing that as I think it is cleaner code.
i.e.
DBGrid.DataSource.DataSet.DisableControls;
for I := 0 to DBGrid.SelectedRows.Count - 1 do
begin
ABSTableContacts.GotoBookmark(DBGrid.SelectedRows.Items[I]);
MessageDlg(ABSTableContacts.FieldByName('DisplayName').AsString,mtInformation,[mbOK],0);
end;
DBGrid.DataSource.DataSet.EnableControls;

MS Word 2010 mailmerge in Delphi 5

could anyone help?
I've inherited some software written in Delphi 5 which allows member data and fields from a database (.ADT file) to be used merged in to word.
It works fine with all version of Word except 2010 where it won't load any documents and shows the error:
"That Method is not available on that object"
I have been told the solution is to replace the preset components OpWord and OpDataSet with Ole variants. I have done so with OpWord using:
wrdApp := CreateOleObject('Word.Application');
and the documents now load up but without any merge field data. Can anyone let me know how to extract this data from the database, as the OpDataSet seems to simply just point at the table?
Or can anyone suggest a better solution than the one I'm trying. I'm very new to Delphi so I'm in abit over my head
Edit: (Requested Info)
Sorry I have more details and code if required.
The components appear to belong to a library called OfficePartner along with TOpExcel,TOpOutlook and others.
The .doc is selected from a popup ListPane on Form30, opened and populated with merge field data from Table 4. Table 1 is the members database:
{Use Table4 as we can Set a range on it}
Table4.SetRange([Table1.FieldByName('Member Id').AsString],[Table1.FieldByName('Member Id').AsString]);
{Open Word}
OpWord1.Connected := True;
{Open the Test Document}
OpWord1.OpenDocument(DocumentDirectory + '\' + Form30.ListBox1.Items[Form30.ListBox1.ItemIndex]);
{Populate the Test Document}
OpWord1.ActiveDocument.MailMerge.OfficeModel := OpDataSetModel1;
OpWord1.ActiveDocument.PopulateMailMerge;
OpWord1.ActiveDocument.ExecuteMailMerge;
I hope this helps...
Here is a little procedure for word mail merge that I used way back for D6, it's a just snippet and you have to include in some class, I don't have Delphi anymore so can't compile to make sure that it works, anyway here it is, hope it helps:
procedure MailMergeWord;
var
WordApp: TWordApplication;
WordDoc: TWordDocument;
doc : WordDocument;
FileName: OleVariant;
xx: integer;
begin
WordApp := TWordApplication.Create(nil);
WordApp.ConnectKind := ckNewInstance;
WordDoc := TWordDocument.Create(WordApp);
FileName := 'TemplateDoc.doc';
doc := WordApp.Documents.Open(FileName,EmptyParam,EmptyParam,EmptyParam,EmptyParam
,EmptyParam,EmptyParam,EmptyParam,EmptyParam
,EmptyParam);
WordDoc.ConnectTo(Doc);
for xx := 1 to WordDoc.Fields.Count do
WordDoc.Fields.Item(xx).Result.Text := OnWordVariable(WordDoc.Fields.Item(xx).Code.Text);
WordDoc.PrintOut;
WordDoc.Free;
WordApp.Free;
end;
function OnWordVariable(varName: string): string;
begin
Result := 'Value based on variable name';
end;

Resources