Where are the Delphi Attributes Real World Examples?
Asked Answered
S

3

15

I know by TMS Aurelius that we can use the "new" 2010 attributes feature to serialize database table fields into object properties at run-time, for example, and I am not an expert on this deep object oriented schema, so I look into the TMS source code and could not understand how to implement it myself, not for DB, not for XML.

So I've looked for all Google's results on Delphi Attributes and all that people post are declaration examples and then stops before even showing their examples in action.

Then where are the real world examples of how can we project, declare, code and USE those juiced classes inside a form/executing code?

Does anyone have an example to share here or know a good article that is complete?

Edit1:

The answer should have a TForm with a TButton where, when clicked, execute some use of the attribute classes created, do not answer showing just the attribute and classes interfaces, because there are many of those declaration examples as I told before

Stationmaster answered 27/7, 2013 at 13:14 Comment(6)
there are some texts where the author bothers to create a full file containing the declaration and construction of the methods of and interesting use of the attributes but gives us no example on it's usage.Stationmaster
I would be most interested - came upon the same 'problem' some time ago and have never used attributes because I don't see the benefit.Strage
I think the fundamental problem is that so many long time Delphi programmers are so used to not having them that we haven't found many places where they provide a clear benefit over the way we've been doing things. For a lot of 3rd party component vendors, making extensive use of attributes prevents them from targeting users of older Delphi versions, limiting their markets even more.Gael
have a check on this link docwiki.embarcadero.com/RADStudio/XE4/de/…Federation
@Jan, it depends what is your problem domain: typical application programmer will not define his/her attributes probably. Primary, this is the instrument to operate with additional information (metadata) associated with traditional data types in run-time when developing something generic, e.g. general purpose ORM framework.Phonic
@Franz, it's clear now the example on Embarcadero, we could, for example use in a web PageController like class, descendant of a Controller that requires authentication and each PageController could have an Attribute Annotation telling which level of user is mandatory for this page to be shown.Stationmaster
D
11

If you want to declare you own attribute, you can do it like this:

type
  TDisplayLabelAttribute = class(TCustomAttribute)
  private
    FText: string;
  public
    constructor Create(const aText: string);
    property Text: string read FText write FText;
  end;

An attribute is a regular class, that has the TCustomAttribute as its ancestor. You implement it as usual:

implementation

constructor TDisplayLabelAttribute.Create(const aText: string);
begin
  FText := aText;
end;

Now the attribute is declared and implemented, you can just use it:

[DisplayLabel('My Class')]
TMyClass = class
end;

So now you have an attribute declared and implemented and you have used it to add a display label to some class. The final phase is to use that attribute, since you have a class decorated with it. The code that uses the attribute does not resides in the attribute nor the decorated class, it is implemented in the service layer that will use the decoration.

Let's say we have a class that returns a possible display label for a class:

type
  TArtifactInspector = class
  public
    class function DisplayLabelFor(aClass: TClass): string;
  end;

That method will inspect a class and return its display label, given it exists. Otherwise it returns an empty string:

implementation

uses
  Rtti;

class function TArtifactInspector.DisplayLabelFor(aClass: TClass): string;
var
  rttiContext: TRttiContext;
  rttiType: TRttiType;
  attribute: TCustomAttribute;
begin
  rttiContext := TRttiContext.Create;
  try
    rttiType := rttiContext.GetType(aClass);
    for attribute in rttiType.GetAttributes do
      if attribute is TDisplayLabelAttribute then
        Exit(TDisplayLabelAttribute(attribute).Text);
    Result := '';
  finally
    rttiContext.Free;
  end; // try to recover and return the DisplayLabel
