5

I'm using FMX on Delphi 10.1 Berlin.

I read this (which is the behavior I want):

https://stackoverflow.com/a/42933567/1343976

Changing ItemIndex programmatically does not result in the OnChange event being fired. It fires only in response to user interaction.

Is this true only for VCL?

I'm asking for this because, unfortunately for me, from what I can test, modifying the ItemIndex property in code triggers the OnChange event.

If this is true, how can I achieve the same behaviour as VCL in FireMonkey?

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
orsomannaro
  • 51
  • 1
  • 4
  • 5
    Set the OnChange to nil before changing the ItemIndex and restore the OnChange handler afterwards. – LU RD Jul 31 '17 at 19:25
  • @LURD That is on my opinion the worse approach you could use. Why? Before setting OnChange to nil you would need to store its current value in order to be able to restore it later which means you need to introduce another variable into your code. – SilverWarior Jul 31 '17 at 20:17
  • Rather than reset the event itself, you could set a variable that the event looks at and exits immediately if the variable is set. But that is still introducing a variable. Which is not the end of the world... – Remy Lebeau Jul 31 '17 at 21:20
  • @SilverWarior, whether or not stopping the event handler to be invoked by nilling it requires an extra variable or not is an implementation detail and can easily be avoided by a function call. – LU RD Jul 31 '17 at 21:28
  • @LURD I don't see how. Care to show a code example of how to achieve this without introducing new variable? – SilverWarior Jul 31 '17 at 21:39
  • @SilverWarrior, Easy, but why care at all? `procedure SetItemIndex(ix : Integer; cb: TComboBox; OnChange: TNotifyEvent); begin cb.OnChange := nil; cb.ItemIndex := ix; cb.Onchange := OnChange; end;` – LU RD Jul 31 '17 at 21:49
  • 2
    @LURD: This would have been better: `procedure SetItemIndex(ix : Integer; cb: TComboBox); var original: TNotifyEvent; begin original := cb.OnChange; cb.OnChange := nil; try cb.ItemIndex := ix; finally cb.OnChange := original; end; end;` – Remy Lebeau Jul 31 '17 at 21:56
  • @RemyLebeau, exactly. – LU RD Jul 31 '17 at 21:57
  • 1
    @kobik, done. Thanks. – LU RD Aug 01 '17 at 08:26

2 Answers2

2

Is this true only for VCL?

Many things are handled in a different way in FMX.

If this is true, how can I achieve the same behaviour as VCL in FireMonkey?

A simple workaround is to nil the OnChange event property before changing the ItemIndex, and afterwards restore event.

A simple routine to do this would be someting like this (as outlined by @Remy):

procedure SetItemIndex(ix : Integer; cb: TComboBox);
var
  original : TNotifyEvent;
begin
  original := cb.OnChange;
  cb.OnChange := nil;
  try
    cb.ItemIndex := ix;
  finally
    cb.OnChange := original;
  end;
end;  
LU RD
  • 34,438
  • 5
  • 88
  • 296
  • For a moment I hoped it was a FireMonkey bug ... Is it a bad idea to set a boolean variable before to call the event from code, and testing it before to execute the OnChange commands? – orsomannaro Aug 01 '17 at 09:29
0

The proper way of dealing with this problem is to first find out where the OnChange handler is being called from. This is being done in the TCustomComboBox.DoChange() method.

So, what you need to do is either:

  1. override the default DoChange() method to not fire the OnChange event method.

  2. override the ItemIndex property setter to use different logic which won't call the DoChange() method.

Both of these approaches require you to create a new class for your modified ComboBox.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
SilverWarior
  • 7,372
  • 2
  • 16
  • 22
  • 3
    If you override `DoChange()`, you would also have to introduce a new property or parameter for it to look at when deciding when to fire the event (user action) and when not to (coding action). You can't override the `ItemIndex` setter because `TCustomComboBox.SetItemIndex()` is not `virtual` to begin with, you would have to modify `FMX.ListBox.pas` and include it in your project. – Remy Lebeau Jul 31 '17 at 21:28
  • @RemyLebeau You are correct. It would not be possible to override `ItemIndex` setter since it is not `virtual`. I have missed that. So it would be necessary to redeclare the `Itemindex` property in derived class which would use different setter method. – SilverWarior Jul 31 '17 at 21:36
  • or, just add a new property that the overriden `DoChange()` can look at. Set that property before setting `ItemIndex` in code, then reset it afterwards. You don't need to redeclare `ItemIndex` itself. – Remy Lebeau Jul 31 '17 at 21:39
  • @RemyLebeau If introducing new property wouldn't it be better to introduce a new property for accessing `ItemIndex` value without calling the `DoChange` method instead. Your suggested approach would require three calls every time you are changing the `ItemIndex` value programmatically which would make the whole code ugly as hell. – SilverWarior Jul 31 '17 at 21:45
  • 1
    Perhaps it would, but redeclaring the property doesn't guarantee the new setter is always called, either. Only when directly accessing the new `TDerivedComboBox.ItemIndex` property and not when accessing the base class `T(Custom)ComboBox.ItemIndex` property. At least my solution would handle that correctly. Besides, the base class setter accesses private members (`FListBox`, `FListPicker`, `FItemIndex`, and `UpdateCurrentItem()`) that a new setter in a derived class would not have access to, so implementing a new setter really isn't an option anyway. – Remy Lebeau Jul 31 '17 at 21:54
  • So basically no easy solution because of somewhat poor design of TComboBox from the Embarcadero in the first place. This is why I'm beginning to dislike newer Delphi versions. In older Delphi versions it was a breeze making custom components but in newer versions you are sometimes better of recreating the whole component from scratch rather than trying to modify existing one. – SilverWarior Jul 31 '17 at 22:08