Saturday, January 21, 2012

A journey to Delphi TParams - Persistence

In the first article I demonstrated some features of TParams we can use at every day development to hold, process and move data structures around. But what if we want to persist such data and come back to them later?

As you will see, persisting is very easy and is based on Delphi’s component streaming mechanism. It is the same core mechanism used by IDE to save and load a form’s design in .dfm files. TStream class introduces methods that work in conjunction with components and filers for loading and saving components in simple and inherited forms. The two TStream methods needed are ReadComponent & WriteComponent and their definitions are:

function ReadComponent(Instance: TComponent): TComponent;
procedure WriteComponent(Instance: TComponent);
Delphi help also introduces two example functions to show how to use the built-in component streaming support to convert any component into a string and convert that string back into a component. These functions are a long time ago in my utilities library!

function ComponentToString(Component: TComponent): string;
var
  BinStream:TMemoryStream;
  StrStream: TStringStream;
  s: string;
begin
  BinStream := TMemoryStream.Create;
  try
    StrStream := TStringStream.Create(s);
    try
      BinStream.WriteComponent(Component);
      BinStream.Seek(0, soFromBeginning);
      ObjectBinaryToText(BinStream, StrStream);
      StrStream.Seek(0, soFromBeginning);
      Result:= StrStream.DataString;
    finally
      StrStream.Free;
    end;
  finally
    BinStream.Free
  end;
end;

function StringToComponent(Value: string): TComponent;
var
  StrStream:TStringStream;
  BinStream: TMemoryStream;
begin
  StrStream := TStringStream.Create(Value);
  try
    BinStream := TMemoryStream.Create;
    try
      ObjectTextToBinary(StrStream, BinStream);
      BinStream.Seek(0, soFromBeginning);
      Result := BinStream.ReadComponent(nil);
    finally
      BinStream.Free;
    end;
  finally
    StrStream.Free;
  end;
end;
Now that we have all the streaming functionality in our hands we can stream in & out a TParams collection, or we cannot?

No, we cannot. Component streaming works with TComponent & descendants and TParams is not one of these, it actually derives from TPersistent->TCollection.

But this is something easily fixed just by declaring a TComponent with a published TParams property. The streaming mechanism can then deal with this component and “magically” save and load the TParams collection.

Here is such a declaration, very simple and clean:

TParamsStorage = class(TComponent)
protected
 FParams: TParams;
published
 property Params: TParams read FParams write FParams;
end;
Now that we have the ability to write and read a TParams collection to and from a string, we can persist it anywhere we want, a local variable, a file, a stream, a database field etc. I personally have a function and procedure to automate the process of converting TParams to string and vice versa. Here they are:

function ParamsToString(Params: TParams): string;
var ps: TParamsStorage;
begin
  ps := TParamsStorage.Create(nil);
  try ps.Params := Params;
   Result := ComponentToString(ps);
  finally ps.Free;
  end;
end;
procedure StringToParams(Value: string; Params: TParams);
var ps: TParamsStorage;
begin
  ps := TParamsStorage.Create(nil);
  try ps.Params := Params;
   ps.Params.Clear;
   StringToComponent(Value,ps);
  finally ps.Free;
  end;
end;
Another interesting effect of having a TParams collection in a string is that we can store this string in a single TParam object, effectively creating a tree structure of TParam collections!You can investigate the whole idea in the code behind the recursive function I use to create/update such structures:

TParamProps = record
  Name: string;
  DataType: TFieldType;
  Value: variant;
end;
function CreateTParams(const aParams: array of TParamProps): TParams;
var i: integer;
begin
  Result := TParams.Create;
  for i:=0 to High(aParams) do
   Result.CreateParam(aParams[i].DataType,aParams[i].Name,ptUnknown).Value := aParams[i].Value;
end;

function UpdateParam(
Params: TParams; const //The root TParams collection
Path: array of string; //TParam names hierarchy path
             FldType: TFieldType; //DataType of TParam to create
Value: Variant //Value of TParam to create/update
): Boolean; //True always 
var WP: TParams;
    P: TParam;
    A: array of string;
    i: integer;
begin
  if Length(Path) = 1 then
     begin
     P := Params.FindParam(Path[0]);
     if not Assigned(P) then
        P := Params.CreateParam(FldType,Path[0],ptUnKnown);
     P.Value := Value;
     Result := True;
     end
  else
     begin
     P := Params.FindParam(Path[0]);
     if not Assigned(P) then
        P := Params.CreateParam(ftString,Path[0],ptUnKnown);
     WP := TParams.Create;
     try if P.AsString <> '' then
           StringToParams(P.AsString, WP);
      SetLength(A,Length(Path)-1);
      for i:=0 to Length(A)-1 do A[i] := Path[i+1];
      Result := UpdateParam(WP,A,FldType,Value);
      P.AsString := UtilDB.ParamsToString(WP);
      finally WP.Free;
      end;
     end;
end;
Have fun developing, because development is fun!
Feel free to modify the above code as per your needs and if you make any enhancements please contact me.

No comments:

Post a Comment