Question

In my custom component I created some TAction-s as subcomponents. They're all published, but I could not assign them at design time since they were not available through object inspector.

How do you make them "iterable" by the object inspector? I have tried to set the Owner of the actions to the Owner of the custom component (which is the hosting Form) to no success.

EDIT: It looks like Embarcadero changed Delphi IDE behaviour related with this problem. If you are using Delphi versions prior XE, you should use solution from my own answer. For XE and above, you should use solution from Craig Peterson.

EDIT: I've added my own answer that solves the problem, i.e. by creating a TCustomActionList instance in my custom component and setting its Owner to the hosting form (owner of the custom component). However I am not too happy with this solution, since I think the instance of TCustomActionList is kind of redundant. So I am still hoping to get better solution.

EDIT: Add code sample

uses
  .., ActnList, ..;

type
  TVrlFormCore = class(TComponent)
  private
    FCancelAction: TBasicAction;
    FDefaultAction: TBasicAction;
    FEditAction: TBasicAction;
  protected
    procedure DefaultActionExecute(ASender: TObject); virtual;
    procedure CancelActionExecute(ASender: TObject); virtual;
    procedure EditActionExecute(ASender: TObject); virtual;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property DefaultAction: TBasicAction read FDefaultAction;
    property CancelAction : TBasicAction read FCancelAction;
    property EditAction   : TBasicAction read FEditAction;
  end;

implementation

constructor TVrlFormCore.Create(AOwner: TComponent);
begin
  inherited;
  FDefaultAction := TAction.Create(Self);
  with FDefaultAction as TAction do
  begin
    SetSubComponent(True);
    Caption := 'OK';
    OnExecute := DefaultActionExecute;
  end;

  FCancelAction := TAction.Create(Self);
  with FCancelAction as TAction do
  begin
    SetSubComponent(True);
    Caption := 'Cancel';
    OnExecute := Self.CancelActionExecute;
  end;

  FEditAction := TAction.Create(Self);
  with FEditAction as TAction do
  begin
    SetSubComponent(True);
    Caption := 'Edit';
    OnExecute := Self.EditActionExecute;
  end;
end;
Was it helpful?

Solution

As far as I can tell you're not supposed to do it that way.

The easy way to do what you want is to create new standalone actions that can work with any TVrlFormCore component and set the target object in the HandlesTarget callback. Take a look in StdActns.pas for examples. The actions won't be available automatically when sommeone drops your component on the form, but they can add them to their action list manually using the New Standard Actions... command. There's a good article on registering standard actions here.

If you really want to auto-create the actions you need to set the action Owner property to the form and you need to set the Name property. That's all that's necessary, but it does introduce a bunch of issues you need to work around:

  • The form owns the actions so it will add them its declaration's published section and will auto-create them as part of the streaming process. To work around that you can just disable streaming by overwriting the action's WriteState method and skip the inherited behavior.
  • Since you aren't writing the state, none of the properties will be persisted. To avoid confusing your users you should switch make the actions descend from TCustomAction instead of TAction, so it doesn't expose anything. There may be way to make the action stream properly, but you didn't say whether it was necessary.
  • You need to register for free notifications in case the form frees the action before you can.
  • If someone drops more than one of your component on the action names will conflict. There's multiple ways to handle that, but the cleanest would probably be to override the component's SetName method and use its name as a prefix for the actions' names. If you do that you need to use RegisterNoIcon with the new class so they don't show up on the form.
  • In the IDE's Structure pane the actions will show up directly under the form, rather than nested like ActionList shows. I haven't found a way around that; none of SetSubComponent, GetParentComponent/HasParent, or GetChildren have any effect, so this may be hard-coded behavior. You can delete the action from the structure pane, separate from the component, too.

I'm sure it can be improved, but this works without any custom property editors:

type
  TVrlAction = class(TCustomAction)
  protected
    procedure WriteState(Writer: TWriter); override;
  end;

  TVrlFormCore = class(TComponent)
  private
    FDefaultAction: TVrlAction;
  protected
    procedure DefaultActionExecute(ASender: TObject); virtual;
    procedure Notification(AComponent: TComponent;
      Operation: TOperation); override;
    procedure SetName(const NewName: TComponentName); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    property DefaultAction: TVrlAction read FDefaultAction;
  end;

procedure Register;

implementation

// TVrlAction

procedure TVrlAction.WriteState(Writer: TWriter);
begin
  // No-op
end;

// TVrlFormCore

constructor TVrlFormCore.Create(AOwner: TComponent);
begin
  inherited;
  FDefaultAction := TVrlAction.Create(AOwner);
  with FDefaultAction do
  begin
    FreeNotification(Self);
    Name := 'DefaultAction';
    Caption := 'OK';
    OnExecute := DefaultActionExecute;
  end;
