Aggregate of multiple UI controls bound to one database column - database

I currently have a TDBRadioGroup bound to a CHAR column on the database using the Values property to specify the value stored in the database column for each radio button. Now we have a requirement to add 2 new radio groups along side the existing one that would modify the value. In other words I basically need 3 controls on the form to act as one in a data-aware configuration for the purposes of determining the value stored. A simple example might be if each radio group has a one-character value, and in the database you concatenate them into a 3-character string. Our mapping is more complex than that but that's the general idea. What would be a good way to do this? If someone tells me live bindings would be the way to go, that would be good to know, as it would be help me to convince management to upgrade Delphi. We're stuck on XE.

Interesting q!
I hope I've understood you correctly. Suppose there is a form with 3 radiogroups, one per line, like so
*A B C
D *E F
G H *I
where the asterisks indicates the positions of the selected buttons and suppose
that these settings translate into a field value AEI in a row of a dataset.
If the G entry of the 3rd RG is clicked, the dataset field value should become
GEI. That's what I've assumed you are looking for.
The code below is a "proof of concept" which implements the above functionality.
It does so using 3 standard (non-db-aware) TRadioGroups and an adaptor class
TDBRadioGroupAdaptor which makes these operate as db-aware RadioGroups. The
TDBRadioGroupAdaptor uses a descendant, TRGFieldDataLink, of the standard TFieldDataLink (see DBCtrls.Pas)
to interface with the dataset. I've used a TClientDataSet so the project can be completely
self-contained and I've avoided the use of generices as you didn't specify a Delphi version.
Most of the db-aware functionality is contained in the TRGFieldDataLink, which
serves to interface the dataset and the RadioGroups and differs from a standard
TFieldDataLink in that it treats the field value as composed of N (3 in the code example but it supports an arbitrary number)
sub-fields which each have their own RadioGroup. As ever with implementing db-aware
functionality, the code for the TDBRadioGroupAdaptor and TRGFieldDataLink is fairly
long-winded, as most of it is necessary but not very interesting. About the only
ones that are interesting, in that they make the wheels go around, are
function TRGFieldDataLink.GetGroupString : String;
// Returns a string from the ItemIndexes of the RadioGroups
procedure TRGFieldDataLink.GetGroupValues;
// Sets the DataSet field from the RadioGroup ItemIndexes
which I hope are self-explanatory from the embedded comments.
Because it's only supposed to be a proof-of-concept, the implementation is incomplete,
lacking f.i. keyboard support, but that could easily be added by mimicking the
source code of the standard TDBRadioGroup.
Obviously, something similar could be used to treat subfields of a dataset field as
a group of strings independently editable in a group of TEdits.
Also obviously, it would be possible to turn the TDBRadioGroupAdaptor into a full-blown compound component which includes its own radiogroups, but I've left that as an exercise for the reader.
Code (warning: long!)
type
TDBRadioGroupAdaptor = class;
TRGFieldDataLink = class(TFieldDataLink)
private
FAdaptor: TDBRadioGroupAdaptor;
FRecordChanging : Boolean;
procedure GetGroupValues;
function GetGroupString: String;
procedure SetGroupValues(AValue: String);
public
constructor Create(AAdaptor : TDBRadioGroupAdaptor);
destructor Destroy; override;
procedure RecordChanged(Field : TField); override;
procedure UpdateData; override;
property Adaptor : TDBRadioGroupAdaptor read FAdaptor write FAdaptor;
end;
TDBRadioGroupAdaptor = class
private
FDataLink : TRGFieldDataLink;
FRadioGroups : TList;
procedure SetDataSource(Value : TDataSource);
function GetDataSource : TDataSource;
function GetRadioGroup(Index: Integer): TRadioGroup;
procedure ItemClicked(Sender : TObject);
procedure SetFieldName(const Value: string);
function GetFieldName : String;
public
constructor Create;
destructor Destroy; override;
procedure Add(ARadioGroup : TRadioGroup);
property DataSource : TDataSource read GetDataSource write SetDataSource;
property FieldName : string read GetFieldName write SetFieldName;
property RadioGroup[Index : Integer] : TRadioGroup read GetRadioGroup;
end;
type
TForm1 = class(TForm)
DBGrid1: TDBGrid;
ClientDataSet1: TClientDataSet;
DataSource1: TDataSource;
DBNavigator1: TDBNavigator;
Button1: TButton;
RadioGroup1: TRadioGroup;
RadioGroup2: TRadioGroup;
RadioGroup3: TRadioGroup;
DBEdit1: TDBEdit;
DBRadioGroup1: TDBRadioGroup;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
protected
public
Adaptor : TDBRadioGroupAdaptor;
end;
[...]
{ TRGFieldDataLink }
constructor TRGFieldDataLink.Create(AAdaptor : TDBRadioGroupAdaptor);
begin
inherited Create;
Adaptor := AAdaptor;
end;
destructor TRGFieldDataLink.Destroy;
begin
inherited;
end;
procedure TRGFieldDataLink.SetGroupValues(AValue : String);
// Sets the ItemIndexes of the RadioGroups by matching each character of AValue
// to the contents of their Items
var
i,
Index : Integer;
S : String;
begin
if AValue = '' then Exit; // To avoid error when CreateDataSet is called
for i := 0 to Adaptor.FRadioGroups.Count - 1 do begin
S := AValue[i + 1];
Index := Adaptor.RadioGroup[i].Items.IndexOf(S);
Adaptor.RadioGroup[i].ItemIndex := Index;
end;
end;
procedure TRGFieldDataLink.RecordChanged(Field : TField);
// called when the dataset goes to a new record, e.g. during scrolling
// and sets the RadioGroups' ItemIndexes from the dataset data
var
FieldValue : String;
begin
Assert(DataSet <> Nil);
if FRecordChanging then exit; // just in case, avoid re-entrancy
try
FRecordChanging := True;
if Field = Nil then
Field := DataSet.FieldByName(FieldName); // Yukky way of setting Field
FieldValue := Field.AsString;
SetGroupValues(FieldValue);
finally
FRecordChanging := False;
end;
end;
function TRGFieldDataLink.GetGroupString : String;
// Returns a string from the ItemIndexes of the RadioGroups
var
i : Integer;
S : String;
begin
Result := '';
for i := 0 to Adaptor.FRadioGroups.Count - 1 do begin
S := Adaptor.RadioGroup[i].Items[Adaptor.RadioGroup[i].ItemIndex];
Result := Result + S [1];
end;
end;
procedure TRGFieldDataLink.GetGroupValues;
// Sets the DataSet field from the RadioGroup ItemIndexes
var
FieldValue,
S : String;
begin
Assert(DataSet <> Nil);
S := Field.AsString;
FieldValue := GetGroupString;
Field.AsString := FieldValue;
end;
procedure TRGFieldDataLink.UpdateData;
// Called by RTL to update the dataset record from the RadioGroups
begin
GetGroupValues;
end;
{ TDBRadioGroupAdaptor }
procedure TDBRadioGroupAdaptor.Add(ARadioGroup: TRadioGroup);
begin
FRadioGroups.Add(ARadioGroup);
ARadioGroup.OnClick := ItemClicked;
end;
constructor TDBRadioGroupAdaptor.Create;
begin
inherited;
FRadioGroups := TList.Create;
FDataLink := TRGFieldDataLink.Create(Self);
end;
destructor TDBRadioGroupAdaptor.Destroy;
begin
FDataLink.Free;
FRadioGroups.Free;
inherited Destroy;
end;
function TDBRadioGroupAdaptor.GetDataSource: TDataSource;
begin
Result := FDataLink.DataSource;
end;
procedure TDBRadioGroupAdaptor.SetDataSource(Value: TDataSource);
begin
FDataLink.DataSource := Value;
end;
function TDBRadioGroupAdaptor.GetRadioGroup(Index: Integer): TRadioGroup;
begin
Result := TRadioGroup(FRadioGroups[Index]);
end;
procedure TDBRadioGroupAdaptor.ItemClicked(Sender: TObject);
// Responds to one of the RadioGroups being clicked to put the DataSet into Edit state
// and updates the DataSet field from the ItemIndexes of the RadioGroups
var
S : String;
begin
if not FDataLink.FRecordChanging then begin
S := FDataLink.GetGroupString;
FDataLink.Edit;
FDataLink.SetGroupValues(S);
end;
end;
procedure TDBRadioGroupAdaptor.SetFieldName(const Value: string);
begin
FDataLink.FieldName := Value;
end;
function TDBRadioGroupAdaptor.GetFieldName: string;
begin
Result := FDataLink.FieldName;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
Field : TField;
begin
// Create 2 fields in the CDS
Field := TIntegerField.Create(Self);
Field.FieldName := 'ID';
Field.FieldKind := fkData;
Field.DataSet := ClientDataSet1;
Field := TStringField.Create(Self);
Field.FieldName := 'Value';
Field.Size := 40;
Field.FieldKind := fkData;
Field.DataSet := ClientDataSet1;
RadioGroup1.Items.Add('A');
RadioGroup1.Items.Add('B');
RadioGroup1.Items.Add('C');
RadioGroup2.Items.Add('D');
RadioGroup2.Items.Add('E');
RadioGroup2.Items.Add('F');
RadioGroup3.Items.Add('G');
RadioGroup3.Items.Add('H');
RadioGroup3.Items.Add('I');
Adaptor := TDBRadioGroupAdaptor.Create;
Adaptor.Add(RadioGroup1);
Adaptor.Add(RadioGroup2);
Adaptor.Add(RadioGroup3);
Adaptor.DataSource := DataSource1;
Adaptor.FieldName := 'Value';
// Next, set up the CDS
ClientDataSet1.CreateDataSet;
ClientDataSet1.InsertRecord([1, 'AEI']);
ClientDataSet1.InsertRecord([2, 'BDG']);
ClientDataSet1.InsertRecord([3, 'ADG']);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Adaptor.Free;
end;

