Question

I will answer this question myself, but feel free to provide your answers if you are faster than me or if you don't like my solution. I just came up with this idea and would like to have some opinions on that.

Goal: a configuration class that is readable (like an INI-file) but without having to write (and adapt after a new configuration item has been added) the load and save methods.

I want to create a class like

TMyConfiguration = class (TConfiguration)
  ...
  property ShowFlags : Boolean read FShowFlags write FShowFlags;
  property NumFlags : Integer read FNumFlags write FNumFlags;
end;

Calling TMyConfiguration.Save (inherited from TConfiguration) should create a file like

[Options]
ShowFlags=1
NumFlags=42

Question: What is the best way to do this?

Was it helpful?

Solution

This is my proposed solution.

I have a base class

TConfiguration = class
protected
  type
    TCustomSaveMethod = function  (Self : TObject; P : Pointer) : String;
    TCustomLoadMethod = procedure (Self : TObject; const Str : String);
public
  procedure Save (const FileName : String);
  procedure Load (const FileName : String);
end;

The Load methods look like this (Save method accordingly):

procedure TConfiguration.Load (const FileName : String);
const
  PropNotFound = '_PROP_NOT_FOUND_';
var
  IniFile : TIniFile;
  Count : Integer;
  List : PPropList;
  TypeName, PropName, InputString, MethodName : String;
  LoadMethod : TCustomLoadMethod;
begin
  IniFile := TIniFile.Create (FileName);
  try
    Count := GetPropList (Self.ClassInfo, tkProperties, nil) ;
    GetMem (List, Count * SizeOf (PPropInfo)) ;
    try
      GetPropList (Self.ClassInfo, tkProperties, List);
      for I := 0 to Count-1 do
        begin
        TypeName  := String (List [I]^.PropType^.Name);
        PropName  := String (List [I]^.Name);
        InputString := IniFile.ReadString ('Options', PropName, PropNotFound);
        if (InputString = PropNotFound) then
          Continue;
        MethodName := 'Load' + TypeName;
        LoadMethod := Self.MethodAddress (MethodName);
        if not Assigned (LoadMethod) then
          raise EConfigLoadError.Create ('No load method for custom type ' + TypeName);
        LoadMethod (Self, InputString);
        end;
    finally
      FreeMem (List, Count * SizeOf (PPropInfo));
    end;
  finally
    FreeAndNil (IniFile);
  end;

The base class could provide load and save methods for the delphi default types. I can then create a configuration for my application like this:

TMyConfiguration = class (TConfiguration)
...
published
  function  SaveTObject (P : Pointer) : String;
  procedure LoadTObject (const Str : String);
published
  property BoolOption : Boolean read FBoolOption write FBoolOption;
  property ObjOption : TObject read FObjOption write FObjOption;
end;

Example of a custom save method:

function TMyConfiguration.SaveTObject (P : Pointer) : String;
var
  Obj : TObject;
begin
  Obj := TObject (P);
  Result := Obj.ClassName;  // does not make sense; only example;
end;       

OTHER TIPS

I use XML for all my application as means of configuration. It is:

  • flexible
  • future feature proof
  • easy to read with any text reader
  • very easy to extend in application. No class modifications needed

I have an XML library that makes it extremely easy to read or modify configuration, without even having to watch for missing values. Now you can also map the XML to a class inside application for faster access if speed is the issue, or certain values are read constantly.

I find other configuration methods far less optional:

  • Ini file: no in depth structure, far less flexible
  • registry: just keep away from that.

My preferred method is to create an interface in my global interfaces unit:

type
  IConfiguration = interface
    ['{95F70366-19D4-4B45-AEB9-8E1B74697AEA}']
    procedure SetConfigValue(const Section, Name,Value:String);
    function GetConfigValue(const Section, Name:string):string;
  end;

This interface is then "exposed" in my main form:

type
  tMainForm = class(TForm,IConfiguration)
  ...
  end;

Most of the time the actual implementation is not in the main form, its just a place holder and I use the implements keyword to redirect the interface to another object owned by the main form. The point of this is that the responsibility of configuration is delegated. Each unit doesn't care if the configuration is stored in a table, ini file, xml file, or even (gasp) the registry. What this DOES allow me to do in ANY unit which uses the global interfaces unit is make a call like the following:

var
  Config : IConfiguration;
  Value : string;
begin
  if Supports(Application.MainForm,IConfiguration,Config) then
    value := Config.GetConfiguration('section','name');
  ...      
end;

All that is needed is adding FORMS and my global interfaces unit to the unit I'm working on. And because it doesn't USE the mainform, if I decide to later reuse this for another project, I don't have to do any further changes....it just works, even if the configuration storage scheme is completely different.

My general preference is to create a table (if I'm dealing with a database application) or an XML file. If it is a multi-user database application, then I will create two tables. One for global configuration, and another for user configuration.

Basically you are asking for a solution to serialize a given object (in your case a configurations to ini files). There are ready made components for that and you can start looking here and here.

Sometime ago I wrote small unit for same task - to save/load the configuration of application in xml-file.

Check the Obj2XML.pas unit in our freeware SMComponent library: http://www.scalabium.com/download/smcmpnt.zip

This would be for Java.

I like to use the java.util.Properties class for reading in config files or properties files. What I like is that you put your file with lines in the same way you showed above (key=value). Also, it uses a # (pound sign) for a line thats a comment, kind of like a lot of scripting languages.

So you could use:

ShowFlags=true
# this line is a comment    
NumFlags=42

etc

Then you just have code like:

Properties props = new Properties();
props.load(new FileInputStream(PROPERTIES_FILENAME));
String value = props.getProperty("ShowFlags");
boolean showFlags = Boolean.parseBoolean(value);

Easy as that.

Nicks answer (using Java Properties) has a point: this simple way to read and pass configuration around between parts of the application does not introduce dependencies on a special configuration class. A simple key/value list can reduce the dependencies between application modules and make code reuse easier.

In Delphi, a simple TStrings-based configuration is an easy way to implement a configuration. Example:

mail.smtp.host=192.168.10.8    
mail.smtp.user=joe    
mail.smtp.pass=*******
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top