Upcoming event

Be-Delphi Delphi Developer Day

Be-Delphi is organizing their first (of many) Delphi Developer Day on November 17th in Edegem near Antwerp. That day will be completely dedicated to Delphi and Prism.

At Be-Delphi, Devia will be holding a talk about the new LiveBindings in Delphi XE2, so be sure to grab a hold of me and say hello !

Simplify your Delphi Code using some OO techniques (Part 4)

written by Stefaan Lesage on 30/03/2010

Part 4 in the series. Actually it's the rest of Part 3, since for some strange reason I couldn't get it into a single post.

Continued

Since for some strange reason I couldn't fit everything in a single post, I had to split it up, so this is the rest of it

The TdvStringSetting class

The explanation

Basically the TdvSetting provides us with a skeleton we can use for our new Setting descendants. I ended up with a TdvStringSetting, TdvIntegerSetting, TdvBooleanSetting and quite a few others, but lets start with the TdvStringSetting first.

The Code
  TdvStringSetting = class( TdvSetting )
  private
    function GetDefaultValueAsString: String;
  protected
    function GetAsBoolean: Boolean; override;
    function GetAsDateTime: TDateTime; override;
    function GetAsFloat: Double; override;
    function GetAsInteger: Longint; override;
    function GetAsString: string; override;
    function GetAsVariant: Variant; override;

    function GetValue(var Value: string): Boolean;

    procedure SetAsBoolean(const Value: Boolean); override;
    procedure SetAsDateTime(const Value: TDateTime); override;
    procedure SetAsFloat(const Value: Double); override;
    procedure SetAsInteger(const Value: Longint); override;
    procedure SetAsString(const aValue: string); override;
    procedure SetVarValue(const aValue: Variant); override;
  public
    procedure SaveToRegIni  ( aRegIni : TRegistryIniFile; const aSection : String ); override;
    procedure LoadFromRegIni( aRegIni : TRegistryIniFile; const aSection : String ); override;

    property DefaultValue  : String       read  GetDefaultValueAsString;
    property Value         : string       read  GetAsString
                                          write SetAsString;
  end;

...

{ TdvStringSetting }

function TdvStringSetting.GetAsBoolean: Boolean;
var
  S: string;
begin
  S := GetAsString;
  Result := (Length(S) > 0) and (S[1] in ['T', 't', 'Y', 'y']);
end;

function TdvStringSetting.GetAsDateTime: TDateTime;
begin
  Result := StrToDateTime(GetAsString);
end;

function TdvStringSetting.GetAsFloat: Double;
begin
  Result := StrToFloat(GetAsString);
end;

function TdvStringSetting.GetAsInteger: Longint;
begin
  Result := StrToInt(GetAsString);
end;

function TdvStringSetting.GetAsString: string;
begin
  if not GetValue(Result) then Result := '';
end;

function TdvStringSetting.GetAsVariant: Variant;
var
  S: string;
begin
  if GetValue(S) then Result := S else Result := Null;
end;

function TdvStringSetting.GetDefaultValueAsString: String;
begin
  Result := FDefaultValue;
end;

function TdvStringSetting.GetValue(var Value: string): Boolean;
begin
  Value  := FValue;
  Result := True;
end;

procedure TdvStringSetting.LoadFromRegIni(aRegIni: TRegistryIniFile;
  const aSection: String);
begin
  inherited LoadFromRegIni( aRegIni, aSection );

  Value := aRegIni.ReadString( aSection, Identifier, DefaultValue );
end;

procedure TdvStringSetting.SaveToRegIni(aRegIni: TRegistryIniFile;
  const aSection: String);
begin
  inherited SaveToRegIni( aRegIni, aSection );

  aRegIni.WriteString( aSection, Identifier, Value );
end;

procedure TdvStringSetting.SetAsBoolean(const Value: Boolean);
const
  Values: array[Boolean] of string[1] = ('F', 'T');
begin
  SetAsString(Values[Value]);
end;

procedure TdvStringSetting.SetAsDateTime(const Value: TDateTime);
begin
  SetAsString(DateTimeToStr(Value));
end;

procedure TdvStringSetting.SetAsFloat(const Value: Double);
begin
  SetAsString(FloatToStr(Value));
end;

procedure TdvStringSetting.SetAsInteger(const Value: Integer);
begin
  SetAsString(IntToStr(Value));
end;

procedure TdvStringSetting.SetAsString(const aValue: string);
begin
  FValue := aValue;
end;

procedure TdvStringSetting.SetVarValue(const aValue: Variant);
begin
  SetAsString(aValue);
end;



What does it do ?

Since we had the skeleton of the TdvSetting in place, the only thing we needed to do was to override the necessary methods and implement our own features. As you can see, we added approximately the same code as can be found in the TStringField in the VCL.

The only additional code is the implementation of the SaveToRegIni and LoadFromRegIni methods. These methods will actually load and save the value of the Setting to the Registry. Additionally when loading the value we will be using the Default value if the setting can't be found in the regsitry. The Section where we will read / write the value can be supplied to the method and we will be using the identifier (name) of the setting as the key

Creating the TdvSettings class

The explanation

Well, now that we can make different type of Setting objects, we also need some kind of container to hold them. For example our application might have quite a few settings : PrintInColor, CheckForUpdates, AutoConnect, ... but we will need some way to access them. As mentioned before I created the TdvSettings class as a simple TObjectList. It will hold a reference to the individual TdvSetting objects and we will be able to access them

The Code
  TdvSettings = class( TObjectList )
  private
    FRootKey: String;
  protected
    procedure CreateSettings; virtual;

    function GetItems(Index: Integer): TdvSetting;
    procedure SetItems(Index: Integer; ASetting: TdvSetting);
  public
    constructor Create( const aRootKey : String );

    function Add(ASetting: TdvSetting): Integer;
    function Extract(Item: TdvSetting): TdvSetting;
    function Remove(ASetting: TdvSetting): Integer;
    function IndexOf(ASetting: TdvSetting): Integer;
    function First: TdvSetting;
    function Last: TdvSetting;
    function SettingByIdentifier( const aIdentifier : String ) : TdvSetting;

    procedure LoadFromRegistry;
    procedure SaveToRegistry;

    procedure Insert(Index: Integer; ASetting: TdvSetting);
    property Items[Index: Integer]: TdvSetting read GetItems write SetItems; default;
    property RootKey : String read FRootKey write FRootKey;
  end;

  ...

{ TdvSettings }

function TdvSettings.Add(ASetting: TdvSetting): Integer;
begin
  Result := inherited Add(ASetting);
end;

constructor TdvSettings.Create(const aRootKey: String);
begin
  inherited Create( True );
  FRootKey := aRootKey;
  CreateSettings;
  // Read the settings from the Registry once the Settings object list has
  // been initialised.
  LoadFromRegistry;
end;

procedure TdvSettings.CreateSettings;
begin

end;

function TdvSettings.Extract(Item: TdvSetting): TdvSetting;
begin
  Result := TdvSetting(inherited Extract(Item));
end;

function TdvSettings.First: TdvSetting;
begin
  Result := TdvSetting(inherited First);
end;

function TdvSettings.GetItems(Index: Integer): TdvSetting;
begin
  Result := TdvSetting(inherited Items[Index]);
end;

function TdvSettings.IndexOf(ASetting: TdvSetting): Integer;
begin
  Result := inherited IndexOf(aSetting);
end;

procedure TdvSettings.Insert(Index: Integer; ASetting: TdvSetting);
begin
  inherited Insert(Index, aSetting);
end;

function TdvSettings.Last: TdvSetting;
begin
  Result := TdvSetting(inherited Last);
end;

procedure TdvSettings.LoadFromRegistry;
var
  lIndex   : Integer;
  lSetting : TdvSetting;
  lRegIni  : TRegistryIniFile;
begin
  lRegIni := TRegistryIniFile.Create('');
  try
    for lIndex := 0 to Pred( Count ) do
    begin
      lSetting := Items[ lIndex ];
      lSetting.LoadFromRegIni( lRegIni, RootKey );
    end;
  finally
    FreeAndNil( lRegIni );
  end;
end;

function TdvSettings.Remove(ASetting: TdvSetting): Integer;
begin
  Result := inherited Remove(aSetting);
end;

procedure TdvSettings.SaveToRegistry;
var
  lIndex   : Integer;
  lSetting : TdvSetting;
  lRegIni  : TRegistryIniFile;
begin
  lRegIni := TRegistryIniFile.Create('');
  try
    for lIndex := 0 to Pred( Count ) do
    begin
      lSetting := Items[ lIndex ];
      lSetting.SaveToRegIni( lRegIni, RootKey );
    end;
  finally
    FreeAndNil( lRegIni );
  end;
end;

procedure TdvSettings.SetItems(Index: Integer; ASetting: TdvSetting);
begin
  inherited Items[Index] := aSetting;
end;

function TdvSettings.SettingByIdentifier(
  const aIdentifier: String): TdvSetting;
var
  lcv : Integer;
begin
  Result := Nil;

  for lcv := 0 to Pred( Count ) do
  begin
    if ( Items[ lcv ].Identifier = aIdentifier ) then
    begin
      Result := Items[ lcv ];
      Break;
    end;
  end;
end;


What does it do ?

Well, it is simply a container for a number of TdvSetting objects. It has some basic code which will allow us to access the individual TdvSetting objects using their index or their Identifier (name). We will be able to set the RootKey of the TdvSettings object and that is the base key which will be used when loading / storing the individual TdvSetting objects.

You willl notice that I added a CreateSettings method which is currently empty. The goal is to implement this in descendant classes. At the TdvSettings level we don't yet know which individual settings we will need, how they are called and what type they are ... Yet, I wanted to be able to create those settings from within the base class and once that is done read the settings if necessary.

In my TdvMyApplicationSettings I will override this method and add the necessary code to set up the individual TdvSetting objects I need for my applicaiton.

What's next ?

Currently we have a basic skeleton in place and we are able to continue from here. If you're up for a challenge, you might want to create your own TdvSetting descendants. I noticed I needed one for Integer values, DataTime values and Boolean values, so you could try to implement those.

As I mentioned at the end of the previous article, this isn't the only way, nor THE way to solve this problem, and I actually did try a few other approaches as well. It did take me a few iterations to get to this stage. For example, initial versions had the actual code for creating the TRegistryIniFile class inside the individual TdvSetting objects.

Quickly I noticed though that I had the exact same code in all my TdvSetting descendants LoadFromRegIni and SaveToRegIni methods so I started thinking again. I thought that constructing & destroying the TRegistryIniFile might be something I don't want to execute for the 20 individual application settings so I ended up refactoring the code to it's current status

Feedback

I'm quite sure the code could be simplified and maybe improved, and I do welcome all suggestions and feedback. So feel free to post all comments on this blog and I'll try to respond ASAP. Maybe your feedback will shape the next article in this series ...

Comments

  • 1

    You keep commenting on how similar this setup is to TField, and it really is.  So why not just use TFields and create a bridge class that allows you to save from a TDataset to the Registry?

    written by Mason Wheeler on 30/03/2010
  • 2

    Hi,

    First of all, I fixed the little display thingie.  Apparently I forgot a closing tag somewhere.

    Well, actually I didn’t want all the TDataSet related code just to load / save something to the registry.  Its a good idea, but it just adds way too much code I don’t really need.

    Although, I must say ... for a while I was thinking of doing everything using a TClientDataSet or another TDataSet descendant.  It would work, but it was easier to start from something clean than to adapt the whole TDataSet to write it’s info to the Registry or an INI file.

    Regards,

    Stefaan

    written by Stefaan Lesage on 30/03/2010
  • 3

    It is pretty handy to store the settings in the actual database as well (provided that your application actually use a database).  We use this for some of our applications, allowing the users to work from multiple locations without losing their settings.

    In that context, having a setting system that is truly configurable (ini, registry, encrypted stream, or database) would be a bit more work, for a lot more flexibility.

    written by Lars Fosdal on 04/04/2010
  • 4

    Hi Lars,

    Storing some settings in a database is a good idea indeed.  A database isn’t the ideal solution for everything though.  You might want to store connection settings which you will read at application launch to know which DB to connect with.

    But you are right, a configurable setting system might be a better idea ... and if our current code is descent enough we can easily make it configurable too.

    Regards,


    Stefaan

    written by Stefaan Lesage on 05/04/2010
  • Commenting is not available in this weblog entry.

    Archive