Search code examples
c++buildervcl

How to know if a TButton click executed a TAction?


I'm using TActions and TButtons in a VCL application. Setting the TButtons Action field to an existing action centralizes code.

An actions Execute method looks like this:

void __fastcall MyFrame::MyActionExecute(TObject *Sender)
{
//Some action code
}

Assigning the MyAction to a TButton named MyBtn and looking at the actions ActionComponent:

void __fastcall MyFrame::MyActionExecute(TObject *Sender)
{

  if(MyAction->ActionComponent == MyBtn)
  {
   //.. action code when the MyBtn was clicked..
  }
}

seem to work good.

However, calling the MyAction's Execute method programatically, like this:

MyActionExcecute(NULL);

seem to not set the ActionComponent to NULL, but 'still' uses the MyBtn as the ActionCompoent. So the if statement above does evaluate to true, even though the button was not clicked.

Question is, what is the proper way to handle both button clicks and manually calling an actions execute method?

I'm aware that I can check Sender parameter for NULL, and if so, I can assume that it was not the button.


Solution

  • The action's ActionComponent property is set only when a UI control is firing the action, via an internal TBasicActionLink object that the control creates internally for itself. The link's Execute() method has an AComponent parameter that the control passes its Self/this pointer to, to set the action's ActionComponent before calling the action's Execute() method.

    For example, this is what the VCL does internally:

    procedure TControl.SetAction(Value: TBasicAction);
    begin
      if Value = nil then
      begin
        ActionLink.Free;
        ActionLink := nil;
        ...
      end
      else
      begin
        ...
        if ActionLink = nil then
          ActionLink := GetActionLinkClass.Create(Self);
        ActionLink.Action := Value;
        ...
      end;
    end;
    
    procedure TControl.Click;
    begin
      { Call OnClick if assigned and not equal to associated action's OnExecute.
        If associated action's OnExecute assigned then call it, otherwise, call
        OnClick. }
      if Assigned(FOnClick) and (Action <> nil) and not DelegatesEqual(@FOnClick, @Action.OnExecute) then
        FOnClick(Self)
      else if not (csDesigning in ComponentState) and (ActionLink <> nil) then
        ActionLink.Execute(Self) // <-- HERE
      else if Assigned(FOnClick) then
        FOnClick(Self);
    end;
    
    function TBasicActionLink.Execute(AComponent: TComponent): Boolean;
    begin
      FAction.ActionComponent := AComponent; // <-- HERE
      Result := FAction.Execute;
    end;
    

    So, don't call your OnExecute event handler directly. That will not update the action at all. Call the action's Execute() method instead. You will just have to set the action's ActionComponent to NULL yourself beforehand, eg:

    MyAction->ActionComponent = NULL;
    MyAction->Execute();
    

    The documentation claims:

    When the user clicks a client control, that client sets ActionComponent before calling the action's Execute method. After the action executes, the action resets ActionComponent to nil (Delphi) or NULL (C++).

    However, the ActionComponent IS NOT reset automatically, only when the next time a UI control decides to execute the action for itself.