Getting property type

Marco Gasi
Marco Gasi used Ask the Experts™
on
Hi all.

I'm building a small class I wnat to use to save session data for a couple of my programs I use frequently.
The goal of the class is to make easier store in a ini file data uswed by program to do specific job. To make it more clear: I have a program I use to make a backup of some project to a backup folder. Since I use for several project, I need the functionality to remember each project's settings (source folder, destination folder, if copy recursively and so on).

Since I have the same need in another program, I decided to develop a class or a component which make this easy: I want to pass a list of components properties:
  with mgProgramDataManager1.PropertiesValues do
  begin
    Add('edtProp.Text');
    Add('mmoProp.Lines');
    Add('cbxItems.Items');
    Add('cbxItems.ItemIndex');
    Add('cbxProp1.Checked');
    Add('cbxProp2.Checked');
    Add('rbt1.Checked');
    Add('rbt2.Checked');
  end;

Open in new window


The SaveData procedure in the component is as folllows:

procedure TmgProgramDataManager.SaveData(AJobName: string);
var
  I: Integer;
  Cmp: TComponent;
  sl: TStringDynArray;
  PropInfo: PPropInfo;
begin
  FFileName := AddBackSlash(FFilePath) + 'jobs.ini';
  FIni := TIniFile.Create(FFileName);
  for I := 0 to FPropertiesValues.Count - 1 do
  begin
    sl := SplitString(FPropertiesValues[I], '.');
    Cmp := FOwner.FindComponent(sl[0]);
    PropInfo := GetPropInfo(Cmp.ClassInfo, sl[1]);
    FIni.WriteString(AJobName, Cmp.Name, GetPropValue(Cmp, PropInfo.Name));
  end;
  ShowMessage('job saved');
end;

Open in new window


But not all works fine: properties like Lines is stored as a number. So I think to have check if a property is of type TStrings and change the code accordingly, but I can't get rid of how I can do it.

I think to have to use RTTI, but I'm struggled trying to understand how to get the property type.

I also know about component streaming and I had yet built a working class, but  I had two problems:
1) as it is, component streaming use default values, so if in a job a checkbox is checked and in another one the checkbox is not checked and I want to pas from the first job to the second one, all values are restored as they were, but the checkbox remains checked because saving the second job the unchecked state is not saved into the stream file (so it can't be restored)
2) I solved this problem checking by myself is a component is a TCheckBox or a TRadioButton but in this case the component will not be able to manage any descendant component (for instance TJvCheckBox)or it will have to create an if statement for any possible component like TCheckListBox an so on.

So the question is: how can I get the type of a property to know if it is TStrings? Or, alternatively, how can deal with this type of task?

Thanks in advance.
Marco
Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®
Sinisa VukSoftware architect
Top Expert 2012

Commented:
If you test cmp if it is stringlist:
...
if cmp.InheritsFrom(TStringList) then
begin
  FIni.WriteString(..., ..., (cmp as TStringList).DelimitedText);
end
else if cmp.InheritsFrom(TCheckBox) then //this will cover TJvCheckBox, TCheckBox..
begin
  FIni.WriteBool(..., ..., (cmp as TCheckBox).Checked);
end
else ...
   //other components
...

Open in new window


... then you can save stringlist as one single row. You can save type of value (boolean, stringlist, ...)
Marco GasiFreelancer
Top Expert 2010

Author

Commented:
Hi Sinisa. Thank you for being always ready to go with my questions :-)

Perfect solution for direct properties but I see a problem: if Cmp is, let say, a TMemo, it doesn't inherit from TStringList nor from TStrings. I should test not for TMemo but for TMemo.Lines. Now, from my main app I pass 'mmoProp.Lines' and my class detects the component type:
    sl := SplitString(FPropertiesValues[I], '.'); //'mmoProp.Lines' is splitted in 'mmoProp' and 'Lines'
    Cmp := FOwner.FindComponent(sl[0]); // Cmp is mmoProp

Open in new window

And following code writes the correct value for let say Checkbox1.checked or edit1.text

    PropInfo := GetPropInfo(Cmp.ClassInfo, sl[1]);
    FIni.WriteString(AJobName, Cmp.Name, GetPropValue(Cmp, PropInfo.Name));

Open in new window


