2
votes

How to set the default of a component object property value?

Component Class Code:

unit CustomClass;

interface

uses
    Classes;

type
  TCustomClass = class;

  TConfiguration = class;

  TCustomClass = class (TComponent)
  private
    FConfiguration: TConfiguration;
    procedure SetConfiguration(const Value: TConfiguration);
  published
    property Configuration: TConfiguration read FConfiguration write SetConfiguration;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

  TConfiguration = class (TPersistent)
  private
    FDelay: Integer;
    FSize: Integer;
    procedure SetDelay(const Value: Integer);
    procedure SetSize(const Value: Integer);
  published
    property Delay: Integer read FDelay write SetDelay;
    property Size: Integer read FSize write SetSize;
  public
    procedure Assign(Source: TPersistent); override;
  end;

implementation

{ TCustomClass }

constructor TCustomClass.Create(AOwner: TComponent);
begin
  inherited;
  Configuration.FileName:= 'FileName';
  Configuration.Size := 10;
end;

destructor TCustomClass.Destroy;
begin
  Configuration.Free;
  inherited;
end;

procedure TCustomClass.SetConfiguration(const Value: TConfiguration);
begin
  FConfiguration.Assign(Value);
end;

{ TConfiguration }

procedure TConfiguration.Assign(Source: TPersistent);
begin
  inherited;
  Delay := (Source as TConfiguration).Delay;
  Size := (Source as TConfiguration).Size;
end;

procedure TConfiguration.SetDelay(const Value: Integer);
begin
  FDelay := Value;
end;

procedure TConfiguration.SetSize(const Value: Integer);
begin
  FSize := Value;
end;

end.

In my IDE it will appear as the object property was modified (bold blue).

I thought in making default and stored functions on the TConfiguration class properties, like so:

TConfiguration

interface

private
  function FileNameStored: Boolean;
published
  property FileName: string read FFileName write SetFileName stored FileNameStored;
  property Size: Integer read FSize write SetSize default 10;

implementation

function TConfiguration.FileNameStored: Boolean;
begin
  Result := FileName <> 'FileName';
end;

It colors the properties of TConfiguration in normal blue, but not the Configuration property of TCustomClass and it's not there that I want to set its default values, is on the TCustomClass, since TConfiguration maybe be used in another components.

Then, I also thought:

TCustomClass

interface

private
  function ConfigurationStored: Boolean;
published
  property Configuration: TConfiguration read FConfiguration write SetConfiguration stored ConfigurationStored;

implementation

function TCustomClass.ConfigurationStored: Boolean;
begin
  Result := Configuration.FileName <> 'FileName' and
    Configuration.Size <> 10;
end;

But, this only sets the TCustomClass Configuration property color to normal blue, not its properties.

ANSWER

As @RemyLebeau pointed out (in the first and top answer), there were errors in the code. In that component and that case I decided to not set any default value to the properties.

2
Did you intend to drop a configuration object onto a form and also drop the custom class onto a form, and then connect them? Or did you want the configuration object to be a property that belongs to the custom class and does not have to be manually created and assigned? - Warren P
Then you really overlooked some obvious things, as Remy points out. He shows you need to explicitly create (in your class constructor) and free (in your class destructor) all TObject-types that belong to (are an indivisible part of) your object. You had the base class correct, so that's good. You may want to add an OnChange:TNotificationEvent to your configuration object. So it can call the main class and have it do something when someone writes to a configuration property. - Warren P
@WarrenP Yes. I fixed it, but as @NGLN pointed out, doesn't makes sense correcting it on the question, since it turns the answer unnecessary. The OnChange: TNotificationEvent is really a good advice, I liked it, since I can change the stored value of properties if they are a class private variable. Thanks. - Luiz Carlos M. Jr.

2 Answers

6
votes

There are several bugs in your code.

  1. your TCustomClass constructor is not creating the TConfiguration object. You need to add that:

    constructor TCustomClass.Create(AOwner: TComponent);
    begin
      inherited;
      FConfiguration := TConfiguration.Create; // <-- add this
      FConfiguration.FileName := 'FileName';
      FConfiguration.Size := 10;
    end;
    

    That being said, the assignment of the FileName and Size properties should probably be moved to the TConfiguration constructor rather than TCustomClass constructor.

  2. A String property cannot be defined with a user-defined default value, the default is always a blank string. So your FileName property will always appear as modified when your component is created, because your constructor is assigning a non-default value to it. Your stored approach is the correct solution to handle that. Or, simply do not assign a default FileName at all, leave it blank. If the user does not assign an explicit FileName, your code could assume a default value if needed.

    An Integer property, on the other hand, can be defined with a user-defined default value. Your Size property is not doing that, though. It should be (especially if you move the assignment of the default value into the TConfiguration constructor):

    property Size: Integer read FSize write SetSize default 10;
    
  3. your TConfiguration.Assign() method is implemented incorrectly. By calling the inherited Assign() method before copying values, your code will always raise an EConvertError exception at runtime if the caller assigns one TConfiguration object to another. The reason is because the base class TPersistent.Assign() implementation simply calls Source.AssignTo(Self), and TConfiguration does not override the AssignTo() method, so TPersistent.AssignTo() gets called, which simply calls Dest.AssignError(Self), which raises the exception. So, DO NOT call the inherited Assign() method if the Source is actually a TConfiguration object:

    procedure TConfiguration.Assign(Source: TPersistent);
    begin
      if Source is TConfiguration then
      begin
        FDelay := TConfiguration(Source).Delay;
        FSize := TConfiguration(Source).Size;
      end else
        inherited;
    end;
    
0
votes

But, this only sets the TCustomClass Configuration property color to normal blue, not its properties.

That is by design. A storage specifier only works for the property it is specified for.

Compare you class with a TFont property on a form. When ParentFont is True, the Font property will not be stored. But its members Color and Name still are not default, thus they appear as if they will be stored. This is because a TFont object does not know about its owner, how could it? One time it is part of a canvas, the other time it is part of a control, or has no owner at all. In all these conditions, the TFont object should be fully functional, so it does not enquiry its parent how it should behave. On the other side: the parent should not enquiry its child on how to behave too.

Now back to your scenario: whether FileName and Size properties need to be stored should be dealt with in TConfiguration. Whether the Configuration property of TCustomClass should be stored may not depend on these FileName and Size properties. If the Configuration property is stored (is bold), but all its members are not stored (not bold), then nothing is stored in the end. If you have a ParentConfiguration property, alike ParentFont, thén you can deside not to store the Configuration property. Otherwise, leave it be and let the boldness not distract you any further.