end;
Digestif answered 28/7, 2013 at 14:19 Comment(10)
found one more example here robstechcorner.blogspot.de/2009/10/…Federation
So, it replaces te need to set properties of the object on a TObject.Create method, is that? I think that for Delphi users it is not necessary at the moment, but for Java ones it's really necessary because they don't have the tools we have, like a real good visual designer.Stationmaster
@EASI: the code I presented is just an example. There are many many uses for attributes even in Delphi. For instance, you can decorate a class with information that will be used to perform the persistence of its instances. In that case the Object Inspector will not help at all. Attributes are an important improvement in the language, even for Delphi. They are usually overlooked by many programmers that rely only on the visual portion of Delphi. However, when it comes to code, attributes can make a big difference. I suggest you open your mind about them!Digestif
@EASI: a very real world example of using attributes is a security system. I had to build an application where the users could have partial access to a form features. For instance, two users could open a form in the application and select records, but one could not delete them. How to proceed? There are many solutions, but the one I used was to declare a GrantedTo attribute that were used to associate certain parts of the form with a certain role. The user that had such a role would be allowed to perform the delete operation. Others do not!Digestif
I dont see clearly, how is the TDisplayLabelAttribute class get the connection to the [DisplayLabel('My Class')]? I would understand if it would be [TDisplayLabel('My Class')], but there is not. Can anyone enlight me please?Subroutine
@tcxbalage: It is a compiler convention. When the compiler finds a language contruction marked with an attribute (in this case [DisplayLabel('My Class')]), it will look for a class named TDisplayLabelAttribute that is a subclass of TCustomAttribute. If such a class is found, the compiler understands it must be instantiated and that instance must be attatched to the language construct.Digestif
@AlexSC: thanks for the answer! unfortunately its not working (XE7) unless I put the T prefix in front of the attribute name. If attribute class name is TDisplayLabelAttribute then [TDisplayLabel()] > works; [DisplayLabel()] > compiler hint > "W1025 Unsupported language feature: 'custom attribute'"Subroutine
@tcxbalage: Very strange. I don't have XE7, only XE3 and it works, even giving the same warning. Since it's just a warning, you can compile and perform a test. The warning is indeed anoying, but to me it works.Digestif
@AlexSC: probably they changed this since XE3, all I found about this is the Delphi help, no mention of the prefix: docwiki.embarcadero.com/RADStudio/Sydney/en/…Subroutine
@tcxbalage: I agree, the example in that page stablishes that the class name should not start with the usual T. However, all the rest seems to be just the same.Digestif
P
12

I must say it's not much clear to me what kind of example do you need. IMHO in http://docwiki.embarcadero.com/RADStudio/Rio/en/Overview_of_Attributes is everything you should need, perhaps providing that you have some basic knowledge of annotation and/or aspect programming resp.

An example depends on the way/purpose an author of particular SW used attributes for. You mentioned the ORM system: the typical usage here is to annotate member of the class representing DB entity with additional information necessary for DB operation in the backend of such framework. Let assume you have a DB entity having field COMPANY CHAR(32) NOT NULL and you want to represent it in Delphi class:

TSomeDBEntity = class(...)
  FCDS: TClientDataset;
  ...
  constructor Create;
  ... 
  [TCharColumn('COMPANY', 32, false)]
  property CompanyName: string read GetCompanyName write SetCompanyName;
end;

then you will define attribute TCharColumn with constructor

constructor TCharColumn.Create(const AFieldName:string; ALength:integer; ANullable:boolean);
begin
  inherited;
  FName := AFieldName;
  FLength := ALength;
  FNullable := ANullable;
end;

And usage of such annotation could look something like this:

FCDS := TClientDataset.Create(nil);
RttiContext := TRttiContext.Create;
try
  RttiType := RttiContext.GetType(self.ClassType);
  Props := RttiType.GetProperties;
  for Prop in Props do
    begin
      Attrs := Prop.GetAttributes;
      case Prop.PropertyType.TypeKind of
        tkUString:
          begin
            for Attr in Attrs do
              if Attr is TCharColumn then
              begin
                ColAttr := TCharColumn(Attr);
                FCDS.FieldDefs.Add(ColAttr.FName, ftString, ColAttr.FLength, not ColAttr.FNullable);
              end;
          end;
        else
          //... ;
      end;
    end;
finally
  RttiContext.Free;
