Jump to content
Lars Fosdal

Using Attributes in class declarations

Recommended Posts

Originally a comment to

 

I find Attributes to be an excellent feature of Delphi.

Saves me a lot of variations of init code which instead is done with attributes.

Share this post


Link to post
Guest
20 minutes ago, Lars Fosdal said:

I find Attributes to be an excellent feature of Delphi.

Saves me a lot of variations of init code which instead is done with attributes.

Well i am not saying that it is not useful, i use them sometimes, just they are ugly and feel wrong and hardcoded.

 

Delphi had this unique feature which is separated declaration from implementation, this had many benefits, specially for compiler it can be fast.

You code is used by the compiler to generate machine language or any other low level code to work.

The compiler will/can generate extra information about your code, RTTI, this can be used by your own code to enhance functionality and what so ever you want, although once you went there it is more like you are interacting with the low level assembly or the generated code at the run time.

Here comes the attributes, i see them as messages or data, you as a developer sending them to your own code in the future at runtime, and this is my problem with their syntax and locality, they are using the squared brackets, just like C#, while the compiler does use these brackets for different declarations, also they look like wild comments at least for my eyes, by really the fact that compiler might not include them in final product hence break the functionality of your code is huge no, and that is wrong.

 

I wish if they were different and declared differently, in more concrete way, didn't thing about them much, but if they were declared like "class helper" something like "attribute helper", "class attribute" or even "attribute".. something like the standard Pascal code, with one new features, like you can have many per class/record, they don't need to be hardcoded eight before the target, though you can put them there, the main feature that we gained even more polymorphism in right way, see if the compiler can handle generics then you can extend any code by what ever data you want anywhere, like you can extend code that is not yours to be used by your own code without touching the original, in local way where you are responsible to write code to consume your added data also that way, you code will be strictly following all the standard visibility of lets say protected or public function in TButton, and here the compiler will lose its ability to mess you attribute or ignore them because you declared them somewhere and you are responsible for their consuming code.

 

It is tricky and i not pretending to have thought of all that deeply, all what i am saying that it is doable like every problem, there must be a neat solution, and of course anything is better than that ugly syntax and mutability, even if it is short it is not extensible, i just see them as anti pattern and more work down the road.

Share this post


Link to post

They are far from perfect.  They don't support generics, nor can you send a structured constant as an argument, even with if they were declared as {$WRITEABLECONST OFF}.

 

