Jump to content
Tommi Prami

How to make "dynamic initialization" procedure

Recommended Posts

Hello,

I was pondering folowin situation, where there are easily quite many repeating code lines, and how to make int onew liner.

var

var
  LField1: TField;
  LField2: TField;
  LField3: TField;  
  LField4: TField;  
begin
  LField1 := Query.FieldByName('FIELD1');
  LField2 := Query.FieldByName('FIELD1');
  LField3 := Query.FieldByName('FIELD1');
  LField4 := Query.FieldByName('FIELD1');  
  // ...
  // Should be nice to have it like this
  InitField(Query, [LField1, LField2, LField3, LField4], ['FIELD1', 'FIELD2', 'FIELD3', 'FIELD4', ]);

It would be easy to make local procedure, case by case with var parameters etc, but that would not be very elegant.

What I mean by dynamic, is that it would handle any number of fields and field names, oas long as there are same number of them passed to the procedure..

Other case would be that could pass local variables and the routine would create those objects for me on single line.

Share this post


Link to post

There are many ways to do this. Will you need to reference the TField components in any way, or are they just for display or editing? Will there be a need for different data format handing for different fields? Different titles, sizes, validation, etc?

Share this post


Link to post
8 minutes ago, Lars Fosdal said:

There are many ways to do this. Will you need to reference the TField components in any way, or are they just for display or editing? Will there be a need for different data format handing for different fields? Different titles, sizes, validation, etc?

In this case I get the field usually when they are needed in the loop, or get rid of with-clause or something like that.

Share this post


Link to post

So it is not about visual use?

If not, I need to understand more about how the TFields will be used.

Share this post


Link to post

Are you doing this within a class or multitude of classes, or is it within a method or multitude of methods?

Is TField your own class?

Share this post


Link to post
1 hour ago, Lars Fosdal said:

So it is not about visual use?

If not, I need to understand more about how the TFields will be used.

var
  LTotal: Double;
  LField1: TField;
  LField2: TField;
  LField3: TField;  
  LField4: TField;  
begin
  LField1 := Query.FieldByName('FIELD1');
  LField2 := Query.FieldByName('FIELD1');
  LField3 := Query.FieldByName('FIELD1');
  LField4 := Query.FieldByName('FIELD1'); 

  Query.First;
  while not Query.Eof do
  begin
    LTotal := LTotal + LField1.AsFloat;
    // More stuff done with local field variables...
    Query.Next;
  end;

In this case, TDataSet and TField are RTL stuff or their descendants...

So implementation should be kind that works if I have no control over the API of the class..

 

-Tee-

Edited by Tommi Prami

Share this post


Link to post

You have also access to the list of fields. 

Query.Fields[0..N]

No need to address the fields always by their names.

Edited by Sinspin

Share this post


Link to post
13 minutes ago, Sinspin said:

You have also access to the list of fields. 


Query.Fields[0..N]

No need to address the fields always by their names.

Yes...

But totally different thing...

And would have exact same problem. Would need to initialize variables for indexes by name. Constant magic number indexes leads to buggy code.

I am looking for solution for getting fields, and also solution for other cases I could adapt it to.

Getting fields is just one use case of this type of procedure I would like to use...

 

-Tee-

Edited by Tommi Prami

Share this post


Link to post

Seems like an array should be the way:

type
  TMyFields = array[1..4] of TField;

procedure TForm1.FormCreate(Sender: TObject);
var
  LFields: TMyFields;
  i: Integer;
begin
  for i := Low(TMyFields) to High(TMyFields) do
    LFields[i] := Query.FieldByName('FIELD' + IntToStr(i));
end;

 

  • Like 1

Share this post


Link to post
15 minutes ago, uligerhardt said:

Seems like an array should be the way:


type
  TMyFields = array[1..4] of TField;

procedure TForm1.FormCreate(Sender: TObject);
var
  LFields: TMyFields;
  i: Integer;
begin
  for i := Low(TMyFields) to High(TMyFields) do
    LFields[i] := Query.FieldByName('FIELD' + IntToStr(i));
end;

 

Nice could be done something like that,. but field names are not indexed, usually and after that it is just the same as the my original example I am trying to solve...

 

But Not looking for workaround...
 

-Tee-

Edited by Tommi Prami

Share this post


Link to post

I am not sure if below approach is suitable for you. 

type
  FieldList = TArray<TField>;
  PFieldList = ^FieldList;

procedure FindAllFields(DataSet: TDataSet; Fields: PFieldList; const FieldNames: TArray<string>; const RaiseExceptionForMissingFields: Boolean = True);
var
  I: Integer;