But for Lines or Items it writes a number, so I should check the second part of splitted string and I'm wondering if there is a more correct solution than an infinite serie of
if sl[1] = 'Lines'
else
if sl[1] = 'Items'

Open in new window


to manage this.
Geert GOracle dba
Top Expert 2009

Commented:
have you checked gexperts.org ?
in the tool, there is a backup project option

also from the website, you can download the complete source code for the delphi plugin
it's rather extensive ... and complicated code ... but it's all in there
Introduction to R

R is considered the predominant language for data scientist and statisticians. Learn how to use R for your own data science projects.

Marco GasiFreelancer
Top Expert 2010

Author

Commented:
Hi Geert, Thanks for the suggestion. But I'm not doing a backup of the project files. I'm trying to make my app can remember its settings: I need to write somehow somewhere the values of several controls (like checkboxes, listboxes, comboboxes and so on) linking all of these values with a name the user can use to restore those values. I don't know if code to backup project files can help me in this...
Software architect
Top Expert 2012
Commented:
you code can be extended with my functions - I use Rtti unit (newer delphi versions) which provides
access to object properties.
...
uses  IniFiles, Rtti;
...
function GetPropertyObj(AInstance: TObject; ObjPath: string; var APropObj: TObject;
  var APropName: String): Boolean;
Var
  ctx: TRttiContext;
  pm: TRttiProperty;
  Obj: TObject;
  i: Integer;
begin
  Result := False;
  APropObj := nil;

  i := Pos('.', ObjPath);
  if i>0 then //get root name
  begin
    APropName := Copy(ObjPath, 1, i-1);
    ObjPath := Copy(ObjPath, i+1, Length(ObjPath)-i);
  end
  else
  begin
    APropName := ObjPath;
    ObjPath := '';
  end;

  //find property
  ctx := TRttiContext.Create;
  try
    for pm in ctx.GetType(AInstance.ClassInfo).GetProperties do
    begin
      if CompareText(APropName, pm.Name) = 0 then
      begin
        if ObjPath = '' then //real value
        begin
          APropObj := AInstance;
          Result := True;
        end
        else
        begin
          Obj := pm.GetValue(AInstance).AsObject;
          if Assigned(Obj) then
          begin
            Result := GetPropertyObj(Obj, ObjPath, APropObj, APropName);
          end;
        end;
        break;
      end;
    end;
  finally
    ctx.Free;
  end;
end;

function GetObjValueEx(AInstance: TObject; ObjPath: string; var AVal: String): Boolean;
Var
  ctx: TRttiContext;
  APropName: string;
  pm: TRttiProperty;
  Obj: TObject;
  val: TValue;
  t: TRttiType;
begin
  Result := False;
  AVal := '';

  if GetPropertyObj(AInstance, ObjPath, Obj, APropName) then
  begin
    ctx := TRttiContext.Create;
    try
      t := ctx.GetType(Obj.ClassInfo);
      pm := t.GetProperty(APropName);

      if pm <> nil then
      begin
        try
          val := pm.GetValue(Obj);
          case val.Kind of
            tkChar, tkString, tkWString, tkUString, tkWChar, tkLString:
              AVal := val.AsString;
            tkInteger, tkEnumeration, tkInt64:
              AVal := IntToStr(val.AsOrdinal);
            tkFloat:
              AVal := FloatToStr(val.AsExtended);
          end;
          Result := True;
        except
        end;
      end;
    finally
      ctx.Free;
    end;
  end;
end;

function SetObjValueEx(AInstance: TObject; ObjPath: string; AVal: String): Boolean;
Var
  ctx: TRttiContext;
  APropName: string;
  pm: TRttiProperty;
  Obj: TObject;
  val: TValue;
  t: TRttiType;
begin
  Result := False;

  if GetPropertyObj(AInstance, ObjPath, Obj, APropName) then
  begin
    ctx := TRttiContext.Create;
    try
      t := ctx.GetType(Obj.ClassInfo);
      pm := t.GetProperty(APropName);

      if pm <> nil then
      begin
        try
          val := pm.GetValue(Obj);
          case val.Kind of
            tkChar, tkString, tkWString, tkUString, tkWChar, tkLString:
              pm.SetValue(Obj, AVal);
            tkEnumeration:
              if AVal<>'' then
              begin
                val := StrToBool(AVal);
                pm.SetValue(Obj, val);
              end;
            tkInteger, tkInt64:
              if AVal<>'' then
              begin
                val := StrToInt(AVal);
                pm.SetValue(Obj, val);
              end;
            tkFloat:
              if AVal<>'' then
                pm.SetValue(Obj, StrToFloat(AVal));
          end;
          Result := True;
        except
        end;
      end;
    finally
      ctx.Free;
    end;
  end;
