- Delphi Cookbook
- Daniele Spinetti Daniele Teti
- 1061字
- 2025-04-04 16:22:47
How to do it...
For the DataSetClassHelpers.dproj project, let's start to talk about the simpler helper—the SaveToCSV method.
The current compiler implementation of class helpers allows only one helper active at a time. So, if you need to add two or more helpers at the same time, you have to merge all the methods and properties in a single helper class. Your TDataSet helper is contained in the DataSetHelpersU.pas unit, and is defined as follows:
TDataSetHelper = class helper for TDataSet
public
procedure SaveToCSVFile(AFileName: string);
function GetEnumerator: TDataSetEnumerator;
end;
To use this helper with your TDataSet instances, you have to add the DataSetHelpersU unit in the uses clause of the unit where you want to use the helper. The helper adds the following features to all the TDataSet descendants:
Method name Description
SaveToCSV
This allows any dataset to be saved as a CSV file. The first row contains all the fieldnames.
All string values are correctly quoted, while the numeric values aren't. The resultant CSV file is compatible with MS Excel, and can be opened directly into it. GetEnumerator
This enables the dataset to be used as an enumerable type in the for...in loops. This removes the necessity to cycle the dataset using the usual while loop, so you cannot forget the DataSet.Next call at the end of the loop.
The dataset is correctly cycled from the current position to the end, and for each record the for loop is executed.
The enumerator item type is a wrapper type called TDSIterator, and is able to access the individual values of the current record using a simplified interface.
To get an idea about what the helpers can do, check the following code:
//all the interface section before implementationuses DataSetHelpersU; // add the TDataSet helper to the compiler scope procedure TClassHelpersForm.btnSaveToCSVClick(Sender: TObject); begin // use the SaveToCSVFile helper method FDMemTable1.SaveToCSVFile('mydata.csv'); ListBox1.Items.LoadFromFile('mydata.csv'); end; procedure TClassHelpersForm.btnIterateClick(Sender: TObject);
var
it: TDSIterator;
begin
ListBox1.Clear;
ListBox1.Items.Add(Format('%-10s %-10s %8s', ['FirstName', 'LastName',
'EmpNo']));
ListBox1.Items.Add(StringOfChar('-', 30));
for it in FDMemTable1 do
begin
ListBox1.Items.Add(Format('%-10s %-10s %8d',
[it.Value['FirstName'].AsString, it.S['LastName'], it.I['EmpNo']]));
end;
end;
Useful, isn't it? The following screenshot shows the status of the demo application after the SaveToCSV button was clicked. The demo application is seen as running:

The following screenshot shows the output of the dataset iteration using the helper:

