1

I'm developing a component for Query. It works like the "Properties" feature of DevExpress, but I need to place the order of the Unpublished Property I wrote to DFM with DefineProperties in the DFM file at the top of the TCollectionItem. It works the same way in DevExpress. If you add a Field to the cxGrid and assign a value to the Properties property, you will see the value "PropertiesClassName" in the DFM file at the top.

When I open the DFM file and bring this Property to the top, the setter property of the "PropertiesClassName" Property works and I create that Class. It works seamlessly when reading data from the DFM stream. But no matter what I did I couldn't get the "PropertiesClassName" Property value to the top. If you create a cxGrid on the form and add Field, and then take the "PropertiesClassName" property from DFM to the bottom of the DFM file, when you open the form again, you will see that it cannot find the relevant Class and an error occurs.

To change the DFM flow, I first assigned a value to the "PropertiesClassName" Property and then created the Class, but the problem was not solved. I did the opposite of this but the problem is still the same.

DFM Context

  object QuerySearchEngine1: TQuerySearchEngine
    SearchFields = <
      item
        FieldName = 'TestField'
        Properties.Convert2String = True
        PropertiesClassName = 'TSearchBooleanProperties'
  end>

DFM Context should be like

    object QuerySearchEngine1: TQuerySearchEngine
    SearchFields = <
      item
        PropertiesClassName = 'TSearchBooleanProperties'
        FieldName = 'TestField'
        Properties.Convert2String = True
  end>

Classes

  TSearchField = class(TCollectionItem)
  private
    FFieldName: string;
    FProperties: TSearchFieldProperties;
    FPropertiesClassName: string;
  private
    procedure SetFieldName(const Value: string);
    procedure SetProperties(const Value: TSearchFieldProperties);
  private
    procedure ReaderProc(Reader: TReader);
    procedure WriterProc(Writer: TWriter);
    procedure SetPropertiesClassName(const Value: string);
  protected
    constructor Create(Collection: TCollection); override;
    procedure DefineProperties(Filer: TFiler); override;
  public
    property PropertiesClassName: string read FPropertiesClassName write SetPropertiesClassName;
  published
    property FieldName: string read FFieldName write SetFieldName;
    property Properties: TSearchFieldProperties read FProperties write SetProperties;
  end;


procedure TSearchField.DefineProperties(Filer: TFiler);
begin
  inherited;
  Filer.DefineProperty('PropertiesClassName', ReaderProc, WriterProc, FPropertiesClassName <> '');
end;

procedure TSearchField.SetPropertiesClassName(const Value: string);
begin
  var Item: TSearchFieldPropertiesItem;
  if TryValidateSearchFieldPropertiesClassName(Value, Item) then
  begin
    if not Assigned(FProperties) or not (FProperties.ClassType = Item.ClassType) then
    begin
      if Assigned(FProperties) then
      begin
        FProperties.Free;
        FProperties := nil;
      end;
      FPropertiesClassName := Item.ClassType.ClassName;
      FProperties := Item.ClassType.Create;
    end;
  end
  else
  begin
    FPropertiesClassName := '';
    if Assigned(FProperties) then
    begin
      FProperties.Free;
      FProperties := nil;
    end;
  end;
end;

Property Editor

type
  TSearchFieldPropertiesProperty = class(TClassProperty)
  private
    function GetInstance: TPersistent;
  public
    function GetAttributes: TPropertyAttributes; override;
    procedure GetValues(Proc: TGetStrProc); override;
    function GetValue: string; override;
    procedure SetValue(const Value: string); override;
  end;

function TSearchFieldPropertiesProperty.GetValue: string;
begin
  for var I := 0 to Self.PropCount - 1 do
  begin
    var Inst := Self.GetComponent(I);
    if Assigned(Inst) and Self.HasInstance(Inst) then
    begin
      if Inst is TSearchField then
      begin
        var PropInst := GetObjectProp(Inst, Self.GetPropInfo);
        if Assigned(PropInst) then
        begin
          for var Item in SearchFieldPropertiesList do
          begin
            if PropInst.ClassType = Item.ClassType then
            begin
              Result := Item.Name;
              Exit;
            end;
          end;
        end;
      end;
    end;
  end;
end;