For my grids wrappers, I have a load of them...

 

  type
    TGridSetAttribute = class abstract(TCustomAttribute);
  
    /// <summary> Disable automatic field creating for GridSet </summary>
    GridManuallyCreateFieldsAttribute = class(TGridSetAttribute);
  
    /// <summary> Allow MultiSelect in Grid</summary>
    GridMultiSelectAttribute = class(TGridSetAttribute)
      MultiSelect: Boolean;
      constructor Create; overload;
      constructor Create(DoMultiSelect:Boolean); overload;
    end;
  
    GridMultiLineCellsAttribute = class(TGridSetAttribute)
      MultiLineCells: Boolean;
      constructor Create; overload;
      constructor Create(DoMultiLineCells:Boolean); overload;
    end;
  
    /// <summary> Autosize entire grid </summary>
    GridAutoSizeAttribute = class(TGridSetAttribute)
      AutoSizeEnabled: Boolean;
      constructor Create; overload;
      constructor Create(DoAutoSize:Boolean); overload;
    end;
  
    /// <summary> Turn on filters for grid </summary>
    GridShowFiltersAttribute = class(TGridSetAttribute)
      ShowFilters: Boolean;
      constructor Create; overload;
      constructor Create(DoShowFilters:Boolean); overload;
    end;

    GridEditModeAttribute  = class(TGridSetAttribute)
      EditMode: Boolean;
      constructor Create; overload;
      constructor Create(DoEditMode:Boolean); overload;
    end;

    ///  ----------------- Field attributes -----------------
    TQueryFieldAttribute = class(TCustomAttribute)
      constructor Create; virtual;
    end;
  
    /// <summary> Allow MultiSelect in Grid</summary>
    CheckBoxAttribute = class(TQueryFieldAttribute);
  
    /// <summary> Text to show as hint for column header </summary>
    HeaderHintAttribute = class(TQueryFieldAttribute)
      Hint: xlt;
      constructor Create; overload; override;
      constructor Create(const aHint: string); reintroduce; overload;
      constructor Create(const aHint: xlt); reintroduce; overload;
    end;
  
    /// <summary> Mark field as UniqueField </summary>
    UniqueFieldAttribute = class(TQueryFieldAttribute);
  
    /// <summary> Read-Only field attribute </summary>
    ReadOnlyFieldAttribute = class(TQueryFieldAttribute)
      ReadOnly: Boolean;
      constructor Create; overload; override;
      constructor Create(const IsReadOnly: Boolean); reintroduce; overload;
    end;
  
    /// <summary> Mark field as hidden </summary>
    HiddenFieldAttribute = class(TQueryFieldAttribute)
      WhenBlank: boolean; //Hides columns if all the data is empty string og 0
  
      constructor Create; overload; override;
      constructor Create(const aWhenBlank: Boolean); reintroduce; overload;
    end;
  
    InternalFieldAttribute = class(TQueryFieldAttribute);
  
    /// <summary> Enable filtering for column </summary>
    DontFilterFieldAttribute = class(TQueryFieldAttribute)
      constructor Create; override;
    end;
  
    /// <summary> Custom Field - populate in DoAfterConvert </summary>
    CustomFieldAttribute = class(TQueryFieldAttribute);
  
    /// <summary> Toggle visibility of hidden field</summary>
    ToggleFieldAttribute = class(TQueryFieldAttribute);
  
    /// <summary> Grouping column </summary>
    GroupingAttribute = class(TQueryFieldAttribute)
      Priority: Integer;
      constructor Create; overload; override;
      constructor Create(const aPriority: Integer); reintroduce; overload;
    end;
  
    UseFieldAsCategoryAttribute = class(TQueryFieldAttribute);
  
    /// <summary> Category Name for GridEdit </summary>
    CategoryAttribute = class(TQueryFieldAttribute)
      Name: string;
      Priority: Integer;
      constructor Create; overload; override;
      constructor Create(const aName: string; const aPriority: Integer = MaxInt); reintroduce; overload;
    end;
  
    /// <summary> Autosize field </summary>
    AutoSizeAttribute = class(TQueryFieldAttribute)
      AutoSizeEnabled: Boolean;
      MinSize: Integer;
      MaxSize: Integer;
      constructor Create; overload; override;
      constructor Create(DoAutoSize:Boolean); reintroduce; overload;
      constructor Create(aMinSize, aMaxSize: Integer); reintroduce; overload;
    end;
  
    /// <summary> Set OwnsReference to true for field:TFieldReference<T> instance </summary>
    OwnsReferenceAttribute = class(TQueryFieldAttribute);

    /// <summary> Mark field as using WordWrap </summary>
    WordWrapAttribute = class(TQueryFieldAttribute)
    public
      WordWrap: TFieldWordWrap;
      constructor Create; overload; override;
      constructor Create(const aWordWrap: TFieldWordWrap); reintroduce; overload;
    end;

    /// <summary> Mark field as using WordWrap </summary>
    NoWordWrapAttribute = class(WordWrapAttribute)
    public
      constructor Create; override;
    end;

    /// <summary> Mark field as default sort-by column </summary>
    DefaultSortFieldAttribute = class(TQueryFieldAttribute)
    public
      SortDescending: Boolean;
      constructor Create; overload; override;
      constructor Create(const aSortDescending: Boolean); reintroduce; overload;
    end;
  
    /// <summary> [InitField(64 {, alignment})] <br/>
    /// [InitField('Title', 64 {, alignment})] <br/>
    /// [InitField('Title', 'Name Override for SQL', 64 {, alignment})]</summary>
    InitFieldAttribute = class(TQueryFieldAttribute)

 

Here is an example of how I use them for what I call a GridSet.  I have a large number of grids that uses gridsets instead of having visual form designed grids.

type
  TSiteGridSet = class(TGridSet<TPSDLocation>)
  public
    [GridShowFilters, GridAutoSize]

    [CustomField]
    Ticked: TFieldCheckBox;

    [InitField(40), UniqueField]
    GlobalLocId: TFieldInteger;

    [InitField(50, hcRightJustify )]
    LocationNo: TFieldString;

    [InitField(110)]
    Name: TFieldString;

    [InitField(70), DefaultSortField]
    Environment: TFieldEnum<TPSDEnviroment>;

    [InitField(70)]
    Role: TFieldEnum<TPSDServerRole>;

    [InitField(70)]
    LocationBuildTrack: TFieldEnum<TPSDLocationBuildTrack>;

    [InitField(70), HiddenField]
    BuildName: TFieldString;

    [InitField(70)]
    AppServer: TFieldString;

    [InitField(70)]
    LocationType: TFieldEnum<TPSDLocationType>;

    [InitField(300)]
    DBInfo: TFieldString;

    [InitField(300)]
    DBHostName: TFieldString;

    [HiddenField]
    Param: TFieldReference<TPSDCreateParameters>;

 