Let's see the implementation details.
The SaveToCSV method has been implemented as shown here:
procedure TDataSetHelper.SaveToCSVFile(AFileName: string);
var
Fields: TArray<string>;
CSVWriter: TStreamWriter;
I: Integer;
CurrPos: TArray<Byte>;
begin
// save the current dataset position
CurrPos := Self.Bookmark;
Self.DisableControls;
try
Self.First;
// create a TStreamWriter to write the CSV file
CSVWriter := TStreamWriter.Create(AFileName);
try
SetLength(Fields, Self.Fields.Count);
for I := 0 to Self.Fields.Count - 1 do
begin
Fields[I] := Self.Fields[I].FieldName.QuotedString('"');
end;
// Write the headers line joining the fieldnames with a ";"
CSVWriter.WriteLine(string.Join(';', Fields));
// Cycle the dataset
while not Self.Eof do
begin
for I := 0 to Self.Fields.Count - 1 do
begin
// DoubleQuote the string values
case Self.Fields[I].DataType of
ftInteger, ftWord, ftSmallint, ftShortInt, ftLargeint, ftBoolean,
ftFloat, ftSingle:
begin
CSVWriter.Write(Self.Fields[I].AsString);
end;
else
CSVWriter.Write(Self.Fields[I].AsString.QuotedString('"'));
end;
// if at the last columns, newline, otherwise ";"
if I < Self.FieldCount - 1 then
CSVWriter.Write(';')
else
CSVWriter.WriteLine;
end;
// next record
Self.Next;
end;
finally
CSVWriter.Free;
end;
finally
Self.EnableControls;
end;
// return to the position where the dataset was before
if Self.BookmarkValid(CurrPos) then
Self.Bookmark := CurrPos;
end;
The other helper is a bit more complex, but all the concepts have been already introduced earlier on in this chapter, in the Writing enumerable types recipe, so this should not be too complex to understand.
The method in the class helper simply returns TDataSetEnumerator by passing the current dataset to the constructor:
function TDataSetHelper.GetEnumerator: TDataSetEnumerator; begin Self.First; Result := TDataSetEnumerator.Create(Self); end;
Now, some magic happens in TDataSetEnumerator! Methods to access the current record are encapsulated in a TDSIterator instance. This class allows you to access field values using a limited and simpler interface (compared to the TDataSet one).
The following is the declaration of the enumerator and the iterator:
TDataSetEnumerator = class(TEnumerator<TDSIterator>)
private
FDataSet: TDataSet; // the current dataset
FDSIterator: TDSIterator; // the current "position"
FFirstTime: Boolean;
public
constructor Create(ADataSet: TDataSet);
destructor Destroy; override;
protected
// methods to override to support the for..in loop
function DoGetCurrent: TDSIterator; override;
function DoMoveNext: Boolean; override;
end; // This is the actual iterator
TDSIterator = class
private
FDataSet: TDataSet;
function GetValue(const FieldName: string): TField;
function GetValueAsString(const FieldName: string): string;
function GetValueAsInteger(const FieldName: string): Integer;
public
constructor Create(ADataSet: TDataSet);
// properties to access the current record values using the fieldname
property Value[const FieldName: string]: TField read GetValue;
property S[const FieldName: string]: string read GetValueAsString;
property I[const FieldName: string]: Integer read GetValueAsInteger;
end;
The TDataSetEnumerator handles the mechanism needed by the enumerable type; however, instead of implementing all the needed methods directly (as you saw in the Write enumerable types recipe), you've inherited from the TEnumerator<T>, so the code to implement is shorter and simpler. The following is the implementation:
{ TDataSetEnumerator } constructor TDataSetEnumerator.Create(ADataSet: TDataSet); begin inherited Create; FFirstTime := True; FDataSet := ADataSet; FDSIterator := TDSIterator.Create(ADataSet); end; destructor TDataSetEnumerator.Destroy; begin FDSIterator.Free; inherited; end; function TDataSetEnumerator.DoGetCurrent: TDSIterator; begin Result := FDSIterator; end; function TDataSetEnumerator.DoMoveNext: Boolean; begin if not FFirstTime then FDataSet.Next; FFirstTime := False; Result := not FDataSet.Eof; end;
It is clear that the current record is encapsulated by a TDSIterator instance that uses the current dataset. This class is in charge of handling real data access to the underlying dataset fields. Here's the implementation:
constructor TDSIterator.Create(ADataSet: TDataSet); begin inherited Create; FDataSet := ADataSet; end; function TDSIterator.GetValue(const FieldName: String): TField; begin Result := FDataSet.FieldByName(FieldName); end; function TDSIterator.GetValueAsInteger(const FieldName: String): Integer; begin Result := GetValue(FieldName).AsInteger; end; function TDSIterator.GetValueAsString(const FieldName: String): String; begin Result := GetValue(FieldName).AsString; end;
Let's summarize the relationship between the three classes involved. The helper class adds a method GetEnumerator to the TDataSet instance, which returns the TDataSetEnumerator. The TDataSetEnumerator method uses the underlying dataset to handle the enumerable mechanism. The current element returned by the DataSetEnumerator is a TDSIterator that encapsulates the dataset's current position, allowing the user code to iterate the dataset using the for...in loop.