end;

destructor TVrlFormCore.Destroy;
begin
  FDefaultAction.Free;
  inherited;
end;

procedure TVrlFormCore.DefaultActionExecute(ASender: TObject);
begin

end;

procedure TVrlFormCore.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited;
  if Operation = opRemove then
    if AComponent = FDefaultAction then
      FDefaultAction := nil;
end;

procedure TVrlFormCore.SetName(const NewName: TComponentName);
begin
  inherited;
  if FDefaultAction <> nil then
    FDefaultAction.Name := NewName + '_DefaultAction';
end;

procedure Register;
begin
  RegisterComponents('Samples', [TVrlFormCore]);
  RegisterNoIcon([TVrlAction]);
end;

OTHER TIPS

EDIT: Use this solution for Delphi versions prior to Delphi XE. For XE and later, use Craig Peterson answer (which does not require redundant TCustomActionList instance).

After meddling around and using information from Craig Peterson's answer, I've decided to instantiate a TCustomActionList in my custom component. So far it is the only way to get list of actions in Object Inspector.

Here is the code:

uses
  ..., ActnList, ...;

type
  TVrlAction=class(TCustomAction)
  protected
    procedure WriteState(Writer: TWriter); override;
  published
    property Caption;
  end;

  TVrlActionList=class(TCustomActionList)
  protected
    procedure WriteState(Writer: TWriter); override;
  end;

  TVrlFormCore = class(TVrlItemSource)
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    procedure SetName(const NewName: TComponentName); override;
  public
    constructor Create(AOwner: TComponent); override;
  end;

implementation

{ TVrlAction }

procedure TVrlAction.WriteState(Writer: TWriter);
begin
end;

{ TVrlActionList }

procedure TVrlActionList.WriteState(Writer: TWriter);
begin
end;

{ TVrlFormCore }

constructor TVrlFormCore.Create(AOwner: TComponent);
begin
  inherited;
  FActions := TVrlActionList.Create(AOwner);

  FDefaultAction := TVrlAction.Create(AOwner);
  with FDefaultAction as TVrlAction do
  begin
    FreeNotification(Self);
    Caption := 'OK';
    OnExecute := DefaultActionExecute;
  end;
  FActions.AddAction(TContainedAction(FDefaultAction));

  FCancelAction := TVrlAction.Create(AOwner);
  with FCancelAction as TVrlAction do
  begin
    FreeNotification(Self);
    Caption := 'Cancel';
    OnExecute := Self.CancelActionExecute;
  end;
  FActions.AddAction(TContainedAction(FCancelAction));

  FEditAction := TVrlAction.Create(AOwner);
  with FEditAction as TVrlAction do
  begin
    FreeNotification(Self);
    Caption := 'Edit';
    OnExecute := Self.EditActionExecute;
  end;
  FActions.AddAction(TContainedAction(FEditAction));
end;

procedure TVrlFormCore.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited;
  if Operation=opRemove then
  begin
    if AComponent = FMaster then
      FMaster := nil
    else if (AComponent is TVrlFormCore) then
      FDetails.Remove(TVrlFormCore(AComponent))
    else if AComponent=FDefaultAction then
      FDefaultAction := nil
    else if AComponent=FCancelAction then
      FCancelAction := nil
    else if AComponent=FEditAction then
      FEditAction := nil;
  end;
end;

procedure TVrlFormCore.SetName(const NewName: TComponentName);
begin
  inherited;
  if FActions<>nil then
    FActions.Name := NewName + '_Actions';

  if FDefaultAction <> nil then
    FDefaultAction.Name := NewName + '_DefaultAction';
  if FCancelAction <> nil then
    FCancelAction.Name := NewName + '_CancelAction';
  if FEditAction <> nil then
    FEditAction.Name := NewName + '_EditAction';
end;

You cannot assign them because they are read only by design:

property DefaultAction: TBasicAction read FDefaultAction; 
property CancelAction : TBasicAction read FCancelAction; 
property EditAction   : TBasicAction read FEditAction; 

You should change your class' interface to:

property DefaultAction: TBasicAction read FDefaultAction write FDefaultAction; 
property CancelAction : TBasicAction read FCancelAction write FCancelAction; 
property EditAction   : TBasicAction read FEditAction write FEditAction; 

or write appropriate setter for each action.

Edit:

What you need is then

  1. to implement your 3 custom actions as Predefined Actions (See StdActns.pas for samples).

  2. to register them by calling ActnList.RegisterActions. (See RAD Studio documentation)

  3. to add to the form a TActionList and/or TActionManager to allow your Predefined Actions appear in the list of predefined actions in the action list editor of every TControl's descendent.

You may do extensive search on google for the topic and find some concrete example.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top