Share this post


Link to post
Guest

your attribute here are serving their purposes, local and you had to hardcoded them in place, how about the following, and it is just thinking out side the box

  TSiteGridSet = class(TGridSet<TPSDLocation>)
  public
    Ticked: TFieldCheckBox;
    GlobalLocId: TFieldInteger;
    LocationNo: TFieldString;
    Name: TFieldString;
    Environment: TFieldEnum<TPSDEnviroment>;
    Role: TFieldEnum<TPSDServerRole>;
    LocationBuildTrack: TFieldEnum<TPSDLocationBuildTrack>;
    BuildName: TFieldString;
    AppServer: TFieldString;
    LocationType: TFieldEnum<TPSDLocationType>;
    DBInfo: TFieldString;
    DBHostName: TFieldString;
    Param: TFieldReference<TPSDCreateParameters>;
  end;

  TSiteGridSetAttr = attributes for class(TSiteGridSet)  // helper attributes ...
  class attribute                                        // attribute word can be remove, due the declaration above
    GridShowFilters, GridAutoSize;
  field attribute                                        // also attribute word can be simply replaced by public, due the declaration scope
    Ticked: CustomField;
    GlobalLocId: InitField(40);
    GlobalLocId: UniqueField;
    LocationNo: InitField(50, hcRightJustify );
    Name: InitField(110);
    Environment: InitField(70);
    Environment: DefaultSortField;
    Role: InitField(70);
    LocationBuildTrack: InitField(70);
    BuildName: InitField(70);
    BuildName: HiddenField;
    AppServer: InitField(70);
    LocationType: InitField(70);
    DBInfo: InitField(300);
    DBHostName: InitField(300);
    Param: HiddenField;
  end;

Also not saying it is less ugly, but it is more liquid and useful and i have them controlled and contained separately, also here i am forcing the compiler to handle them right and warn and report every wrong imlementation.

So just few ideas about the above

1) Attributes as i described them messages for the future online code, they definitely can be removed and replaced by complex "if then" and "switch cases", so they bring shorter code and lot of clarity of logic, i am ok with that.

2) The imaginary format i pasted above have many benefits (advantages),

  • One can now extend any class or record when can see it, so you can add them to anything and they will be local to your code even if the extended class/record is not yours.
  • Instead of one like the above, one can have multiple like one InitField (TGridFieldInitAttr) and another TGridHiddenFieldAttr.... and they will be also local to your usage.
  • The compiler is forced to handle them right, by any way ( more deeper discuss needed here and most important is out of the box logic) even copy the extended class/record to a local copy when needed, and prevent some where they doesn't make sense, overloaded procedures will be problematic for sure here, and class function attribute will simply doesn't make sense so it will not be copied and will stay pointing to the original.
  • Now to the syntax, if you prefer the interleaved way then let it be, but let the compiler accept the format as syntax that belong to the Pascal realm, something new like "Attribute" so the syntax might be like 
      DBInfo: TFieldString; attribute InitField(300); ...
      // or
      DBInfo: attribute InitField(300);
      DBInfo: TFieldString;
      // or
      DBInfo: TFieldString;
      DBInfo: attribute InitField(300);

    The point here is to remove the script-like syntax, as we have nice language even if this meant few extra lines to write, attributes with the above code belongs to realm code with not so-strict to implement or to exist.

  • Also by this separation, now you can have your multi attributes with different implementation, means you can have one to work with FMX on Android and one to use with VCL on Windows, you can't do that now, and if really wanted it it will be easier and shorter to use HTML and CSS, it will looks like salad by repeating fields with different names and values, you can have your grid to show the perfect view in your own view for different screen sizes, without the mess, and will still very close to current script style in portability, so we have more flexible attributes.

  • May be, and this is not easy for for see every aspect without deep discuss, these attributes can be implemented where you can build and modify at runtime, this will be very anti pattern in respect to attributes as they are, but will be very powerful tool in OP code to use when you need it or see it fit, i always like more tools, ugly or not, i like more options and the option to choose.

  • By accepting the separated declaration and right syntax, we can think of dropping or skipping the separated declaration of the TxxAttribute itself, in many cases it will do with local function/procedure in the separated class/record attribute declaration, here we saved few lines to write, and the compiler will warn or fail to compile when the declaration is broken or causing a contradiction.

 

