7

I'm trying to use an action to control the visibility of a control. My code looks like this:

Pascal file

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ActnList, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    ActionList1: TActionList;
    Action1: TAction;
    CheckBox1: TCheckBox;
    procedure Action1Update(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Action1Update(Sender: TObject);
begin
  (Sender as TAction).Visible := CheckBox1.Checked;
end;

end.

Form file

object Form1: TForm1
  object Button1: TButton
    Left = 8
    Top = 31
    Action = Action1
  end
  object CheckBox1: TCheckBox
    Left = 8
    Top = 8
    Caption = 'CheckBox1'
    Checked = True
    State = cbChecked
  end
  object ActionList1: TActionList
    Left = 128
    Top = 8
    object Action1: TAction
      Caption = 'Action1'
      OnUpdate = Action1Update
    end
  end
end

When the form first runs the button is visible and the check box is checked. Then I un-check the check box and the button disappears. When I re-check the check box the button fails to re-appear.

I think the reason for this can be found inside the following local function in TCustomForm.UpdateActions:

procedure TraverseClients(Container: TWinControl);
var
  I: Integer;
  Control: TControl;
begin
  if Container.Showing and not (csDesigning in Container.ComponentState) then
    for I := 0 to Container.ControlCount - 1 do
    begin
      Control := Container.Controls[I];
      if (csActionClient in Control.ControlStyle) and Control.Visible then
          Control.InitiateAction;
      if (Control is TWinControl) and (TWinControl(Control).ControlCount > 0) then
        TraverseClients(TWinControl(Control));
    end;
end;

The check of Control.Visible appears to block my action ever getting a chance to update itself again.

Have I diagnosed the issue correctly? Is this by design or should I submit a QC report? Does anyone know of a workaround?

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Uhm, why are you using [`OnUpdate`](http://docwiki.embarcadero.com/Libraries/en/Vcl.ActnList.TAction.OnUpdate) ? – TLama Apr 12 '12 at 16:34
  • @TLama: Because that is what it is for. Most often, I guess, you set `Enabled` rather than `Visible`, though. – Andreas Rejbrand Apr 12 '12 at 16:36
  • This IS a bug, or at least a Misbegotten-Feature. You should be able to set Enabled and Visible on any action, and any linked control should be refreshed to contain that value of Enabled and that value of Visible. If ActionUpdate is required here, then it breaks the kind of decoupling you're trying to do. The checkbox now has to be coupled to the action. I would just STOP using OnUpdate for any changes to visible, and simply set the action visible or not visible, as a single line, in the checkbox onclick. – Warren P Apr 12 '12 at 16:37
  • Related to http://stackoverflow.com/questions/8447127/why-doesnt-onupdate-trigger-for-invisible-components – Svein Bringsli Apr 13 '12 at 08:42
  • @SveinBringsli Yeah, that's exactly the same question and my question is a dupe. – David Heffernan Apr 13 '12 at 08:45
  • A dupe it may be, but you did more research :) – Svein Bringsli Apr 13 '12 at 08:48
  • Although the question may be a dupe, the answers here are much better – Neville Cook Nov 10 '12 at 01:52

3 Answers3

8

Your diagnosis is correct. Actions have worked that way since they were first introduced to Delphi.

I expect it's by design (an optimization to avoid excessive updating of the text and other visual aspects of invisible controls, probably), but that doesn't mean the design is good.

Any workaround I can imagine will involve your checkbox code directly manipulating the affected actions, which isn't very elegant since it shouldn't have to be aware of everything else it might affect — that's what the OnUpdate event is supposed to do for you. When the checkbox gets checked, call Action1.Update, or if that doesn't work, then Action1.Visible := True.

You could also put your action-updating code in the TActionList.OnUpdate event instead.

Rob Kennedy
  • 161,384
  • 21
  • 275
  • 467
  • 1
    Thanks. I'm amazed I've not noticed this before but I'm reassured that you were already aware of the issue. I already have an override of `TCustomForm.UpdateActions` in my common base class form. So I can do work in there to make the updating happen the way I want it to. That's probably the easiest route for me. – David Heffernan Apr 12 '12 at 16:49
4

Yes your diagnosis is correct as Rob already said and explained. A work around is not to use individual TAction's OnUpdate event handlers, but to use the TActionList's OnUpdate event handler.

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  Handled := True;
  Action1.Visible := CheckBox1.Checked;
end;

You do need at least one visible control with a linked action for this to work, otherwise the ActionList's OnUpdate handler won't be called either.

Btw, when Actions and ActionLists were first introduced, Ray Konopka (Raize components) wrote a couple of articles on their implementation and gave very sound advice on how to use them. Ever since, I have adopted the practice of to use the OnExecute of each individual Action, but to use the OnUpdate of the ActionList. Also, the first thing I do in that handler is to set Handled to True so it won't be called more than necessary, and to only ever change an Action's Enabled property once in that handler so the GUI won't flicker as a result of turning it off and then on.

Article by Ray Konopka is "Effectively using Action Lists" : http://edn.embarcadero.com/article/27058 Ray used to have three articles on his own site, but on embarcadero there is but one, but it may well be the "combined" version (don't have the originals handy).

Marjan Venema
  • 19,136
  • 6
  • 65
  • 79
  • 2
    Thanks for this. I'll look up Ray's articles. I was never really aware of `OnUpdate` for the list but can imagine that it would result in a good centralisation of code. – David Heffernan Apr 12 '12 at 19:12
2

ActionUpdate event is not called when related controls are not visible. Try to explicity call ActionUpdate on Checkbox1's click event.

Christopher Ramírez
  • 1,710
  • 10
  • 13
  • That's like saying "stop using actions". – David Heffernan Apr 12 '12 at 16:43
  • No David. Actions are good. But as Rob said, their design is evil in your scenario. – Christopher Ramírez Apr 12 '12 at 16:54
  • But the best solution to the problem cannot be coupling my GUI controls together. Avoiding that hell is why I started using actions in the first place. – David Heffernan Apr 12 '12 at 16:57
  • mmm... ``(Sender as TAction).Visible := CheckBox1.Checked;`` could be seem as coupling also. Transverse that line in the OnClick event of the ``Checkbox``. Keep it simple! ;) – Christopher Ramírez Apr 12 '12 at 17:04
  • 1
    @ChristopherRamírez: The important difference is that `Sender` is an action, not a GUI control. This action may be used by zero, one, or twenty-four GUI controls. – Andreas Rejbrand Apr 12 '12 at 17:07
  • OK that's a coupling. But what about all the other ways in which the checkbox might change state. What if there were two or three flags that determined visibility? And what about the initial setting when the form loads and when the check box clicked event does not fire? I remember life before actions. Not pretty. – David Heffernan Apr 12 '12 at 17:08
  • You're right @David. I don't know if Delphi's ``TCheckbox`` has a ``OnChange`` event like VS does. – Christopher Ramírez Apr 12 '12 at 17:24
  • @ChristopherRamírez Oh don't even get me started on WinForms ;-) No actions there! – David Heffernan Apr 12 '12 at 17:55