Delphi RIO - have defined a class called TBizObj. One of the properties has to do with DUNS numbers. DUNS numbers are sometimes '0' padded on the left to be exactly 9 characters long, so I have a property called SiteDUNS9 (based off of fSiteDUNS9). The calling program sets the SiteDUNS9 property, but I don't the caller to have to worry about if the DUNS is 9 characters or not, I will handle that in getter/setter properties.
When I define my property to call this function, I get an error 'Incompatible types'. Everything is string... no other types involved. Here is the relevant portion of code:
type
TBizObj = class(TObject)
private
...
fSiteDUNS9: string;
...
function FixDunsLength9(DUNS:string) :string;
published
...
property SiteDUNS9: string read fSiteDUNS9 write FixDunsLength9;
end; // End of the tBizObj Class;
implementation
...
function TBizObj.FixDunsLength9(DUNS:string):string;
begin
// This is a setter function for the DUNS9 routine
result := glib_LeftPad(DUNS, 9, '0');
end;
I have followed the examples on the Embaracadero site but still cannot determine what I am doing wrong.
http://docwiki.embarcadero.com/RADStudio/Rio/en/Properties_(Delphi)
If I change my property definition to
property SiteDUNS9: string read fSiteDUNS9 write fSiteDUNS9;
then my program compiles correctly.
You need to use a procedure instead of a function for a property setter. I would leave the existing function as-is, in case you need it for other purposes, and define a separate procedure for the setter:
type
TBizObj = class(TObject)
private
...
fSiteDUNS9: string;
...
function FixDunsLength9(const DUNS: string): string;
procedure SetSiteDUNS9(const Value: string);
published
...
property SiteDUNS9: string read fSiteDUNS9 write SetSiteDUNS9;
end;
// End of the tBizObj Class;
implementation
...
function TBizObj.FixDunsLength9(const DUNS: string): string;
begin
Result := glib_LeftPad(DUNS, 9, '0');
end;
procedure TBizObj.SetSiteDUNS9(const Value: string);
var
NewValue: string;
begin
NewValue := FixDunsLength9(Value);
if fSiteDUNS9 <> NewValue then
begin
fSiteDUNS9 := NewValue;
...
end;
end;
You need to declare a procedure for the setter method. As the Property Access help says:
write fieldOrMethod
In a write specifier, if fieldOrMethod is a method, it must be a
procedure that takes a single value or const parameter of the same
type as the property (or more, if it is an array property or indexed
property).
In your case you can then write a setter like this:
type
TBizObj = class(TObject)
private
FSiteDUNS9: string;
procedure FixDunsLength9(const DUNS: string);
published
property SiteDUNS9: string read FSiteDUNS9 write FixDunsLength9;
end;
implementation
procedure TBizObj.FixDunsLength9(const DUNS: string);
begin
if DUNS <> FSiteDUNS9 then
begin
DoSomeExtraStuff;
FSiteDUNS9 := DUNS;
end;
end;
But following the naming conventions I would recommend you to name your setter like SetSiteDUNS9 and the parameter call Value.
Related
I have a class which stores generic items in an array. These items are accessed by a default array property:
TMyList<TData> = class
private
items: array of TItem<TData>;
public
function get(position: integer): TData;
procedure edit(position: integer; data: TData);
property Values[position: integer]: TData read get write edit; default;
end;
implementation
function TMyList<TData>.get(position: integer): TData;
begin
result:= items[position];
end;
procedure TMyList<TData>.edit(position: integer; data: TData);
var
item: TItem<TData>;
begin
items[position]:= item;
end;
end;
In this case the items I am storing are all of the type TTest which also has its own properties:
TTest = record
private
FTest: string;
procedure setFTest(const Value: string);
public
property Test: string read FTest write setFTest;
end;
implementation
procedure TColouredPoint.setFTest(const Value: String);
begin
FTest:= Value;
end;
end;
I want to be able to change the value of FTest for an instance of TTest like this:
var
points: TMyList<TTest>;
...
points[index].Test:= 'test';
but this doesn't do anything. There is no error message but the value of points[index].Test doesn't change.
Instead I have to do this:
var
points: TMyList<TTest>;
temp: TTest;
...
temp:= points[index];
temp.Test:= 'test';
points[index]:= temp;
Why does the first version not work?
Why does the first version not work?
Consider this code
points[index].Test := 'test';
The indexed property is converted by the compiler into the a function call and so the compiler effectively compiles this:
points.get(index).Text := 'test';
Now, points.get(index) returns a copy of the TTest value. Since you don't assign that to anything, the compiler introduces a local variable to hold the return value.
So your code becomes, in effect:
var
tmp: TTest;
...
tmp := points.get(index);
tmp.Text := 'test';
That's the last thing that is ever done with tmp, and so the modifications that you make to tmp.Text are simply discarded leaving the underlying object untouched.
This issue is pretty hard to get around when working with value types.
The generic Delphi TList<T> collection allows you direct access to the underlying array, which allows you to operate on the stored values directly.
Another approach is to use a reference rather than a value. One simple way to achieve that is to use a T that is a class rather than a record, i.e. a reference type rather than a value type.
It is because TTest is a record. The getter of the list returns a copy of the actual record and that is what you are changing. It should work when you declare TTest as a class, but then you have to take care of creating and destroying it.
Based on some code like the following:
TListWrapper = class
strict private
FList: TStringList;
function GetItem(index: Integer): TObject; overload;
function GetItem(index: string): TObject; overload;
public
property Items[index: Integer]: TObject read GetItem; default;
property Items[index: string]: TObject read GetItem; default;
end;
I want to write a piece of code which get the value of the string indexed property using RTTI. Something like this:
var
MyList: TListWrapper;
InstanceType: TRttiInstanceType;
IndexedProperty: TRttiIndexedProperty;
begin
MyList:=TListWrapper.Create;
LContext:=TRttiContext.Create;
InstanceType:=LContext.GetType(MyList.ClassType) as TRttiInstanceType;
for IndexedProperty in InstanceType.GetIndexedProperties do
if IndexedProperty.Name.ToLower = 'items' then
begin
//There are two indexed properties with name 'items'
end;
LContext.Free;
MyList.Free;
end;
Question: How can I know which of the indexed property has the string index so that I can then get the value like this?
IndexedProperty.GetValue(MyList, ['some_string_index']);
Note: I am using Delphi 10.2.3 (Tokyo)
You should be able to use the read method parameters. Something like this:
readingMethod := IndexedProperty.ReadMethod;
readMethodParameters := readingMethod.GetParameters;
if readMethodParameters[0].ParamType.TypeKind = tkUString then
// We have the string version
You should obviously check that the readMethod is assigned and that the number of parameters is greater than zero, etc.
From Remy:
In this case, the string type is tkUString (UnicodeString) and it has been this way since Delphi 2009.
is it possible to have different kind of results when declaring property in delphi class?
Example:
property month: string read monthGet(string) write monthSet(integer);
In the example, I want, with the property month, that when I :
READ, I get a string; SET, I set an integer;
The closest you can get is to use Operator Overloading but the Getter/Setter must be the same type. There is no way to change that.
program so_26672343;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils;
type
TMonth = record
private
FValue: Integer;
procedure SetValue( const Value: Integer );
public
class operator implicit( a: TMonth ): string;
class operator implicit( a: Integer ): TMonth;
property Value: Integer read FValue write SetValue;
end;
TFoo = class
private
FMonth: TMonth;
public
property Month: TMonth read FMonth write FMonth;
end;
{ TMonth }
class operator TMonth.implicit( a: TMonth ): string;
begin
Result := 'Month ' + IntToStr( a.Value );
end;
class operator TMonth.implicit( a: Integer ): TMonth;
begin
Result.FValue := a;
end;
procedure TMonth.SetValue( const Value: Integer );
begin
FValue := Value;
end;
procedure Main;
var
LFoo: TFoo;
LMonthInt: Integer;
LMonthStr: string;
begin
LFoo := TFoo.Create;
try
LMonthInt := 4;
LFoo.Month := LMonthInt;
LMonthStr := LFoo.Month;
finally
LFoo.Free;
end;
end;
begin
try
Main;
except
on E: Exception do
Writeln( E.ClassName, ': ', E.Message );
end;
end.
That is not possible. But properties do not have to correspond to internal storage directly, so you can do:
private
FMonth: Integer;
function GetMonthName: string;
...
property Month: Integer read FMonth write FMonth;
property MonthName: string read GetMonthName;
...
procedure TMyClass.GetMonthName: string;
begin
// code that finds name that corresponds to value of FMonth and returns it in Result.
end;
In other words, you'll have to use two properties, one write-only (or normal), one read-only.
You can't directly do that in Delphi.
What you can do is having a "casting property" like:
private
//...
intMonth: integer
//...
public
//...
property StrMonth: string read GetStrMonth write SetStrMonth;
property IntMonth: integer read intMonth write intMonth;
//...
end;
function YourClass.GetStrMonth: string;
begin
case intMonth of
1: Result := "January";
//...
end;
end;
procedure YourClass.SetStrMonth(Value: string);
begin
if StrMonth = "January" then
intMonth := 1;
//...
end;
end;
There's no way to do that for a property. A property has a single type.
The obvious way to achieve you goal is to have getter and setter functions that you use directly.
function GetMonth: string;
procedure SetMonth(Value: Integer);
You might decide to make the type part of the name to reduce confusion in the calling code. Say GetMonthStr and SetMonthOrd.
You could expose these functions as two separate properties. One read only, the other write only.
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.
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.
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:
The direct access to the record property SomeRecInteger
The use of SetSomeRecString to implement some special processing on this field only.
Hope this helps.
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.
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;
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;
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".
type
TStaticArray = array[1..10] of integer;
TDynamicArray = array of integer;
TMyClass = class(TObject)
private
FStaticArray: TStaticArray;
FDynamicArray: TDynamicArray;
published
property staticArray: TStaticArray read FStaticArray write FStaticArray; //compiler chokes on this
property dynamicArray: TDynamicArray read FDynamicArray write FDynamicArray; //compiler accepts this one just fine
end;
What's going on here? A static array gives the error, "published property 'staticArray' cannot be of type ARRAY" but dynamic arrays are just fine? I'm confused. Anyone know the reasoning behind this, and how I can work around it? (And no, I don't want to redeclare all my static arrays as dynamic. They're the size they are for a reason.)
Published declaration tells the compiler to store information in the virtual method table. Only certain kinds of information can be stored.
The type of a published property cannot be a pointer, record, or array. If it is a set type, it must be small enough to be stored in an integer.
(O'REILLY, DELPHÄ° IN A NUTSHELL)
You have to have getters and setters. Under D2009 (didn't check other versions), the parameters to the getters/setters can't, for some reason, be const. ?
This works fine under D2009:
type
TMyArray = array[0..20] of string;
type
TMyClass=class(TObject)
private
FMyArray: TMyArray;
function GetItem(Index: Integer): String;
procedure SetItem(Index: Integer; Value: string);
public
property Items[Index: Integer]: string read GetItem write SetItem;
end;
implementation
function TMyClass.GetItem(Index: Integer): string;
begin
Result := '';
if (Index > -1) and (Index < Length(FMyArray)) then
Result := FMyArray[Index];
end;
procedure TMyClass.SetItem(Index: Integer; Value: string);
begin
if (Index > -1) and (Index < Length(FMyArray)) then
FMyArray[Index] := Value;
end;
NOTE: I would not typically just ignore Index values out of range, obviously. This was a quick example of how to make static array properties in a class definition; IOW, it's a compilable example only.
The reason why you can publish a dynamic array property, is that dynamic arrays is implemented as references, or 'implicitly pointer'. They work more like strings, really.
The reason why you can't publish a static array, I don't know. It's just the way it's made, I guess..
For more details on how dynamic arrays work, take a look at DrBobs site
TMyClass = class(TObject)
private
FStaticArray: TStaticArray;
FIdx: Integer;
function GetFoo(Index: Integer): Integer;
procedure SetFoo(Index: Integer; Value: Integer);
public
property Foo[Index: Integer] : Integer read GetFoo write SetFoo;
published
property Idx: Integer read fidx write fidx;
property AFoo: Integer read GetAFoo writte SetAFoo;
end;
implementation
function TMyClass.GetAFoo: Integer;
begin
result := FStaticArray[FIdx];
end;
procedure TMyClass.SetAFoo(Value: Integer);
begin
FStaticArray[FIdx] := Value;
end;
Array properties cannot be published.
So the following code does not work. The work around probably is to make it public.
TMyClass = class(TObject)
private
FStaticArray: TStaticArray;
function GetFoo(Index: Integer): Integer;
procedure SetFoo(Index: Integer; Value: Integer);
public
property Foo[Index: Integer] : Integer read GetFoo write SetFoo;
end;