procedure TSearchFieldPropertiesProperty.SetValue(const Value: string);
begin
  var Item: TSearchFieldPropertiesItem;
  if TryValidateSearchFieldPropertiesName(Value, Item) then
  begin
    var Inst := GetInstance;
    if Assigned(Inst) then
    begin
      var Context := TRttiContext.Create;
      var Rtype := Context.GetType(Inst.ClassType);
      for var Prop in Rtype.GetProperties do
      begin
        if SameText(Prop.Name, 'PropertiesClassName') then
        begin
          Prop.SetValue(Inst, TValue.From<string>(Item.ClassType.ClassName));
          Break;
        end;
      end;
    end;
  end;
end;

Pic for Design Time

The only problem is changing the order of the Property in that DFM flow.

  • Why are you using DefineProperties instead of simply declaring regular published property? – Dalija Prasnikar Oct 12 '22 at 11:05
  • Because I dont want to show it at Object Explorer, like DevExpress. I have Property Editor which is show String List to choose for Properties at the Object Explorer, It creates descendant class for Properties. Now I use workaround like what you say as Published for PropertiesClassName. – Ahmet Yeşilçimen Oct 12 '22 at 13:21

1 Answers1

0

Original answer at the bottom, here is a new suggestion:

We actually have something very similar in the JVCL where TJvHotTrackPersistent publishes a HotTrackOptions property. This property is backed by an instance of TJvHotTrackOptions that gets derived in other classes that need specialized versions of it. To tell the streaming subsystem to use the actual class found at streaming time, the constructor of that options class calls SetSubComponent(True); which places csSubComponent in the ComponentStyle property.

So what you should do is get rid of your DefineProperties, have TSearchFieldProperties inherit from TComponent and call SetSubComponent(True) in its constructor. Then you create as many classes derived from TSearchFieldProperties as you need, each with its own set of published properties.

This means you should also get rid of the methods you showed in your submission.

In the end, you should have something along those lines:

type
  TSearchFieldProperties = class(TComponent)
  public
    constructor Create(AOwner: TComponent); override;
  end;

  TIntegerSearchFieldProperties = class(TSearchFieldProperties)
  private
    FIntValue: Integer;
  published
    property IntValue: Integer read FIntValue write FIntValue;
  end;

constructor TSearchFieldProperties.Create(AOwner: TComponent); 
begin
  inherited Create(AOwner);

  SetSubComponent(True);
end;
   

With this you do not fight against the streaming system but rather work with it in the way it is meant to be used.

But if you stop there, you'll notice there is no way for you to specify the actual class name to be used for the TSearchFieldProperties instance used for the TSearchField.Properties property.

The only way to get the class name to be streamed before the subcomponent is streamed is to actually declare the class name as a published property, declared before the subcomponent like this:

type
  TSearchField = class(TCollectionItem)
  published
    // DO NOT change the order of those two properties, PropertiesClassName must come BEFORE Properties for DFM streaming to work properly 
    property PropertiesClassName: string read GetPropertiesClassName write SetPropertiesClassName; 
    property Properties: TSearchFieldProperties read FProperties write SetProperties;
  end;

function TSearchField.GetPropertiesClassName: string;
begin
  Result := Properties.ClassName;
end;

procedure TSearchField.SetPropertiesClassName(const AValue: string);
begin
  FProperties.Free; // no need to test for nil, Free already does it
  FProperties := TSearchFieldPropertiesClass(FindClass(AValue)).Create(self);
end;

It might work if you just declare the published property like without creating a csSubComponent hierarchy but you'll most likely stumble on other hurdles along the way.


Note: this answer is wrong because DefineProperties is called last in TWriter.WriteProperties and so there is no way to change the order properties defined like this are written.

What if you change your DefineProperties override from this:

procedure TSearchField.DefineProperties(Filer: TFiler);
begin
  inherited;
  Filer.DefineProperty('PropertiesClassName', ReaderProc, WriterProc, FPropertiesClassName <> '');
end;

to this:

procedure TSearchField.DefineProperties(Filer: TFiler);
begin
  Filer.DefineProperty('PropertiesClassName', ReaderProc, WriterProc, FPropertiesClassName <> '');

  inherited DefineProperties(Filer);
end;

Basically, call the inherited method AFTER you have defined your own property. Note that I also specified which inherited method is called. I know it's not required, but it makes intent clearer and allows for Ctrl-Click navigation.

OBones
  • 310
  • 2
  • 13