Reputation: 27266
I'd like to make a record as an object's property. The problem is that when I change one of the fields of this record, the object isn't aware of the change.
type
TMyRecord = record
SomeField: Integer;
end;
TMyObject = class(TObject)
private
FSomeRecord: TMyRecord;
procedure SetSomeRecord(const Value: TMyRecord);
public
property SomeRecord: TMyRecord read FSomeRecord write SetSomeRecord;
end;
And then if I do...
MyObject.SomeRecord.SomeField:= 5;
...will not work.
So how do I make the property setting procedure 'catch' when one of the record's fields is written to? Perhaps some trick in how to declare the record?
More Info
My goal is to avoid having to create a TObject
or TPersistent
with an OnChange
event (such as the TFont
or TStringList
). I'm more than familiar with using objects for this, but in an attempt to cleanup my code a little, I'm seeing if I can use a Record instead. I just need to make sure my record property setter can be called properly when I set one of the record's fields.
Upvotes: 13
Views: 11061
Reputation: 1670
this is an alternative to @SourceMaid's answer.
You could use a record (not a pointer to a record) inside your object and have a read only property which returns a pointer to your record.
the class:
type
TMyRecord = record
I:Integer;
end;
PMyRecord = ^TMyRecord;
TMyObject = class
private
FMyRecord:TMyRecord;
function GetMyRecordPointer: PMyRecord;
public
property MyRecord: PMyRecord read GetMyRecordPointer;
end;
the getter:
function TMyObject.GetMyRecordPointer: PMyRecord;
begin
result := @FMyRecord;
end;
usage:
o := TMyObject.Create;
o.MyRecord.I := 42;
ShowMessage(o.MyRecord.I.ToString);
o.MyRecord.I := 23;
ShowMessage(o.MyRecord.I.ToString);
o.Free;
you dont need a setter because you get a reference and work with. that mean that you cannot change the entire record by assigning a new one.
but you can manipulate the elements of the record directly without getting the error "Left side cannot be assigned to"
.
Upvotes: 3
Reputation: 6053
How about using a TObject instead of a Record?
type
TMyProperties = class(TObject)
SomeField: Integer;
end;
TMyObject = class(TObject)
private
FMyProperties: TMyProperties;
public
constructor Create;
destructor Destroy; override;
property MyProperties: TMyRecord read FMyProperties;
end;
implementation
constructor TMyObject.Create;
begin
FMyProperties := TMyProperties.Create;
end;
destructor TMyObject.Destroy;
begin
FMyProperties.Free;
end;
You can now read and set the properties of TMyProperties like this:
MyObject.MyProperties.SomeField := 1;
x := MyObject.MyProperties.SomeField;
Using this method, you don't need to individually get/set the values to/from the record. If you need to catch changes in FMyProperties, you can add a 'set' procedure in the property declaration.
Upvotes: 6
Reputation: 1570
Why not make the setter/getter part of the record?
TMyRecord = record
fFirstname: string;
procedure SetFirstName(AValue: String);
property
Firstname : string read fFirstname write SetFirstName;
end;
TMyClass = class(TObject)
MyRecord : TMyRecord;
end;
procedure TMyRecord.SetFirstName(AValue: String);
begin
// do extra checking here
fFirstname := AValue;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
MyClass: TMyClass;
begin
MyClass := TMyClass.Create;
try
MyClass.MyRecord.Firstname := 'John';
showmessage(MyClass.MyRecord.Firstname);
finally
MyClass.Free;
end;
end;
Upvotes: 5
Reputation: 13454
Ultimately you will want to access the record's fields, yet as you propose, a record is often a suitable abstraction choice within a class. A class can neatly access the properties of a record as follows:
type
TMyRec = record
SomeRecInteger: integer;
SomeRecString: string;
end;
TMyClass = class(TObject)
private
FMyRec: TMyRec;
procedure SetSomeString(const AString: string);
public
property SomeInteger: integer read FMyRec.SomeRecInteger write FMyRec.SomeRecInteger;
property SomeString: string read FMyRec.SomeRecString write SetSomeString;
end;
procedure TMyClass.SetSomeRecString(const AString: string);
begin
If AString <> SomeString then
begin
// do something special if SomeRecString property is set
FMyRec.SomeRecString := AString;
end;
end;
Note:
Hope this helps.
Upvotes: 12
Reputation: 612794
Consider this line:
MyObject.SomeRecord.SomeField := NewValue;
This is in fact a compile error:
[DCC Error]: E2064 Left side cannot be assigned to
Your actual code is probably something like this:
MyRecord := MyObject.SomeRecord;
MyRecord.SomeField := NewValue;
What happens here is that you copy the value of the record type to the local variable MyRecord
. You then modify a field of this local copy. That does not modify the record held in MyObject. To do that you need to invoke the property setter.
MyRecord := MyObject.SomeRecord;
MyRecord.SomeField := NewValue;
MyObject.SomeRecord := MyRecord;
Or switch to using a reference type, i.e. a class, rather than a record.
To summarise, the problem with your current code is that SetSomeRecord is not called and instead you are modifying a copy of the record. And this is because a record is a value type as opposed to being a reference type.
Upvotes: 15
Reputation: 473
You're passing the record by value, so a copy of the record is stored by the object. From that point on there are effectively two objects; the original one and the copy held by the object. Changing one won't change the other.
You need to pass the record by reference.
type
TMyRecord = record
SomeField: Integer;
end;
PMyRecord = ^TMyRecord;
TMyObject = class(TObject)
private
FSomeRecord: PMyRecord;
procedure SetSomeRecord(const Value: PMyRecord);
public
property SomeRecord: PMyRecord read FSomeRecord write SetSomeRecord;
end;
Upvotes: 4