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:

Figure 2.6: The form after the Save To CSV button is clicked

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

Figure 2.7: The form after the Iterate on DataSet button is clicked; the iteration is used to show dataset data in the listbox

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.