begin
  Assert(Length(Fields^) = Length(FieldNames), 'Fields count <> FieldNames count!');

  for I := Low(Fields^) to High(Fields^) do
  begin
    Fields^[I] := DataSet.FindField(FieldNames[I]);
    if (Fields^[I] = nil) and RaiseExceptionForMissingFields then
      raise Exception.Create('Missing field dataset "' + FieldNames[I] + '"');
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  LFieldList: FieldList;
begin
  SetLength(LFieldList, 4);
  // Below will raise an exception because of the last empty field name and RaiseExceptionForMissingFields=True
  FindAllFields(VirtualTable1, @LFieldList, ['a', 'b', 'c', '']);
  //...
end;

This would require you to remember Indexes for your fieldnames. Not very convenient if you are to have a lot of fields and do different calculations or such with them.

 

Share this post


Link to post

Not sure if Totalling fields is the best example for the use case, as that is far more efficient to do in the queries

3 hours ago, Tommi Prami said:

So implementation should be kind that works if I have no control over the API of the class..

By API, do you mean the interface section of the class?
The challenge is that you need a predictable way to associate the fields with the field name.


You could write a routine like this

procedure ConnectFields(Query: TDataSet; const Fields: TArray<TField>; const Names: TArray<string>);
begin
  Assert(Length(Fields) = Length(Names), 'Number of fields and number of names must match');
  for var ix := 0 to Length(Fields) - 1
  do begin
    Fields[ix] := Query.FieldByName(Names[ix]);
    if not Assigned(Fields[ix])
    then raise Exception.Create(Format('Field %s not found.', [Names[ix]]); 
  end;
end
  
// usage
var
  Field1, Field2, Field3, Field4: TField;
begin
  ConnectFields(Query,
   [ Field1,   Field2,   Field3,   Field4], 
   ['Field1', 'Field2', 'Field3', 'Field4']);
...

 which doesn't save you that much code, really.

Share this post


Link to post
44 minutes ago, Lars Fosdal said:

 


procedure ConnectFields(Query: TDataSet; const Fields: TArray<TField>; const Names: TArray<string>);
begin
  Assert(Length(Fields) = Length(Names), 'Number of fields and number of names must match');
  for var ix := 0 to Length(Fields) - 1
  do begin
    Fields[ix] := Query.FieldByName(Names[ix]);
    if not Assigned(Fields[ix])
    then raise Exception.Create(Format('Field %s not found.', [Names[ix]]); 
  end;
end
  
// usage
var
  Field1, Field2, Field3, Field4: TField;
begin
  ConnectFields(Query,
   [ Field1,   Field2,   Field3,   Field4], 
   ['Field1', 'Field2', 'Field3', 'Field4']);
...

 which doesn't save you that much code, really.

Does not work, know because I've tried that approach in the past, and retried your code and it does nothing. (outside of procedure, which is the important part),

 

// usage
var
  Field1, Field2, Field3, Field4: TField;
begin
  Field1 := nil; // ...   Field2,   Field3,   Field4
  ConnectFields(Query,
   [ Field1,   Field2,   Field3,   Field4], 
   ['Field1', 'Field2', 'Field3', 'Field4']);
 // Fields star as they are in here, nil or not nil, doesnt matter... But eassy to check by nilling the fields becore and after call.

 

Other problem in this tat compiler starts to complain about Fields are uninitialized. But that is separate issue...

Edited by Tommi Prami

Share this post


Link to post
39 minutes ago, Lars Fosdal said:

 which doesn't save you that much code, really.

Depends of number of calls and fields. If 8 field 50 places, it would be from 400 -> 50 lines of code. I think it would be significant, Would be net win after first one.

And would make the procedure way more clean, without initialization code in it.

Share this post


Link to post
37 minutes ago, Tommi Prami said:
1 hour ago, Lars Fosdal said:

 



procedure ConnectFields(Query: TDataSet; const Fields: TArray<TField>; const Names: TArray<string>);
begin
  Assert(Length(Fields) = Length(Names), 'Number of fields and number of names must match');
  for var ix := 0 to Length(Fields) - 1
  do begin
    Fields[ix] := Query.FieldByName(Names[ix]);
    if not Assigned(Fields[ix])
    then raise Exception.Create(Format('Field %s not found.', [Names[ix]]); 
  end;
end
  
// usage
var
  Field1, Field2, Field3, Field4: TField;
begin
  ConnectFields(Query,
   [ Field1,   Field2,   Field3,   Field4], 
   ['Field1', 'Field2', 'Field3', 'Field4']);
...

 which doesn't save you that much code, really.

Does not work, know because I've tried that approach in the past, and retried your code and it does nothing. (outside of procedure, which is the important part),

What do you mean with "not working"?

The TField fields are not linked to the fields of your query?

 

When do you try to do the binding?

 

Btw, FieldByName create already an exception if the FieldName is not found. To prevent that use FindField instead.

Share this post


Link to post
1 hour ago, Lars Fosdal said:

Fields[ix] := Query.FieldByName(Names[ix]);

This will only change the TField array item, but not the local variable FieldX as expected.

 

 A const or var array of TField is no replacement for a couple of var par: TField parameters. As soon as you construct that array parameter, the addresses of the local variables are gone, while their current content (which may be undefined here) is copied to the array item.

 

Perhaps this may be a better approach (didn't try as I am too lazy to create a fake dataset with these fields):

procedure ConnectFields(Query: TDataSet; const Fields: TArray<^TField>; const Names: TArray<string>);
begin
  Assert(Length(Fields) = Length(Names), 'Number of fields and number of names must match');
  for var ix := 0 to Length(Fields) - 1
  do begin
    Fields[ix]^ := Query.FindField(Names[ix]);
    if not Assigned(Fields[ix]^) then
      raise Exception.Create(Format('Field %s not found.', [Names[ix]]));
  end;
end;

procedure Test;
var
  Field1, Field2, Field3, Field4: TField;
begin
  ConnectFields(Query,
   [ @Field1,   @Field2,   @Field3,   @Field4],
   ['Field1', 'Field2', 'Field3', 'Field4']);
   ...
end;

 

  • Like 1
  • Thanks 2

Share this post


Link to post
16 hours ago, Sinspin said:

What do you mean with "not working"?

The TField fields are not linked to the fields of your query?

 

When do you try to do the binding?

 

Btw, FieldByName create already an exception if the FieldName is not found. To prevent that use FindField instead.

As Uwe explained...

 

Share this post


Link to post

You can take a different approach, these fields are reflecting some query, since you declare all the fields I assume query is static. This means that you can generate Delphi code based on this query, that will declare and assign all the variables, so you won't have to write it yourself and in addition this will make it easy to add new queries, and if you change query compiler will tell you about errors in dependent code.

Share this post


Link to post
1 hour ago, EugeneK said:

if you change query compiler will tell you about errors in dependent code

If you are referring to persistent fields added via the Field Editor IDE window, the compiler will only complain if you update the field list after you update a query. If the query is changed and no longer returns one of the persistent fields, or returns a different data type for a persistent field, the code will still compile. Those "errors in dependent code" will not occur until run-time.

Share this post


Link to post

Or you try a total different approach described in this article: Dataset Enumerator Reloaded

 

The old repository can be found on GitHub: https://github.com/UweRaabe/DataSetEnumerator, although the current development happens in https://github.com/UweRaabe/CmonLib

 

Taking your code example from above:

var
  LTotal: Double;
  LField1: TField;
  LField2: TField;
  LField3: TField;  
  LField4: TField;  
begin
  LField1 := Query.FieldByName('FIELD1');
  LField2 := Query.FieldByName('FIELD1');
  LField3 := Query.FieldByName('FIELD1');
  LField4 := Query.FieldByName('FIELD1'); 

  Query.First;
  while not Query.Eof do
  begin
    LTotal := LTotal + LField1.AsFloat;
    // More stuff done with local field variables...
    Query.Next;
  end;

a corresponding solution based on my unit could look like this:

uses Cmon.DataSetHelper;

type
  [DBFields(mapAuto)]  // make sure record fields are named similar to DB fields!
  TMyFields = record
    Field1: Double;
    Field2: Double;
    Field3: Double;
    Field4: Double;
  end;

var
  LTotal: Double;
begin
  LTotal := 0;
  for var rec in Query.Records<TMyFields> do begin
    LTotal := LTotal + rec.Field1;
    // More stuff done with rec
  end;

 

Share this post


Link to post

Which again makes me wonder why not

SELECT SUM(Field1) AS Total FROM YourSourceView

but as mentioned, I suspect the example differs from the real world need...

Share this post


Link to post
5 minutes ago, Lars Fosdal said:

Which again makes me wonder why not


SELECT SUM(Field1) AS Total FROM YourSourceView

but as mentioned, I suspect the example differs from the real world need...

True,

Calculating the sum was just placeholder of some calculation going on in the loop

But very true, if can get sum forehand, would be smart to get it.

 

-Tee-

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×