This is few thoughts i have about attributes and how they might be better or less ugly, but it is also very personal and i would love to read what do you think, i know you like them and like their usage, the question is can they be better while looking right.

Share this post


Link to post

I am pretty sure that the syntax of attributes won't change, so that discussion is not really interesting. 

 

Your suggested helper separates the attributes from the field/property declarations, which is not a good thing, IMO.

As for cross platform - there would be two means of managing that.  Defines around the attributes, or ensuring that values are appropriately scaled in the TGridView class (See last paragraphs).

 

The attributes are just the means to change a default.  I can (and do) change attributes of the fields in a Create method as well.

I would have loved to be able to set all these from attributes as well, but the compiler won't allow it.
If we had lambda expressions and the compiler would accept complex constants as arguments, I'd move of this to attributes.

constructor TSiteGridSet.Create;
begin
  inherited;
  LocationNo.Title := titles.LocationNo;
  GlobalLocId.Title := titles.GlobalLocId;
  Name.Title := titles.Name;
  Environment.Title := titles.Environment;
  Role.Title := titles.Role;
  AppServer.Title := titles.AppServer;
  LocationType.Title := titles.LocationType;
  DBInfo.Title := titles.DBInfo;
  BuildName.Title := titles.LocationType;

  LocationBuildTrack.Title := xlt.Create('Build');
  LocationBuildTrack.Formatter := function (const aValue: TPSDLocationBuildTrack): string
  begin
    case aValue of
      lbtDefault,
      lbtLive  : Result := 'Live';
      lbtPilot : Result := 'Pilot';
      lbtTrunk : Result := 'Trunk';
      else Result := '';
    end;
  end;

  RowStyler := HighlightOffline;

  Environment.SecondaryKeys := [LocationType, Role, Name];
  Environment.Formatter := function(const aValue: TPSDEnviroment):String
    begin
      Result := aValue.ToString
    end;

  Name.ColumnStyler := Self.ServerNameColor;

  Role.SecondaryKeys := [LocationType, Environment, Name];
  Role.Formatter := function(const aValue: TPSDServerRole):String
    begin
      Result := aValue.ToString;
    end;
  Role.CellStyler := ServerRoleColor;

  LocationType.SecondaryKeys := [Environment, Role, Name];
  LocationType.Formatter := function(const aValue: TPSDLocationType):String
    begin
      Result := aValue.ToString;
    end;

  Param.OwnsReference := True;

  FComboLayout := Layout.AddLayout(xlt.Create('Combo'), [LocationNo, Name, Environment, LocationBuildTrack, Role, GlobalLocId]);
end;

 

FYI - the TGridSet is non-visual.  There is a TGridView proxy class that takes a TGridSet and applies the visual stuff to the actual grid.

  TSiteQueryGrid = class(TGridViewPSD<TPSDLocation, TSiteGridSet>);

Currently, the TGridView was written to only support TMS TAdvStringGrid, but could be refactored into a multiplatform/multigrid adapter.

 

Share this post


Link to post
2 hours ago, Lars Fosdal said:

Here is an example of how I use them for what I call a GridSet.  I have a large number of grids that uses gridsets instead of having visual form designed grids.

Honestly, this example rather shows how awkward attributes are. All options you provided seem to be better defined in form props/code than in class declaration. Drawbacks of attributes IMHO:

- hard-coded values

- constants are scattered and repeating

- violation of abstraction of structure from style (like visual styles defined right in HTML)

 

I could be wrong but so far I see only one really useful application - defining structures for (de)serialization but even here attributes are not required for simple cases. 2nd application, handy but not necessary (the only one I use myself) is marking DUnitX's test methods

Edited by Fr0sT.Brutal
  • Like 1

Share this post


Link to post
Guest
15 minutes ago, Lars Fosdal said:

FYI - the TGridSet is non-visual.  There is a TGridView proxy class that takes a TGridSet and applies the visual stuff to the actual grid.

I do understand that it is non visual, but did it worth it ? i mean the whole of this extra layers of code, while you could simply either inherit the visual grid or have a small usual class that adjust it visually.

 

With REST implementation by vfbb, it did make sense as it hid everything and really simplified the usage to almost zero code for any user, it did serve the perfect purpose, in your usage, well, you look like to torture and abuse a keyboard along with your fingers.

