Does TAction.OnUpdate event degrade the performance?
Asked Answered
D

4

12

In Delphi XE7, I use this trick to automatically enable or disable a toolbar button ("Edit ListView Item") according to whether an item in the ListView is selected or not, to prevent the user to click on the button if there is no ListView Item selected:

  • Put a TActionList on a VCL Form.
  • In the ActionList create an action actTest.
  • Put a TButton on the form.
  • Assign the action actTest to the button.
  • Put a TListView on the form.
  • In the ListView create two items.
  • In the OnUpdate event of the actTest action write:

     procedure TForm1.actTestUpdate(Sender: TObject);
     begin
       actTest.Enabled := ListView1.SelCount > 0;
       CodeSite.Send('actTestUpdate'); // gets fired very often!
     end;
    

Now you can see that the button becomes enabled or disabled according to whether an item in the ListView is selected or not, independently from whether you select/deselect items with the mouse or with the keyboard or programmatically.

However, in the CodeSite Live Viewer I can see that the actTestUpdate event is fired continuously and very often, so the statement actTest.Enabled := ListView1.SelCount > 0; gets executed VERY OFTEN.

So my question is: Does this degrade the performance? If yes, is there another trick to achieve the above purpose?

Durgy answered 1/3, 2015 at 10:57 Comment(19)
The TAction.OnUpdate intention is to do what you are doing, see docwiki.embarcadero.com/CodeExamples/XE7/en/…Intumesce
You are doing it as was intended. But, it is an unecessary wasting of resources. What's worse, there might be situations where you need to e.g. disable a certain action in the middle of a method execution and using the OnUpdate way may lead to doing it unsafely late. I'm trying to completely avoid OnUpdate personally.Appal
@Appal Which alternative would you suggest?Durgy
I would update the action state right when something happens. In this case when the list view's underlying model selection changes.Appal
@Appal How would you do that? I've tried them all. Remember, the goal is to update the button independently from whether you select/deselect items with the mouse or with the keyboard.Durgy
The crux is that TListView.OnSelectItem is triggered several times on selection change, and the various click and keyboard events each cover only either the mouse or the keyboard. So TListView lacks a proper event type for this purpose.Durgy
The OnSelectItem event should be fired only when the selection state of an item changes and it is so the ideal trigger for this update (whether you update a model, or the action itself when having no view model concept). That's better than wasteful sending of LVM_GETSELECTEDCOUNT messages to get the selected item count whenever the application goes idle (and nothing in the selection actually changes).Appal
@Appal ...may lead to doing it unsafely late in which case you can update manually anyway.Vasquez
OnSelectItem fires twice when a selection is changing. Once with "selected" false, disable your button there. And once with "selected" true, enable your button then. Doesn't look like excessive processing..Anfractuous
As I read it your OnUpdate is being fired repeatedly even when there is no user interaction. Is that so? If so, please make a repro.Searching
@David, OnUpdate, occurs when the application is idle..., that's where the wasting origins.Appal
@Appal It occurs when the app becomes idle. Not when it is idle. That means, when the message queue has just been emptied. So if no input messages, or timers, or paint messages are pulled from the queue it won't fire. Now, if user has a timer running, OnUpdate will happen after every timer event.Searching
@David I'm pretty sure OP means by continuously and very often that he didn't notice that without mouse movement.Vasquez
@Vasquez I'd like to be sure. If user is sitting there providing no input and OnIdle is firing repeatedly, it would be nice to know why. I've see OnIdle lead to significant CPU utilization when an app is idle.Searching
@David, which happens still unecessarily often (and randomly) for such purpose. I'm happy to update my action states just when they need, not when the application becomes idle, sun goes down, or moon enters a certain phase :)Appal
@DavidHeffernan Yes, I am sitting here at my desk, no mouse moving, no keyboard typing, having no timer running and watching the messages running down like a waterfall in CodeSite Live Viewer. It's very relaxing, better than an aquarium...Durgy
@user1580348: Then there is most likely a timer running in your code, or maybe inside one of your UI controls. You can use the TApplication.OnMessage event to verify that.Ageold
Yes, timer is the likely explanation. OnIdle is quite crude.Searching
I was hit by the same problem, if you have many actions you will find high cpu usage, even when you do nothing, you can set Application.ActionUpDateDelay to higher value so that the event fires less often.Odiliaodille
B
9

If you have (or plan to have) many actions you might want to set Application.ActionUpdateDelay to e.g. 50 milliseconds. This can improve the performance noticeable.