Related

Is there a way I can validate multiple inputs through InputBox for integers?

I am basically new to Delphi and I have been given an assignment where I have to sort an integer array, and also validate every input.
I have taken inputs of the array through the InputBox() function. The code for which goes like this:
procedure TForm1.Button1Click(Sender: TObject);
begin
for i := 0 to Length(arr)-1 do
begin
arr[i] := StrToInt(InputBox('Program to sort integers', 'Enter 5 integers', ''));
end;
I have also tried putting a try..except block to catch errors, but it is not working as expected. The try..except block goes something like this:
procedure TForm1.Button1Click(Sender: TObject);
begin
for i := 0 to Length(arr)-1 do
begin
try
arr[i] := StrToInt(InputBox('Program to sort integers', 'Enter 5 integers', ''));
except
ShowMessage('Invalid input');
end;
The problem is that on clicking the button, the above OnClick event handler gets executed, but the except block doesn't.
What should I now do so that input gets validated?
If you want to know more things about my form components, then I’ve added one label, one ListBox to display a sorted array and one button (which you know by name as Button1.)
Thanks in advance.
Using the OnKeyPress() event as you show in your comment! But, if you want that the user can simply hit Enter to mark the end of a value, you also need to accept #13 as a valid key.
Thus, the first test in the OnKeyPress event becomes
if not (Key in [#8, #13, '0'..'9']) then
begin
Key := #0;
Exit;
end;
Then you need to handle the #13 key: (pseudocode)
if key = #13 then
begin
// get the value of entered digits using `StrToInt()` function
// to generalize, use `TEdit(Sender)` to refer to the current edit control
// verify value against min and max values
// if previous test succeeds,
// add to array
// clear the edit control text
// else show error msg
end
All other accepted keys are handled by the control.
You can't restrict InputBox but you can make your own InputBox with a simple form with Edit box and label and buttons.
To restrict edit box to accept only integers of certain range you have to options :
Make a new Edit class inherited from TEdit
TIntegerEdit = class(TEdit)
protected
procedure KeyDown(var Key: Word; Shift: TShiftState); override;
procedure KeyUp(var Key: Word; Shift: TShiftState); override;
procedure KeyPress(var Key: Char); override;
procedure DoEnter; override;
procedure DoExit; override;
end;
2)Use TEdit events to reach your goal. Look at the following sample.
Our form contains an edit control and two buttons. The Edit OnKeyUp & OnKeyDown use the same event.
TForm1 = class(TForm)
Edt1: TEdit;
BtnOk: TButton;
BtnCancel: TButton;
procedure Edt1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
procedure Edt1KeyPress(Sender: TObject; var Key: Char);
procedure Edt1Enter(Sender: TObject);
procedure Edt1Exit(Sender: TObject);
private
{ Private declarations }
FOldText : string;
public
{ Public declarations }
end;
Here is the code used to restrict the Edit to range 1000 to 5000.
procedure TForm1.Edt1KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
// if not in range '0' .. '9' , VK_BACK, VK_TAB, VK_RETURN
if not ((Key in [48..57]) or (Key in [VK_BACK, VK_TAB, VK_RETURN])) then
Key := 0;
end;
procedure TForm1.Edt1KeyPress(Sender: TObject; var Key: Char);
var
i : Integer;
begin
if not ((Key in ['0'..'9']) or (Ord(Key) in [VK_BACK, VK_TAB, VK_RETURN])) then
Key := #0;
end;
procedure TForm1.Edt1Enter(Sender: TObject);
begin
FOldText := '';
Edt1Exit(Self);
FOldText := Edt1.Text;
end;
procedure TForm1.Edt1Exit(Sender: TObject);
const
MinVal = 1000;
MaxVal = 5000;
var
IntVal, E: Integer;
begin
Val(Edt1.Text, IntVal, E);
if E 0 then //Do we have an error?
Edt1.Text := FOldText;
if (IntVal MaxVal) then
Edt1.Text := FOldText;
end;

How to migrate from ADO Filtering code to Firedac

I have this code:
datamodule1.tbabonne.Filter := '';
if (scGPEdit2.Text) = '' then exit ;
try
ref_Abonne:= QuotedStr (scGPEdit2.Text + '*');
if (scGPEdit2.Text <> '') then
datamodule1.tbabonne.Filter:= Format('(ref_Abonne LIKE %s)', [ref_abonne])
else
datamodule1.tbabonne.Filtered := Trim((scGPEdit2.Text)) <> '' ;
except
abort;
end;
//edit1.Text := '';
end;
My question is :
the code Above didn't work with Firedac while is working as charm in ADO
In FireDAC filters, the wildcards are _ for a single character and % for multiple characters - see http://docwiki.embarcadero.com/Libraries/Sydney/en/FireDAC.Comp.Client.TFDQuery.Filter which gives this example
You can use standard SQL wildcards such as percent (%) and underscore (_) in the condition when you use the LIKE operator. The following filter condition retrieves all Countries beginning with 'F'
Country LIKE 'F%'
So you need to adjust your line
ref_abonne:= QuotedStr (scGPEdit2.Text + '*');
accordingly, to use the LIKE operator and the % wildcard.
Just guessing but maybe ADO used the * wildcard and = operator to insulate e.g. VB users from SQL wildcards and syntax.
UpdateHere is a sample project which uses the FireDAC % wildcard and LIKE operator
in a filter. Take careful note of the inline comments.
TForm1 = class(TForm)
// Create a new VCL project and drop the following components
// onto it. There is no need to set any of their properties
FDMemTable1: TFDMemTable;
DataSource1: TDataSource;
DBGrid1: TDBGrid;
edFilter: TEdit;
// Use the Object Inspector to create the following event handlers
// and add the code shown in the implementation section to them
procedure FormCreate(Sender: TObject);
procedure edFilterChange(Sender: TObject);
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.edFilterChange(Sender: TObject);
begin
UpdateFilter;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
DBGrid1.DataSource := DataSource1;
DataSource1.DataSet := FDMemTable1;
// Adjust the following line to suit the location of Employee.Fds on your system
FDMemTable1.LoadFromFile('D:\D10Samples\Data\Employee.Fds');
FDMemTable1.IndexFieldNames := 'LastName;FirstName';
FDMemTable1.Open;
FDMemTable1.First;
// Make the filter disregard string case
FDMemTable1.FilterOptions := [foCaseInsensitive];
UpdateFilter;
end;
procedure TForm1.UpdateFilter;
var
FilterExpr : String;
begin
FilterExpr := edFilter.Text;
if FilterExpr <> '' then
FilterExpr := 'LastName Like ' + QuotedStr(FilterExpr + '%');
FDMemTable1.Filter := FilterExpr;
FDMemTable1.Filtered := FDMemTable1.Filter <> '';
end;
Then just compile and run

Delphi TRichEdit to Array and local storage

I'm looking for informations on how to put TRichEdit into an Array and save it to Local file (ex: File.dat).
The goal is to store a number of text, with a description, and the 'name' of it.
I think I have to start with:
type
TMessage = record
Name : string;
Desc : string;
Text : TMemoryStream;
end;
var ARListMessages: array[1..50] of TMessage
And add data with something like:
richedit.Lines.SaveToStream( ARListMessages[i].Text );
How to create the Array, and make manipulations on it (Add, remove
...) with the 'name'?
How can I save it (Array), and load it easily from local storage ? (Ex:
File.dat)
I've found some informations here, without beeing able to make something working.
Thanks for your time.
[EDIT 18/09/2017]
I'm Still looking to find a solution, and try to find a way to save my informations to a local file.
My actual code to test is :
var
MessageArray : array of TMessage;
// // // //
SetLength(MessageArray, 1);
MessageArray[0].Name := 'Hey You';
MessageArray[0].Desc := 'Im here and will stay here, just in case';
MessageArray[0].Text := TMemoryStream.Create;
MessageArray[0].Text.Position := 0;
RichEdit1.plaintext := false;
RichEdit1.Lines.SaveToStream( MessageArray[0].Text );
So, looking to save MessageArray, but haven't find how to do that yet.
I've take a look on SuperObjet, but can't find how to deal with it.
Omxl was looking Great and easy, but free trial ... :(
I've been able to have an answer.
I share it here if someone need the solution.
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, System.Generics.Collections, Classes, System.Zip;
type
TPersonne = class
strict private
FNom : string;
FPrenom: string;
public
property Nom: string read FNom write FNom;
property Prenom: string read FPrenom write FPrenom;
end;
TFamille = class(TDictionary<string, TPersonne>)
private
procedure LoadFromStream(stream: TStream);
procedure SaveToStream(stream: TStream);
public
procedure LoadFromFile(const AFileName: string);
procedure SaveToFile(const AFileName: string);
end;
procedure TFamille.SaveToStream(stream: TStream);
var
i: Integer;
Personne: TPersonne;
Pair: System.Generics.Collections.TPair<string, TPersonne>;
Writer: TWriter;
begin
Writer := TWriter.Create(stream, 4096);
try
Writer.WriteListBegin;
for Pair in Self do
begin
Writer.WriteString(Pair.Key);
Writer.WriteListBegin;
Personne := Pair.Value;
Writer.WriteString(Personne.Nom);
Writer.WriteString(Personne.Prenom);
Writer.WriteListEnd;
end;
Writer.WriteListEnd;
finally
Writer.Free;
end;
end;
procedure TFamille.LoadFromStream(stream: TStream);
var
Personne: TPersonne;
Reader: TReader;
sKey: string;
begin
Clear;
Reader := TReader.Create(stream, 1024);
try
Reader.ReadListBegin;
while not Reader.EndOfList do
begin
sKey := Reader.ReadString;
Personne := TPersonne.Create;
Reader.ReadListBegin;
while not Reader.EndOfList do
begin
Personne.Nom := Reader.ReadString;
Personne.Prenom := Reader.ReadString;
end;
Reader.ReadListEnd;
Add(sKey, Personne);
end;
Reader.ReadListEnd;
finally
Reader.Free;
end;
end;
procedure TFamille.LoadFromFile(const AFileName: string);
var
Stream: TStream;
begin
Stream := TFileStream.Create(AFileName, fmOpenRead);
try
LoadFromStream(Stream);
finally
Stream.Free;
end;
end;
procedure TFamille.SaveToFile(const AFileName: string);
var
stream: TStream;
begin
stream := TFileStream.Create(AFileName, fmCreate);
try
SaveToStream(stream);
finally
stream.Free;
end;
end;
var
Famille: TFamille;
Personne: TPersonne;
begin
try
Famille := TFamille.Create;
try
Personne := TPersonne.Create;
Personne.Nom := 'Julie';
Personne.Prenom := 'Albertine';
Famille.Add('1', Personne);
Famille.SaveToFile('D:\famille.txt');
finally
FreeAndNil(Famille);
end;
Famille := TFamille.Create;
try
Famille.LoadFromFile('D:\famille.txt');
if Famille.Count > 0 then
Writeln(Famille['1'].Nom + ' ' + Famille['1'].Prenom);
finally
FreeAndNil(Famille);
end;
Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Thanks all for your Help !
How to create the Array, and make manipulations on it (Add, remove
...) with the 'name'?
From your first code the array is already created. No further work is needed. And even if you use dynamic arrays you still don't have to do any thing just declaring it will suffice.
If you are asking "How to create the array ?" as in "How to create such a list, so I could add, remove with the 'name' field" then I would suggest TDictionary (instead of the city names use your field 'name' as a key) to do the manipulations and then when saving use this
How can I save it (Array), and load it easily from local storage ?
(Ex: File.dat)
You can't directly just like array.savetofile;.You need to use one of file handling methods like TFileStream to store the array.
Note: in your edit the memory stream will cause you only trouble because each time you create it you will need to free it after its use instead use these functions to extract formatted text and change the field Text : TMemoryStream; to Text : string;
RichTextToStr: Convert the rich format text to a string.
function RichTextToStr(RichEdit:TRichEdit):string;
var
SS : TStringStream;
begin
SS := TStringStream.Create('');
try
RichEdit.Lines.SaveToStream(SS);
Result := SS.DataString;
finally
SS.Free;
end;
end;
loadToRichedit: Load the formatted text to the RichEdit again.
procedure loadToRichedit(Const St:string;RichEdit:TRichEdit);
var
SS : TStringStream;
begin
SS := TStringStream.Create(St);
try
RichEdit.Lines.LoadFromStream(SS);
finally
SS.Free;
end;
end;

How to get the SQL content of the stored procedure in Delphi code?

I'm debugging Delphi code with Delphi 7 IDE, the code is not mine. My goal is to understand how this code works. This will help me to write my application which will interface with this code.
I never used Delphi in my life. I'm a C, C++, C# programmer.
I'm able to see the output (result) of the stored procedure but I need to see the content of the stored procedure (I mean the SQL statement). I looked in SQL Server side but I didn't find anything.
The stored procedure is called in this function :
function TDBDM.getID( prKeyName : string ) : integer;
begin
try
spGetSN.Parameters.ParamValues['#prKeyname'] := prKeyName;
spGetSN.open;
result := spGetSN.Fields[0].AsInteger;
finally
spGetSN.close;
end;
end;
and the stored procedure 'spGetSN' is declared here :
unit dmDB;
interface
uses
SysUtils, Classes, Forms, ADODB, DB, TypInfo, Dialogs, StdCtrls,
Controls,Registry, Windows;
type
eConnectionType = (ctSN);
TDBDM = class(TDataModule)
ADOConnectionSN: TADOConnection;
spGetSN: TADOStoredProc;
ADOQueryGetPartNoFromRMA: TADOQuery;
ADOQueryGetPartNoFromOrder: TADOQuery;
procedure ADOConnectionSNBeforeConnect(Sender: TObject);
procedure DataModuleCreate(Sender: TObject);
private
{ Private declarations }
function GetConnection( prConnectionType : eConnectionType ) :
TADOConnection;
public
{ Public declarations }
function getID( prKeyName : string ) : integer;
function getSN( prKeyName : string ) : string;
function getCommaValues( prSelect : string; prConnection :
eConnectionType = ctSN ) : string;
property pConnection[prType : eConnectionType] : TADOConnection read
GetConnection; default;
end;
var
DBDM: TDBDM;
implementation
uses dmRegistryClasses;
{$R *.dfm}
The text of the object TADOSTOREDPROC 'spGetSN' as defined in the IDE is :
object spGetSN: TADOStoredProc
Connection = ADOConnectionSN
ProcedureName = 'spGetSN;1'
Parameters = <
item
Name = '#RETURN_VALUE'
DataType = ftInteger
Direction = pdReturnValue
Precision = 10
Value = Null
end
item
Name = '#prKeyname'
Attributes = [paNullable]
DataType = ftWideString
Size = 30
Value = Null
end>
Left = 268
Top = 80
end
How can I print or watch the SQL code of this stored procedure in Delphi debugger? Thanks
The following should give you an idea how to do it:
Add a TAdoQuery called AdoQuerySP to your form and set its connection to whatever AdoConnection component you're using at the moment.
Add a TMemo to the form.
Add a TButton to the form and set its OnClick handler to execute the code below (after changing checklibuse to the name of your SP).
Code:
procedure TForm1.GetSPDefinition;
var
S : String;
begin
S := '';
AdoQuerySP.Sql.Text := 'exec sp_helptext checklibuse'; // sp_helptext
// is a Sql Server system stored proc to retrieve the definition of
// a stored proc. The definition is returned as a series of rows which need to be
// concatenated together as in the `while ...` loop below.
AdoQuerySP.Open;
while not AdoQuerySP.Eof do begin
if S <> '' then
S := #13#10 + S;
S := S + AdoQuerySP.FieldByName('Text').AsString;
AdoQuerySP.Next;
end;
// The retrieved definition is typically preceded by a number of blank lines
// which the call to Trim() remove.
Memo1.Lines.Text := Trim(S);
end;
You could query the sys.sql_modules table e.g.:
function MSSQL_GetModuleText(conn: TADOConnection; const ModuleName: WideString): WideString;
var
SQL: WideString;
rs: Variant;
begin
SQL := Format('SELECT [definition] FROM sys.sql_modules WHERE [object_id]=OBJECT_ID(N''%s'')', [ModuleName]);
rs := conn.Execute(SQL);
Result := VarToWideStr(rs.Fields[0]);
end;
Usage:
S := MSSQL_GetModuleText(ADOConnectionSN, 'dbo.spGetSN');

Delphi stack overflow and access violation error when setting array (of records) length

I am busy building an application in which I am reading data from more two files of "records". I have a very strange error, which pops up depending on the sequence in which I open the files (see code below).
If I click button1 followed by button 2, thus calling the file of "weather data records" followed by the file of "parameters records", all is fine. If I do this the other way around, I get a "stack overflow" followed by "access violation at 0x7c90e898: write of address" error. This happens when I call SetLength for the array in Button1Click.
The weather data file has about 550 records, and the parameters file has about 45 records.
Can anyone see anything obvious wrong with my code? I am not sure how to attach files, or make them available, if anyone wants to use them to test...
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, Buttons, ExtCtrls, Grids,FileCtrl,Contnrs;
type
TWeatherData = record
MyDate : TDate;
Rainfall : Double;
Temperature : Double;
end;
TParameters = record
Species : string[50];
ParameterName: string[50];
ParameterValue : double;
end;
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
Var
WeatherDataFile : file of TWeatherData;
j : integer;
WeatherDataArray : array of TWeatherData;
MyFileSize : Integer;
begin
AssignFile(WeatherDataFile,'C:\Test5.cmbwthr') ;
Reset(WeatherDataFile);
MyFileSize := FileSize(WeatherDataFile);
SetLength(WeatherDataArray,MyFileSize);
j := 0;
try
while not Eof(WeatherDataFile) do begin
j := j + 1;
Read (WeatherDataFile, WeatherDataArray[j]) ;
end;
finally
CloseFile(WeatherDataFile) ;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
ParametersFile : file of TParameters;
j : integer;
CurrentParameters : array of TParameters;
MyFileSize : Integer;
begin
AssignFile(ParametersFile,'C:\Test5.cmbpara') ;
Reset(ParametersFile);
Reset(ParametersFile);
MyFileSize := FileSize(ParametersFile);
SetLength(CurrentParameters,MyFileSize);
j := 0;
try
while not Eof(ParametersFile) do begin
j := j + 1;
Read (ParametersFile, CurrentParameters[j]) ;
end;
finally
CloseFile(ParametersFile) ;
end;
end;
end.
You're writing past the ends of the arrays by incrementing the index before writing to the array instead of afterward. Since you're writing into memory that doesn't belong to the array, any number of problems may occur.
AssignFile(ParametersFile, 'C:\Test5.cmbpara');
Reset(ParametersFile);
try // Enter "try" block as soon as the file is opened.
MyFileSize := FileSize(ParametersFile);
SetLength(CurrentParameters, MyFileSize);
j := 0;
while not Eof(ParametersFile) do begin
Read(ParametersFile, CurrentParameters[j]);
Inc(j);
end;
finally
CloseFile(ParametersFile);
end;
if j <> MyFileSize then
raise Exception.CreateFmt('Parameter count mismatch: expected %d but got %d instead.',
[MyFileSize, j]);
You need packed records to save to a file.
type
TWeatherData = packed record
MyDate : TDate;
Rainfall : Double;
Temperature : Double;
end;
TParameters = packed record
Species : string[50];
ParameterName: string[50];
ParameterValue : double;
end;
Take a look at our TDynArray wrapper available in our SynCommons.pas unit. There is serialization feature included.
And you could put regular string inside the records, instead of shortstring: it will use less space on disk, and will be Unicode Ready since Delphi 2009.
type
TWeatherData = record
MyDate : TDate;
Rainfall : Double;
Temperature : Double;
end;
TWeatherDatas = array of TWeatherData;
TParameter = record
Species : string;
ParameterName: string;
ParameterValue : double;
end;
TParameters = array of TParameter;
var
Stream: TMemoryStream;
Params: TParameters;
Weather: TWeatherDatas;
begin
Stream := TMemoryStream.Create;
try
Stream.LoadFromFile('C:\Test5.cmbpara');
DynArray(TypeInfo(TParameters),Params).LoadFromStream(Stream));
Stream.LoadFromFile('C:\Test5.cmbwthr');
DynArray(TypeInfo(TWeatherDatas),Weather).LoadFromStream(Stream));
finally
Stream.Free;
end;
end;
With TDynArray, you can access any dynamic array using TList-like properties and methods, e.g. Count, Add, Insert, Delete, Clear, IndexOf, Find, Sort and some new methods like LoadFromStream, SaveToStream, LoadFrom and SaveTo which allow fast binary serialization of any dynamic array, even containing strings or records - a CreateOrderedIndex method is also available to create individual index according to the dynamic array content. You can also serialize the array content into JSON, if you wish.
For Delphi 6 up to XE.

Resources