It is personal preferences after all, as it is still a tool to use.

Share this post


Link to post

The grid sets are multipurpose.  I can create gridsets dynamically from an SQL query, or dynamically from records or objects, or I can specify the design as in the example.
All the actual grids have the same features, look and feel and the same behaviour.

Visual inheritance is a nightmare. 

 

5 hours ago, Fr0sT.Brutal said:

- hard-coded values

Just the same way as values in a constructor or in a designtime form are hardcoded.

Except - the values are in code, right next to the declaration. 

 

5 hours ago, Fr0sT.Brutal said:

- constants are scattered and repeating

Please elaborate? 

 

5 hours ago, Fr0sT.Brutal said:

- violation of abstraction of structure from style (like visual styles defined right in HTML)

Where and how?

As for torture of fingers.  It is not like you need a huge amount of square brackets and using them is no different from when using them for arrays.

 [InitField(50, hcLeftJustify), UniqueField, DefaultSortField, ReadOnlyField]
 GlobalLocId: TFieldInteger;

 

Anyways...
Don't be blinded by one man's implementation in a specific context tailored for his needs.

Attributes can be very useful and applied for a wide range of purposes.

Share this post


Link to post
29 minutes ago, Lars Fosdal said:

Visual inheritance is a nightmare. 

At best.

Share this post


Link to post
Guest
12 minutes ago, Bill Meyer said:
42 minutes ago, Lars Fosdal said:

Visual inheritance is a nightmare. 

At best.

If you are going after each use, then yes that will be waste of time, i would use one single inherited component that can be fed a record or class both are more like constants and simple fields without any code, this will remove all the hustle of checking everything by eyes without the compiler help, also with one extended visual component so can simply put the lines calling the specified modifications right when he chose the record or object, the result will be same amount of lines but grouped and validated by the compiler, with zero attributes declaration.

Share this post


Link to post
1 hour ago, Lars Fosdal said:

Just the same way as values in a constructor or in a designtime form are hardcoded.

But you can load them from config.

1 hour ago, Lars Fosdal said:

Please elaborate? 

In your sample, there are repeating 70, 40 etc. Unclear what these numbers mean

1 hour ago, Lars Fosdal said:

Where and how?

You define structure in declaration but mix it with representation properties

 

I'm not against attributes, I just don't see their real benefit. Any other examples are gladly welcome!

Share this post


Link to post
6 hours ago, Fr0sT.Brutal said:

All options you provided seem to be better defined in form props/code than in class declaration.

Attributes are about that, that you don't have to have form props/code.

It's like templating for recurring things.

Like Lars' grids or in my case my ORM's.

 

It has a bunch of benefits, like I can easily include them anywhere without any dependency.

 

Also, for the grid example, it can be the default layout which you could overload with custom settings but on deleting them or on a new install, you don't have to deal with the initial data.

Also, attributed things can be tested very good.

Also, on many parameters it's ugly AF, I'm using JSON strings on complex attributes. Which are very error prone, but can be tested very easily.

 

Yes, it's also not perfect and misses a lot of things like a NameOf(), but we are used to waiting centuries for cookies.

No...

Yes...

Whatever...

It's longer than green.

 

  • Like 1

Share this post


Link to post
17 hours ago, Fr0sT.Brutal said:

But you can load them from config.

In your sample, there are repeating 70, 40 etc. Unclear what these numbers mean

You define structure in declaration but mix it with representation properties

 

I'm not against attributes, I just don't see their real benefit. Any other examples are gladly welcome!

I see that I've cut off the source I pasted.  If you had the full source, you would have seen that the numbers are the default widths in pixels.

 

image.thumb.png.1371595174b83e374df9c670c8390a7c.png

This is the initial default value. I.e. those you need before you have a config to load.
The same default as you would define in a constructor or design in the form editor. 
Likewise, with the type of the field, there is a default alignment which can be overridden with the attribute.
These are indeed presentation properties and they are defaults explicitly bound to each instance of a field. 

 

The idea is to keep the default properties together with the field declarations instead of having them spread out through the code or defined in the .dfm.

Unfortunately, I cannot use record constants to initialize the titles or specify the secondary sorting columns due to limitations in the language. 
 

  • Like 1

Share this post


Link to post
2 hours ago, Lars Fosdal said:

The idea is to keep the default properties together with the field declarations instead of having them spread out through the code or defined in the .dfm.

Very good point 👍

  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×