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- posted in Software Development Windows Delphi
- archive
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
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.
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
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;
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