end;

Open in new window


... helper function to read/write from/to ini...
procedure SaveControl(toIniFile: TIniFile; sSect: String; obj: TComponent;
  propArr: array of String);
var
  j: Integer;
  s, sValue: string;
begin
  // for each property defined in array
  for j := Low(propArr) to High(propArr) do
  begin
    // check if component has that property using RTTI
    if GetObjValueEx(Obj, propArr[j], sValue) then
    begin
      // format the string ComponentName.Property
      s := Format('%s.%s', [Obj.Name, propArr[j]]);
      // write data to ini file
      toIniFile.WriteString(sSect, s, sValue);
    end;
  end;
end;

procedure LoadControl(fromIniFIle: TIniFile; sSect: String; obj: TComponent;
  propArr: array of String);
var
  j: Integer;
  s, value: string;
begin
  // for each property defined in array
  for j := Low(propArr) to High(propArr) do
  begin
    // check if component has that property using RTTI
    if GetObjValueEx(Obj, propArr[j], value) then
    begin
      // format the string ComponentName.Property
      s := Format('%s.%s', [Obj.Name, propArr[j]]);
      // read data from ini file
      value := fromIniFIle.ReadString(sSect, s, EmptyStr);
      // check if value is not '' (EmptyStr)
      if value <> EmptyStr then
        // set the property
        SetObjValueEx(Obj, propArr[j], value);
    end;
  end;
end;

procedure SaveControlToFile(const FileName: string; const sSect: String; obj: TComponent;
  propArr: array of String);
var
  ini : TIniFile;
begin
  ini := TIniFile.Create(FileName);
  try
    SaveControl(ini, sSect, obj, propArr);
  finally
    FreeAndNil(ini);
  end;
end;

procedure LoadControlFromFile(const FileName: string; const sSect: String; obj: TComponent;
  propArr: array of String);
var
  ini : TIniFile;
begin
  ini := TIniFile.Create(FileName);
  try
    LoadControl(ini, sSect, obj, propArr);
  finally
    FreeAndNil(ini);
  end;
end;

Open in new window


...and below is example of usage:
procedure TForm12.Button3Click(Sender: TObject);
begin
  SaveControlToFile(ChangeFileExt(Application.ExeName, '.ini') , Self.Name, Memo1,
    ['Lines.DelimitedText']);
  SaveControlToFile(ChangeFileExt(Application.ExeName, '.ini') , Self.Name, CheckBox1,
    ['Checked']);
  SaveControlToFile(ChangeFileExt(Application.ExeName, '.ini') , Self.Name, ComboBox1,
    ['Text','Items.DelimitedText','ItemIndex']);
end;

procedure TForm12.Button4Click(Sender: TObject);
begin
  LoadControlFromFile(ChangeFileExt(Application.ExeName, '.ini') , Self.Name, Memo1,
    ['Lines.DelimitedText']);
  LoadControlFromFile(ChangeFileExt(Application.ExeName, '.ini') , Self.Name, CheckBox1,
    ['Checked']);
  LoadControlFromFile(ChangeFileExt(Application.ExeName, '.ini') , Self.Name, ComboBox1,
    ['Text','Items.DelimitedText','ItemIndex']);
end;

Open in new window

... add component and list of properties in array which you want to save in ini file.
Marco GasiFreelancer
Top Expert 2010

Author

Commented:
Wow, Sinisa, a lot of code to study: thank you very very mutch!  I thouhgt you had abandoned this question and I'm happy you didn't it.
As I can understand at the first look, that's exactly what I was looking for. Thenk you again

Marco
Marco GasiFreelancer
Top Expert 2010

Author

Commented:
Thank you!
Sinisa VukSoftware architect
Top Expert 2012

Commented:
I'm glad to help.

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial