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 3)

written by Stefaan Lesage on 30/03/2010

Now that we have a basic idea of what we want to achieve and how we could do it, it's time to write some code and create some classes.

Introduction

So, basically we need a class / object which we can use to read and write some application settings from and to the Windows Registry. Sounds pretty straightforward ... but we did some additional thinking and found out that we might need to add some stuff in the future.

Basic Requirements of the code

Compatibility with Delphi 7

Although quite a few new features were added to the language in the last years, we won't be using them 'yet'. Our goal is that the code should compile / work under Delphi 7.

You may be asking yourselves "Who going though all that trouble for an old Delphi version ?", well I noticed that even today some of my clients still have older projects which get compiled in Delphi 7. In later articles I might show you how to do it using some newer techniques, but for now lets just stick with something that will compile under Delphi 7

Not limited to the Windows Registry

Although we will be writing the code based on using the Windows Registry to Load and Save our data. We want an easy way to adapt our code for some other things like storing the data in XML or an Ini file. After all, we don't know what the future will bring yet. We might be able to build applications for Windows Mobile, the Mac, iPhone or even iPad in the future (Would that be nice), and the Windows Registry might not be available on those devices.

For now we will focus on the Windows Registry, but as you notice it is a good idea to keep a few other possibilities ready. In the end, the only thing we care about is that we can store / load some settings. How or where these settings are stored isn't all that important, it just needs to get done !

Additional Things

So far, we know we will need something to hold a set of settings. We need to be able to load and save those settings. We will probably be supplying a name for each individual setting, maybe even a default value and a description. We need to be able to store Integers, Strings, but who knows even Passwords, Dates, ...

Lets get Coding !

... well almost ...

Well, ... actually before we start coding we could take a look at how some of these things get solved in the VCL. Of course, we could do all the coding ourselves, but it might be a good idea to let our new classes inherit from some existing classes. Since we need a list of things, you might want to take a look at the TList for example.

In my case, I knew I want to have a Setting which I will use to store Strings, one to store Integers and another one to store Booleans. Once I had those, I quickly noticed I wanted some others for DateTime values and quite a few other things as well. I actually ended up doing something quite similar to TField and TIntegerField, TStringField, ...

So, now that I know I will be using different types of Setting objects and I want to keep a list of those Setting Objects, I quickly decided that the TObjectList was a very good class to start with.

Creating the TdvSetting class

The explanation

Basically I need an object with a few properties like an Identifier (or Name, Caption), a Description (or Hint) and of course a Value. I will need to be able to read the value from the Registry and write it to the registry. Additionally when reading the value from the Registry I want to check if there is already something in the registry for the setting, and if nothing is found, the Default value should be used.

Just as with the TField and TStringField, I want to be able to get the Value of the TdvSetting as a String or as a Variant, so I added that code as well. Additionally I want to set the value of the TdvSetting as well. In the end, the descendant classes will implement most of this, but like with the TField in the VCL, I added some code which will raise an exception if a descendant class doens't implement a specific accessor.

This might sound a bit complex, but lets compare it with the TField and TStringField again. With a TStringField, you can set the value using aField.Value := theValue or with aField.AsString := theValue. Both things will work, but if aField is in instance of TField instead of TStringField an exception will be raised. What I did was implement that functionality as well.

For now we will focus on the Windows Registry, but as you notice it is a good idea to keep a few other possibilities ready. In the end, the only thing we care about is that we can store / load some settings. How or where these settings are stored isn't all that important, it just needs to get done !

The Code
  TdvSetting = class( TObject )
  private
    FValue        : Variant;
    FDefaultValue : Variant;
    FIdentifier   : String;
    FCaption      : String;
    procedure SetCaption(const Value: String);
    procedure SetVisible(const Value: Boolean);
  protected
    function GetAsBoolean: Boolean; virtual;
    function GetAsDateTime: TDateTime; virtual;
    function GetAsFloat: Double; virtual;
    function GetAsInteger: Longint; virtual;
    function GetAsString: string; virtual;
    function GetAsVariant: Variant; virtual;

    procedure SetAsBoolean(const Value: Boolean); virtual;
    procedure SetAsDateTime(const Value: TDateTime); virtual;
    procedure SetHint(const Value: String);
    procedure SetAsFloat(const Value: Double); virtual;
    procedure SetIdentifier(const Value: String);
    procedure SetAsInteger(const Value: Longint); virtual;
    procedure SetAsString(const Value: string); virtual;
    procedure SetAsVariant(const Value: Variant); virtual;
  protected
    function AccessError(const TypeName: string): Exception; dynamic;
    procedure SetVarValue( const Value : Variant ); virtual;
  public
    Constructor Create( const aIdentifier, aCaption : String;
                        const aDefaultValue : Variant ); virtual;

    destructor Destroy; override;

    procedure SaveToRegIni  (       aRegIni  : TRegistryIniFile;
                              const aSection : String ); virtual;
    procedure LoadFromRegIni(       aRegIni  : TRegistryIniFile;
                              const aSection : String ); virtual;

    procedure Clear; virtual;

    property DefaultValue  : Variant       read  FDefaultValue;
    property AsBoolean     : Boolean       read  GetAsBoolean
                                           write SetAsBoolean;
    property AsDateTime    : TDateTime     read  GetAsDateTime
                                           write SetAsDateTime;
    property AsFloat       : Double        read  GetAsFloat
                                           write SetAsFloat;
    property AsInteger     : Longint       read  GetAsInteger
                                           write SetAsInteger;
    property AsString      : string        read  GetAsString
                                           write SetAsString;
    property AsVariant     : Variant       read  GetAsVariant
                                           write SetAsVariant;

    property Identifier  : String          read  FIdentifier
                                           write SetIdentifier;
    property Caption     : String          read  FCaption
                                           write SetCaption;
    property Value       : Variant         read  GetAsVariant
                                           write SetAsVariant;
  end;

...

function TdvSetting.AccessError(const TypeName: string): Exception;
resourcestring
  SSettingAccessError = 'Cannot access Setting ''%s'' (%s) as type %s';
begin
  Result := Exception.CreateResFmt( @SSettingAccessError,
                                    [ Identifier, Caption, TypeName ] );
end;

procedure TdvSetting.Clear;
begin
  FValue := Null;  
end;

constructor TdvSetting.Create(const aIdentifier, aCaption: String;
  const aDefaultValue: Variant);
begin
  Create( aIdentifier, aCaption, aCaption, True, aDefaultValue );
end;

function TdvSetting.GetAsBoolean: Boolean;
begin
  raise AccessError('Boolean'); { Do not localize }
end;

function TdvSetting.GetAsDateTime: TDateTime;
begin
  raise AccessError('DateTime'); { Do not localize }
end;

function TdvSetting.GetAsFloat: Double;
begin
  raise AccessError('Float'); { Do not localize }
end;

function TdvSetting.GetAsInteger: Longint;
begin
  raise AccessError('Integer'); { Do not localize }
end;

function TdvSetting.GetAsString: string;
begin
  Result := ClassName;
end;

function TdvSetting.GetAsVariant: Variant;
begin
  raise AccessError('Variant'); { Do not localize }
end;

procedure TdvSetting.LoadFromRegIni(aRegIni: TRegistryIniFile;
  const aSection: String);
begin
  Assert( Assigned( aRegIni ), 'The aRegIni parameter should contain a TRegIni Instance' );
end;

procedure TdvSetting.SaveToRegIni(aRegIni: TRegistryIniFile;
  const aSection: String);
begin
  Assert( Assigned( aRegIni ), 'The aRegIni parameter should contain a TRegIni Instance' );
end;

procedure TdvSetting.SetAsBoolean(const Value: Boolean);
begin
  raise AccessError('Boolean'); { Do not localize }
end;

procedure TdvSetting.SetAsDateTime(const Value: TDateTime);
begin
  raise AccessError('DateTime'); { Do not localize }
end;

procedure TdvSetting.SetAsFloat(const Value: Double);
begin
  raise AccessError('Float'); { Do not localize }
end;

procedure TdvSetting.SetAsInteger(const Value: Longint);
begin
  raise AccessError('Integer'); { Do not localize }
end;

procedure TdvSetting.SetAsString(const Value: string);
begin
  raise AccessError('String'); { Do not localize }
end;

procedure TdvSetting.SetAsVariant(const Value: Variant);
begin
  if ( VarIsNull( Value ) ) then
  begin
    Clear;
  end
  else
  begin
    SetVarValue( Value );
  end;
end;

procedure TdvSetting.SetCaption(const Value: String);
begin
  FCaption := Value;
end;

procedure TdvSetting.SetHint(const Value: String);
begin
  FHint := Value;
end;

procedure TdvSetting.SetIdentifier(const Value: String);
begin
  FIdentifier := Value;
end;

procedure TdvSetting.SetVarValue(const Value: Variant);
begin
  raise AccessError('Variant'); { Do not localize }
end;

What does it do ?

Actually this piece of code doesn't do all that much. It just provides us with a base class we can now use as an ancestor for our other classes. Basically we have some ErrorHandling and a skeleton for our specific Setting classes.

Continued in Part 4

For some reason I had problems fitting everything into one single post, so I had to split it up an two parts. Go ahead and read the rest in part 4.

Comments

  • 1

    Not directly related to your post subject, but a general comment on the use of “do not localise” comments.

    I come across this quite often and it frustrates me…  why adopt a convention that is a) fallible (what if someone forgets the comment) b) cumbersome (when the string is part of a complex expression that is not easily “1-line” commented, or where there are other comments: “blah blah blah explaining the code blah blah… oh, and do not localise the literal string” c) that localisation tools such as “string scanners” won’t (generally speaking) be able to take account of

    The language provides a means to identify non-localised strings as opposed to localised strings:  that is constants vs resource strings, e.g:

    procedure TdvSetting.SetAsDateTime(const Value: TDateTime);
    begin
      raise AccessError(‘DateTime’); { Do not localize }
    end;

    vs

    procedure TdvSetting.SetAsDateTime(const Value: TDateTime);
    const
      STR_DATETIME = ‘DateTime’;
    begin
      raise AccessError(STR_DATETIME); { Do not localize }
    end;

    Apart from adhering to the “good idea” of not using literals in code, this could centralise your strings (in this case, make the const a unit level declaration which can then be shared by both getter/setter).  Localisation string scanner tools can usually be configured to ignore strings in constant declarations.

    This then leaves/identifies literal strings as potential (suspect) candidates for localisation, resource strings as strings that clearly ARE to be localised and constants as strings that are specifically NOT to be localised (they are “constant”, after all).

    :)

    Just my 2 cents on that subject.

    written by Jolyon Smith on 30/03/2010
  • 2

    Hi,

    You’re probably right here.  I actually used to use String constants for that purpose in the past, but I think I started overdoing it, which in the end made my code less ‘readable’ for the human eye.

    In fact at some point I had a separate unit with some string constants, since some of them were getting used in quite a few places.  But people complained that this resulted in ‘less’ readable code.  Additionally you had no idea where the text would get updated if you changed the constant.

    Defining a constant which is local to the procedure might be a better option indeed.

    Regards,

    Stefaan

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

    I agree with Jolyon, but believe that he meant to write:

    procedure TdvSetting.SetAsDateTime(const Value: TDateTime);
    resourcestring
      STR_DATETIME = ‘DateTime’;
    begin
      raise AccessError(STR_DATETIME);
    end;

    written by Magne Rekdal on 07/04/2010
  • 4

    Hi Magne,

    Actually Jolyon wanted to do exactly the opposite.  If we don’t want those strings to get translated, we certainly do NOT want them to be Resourcestrings.

    Regards,


    Stefaan

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

    Archive