views:

1016

answers:

7

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?

+6  A: 

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;
Smasher
This looks quite OK to me. What makes you think there might be a smarter solution?
Jeroen Pluimers
@Jeroen: The experience that in most cases when I ask here I get a lot of smart comments, suggestions for improvement and criticism :) In addition to that, I wanted to share that piece of code so that others can potentially benefit.
Smasher
A: 

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.

Nick
Doesn't help a lot since I was asking for Delphi and the class you propose is Java-specific...
Smasher
yea sorry about that. You actually weren't specifying, so I gave it a shot. Question was worded very generally. Didn't see your delphi tag.
Nick
no problem. thanks anyway.
Smasher
+1  A: 

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.

Tihauan
+6  A: 

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.
Runner
I do much the same, but I use Delphi's XML Data Binding Wizard to map the file to objects.
Craig Stuntz
The thing is: I think that INI files can be easily understood even by users, while not everybody is able to understand XML. I know that I should have settings dialogs for that, but I would like anybody to be able to change the more advanced settings without diving into XML.
Smasher
XML files are really easy to read. I am talking about basic XML here. Just nodes with attributes and values (text).It is hardly any less readable compared to an INI. Ok, you have more "boiler plating" in the tags. But if you worry about users modifying then make a GUI for them. You cannot relly on user to modify a file on its own. Even an INI.
Runner
You're probably right, but in my case users are used to work with INI files and I would like to keep that. If I use XML in such a flat way (no depth etc) there's just a lot of overhead in the files.
Smasher
Agreed, if you use an XML like that, its better to stay with INI files. It also comes down to tools. If you have the proper tools, XML can became very, very powerful, otherwise it can just be clumsy. I did not say INI files are bad, just that they offer less flexibility ;)
Runner
If the user has to edit a config file manually, you've already lost the game.
Craig Stuntz
They don't have to. They can if they want to tweak some extra settings. Most have no problem with editing an INI file.
Smasher
The only reason the users would ever "want" to do that is the (correct) reason you already gave: "I know I should have settings dialogs for that." That was right. Saying that INI files are "good enough" is a cop-out.
Craig Stuntz
Well, that's to black-and-white for me. Most advanced users do not fear modifying an INI file, so why clutter the settings dialog with very special and advanced settings? In addition to that, it is time-consuming to keep the settings dialog up-to-date. Again: I'm not talking about basic settings here, I'm talking about tweaking settings.
Smasher
Time consuming? With XML you could (should) generate the UI dynamically! INI files are too limited to do this, but XML files aren't; you can have attributes for display names, control types, etc. It's odd that you're trying go simultaneously say that XML files are too complicated for a user to understand and that you really should have a UI (with which I agree) while simultaneously claiming that only power users (who would understand XML) would ever do this and that doing a UI (close to free with an XML config) is too much work!
Craig Stuntz
As I stated above: we are currently using INI files and this is nothing that should change. I'm not against XML and I accept your opinion that everything should be integrated into the UI. But that wasn't my question really. My question was how to create an INI-file like configuration file, so maybe we should all just accept that precondition.
Smasher
And don't get me wrong: I appreciate your input on this topic.
Smasher
+3  A: 

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.

skamradt
+1, because I agree with most of what you wrote, and the differences are mostly issues of style. But the answer as it stands has not much to do with the question, which I understood as "what is the best way to write a base class that is able to load itself from and store itself to persistent storage without the need for changes in descendant classes". That's at least the question the OP answered, the title just doesn't match the question text. Still, smasher should follow your advise instead of hard-coding an INI-file solution.
mghie
I want my configuration setup to take as little time as needed, because it gets repeated with every application. While I like your approach to some extent it is not KISS enough for me. Simple global singleton with XML backend and good interface to access it is good enought in most cases. And more than enough for small simple apps.Even completely database oriented app still needs a simple configuration file to access that data :)
Runner
I couldn't agree more with mghie's comment. You propose a nice alternative to the global variable using an interface and delegation but you say nothing about how serialization is performed. It would be no problem to extract a IPropertyStorer from my solution to separate the configuration from the concrete implementation (INI file / XML / database). But that does not change the points about persistent storage with minimal effort.
Smasher
@mghie: I updated the title. I agree that factoring out the INI-file stuff would be nice.
Smasher
+1  A: 

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

A: 

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=*******
mjustin