Let's say that you have a configuration class like this for your Golf club member management software:
TConfig=class public property MinMemberAge: Integer ... ; end;Property MinMemberAge explains the minimum age on members that could be considered to sign up for the organization. In our application, users could change this according to their situations. However we want to ensure that they could not enter ridiculous values (like for example 1 year). Of course you can do the validation in the setter method. However it will be much easier to understand and maintain when we use the power of Attribute.
Let's say that we want to make sure that the minimum member age is 10. With Attribute, you will add a simple notation like this:
TConfig=class public [MinValue(10)] property MinMemberAge: Integer ... ; end;From the above codes, we can easily understand that the minimum value of member MinMemberAge is 10. Of course the validation will not be happen magically. You need to write the codes to do the validation. So, how?
First you need to write an attribute class with three requirements.
- descendant form TCustomAttribute
- have proper name. In our example above, the proper name of the attribute class could be either MinValue or MinValueAttribute. More about the names later.
- have constructor(s) with matching parameters as in the annotation(s).
I think there is no problem with this requirement. Especially since TCustomAttribute was declared in System unit. So it's accessible from anywhere.
2. Have Proper Name
The compiler will use the attribute class' name to match against the annotation. In the matching process, Delphi has an interesting behavior. When no full name match found, it will append Attribute to the end of the name used in the annotation and will do another search for that name. That is why in our example above, we could use either MinValue or MinValueAttribute. Technically I don't have any recommendation of which to use. However, personally I prefer to consistently use the Attribute suffix in most of my attribute classes.
So now for our example, we could declare the class either like this:
type MinValue=class(TCustomAttribute) ... end;or this:
type MinValueAttribute=class(TCustomAttribute) ... end;3. Have constructor(s) with matching parameters as in the annotation(s)
The annotation [MinValue(10)] actually tells the compiler how to create an instance of the corresponding attribute class. In that example, the compiler actually will interpret it into:
MinValue.Create(10);That is why we need for MinValue or MinValueAttribute class to have constructor that accepts value of 10. Since we know we are dealing with integer, then we need to have a constructor like this:
constructor Create(const AMinValue: Integer);Now that we have all the information, let's complete our implementation of the example attribute class.
interface type MinValueAttribute=class(TCustomAttribute) private FMinValue: Integer; public constructor Create(const AMinValue: Integer); property MinValue: Integer read FMinValue; end; implementation constructor MinValueAttribute.Create(const AMinValue: Integer); begin FMinValue := AMinValue; end;How to Use Attribute Information?
In run time, we can query the information set through attributes using RTTI. Once we have the information, it's up to us of what to do next.
Here is one example of how to query the attributes of a given object and its properties.
procedure QueryAttributesOf(AObj: TObject); var vCtx: TRttiContext; // rtti context of the whole system vType: TRttiType; // the object's rtti information vProp: TRttiProperty; // a property rtti information vAttr: TCustomAttribute; // an attribute begin // Get a reference to our system's rtti context vCtx := TRttiContext.Create; try // get rtti information about our object's class vType := vCtx.GetType(AObj.ClassType); // check every attribute linked to the class for vAttr in vType.GetAttributes do begin // do anything here with the attribute linked to the object end; // query every properties of the class for vProp in vType.GetProperties do // check every attribute linked to a property for vAttr in vProp.GetAttributes do begin // do anything here with the attribute linked to the current property of the object end; finally vCtx.Free; end; end;
Initially It might look complex. But in most cases, you only need to write this kind of codes once, and will be able to reuse it in many projects. I will prove it in next tutorial when I show you how to use attributes to easily store/reload information from ini file or registry.
In our current example, we want to compare attribute's minimum value with the current value of the property it linked to. When the property's value is lower that specified by the attribute, we will raise an exception. Let's implement this as a procedure named Validate. The implementation codes will be like this:
procedure Validate(AObj: TObject); var vCtx: TRttiContext; // rtti context of the system vType: TRttiType; // the object's rtti information vProp: TRttiProperty; // a property rtti information vAttr: TCustomAttribute; // an attribute vPropValue: Integer; begin // Get a reference to our system's rtti context vCtx := TRttiContext.Create; try // get rtti information about our object's class vType := vCtx.GetType(AObj.ClassType); // query every properties of the class for vProp in vType.GetProperties do // check every attribute linked to a property for vAttr in vProp.GetAttributes do // if the attribute is of "MinValueAttribute" class, do further processing if vAttr is MinValueAttribute then begin // get the property value vPropValue := vProp.GetValue(AObj).AsInteger; // compare the property's value with the minimum value explained by the attribute // raise exception when the property's value is lower if vPropValue < MinValueAttribute(vAttr).MinValue then raise Exception.Create('Property ' + vProp.Name + ' has lower value (' + IntToStr(vPropValue) + ') that suggested (' + IntToStr(MinValueAttribute(vAttr).MinValue) + ')'); end; finally vCtx.Free; end; end;Demo Project
I have written a demo project for this tutorial. It's attached and you can download it at the end of this tutorial.
- Upon running you will get something like this:
- Now let's enter an invalid value (i.e. a value less than 10), for example 4. Like this.
- Click the Validate! button and this is what we get:
- Now let's try valid value, i.e. values of at least 10. This time we will use 10, like this:
- Now click the Validate! button again, now we get confirmation that the value is okay. Like this:
Next time we will use attribute in real action.