end;

This piece of the program demonstrates, how to define fields in a dataset in run-time based on annotation in Delphi. We are limited little bit due lack of named parameters, hence working with parameter list is not flexible as should be e.g. like in Java (compare TMS Aurelius annotation set http://www.tmssoftware.com/site/manuals/aurelius_manual.pdf and http://www.techferry.com/articles/hibernate-jpa-annotations.html

Phonic answered 27/7, 2013 at 21:28 Comment(2)
From what I saw in the TMS ORM site, the code, at the beginning does not know which are the fields of the tabe, so when we use the Create method, the framework will read the database and allow us to type MyString := MyORM.TableName.TableField; and in your example you have to declare [TCharColumn('COMPANY', 32, false)]. Did I misunderstood the real use of the Attributes?Stationmaster
Traditional approach to create DB table is to write SQL command CREATE TABLE and you operate with such table using SQL. OTOH, in ORM, you don't use SQL commands (except where clause). DB table is annotated class (contains attributes) and you operate data via ORM. I don't know TMS, but e.g. Hibernate on start-up checks cfg how to treat DB schema in RDBMS regarding your annotations: validate: production, throws exception on diff, update: tries update DB schema in RDBMS (not everything can be updated), create, create-drop: drops existing DB incl. data and creates new DB schemaPhonic
D
11

If you want to declare you own attribute, you can do it like this:

type
  TDisplayLabelAttribute = class(TCustomAttribute)
  private
    FText: string;
  public
    constructor Create(const aText: string);
    property Text: string read FText write FText;
  end;

An attribute is a regular class, that has the TCustomAttribute as its ancestor. You implement it as usual:

implementation

constructor TDisplayLabelAttribute.Create(const aText: string);
begin
  FText := aText;
end;

Now the attribute is declared and implemented, you can just use it:

[DisplayLabel('My Class')]
TMyClass = class
end;

So now you have an attribute declared and implemented and you have used it to add a display label to some class. The final phase is to use that attribute, since you have a class decorated with it. The code that uses the attribute does not resides in the attribute nor the decorated class, it is implemented in the service layer that will use the decoration.

Let's say we have a class that returns a possible display label for a class:

type
  TArtifactInspector = class
  public
    class function DisplayLabelFor(aClass: TClass): string;
  end;

That method will inspect a class and return its display label, given it exists. Otherwise it returns an empty string:

implementation

uses
  Rtti;

class function TArtifactInspector.DisplayLabelFor(aClass: TClass): string;
var
  rttiContext: TRttiContext;
  rttiType: TRttiType;
  attribute: TCustomAttribute;
begin
  rttiContext := TRttiContext.Create;
  try
    rttiType := rttiContext.GetType(aClass);
    for attribute in rttiType.GetAttributes do
      if attribute is TDisplayLabelAttribute then
        Exit(TDisplayLabelAttribute(attribute).Text);
    Result := '';
  finally
    rttiContext.Free;
  end; // try to recover and return the DisplayLabel
end;
Digestif answered 28/7, 2013 at 14:19 Comment(10)
found one more example here robstechcorner.blogspot.de/2009/10/…Federation
So, it replaces te need to set properties of the object on a TObject.Create method, is that? I think that for Delphi users it is not necessary at the moment, but for Java ones it's really necessary because they don't have the tools we have, like a real good visual designer.Stationmaster
@EASI: the code I presented is just an example. There are many many uses for attributes even in Delphi. For instance, you can decorate a class with information that will be used to perform the persistence of its instances. In that case the Object Inspector will not help at all. Attributes are an important improvement in the language, even for Delphi. They are usually overlooked by many programmers that rely only on the visual portion of Delphi. However, when it comes to code, attributes can make a big difference. I suggest you open your mind about them!Digestif
@EASI: a very real world example of using attributes is a security system. I had to build an application where the users could have partial access to a form features. For instance, two users could open a form in the application and select records, but one could not delete them. How to proceed? There are many solutions, but the one I used was to declare a GrantedTo attribute that were used to associate certain parts of the form with a certain role. The user that had such a role would be allowed to perform the delete operation. Others do not!Digestif
I dont see clearly, how is the TDisplayLabelAttribute class get the connection to the [DisplayLabel('My Class')]? I would understand if it would be [TDisplayLabel('My Class')], but there is not. Can anyone enlight me please?Subroutine
@tcxbalage: It is a compiler convention. When the compiler finds a language contruction marked with an attribute (in this case [DisplayLabel('My Class')]), it will look for a class named TDisplayLabelAttribute that is a subclass of TCustomAttribute. If such a class is found, the compiler understands it must be instantiated and that instance must be attatched to the language construct.Digestif
@AlexSC: thanks for the answer! unfortunately its not working (XE7) unless I put the T prefix in front of the attribute name. If attribute class name is TDisplayLabelAttribute then [TDisplayLabel()] > works; [DisplayLabel()] > compiler hint > "W1025 Unsupported language feature: 'custom attribute'"Subroutine
@tcxbalage: Very strange. I don't have XE7, only XE3 and it works, even giving the same warning. Since it's just a warning, you can compile and perform a test. The warning is indeed anoying, but to me it works.Digestif
@AlexSC: probably they changed this since XE3, all I found about this is the Delphi help, no mention of the prefix: docwiki.embarcadero.com/RADStudio/Sydney/en/…Subroutine
@tcxbalage: I agree, the example in that page stablishes that the class name should not start with the usual T. However, all the rest seems to be just the same.Digestif
O
9

Not sure if the question is asking for real world examples of attribute use or how to serialize db tables into objects using attributes. The example below is a contrived simple one (but an example none the less) showing how to use attributes to log changes to object properties.

Define your custom attribute

//By convention attributes are *not* prefixed with a `T` 
//and have the word `Attribute` in their name
LoggableAttribute = class(TCustomAttribute)
  private
    FDescription : String;
  public
    constructor Create(Description: String);
    property Description: String read FDescription;
  end;

The "hello world" of classes TProduct using the attribute

TProduct = Class(TObject)
   private
    FPrice: Double;
    FDescription: String;
    ..
   public  
    [LoggableAttribute('Product Price')]
    property Price : Double read FPrice write SetPrice;
    [Loggable('Product Description')]   {the `Attribute` part is optional}
    property Description : String read FDescription write SetDescription;
    property IsDirty : Boolean read FIsDirty;
  End;

Any class that has a "loggable attribute" can be passed to this method to iterate through the properties and log them.

procedure LogChanges(LoggableClass: TObject);
var
 c : TRttiContext;
 t : TRttiType;
 p : TRttiProperty;
 a : TCustomAttribute;
 Value : TValue;
begin
 c := TRttiContext.Create;    
 try
   t := c.GetType(LoggableClass.ClassType);
   for p in t.getProperties do
     for a in p.GetAttributes do
       if a is TLoggableProperty then begin
         Value := p.GetValue(LoggableClass);   
         // log to db.. 
         AddLogEntry(p.Name, TLoggableProperty(a).Description, Value.ToString);
       end;
 finally
   c.Free;
 end;

end;

Example of use:

var
 P : TProduct;
begin    
 P := TProduct.Create; 
 P.LoadPropertiesFromDB;
 ...
 ... User edits price ...    
 ... 
 P.Price := 499.99;
 ...
 ... Save product to DB 
 if P.IsDirty then  // save and log
   LogChanges(P);
Olympias answered 27/7, 2013 at 23:36 Comment(4)
Maybe I should change the question to "how to serialize db tables into objects using attributes"... :-)Stationmaster
By convention attributes do not have a T in front of them. Also the Attribute part of the name can be omitted in the annotation. So a attribute called LoggableAttribute can be used like so: [Loggable('test')].Muskrat
what is TLoggableProperty ?Atkinson
@Mohamad: it's the class that declares the attribute, to be used to decorate other language constructions.Digestif

© 2022 - 2024 — McMap. All rights reserved.