Also, again if you have many actions, I would suggest you try to use TForm.UpdateActions instead of defining TAction.OnUpdate for each action. It will make the code more readable.

Bogart answered 1/3, 2015 at 21:18 Comment(1)
yes this is the problem, I would take a higher value though.Odiliaodille
V
5

Generally

Yes, an OnUpdate event handler takes time, just as any other routine does. Multiple handlers take a multiple of that time. The gross of all that code will evaluate conditions resulting in just nothing to do. In that sense, you could conclude that this update mechanism degrades performance. Especially considering these update events occur quite often:

Occurs when the application is idle or when the action list updates.

That could be a reason to spare its use. But you should realize that the evaluation of a single expression mostly does not take that much time. Also, realize that regardless of action updates, your application performs (much more heavy) calculations and operations on every single mouse move.

When you keep the code duration in action update events to a minimal, e.g. no password checking via a database connection, then performance will appear just normal. If you háve lengthy operations linked to updating actions, then fall back on manual updates in those specific situations.

Note that performance can be slightly gained by not using the individual OnUpdate events of the Actions, but the OnUpdate event of the ActionList instead which has a Handled parameter to cancel further processing, with the additional benefit of centralization and categorization.

Specifically

Your ListView1.SelCount sends a WinAPI message to the control to retrieve the selection count. That is a tiny operation and I would not bother its time needed.

An alternative is to update the action in the ListView's OnSelectItem event. That event will catch all selection changes due to mouse and keyboard interaction, as well as setting the individual items' Selected property:

procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
begin
  actTest.Enabled := ListView1.SelCount > 0;
end;

However, the ListView nor the VCL do not provide anything to signal only between SelCount = 0 and SelCount > 0, so you will evaluate that line of code more then strictly necessary anyway.

Assuming MultiSelect is true, you could also count the selection changes yourself to eliminate the need for calling SelCount:

  private
    FListViewSelected: Longbool;

procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
begin
  if Selected then
    Inc(FListViewSelected)
  else
    Dec(FListViewSelected);
  actTest.Enabled := FListViewSelected;
end;

Or test for the selected item being nil:

procedure TForm1.ListView1SelectItem(Sender: TObject; Item: TListItem;
  Selected: Boolean);
begin
  actTest.Enabled := ListView1.Selected <> nil;
end;

But then again, there really is no reason left for not using the OnUpdate event:

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  actTest.Enabled := ListView1.Selected <> nil;
  Handled := True;
end;
Vasquez answered 1/3, 2015 at 12:58 Comment(2)
NGLN, if you provide a working example with OnSelectItem which has no flaws, I will propose you for the next Nobel Prize. I have tried it...Durgy
@user Do you understand OnIdle, when it fires, why that influences OnUpdate?Searching
N
0

The Action update events are (mostly) executed inside Application.Idle. As long as you don't do time critical things inside the event handlers there should be no noticeable performance degradation.

Norvan answered 1/3, 2015 at 11:17 Comment(1)
It's not about time criticality (the thing itself depends on time spend), but about time taking/duration (the thing itself having consequence for things to come).Vasquez
I
0

I was struggling with the same problem no wanting to repeatedly call control update while the app is idle, and came up with the following solution:

You could write an event which triggers OnExecute action and compare which action it was triggered from, by using its Action pointer parameter. After executing the desired action's code but still in the OnExecute event, you can call some sort of UpdateAllControlsVisibilityState() procedure which could update your buttons enabled/disabled states for you at the end of this event for example. E.g.

procedure TForm1.ActionManager1Execute(Action: TBasicAction; var Handled: Boolean);
begin

  if Action = DesiredAction1 then
     // Execute code for DesiredAction1...
  else if Action = DesiredAction2 then
     // Execute code for DesiredAction2...
  // ...

  UpdateAllControlsVisibilityState; // Write procedure here to update your controls

end;

In this case, the OnExecute would be called only once and only after you trigger a certain action, with the appropriate control visibility update. Also, by managing it that way, you get centralized action management rather than having each action OnExecute event.

I have tested which OnExecute event gets called first and it appears that the OnExecute of the TActionManager gets called first, then the OnExecute event of a particular action gets called next (if assigned), at least in 10.4.2 which I am using at the moment and I really like the centralized management of all the actions which makes debugging much easier (for me at least).

It also depends on the Handled parameter - if the OnExecute event handler does not set its Handled parameter to True, the application’s OnActionExecute event occurs next.

Iodous answered 4/10, 2023 at 13:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.